mirror of
https://github.com/rowyio/rowy.git
synced 2025-12-29 00:16:39 +01:00
add context menu feature with cut, copy, and paste
This commit is contained in:
@@ -51,6 +51,7 @@ type SelectedColumnHeader = {
|
||||
column: Column<any> & { [key: string]: any };
|
||||
anchorEl: PopoverProps["anchorEl"];
|
||||
};
|
||||
|
||||
export type ColumnMenuRef = {
|
||||
selectedColumnHeader: SelectedColumnHeader | null;
|
||||
setSelectedColumnHeader: React.Dispatch<
|
||||
|
||||
42
src/components/Table/ContextMenu/MenuContent.tsx
Normal file
42
src/components/Table/ContextMenu/MenuContent.tsx
Normal file
@@ -0,0 +1,42 @@
|
||||
import { Menu } from "@mui/material";
|
||||
import MenuRow, { IMenuRow } from "./MenuRow";
|
||||
|
||||
interface IMenuContents {
|
||||
anchorEl: HTMLElement;
|
||||
open: boolean;
|
||||
handleClose: () => void;
|
||||
items: IMenuRow[];
|
||||
}
|
||||
|
||||
export function MenuContents({
|
||||
anchorEl,
|
||||
open,
|
||||
handleClose,
|
||||
items,
|
||||
}: IMenuContents) {
|
||||
const handleContext = (e: React.MouseEvent) => e.preventDefault();
|
||||
return (
|
||||
<Menu
|
||||
id="cell-context-menu"
|
||||
aria-labelledby="cell-context-menu"
|
||||
anchorEl={anchorEl}
|
||||
open={open}
|
||||
onClose={handleClose}
|
||||
anchorOrigin={{
|
||||
vertical: "bottom",
|
||||
horizontal: "left",
|
||||
}}
|
||||
transformOrigin={{
|
||||
vertical: "top",
|
||||
horizontal: "left",
|
||||
}}
|
||||
sx={{ "& .MuiMenu-paper": { backgroundColor: "background.default" } }}
|
||||
MenuListProps={{ disablePadding: true }}
|
||||
onContextMenu={handleContext}
|
||||
>
|
||||
{items.map((item, indx: number) => (
|
||||
<MenuRow key={indx} {...item} />
|
||||
))}
|
||||
</Menu>
|
||||
);
|
||||
}
|
||||
17
src/components/Table/ContextMenu/MenuRow.tsx
Normal file
17
src/components/Table/ContextMenu/MenuRow.tsx
Normal file
@@ -0,0 +1,17 @@
|
||||
import { ListItemIcon, ListItemText, MenuItem } from "@mui/material";
|
||||
|
||||
export interface IMenuRow {
|
||||
onClick: () => void;
|
||||
icon: JSX.Element;
|
||||
label: string;
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
export default function MenuRow({ onClick, icon, label, disabled }: IMenuRow) {
|
||||
return (
|
||||
<MenuItem disabled={disabled} onClick={onClick}>
|
||||
<ListItemIcon>{icon} </ListItemIcon>
|
||||
<ListItemText> {label} </ListItemText>
|
||||
</MenuItem>
|
||||
);
|
||||
}
|
||||
56
src/components/Table/ContextMenu/index.tsx
Normal file
56
src/components/Table/ContextMenu/index.tsx
Normal file
@@ -0,0 +1,56 @@
|
||||
import React from "react";
|
||||
import _find from "lodash/find";
|
||||
import { PopoverProps } from "@mui/material";
|
||||
|
||||
import { getFieldProp } from "@src/components/fields";
|
||||
import { useProjectContext } from "@src/contexts/ProjectContext";
|
||||
|
||||
import { MenuContents } from "./MenuContent";
|
||||
|
||||
export type SelectedCell = {
|
||||
rowIndex: number;
|
||||
colIndex: number;
|
||||
};
|
||||
|
||||
export type ContextMenuRef = {
|
||||
selectedCell: SelectedCell;
|
||||
setSelectedCell: React.Dispatch<React.SetStateAction<SelectedCell | null>>;
|
||||
anchorEl: HTMLElement | null;
|
||||
setAnchorEl: React.Dispatch<
|
||||
React.SetStateAction<PopoverProps["anchorEl"] | null>
|
||||
>;
|
||||
};
|
||||
|
||||
export default function ContextMenu() {
|
||||
const { contextMenuRef, tableState }: any = useProjectContext();
|
||||
const [anchorEl, setAnchorEl] = React.useState<any | null>(null);
|
||||
const [selectedCell, setSelectedCell] = React.useState<any | null>();
|
||||
const open = Boolean(anchorEl);
|
||||
const handleClose = () => setAnchorEl(null);
|
||||
|
||||
if (contextMenuRef)
|
||||
contextMenuRef.current = {
|
||||
anchorEl,
|
||||
setAnchorEl,
|
||||
selectedCell,
|
||||
setSelectedCell,
|
||||
} as {};
|
||||
|
||||
const selectedColIndex = selectedCell?.colIndex;
|
||||
const selectedCol = _find(tableState?.columns, { index: selectedColIndex });
|
||||
const getActions =
|
||||
getFieldProp("contextMenuActions", selectedCol?.type) ||
|
||||
function empty() {};
|
||||
const actions = getActions() || [];
|
||||
const hasNoActions = Boolean(actions.length === 0);
|
||||
|
||||
if (!contextMenuRef.current || !open || hasNoActions) return <></>;
|
||||
return (
|
||||
<MenuContents
|
||||
anchorEl={anchorEl}
|
||||
open={open}
|
||||
handleClose={handleClose}
|
||||
items={actions}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
import { useProjectContext } from "@src/contexts/ProjectContext";
|
||||
import { Fragment } from "react";
|
||||
import { Row, RowRendererProps } from "react-data-grid";
|
||||
|
||||
@@ -8,9 +9,27 @@ export default function TableRow(props: RowRendererProps<any>) {
|
||||
return (
|
||||
<Fragment key={props.row.id}>
|
||||
<OutOfOrderIndicator top={props.top} height={props.height} />
|
||||
<Row {...props} />
|
||||
<ContextMenu>
|
||||
<Row {...props} />
|
||||
</ContextMenu>
|
||||
</Fragment>
|
||||
);
|
||||
|
||||
return <Row {...props} />;
|
||||
return (
|
||||
<ContextMenu>
|
||||
<Row {...props} />
|
||||
</ContextMenu>
|
||||
);
|
||||
}
|
||||
|
||||
const ContextMenu = (props: any) => {
|
||||
const { contextMenuRef }: any = useProjectContext();
|
||||
function handleClick(e: any) {
|
||||
e.preventDefault();
|
||||
const input = e?.target as HTMLElement;
|
||||
if (contextMenuRef?.current) {
|
||||
contextMenuRef?.current?.setAnchorEl(input);
|
||||
}
|
||||
}
|
||||
return <span onContextMenu={(e) => handleClick(e)}>{props.children}</span>;
|
||||
};
|
||||
|
||||
@@ -19,6 +19,7 @@ import TableContainer, { OUT_OF_ORDER_MARGIN } from "./TableContainer";
|
||||
import TableHeader from "../TableHeader";
|
||||
import ColumnHeader from "./ColumnHeader";
|
||||
import ColumnMenu from "./ColumnMenu";
|
||||
import ContextMenu from "./ContextMenu";
|
||||
import FinalColumnHeader from "./FinalColumnHeader";
|
||||
import FinalColumn from "./formatters/FinalColumn";
|
||||
import TableRow from "./TableRow";
|
||||
@@ -48,6 +49,7 @@ export default function Table() {
|
||||
tableState,
|
||||
tableActions,
|
||||
dataGridRef,
|
||||
contextMenuRef,
|
||||
sideDrawerRef,
|
||||
updateCell,
|
||||
} = useProjectContext();
|
||||
@@ -262,6 +264,13 @@ export default function Table() {
|
||||
});
|
||||
}
|
||||
}}
|
||||
onSelectedCellChange={({ rowIdx, idx }) => {
|
||||
console.log("firing");
|
||||
contextMenuRef?.current?.setSelectedCell({
|
||||
rowIndex: rowIdx,
|
||||
colIndex: idx,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</DndProvider>
|
||||
) : (
|
||||
@@ -270,6 +279,7 @@ export default function Table() {
|
||||
</TableContainer>
|
||||
|
||||
<ColumnMenu />
|
||||
<ContextMenu />
|
||||
<BulkActions
|
||||
selectedRows={selectedRows}
|
||||
columns={columns}
|
||||
|
||||
@@ -7,6 +7,7 @@ import BasicCell from "../_BasicCell/BasicCellValue";
|
||||
import TextEditor from "@src/components/Table/editors/TextEditor";
|
||||
|
||||
import { filterOperators } from "./Filter";
|
||||
import BasicContextMenuActions from "../_BasicCell/BasicCellContextMenuActions";
|
||||
const SideDrawerField = lazy(
|
||||
() =>
|
||||
import(
|
||||
@@ -26,6 +27,7 @@ export const config: IFieldConfig = {
|
||||
initializable: true,
|
||||
icon: <ShortTextIcon />,
|
||||
description: "Text displayed on a single line.",
|
||||
contextMenuActions: BasicContextMenuActions,
|
||||
TableCell: withBasicCell(BasicCell),
|
||||
TableEditor: TextEditor,
|
||||
SideDrawerField,
|
||||
|
||||
@@ -0,0 +1,70 @@
|
||||
import _find from "lodash/find";
|
||||
import CopyCells from "@src/assets/icons/CopyCells";
|
||||
import Cut from "@src/assets/icons/Cut";
|
||||
import Paste from "@src/assets/icons/Paste";
|
||||
import { useProjectContext } from "@src/contexts/ProjectContext";
|
||||
|
||||
export default function BasicContextMenuActions() {
|
||||
const { contextMenuRef, tableState, deleteCell, updateCell } =
|
||||
useProjectContext();
|
||||
const columns = tableState?.columns;
|
||||
const rows = tableState?.rows;
|
||||
const selectedRowIndex = contextMenuRef?.current?.selectedCell
|
||||
.rowIndex as number;
|
||||
const selectedColIndex = contextMenuRef?.current?.selectedCell?.colIndex;
|
||||
const selectedCol = _find(columns, { index: selectedColIndex });
|
||||
const selectedRow = rows?.[selectedRowIndex];
|
||||
|
||||
const handleClose = () => {
|
||||
contextMenuRef?.current?.setSelectedCell(null);
|
||||
contextMenuRef?.current?.setAnchorEl(null);
|
||||
};
|
||||
|
||||
const handleCopy = () => {
|
||||
const cell = selectedRow?.[selectedCol.key];
|
||||
const onFail = () => console.log("Fail to copy");
|
||||
const onSuccess = () => console.log("Save to clipboard successful");
|
||||
const copy = navigator.clipboard.writeText(JSON.stringify(cell));
|
||||
copy.then(onSuccess, onFail);
|
||||
|
||||
handleClose();
|
||||
};
|
||||
|
||||
const handleCut = () => {
|
||||
const cell = selectedRow?.[selectedCol.key];
|
||||
const notUndefined = Boolean(typeof cell !== "undefined");
|
||||
if (deleteCell && notUndefined)
|
||||
deleteCell(selectedRow?.ref, selectedCol?.key);
|
||||
|
||||
handleClose();
|
||||
};
|
||||
|
||||
const handlePaste = () => {
|
||||
console.log("home", rows);
|
||||
const paste = navigator.clipboard.readText();
|
||||
paste.then(async (clipText) => {
|
||||
try {
|
||||
const paste = await JSON.parse(clipText);
|
||||
updateCell?.(selectedRow?.ref, selectedCol.key, paste);
|
||||
} catch (error) {
|
||||
//TODO check the coding style guide about error message
|
||||
//Add breadcrumb handler her
|
||||
console.log(error);
|
||||
}
|
||||
});
|
||||
|
||||
handleClose();
|
||||
};
|
||||
|
||||
// const handleDisable = () => {
|
||||
// const cell = selectedRow?.[selectedCol.key];
|
||||
// return typeof cell === "undefined" ? true : false;
|
||||
// };
|
||||
|
||||
const cellMenuAction = [
|
||||
{ label: "Cut", icon: <Cut />, onClick: handleCut },
|
||||
{ label: "Copy", icon: <CopyCells />, onClick: handleCopy },
|
||||
{ label: "Paste", icon: <Paste />, onClick: handlePaste },
|
||||
];
|
||||
return cellMenuAction;
|
||||
}
|
||||
@@ -17,6 +17,7 @@ export interface IFieldConfig {
|
||||
icon?: React.ReactNode;
|
||||
description?: string;
|
||||
setupGuideLink?: string;
|
||||
contextMenuActions?: () => void;
|
||||
TableCell: React.ComponentType<FormatterProps<any>>;
|
||||
TableEditor: React.ComponentType<EditorProps<any, any>>;
|
||||
SideDrawerField: React.ComponentType<ISideDrawerFieldProps>;
|
||||
|
||||
@@ -14,6 +14,7 @@ import useSettings from "@src/hooks/useSettings";
|
||||
import { useAppContext } from "./AppContext";
|
||||
import { SideDrawerRef } from "@src/components/SideDrawer";
|
||||
import { ColumnMenuRef } from "@src/components/Table/ColumnMenu";
|
||||
import { ContextMenuRef } from "@src/components/Table/ContextMenu";
|
||||
import { ImportWizardRef } from "@src/components/Wizards/ImportWizard";
|
||||
|
||||
import { rowyRun, IRowyRunRequestProps } from "@src/utils/rowyRun";
|
||||
@@ -104,6 +105,8 @@ export interface IProjectContext {
|
||||
dataGridRef: React.RefObject<DataGridHandle>;
|
||||
// A ref to the side drawer state. Prevents unnecessary re-renders
|
||||
sideDrawerRef: React.MutableRefObject<SideDrawerRef | undefined>;
|
||||
//A ref to the cell menu. Prevents unnecessary re-render
|
||||
contextMenuRef: React.MutableRefObject<ContextMenuRef | undefined>;
|
||||
// A ref to the column menu. Prevents unnecessary re-renders
|
||||
columnMenuRef: React.MutableRefObject<ColumnMenuRef | undefined>;
|
||||
// A ref ot the import wizard. Prevents unnecessary re-renders
|
||||
@@ -398,6 +401,7 @@ export const ProjectContextProvider: React.FC = ({ children }) => {
|
||||
// A ref to the data grid. Contains data grid functions
|
||||
const dataGridRef = useRef<DataGridHandle>(null);
|
||||
const sideDrawerRef = useRef<SideDrawerRef>();
|
||||
const contextMenuRef = useRef<ContextMenuRef>();
|
||||
const columnMenuRef = useRef<ColumnMenuRef>();
|
||||
const importWizardRef = useRef<ImportWizardRef>();
|
||||
|
||||
@@ -418,6 +422,7 @@ export const ProjectContextProvider: React.FC = ({ children }) => {
|
||||
table,
|
||||
dataGridRef,
|
||||
sideDrawerRef,
|
||||
contextMenuRef,
|
||||
columnMenuRef,
|
||||
importWizardRef,
|
||||
rowyRun: _rowyRun,
|
||||
|
||||
Reference in New Issue
Block a user