Skip to content

Commit

Permalink
feat(weave): dataset editing UI
Browse files Browse the repository at this point in the history
  • Loading branch information
bcsherma committed Jan 11, 2025
1 parent 6841aa7 commit dd0841d
Show file tree
Hide file tree
Showing 6 changed files with 1,164 additions and 58 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,232 @@
import {Box, TextField, Typography} from '@mui/material';
import {Popover} from '@mui/material';
import {
GridRenderCellParams,
GridRenderEditCellParams,
} from '@mui/x-data-grid-pro';
import {Button} from '@wandb/weave/components/Button';
import {Icon} from '@wandb/weave/components/Icon';
import React, {useState} from 'react';
import {ResizableBox} from 'react-resizable';

import {DraggableGrow, DraggableHandle} from '../../../../DraggablePopups';

const cellViewingStyles = {
height: '100%',
width: '100%',
fontFamily: '"Source Sans Pro", sans-serif',
fontSize: '14px',
lineHeight: '1.5',
padding: '8px 12px',
display: 'flex',
alignItems: 'center',
transition: 'background-color 0.2s ease',
};

interface CellViewingRendererProps extends GridRenderCellParams {
isEdited?: boolean;
isDeleted?: boolean;
isNew?: boolean;
}

export const CellViewingRenderer: React.FC<CellViewingRendererProps> = ({
value,
isEdited = false,
isDeleted = false,
isNew = false,
api,
id,
field,
}) => {
const [isHovered, setIsHovered] = useState(false);

const handleEditClick = (event: React.MouseEvent) => {
event.stopPropagation();
api.startCellEditMode({id, field});
};

const getBackgroundColor = () => {
if (isDeleted) {
return 'rgba(255, 0, 0, 0.1)';
}
if (isEdited) {
return 'rgba(0, 128, 128, 0.1)';
}
if (isNew) {
return 'rgba(0, 255, 0, 0.1)';
}
return 'transparent';
};

return (
<Box
onClick={handleEditClick}
onMouseEnter={() => setIsHovered(true)}
onMouseLeave={() => setIsHovered(false)}
sx={{
...cellViewingStyles,
position: 'relative',
cursor: 'pointer',
backgroundColor: getBackgroundColor(),
opacity: isDeleted ? 0.5 : 1,
textDecoration: isDeleted ? 'line-through' : 'none',
'&:hover': {
backgroundColor: 'rgba(0, 0, 0, 0.04)',
},
}}>
<span style={{flex: 1}}>{value}</span>
{isHovered && (
<Box
sx={{
display: 'flex',
alignItems: 'center',
opacity: 0,
transition: 'opacity 0.2s ease',
cursor: 'pointer',
animation: 'fadeIn 0.2s ease forwards',
'@keyframes fadeIn': {
from: {opacity: 0},
to: {opacity: 0.5},
},
'&:hover': {
opacity: 0.8,
},
}}>
<Icon name="pencil-edit" height={14} width={14} />
</Box>
)}
</Box>
);
};

export const CellEditingRenderer: React.FC<
GridRenderEditCellParams
> = params => {
const {id, value, field, hasFocus, api} = params;
const inputRef = React.useRef<HTMLTextAreaElement>(null);
const [anchorEl, setAnchorEl] = useState<HTMLDivElement | null>(null);

// Calculate initial position when edit mode starts
React.useLayoutEffect(() => {
const element = api.getCellElement(id, field);
if (element) {
setAnchorEl(element);
}
}, [api, id, field]);

React.useLayoutEffect(() => {
if (hasFocus) {
inputRef.current?.focus();
}
}, [hasFocus]);

const handleValueChange = (event: React.ChangeEvent<HTMLInputElement>) => {
const newValue = event.target.value;
api.setEditCellValue({id, field, value: newValue});
};

const handleKeyDown = (event: React.KeyboardEvent) => {
if (event.key === 'Enter' && !event.metaKey) {
event.stopPropagation();
}
};

return (
<>
<CellViewingRenderer {...params} />
<Popover
open={!!anchorEl}
anchorEl={anchorEl}
anchorOrigin={{
vertical: 'bottom',
horizontal: 'left',
}}
transformOrigin={{
vertical: 'top',
horizontal: 'left',
}}
onClose={() => setAnchorEl(null)}
TransitionComponent={DraggableGrow}
sx={{
'& .MuiPaper-root': {
backgroundColor: 'white',
boxShadow:
'0px 4px 20px rgba(0, 0, 0, 0.15), 0px 0px 40px rgba(0, 0, 0, 0.05)',
border: 'none',
borderRadius: '4px',
overflow: 'hidden',
padding: '0px',
},
}}>
<ResizableBox width={400} height={300}>
<Box
sx={{
height: '100%',
display: 'flex',
flexDirection: 'column',
}}>
<DraggableHandle>
<Box
sx={{
cursor: 'grab',
backgroundColor: 'white',
display: 'flex',
justifyContent: 'space-between',
padding: '8px',
borderBottom: '1px solid rgba(0, 0, 0, 0.05)',
}}>
<Typography
variant="caption"
sx={{
fontFamily: '"Source Sans Pro", sans-serif',
color: 'gray.500',
}}>
⌘+Enter to close
</Typography>
<Button
icon="close"
size="small"
variant="ghost"
onClick={() => api.stopCellEditMode({id, field})}
/>
</Box>
</DraggableHandle>
<Box
sx={{
flex: 1,
overflow: 'auto',
padding: '12px',
}}>
<TextField
inputRef={inputRef}
value={value}
onChange={handleValueChange}
onKeyDown={handleKeyDown}
fullWidth
multiline
autoFocus
sx={{
'& .MuiInputBase-root': {
fontFamily: '"Source Sans Pro", sans-serif',
fontSize: '12px',
border: 'none',
backgroundColor: 'white',
},
'& .MuiInputBase-input': {
padding: 0,
},
'& .MuiOutlinedInput-notchedOutline': {
border: 'none',
},
'& textarea': {
overflow: 'visible !important',
},
}}
/>
</Box>
</Box>
</ResizableBox>
</Popover>
</>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
import React, {createContext, useCallback, useContext, useState} from 'react';

interface DatasetRow {
// id: string | number;
[key: string]: any;
___weave: {
id: string;
// The index must be set for rows of an existing dataset.
index?: number;
isNew?: boolean;
};
}

interface EditedCell {
[fieldName: string]: unknown;
}

interface DatasetEditContextType {
/** Map of edited cells, keyed by row absolute index */
editedCellsMap: Map<number, EditedCell>;
setEditedCellsMap: React.Dispatch<
React.SetStateAction<Map<number, EditedCell>>
>;
/** Map of complete edited rows, keyed by row absolute index */
editedRows: Map<number, DatasetRow>;
setEditedRows: React.Dispatch<React.SetStateAction<Map<number, DatasetRow>>>;
/** Callback to process row updates from the data grid */
processRowUpdate: (newRow: DatasetRow, oldRow: DatasetRow) => DatasetRow;
/** Array of row indices that have been marked for deletion */
deletedRows: number[];
setDeletedRows: React.Dispatch<React.SetStateAction<number[]>>;
/** Map of newly added rows, keyed by temporary row ID */
addedRows: Map<string, DatasetRow>;
setAddedRows: React.Dispatch<React.SetStateAction<Map<string, DatasetRow>>>;
/** Reset the context to its initial state */
reset: () => void;
}

export const DatasetEditContext = createContext<
DatasetEditContextType | undefined
>(undefined);

export const useDatasetEditContext = () => {
const context = useContext(DatasetEditContext);
if (!context) {
throw new Error(
'useDatasetEditContext must be used within a DatasetEditProvider'
);
}
return context;
};

interface DatasetEditProviderProps {
children: React.ReactNode;
}

export const DatasetEditProvider: React.FC<DatasetEditProviderProps> = ({
children,
}) => {
const [editedCellsMap, setEditedCellsMap] = useState<Map<number, EditedCell>>(
new Map()
);
const [editedRows, setEditedRows] = useState<Map<number, DatasetRow>>(
new Map()
);
const [deletedRows, setDeletedRows] = useState<number[]>([]);
const [addedRows, setAddedRows] = useState<Map<string, DatasetRow>>(
new Map()
);

const processRowUpdate = useCallback(
(newRow: DatasetRow, oldRow: DatasetRow): DatasetRow => {
const changedField = Object.keys(newRow).find(
key => newRow[key] !== oldRow[key] && key !== 'id'
);

if (changedField) {
const rowKey = String(oldRow.___weave.id);
const rowIndex = oldRow.___weave.index;
if (oldRow.___weave.isNew) {
setAddedRows(prev => {
const updatedMap = new Map(prev);
updatedMap.set(rowKey, newRow);
return updatedMap;
});
} else {
setEditedCellsMap(prev => {
const existingEdits = prev.get(rowIndex!) || {};
const updatedMap = new Map(prev);
updatedMap.set(rowIndex!, {
...existingEdits,
[changedField]: newRow[changedField],
});
return updatedMap;
});
setEditedRows(prev => {
const updatedMap = new Map(prev);
updatedMap.set(rowIndex!, newRow);
return updatedMap;
});
}
}
return newRow;
},
[]
);

const reset = useCallback(() => {
setEditedCellsMap(new Map());
setEditedRows(new Map());
setDeletedRows([]);
setAddedRows(new Map());
}, []);

return (
<DatasetEditContext.Provider
value={{
editedCellsMap,
setEditedCellsMap,
editedRows,
setEditedRows,
processRowUpdate,
deletedRows,
setDeletedRows,
addedRows,
setAddedRows,
reset,
}}>
{children}
</DatasetEditContext.Provider>
);
};
Loading

0 comments on commit dd0841d

Please sign in to comment.