From e59a10c944262e5b6df9b992b855252195c8872e Mon Sep 17 00:00:00 2001 From: Anish Roy <62830866+iamanishroy@users.noreply.github.com> Date: Wed, 4 Jan 2023 09:35:40 +0000 Subject: [PATCH] worked on copy/paste feature --- package.json | 1 + .../BasicCellContextMenuActions.tsx | 87 +-------- src/components/Table/Table.tsx | 21 +- src/contexts/TableKbShortcutContext.tsx | 179 ++++++++++++++++++ yarn.lock | 5 + 5 files changed, 203 insertions(+), 90 deletions(-) create mode 100644 src/contexts/TableKbShortcutContext.tsx diff --git a/package.json b/package.json index 4fac6672..53fb464e 100644 --- a/package.json +++ b/package.json @@ -10,6 +10,7 @@ "dependencies": { "@emotion/react": "^11.10.5", "@emotion/styled": "^11.10.5", + "@mantine/hooks": "^5.10.0", "@mdi/js": "^6.6.96", "@monaco-editor/react": "^4.4.4", "@mui/icons-material": "^5.10.16", diff --git a/src/components/Table/ContextMenu/BasicCellContextMenuActions.tsx b/src/components/Table/ContextMenu/BasicCellContextMenuActions.tsx index fdaa1557..a82c6e78 100644 --- a/src/components/Table/ContextMenu/BasicCellContextMenuActions.tsx +++ b/src/components/Table/ContextMenu/BasicCellContextMenuActions.tsx @@ -1,94 +1,19 @@ -import { useAtom, useSetAtom } from "jotai"; -import { useSnackbar } from "notistack"; -import { get, find } from "lodash-es"; - -// import Cut from "@mui/icons-material/ContentCut"; import { Copy as CopyCells } from "@src/assets/icons"; +// import Cut from "@mui/icons-material/ContentCut"; import Paste from "@mui/icons-material/ContentPaste"; - -import { - tableScope, - tableSchemaAtom, - tableRowsAtom, - updateFieldAtom, -} from "@src/atoms/tableScope"; -import { getFieldProp, getFieldType } from "@src/components/fields"; import { IFieldConfig } from "@src/components/fields/types"; +import { useMenuAction } from "@src/contexts/TableKbShortcutContext"; // TODO: Remove this and add `handlePaste` function to column config export const BasicContextMenuActions: IFieldConfig["contextMenuActions"] = ( selectedCell, reset ) => { - const { enqueueSnackbar } = useSnackbar(); - - const [tableSchema] = useAtom(tableSchemaAtom, tableScope); - const [tableRows] = useAtom(tableRowsAtom, tableScope); - const updateField = useSetAtom(updateFieldAtom, tableScope); - - const selectedCol = tableSchema.columns?.[selectedCell.columnKey]; - if (!selectedCol) return []; - - const selectedRow = find(tableRows, ["_rowy_ref.path", selectedCell.path]); - const cellValue = get(selectedRow, selectedCol.fieldName); - const handleClose = async () => await reset?.(); - - const handleCopy = async () => { - try { - await navigator.clipboard.writeText(cellValue); - enqueueSnackbar("Copied"); - } catch (error) { - enqueueSnackbar(`Failed to copy:${error}`, { variant: "error" }); - } - handleClose(); - }; - - // const handleCut = async () => { - // try { - // await navigator.clipboard.writeText(cellValue); - // if (typeof cellValue !== "undefined") - // updateField({ - // path: selectedCell.path, - // fieldName: selectedCol.fieldName, - // value: undefined, - // deleteField: true, - // }); - // } catch (error) { - // enqueueSnackbar(`Failed to cut: ${error}`, { variant: "error" }); - // } - // handleClose(); - // }; - - const handlePaste = async () => { - try { - if (!selectedCol) return; - const text = await navigator.clipboard.readText(); - const cellDataType = getFieldProp("dataType", getFieldType(selectedCol)); - let parsed; - switch (cellDataType) { - case "number": - parsed = Number(text); - if (isNaN(parsed)) throw new Error(`${text} is not a number`); - break; - case "string": - parsed = text; - break; - default: - parsed = JSON.parse(text); - break; - } - updateField({ - path: selectedCell.path, - fieldName: selectedCol.fieldName, - value: parsed, - }); - } catch (error) { - enqueueSnackbar(`Failed to paste: ${error}`, { variant: "error" }); - } - - handleClose(); - }; + const { handleCopy, handlePaste, cellValue } = useMenuAction( + selectedCell, + handleClose + ); const contextMenuActions = [ // { label: "Cut", icon: , onClick: handleCut }, diff --git a/src/components/Table/Table.tsx b/src/components/Table/Table.tsx index 092ff0ab..0b772a2b 100644 --- a/src/components/Table/Table.tsx +++ b/src/components/Table/Table.tsx @@ -35,6 +35,7 @@ import { getFieldType, getFieldProp } from "@src/components/fields"; import { useKeyboardNavigation } from "./useKeyboardNavigation"; import { useSaveColumnSizing } from "./useSaveColumnSizing"; import type { TableRow, ColumnConfig } from "@src/types/table"; +import { TableKbShortcutProvider } from "@src/contexts/TableKbShortcutContext"; export const DEFAULT_ROW_HEIGHT = 41; export const DEFAULT_COL_WIDTH = 150; @@ -267,15 +268,17 @@ export default function Table({ {tableRows.length === 0 ? ( emptyState ?? ) : ( - + + + )} diff --git a/src/contexts/TableKbShortcutContext.tsx b/src/contexts/TableKbShortcutContext.tsx new file mode 100644 index 00000000..507ed5eb --- /dev/null +++ b/src/contexts/TableKbShortcutContext.tsx @@ -0,0 +1,179 @@ +import { + createContext, + useContext, + useCallback, + useState, + useEffect, +} from "react"; +import { useAtom, useSetAtom } from "jotai"; +import { useSnackbar } from "notistack"; +import { get, find } from "lodash-es"; +import { useHotkeys } from "@mantine/hooks"; + +import { + tableScope, + tableSchemaAtom, + tableRowsAtom, + updateFieldAtom, + selectedCellAtom, + SelectedCell, +} from "@src/atoms/tableScope"; +import { getFieldProp, getFieldType } from "@src/components/fields"; +import { ColumnConfig } from "@src/types/table"; + +import { FieldType } from "@src/constants/fields"; + +const SUPPORTED_TYPES = new Set([ + FieldType.shortText, + FieldType.longText, + FieldType.number, + FieldType.email, + FieldType.percentage, + FieldType.phone, + FieldType.richText, + FieldType.url, +]); + +export function useMenuAction( + selectedCell: SelectedCell | null, + handleClose?: Function +) { + const { enqueueSnackbar } = useSnackbar(); + const [tableSchema] = useAtom(tableSchemaAtom, tableScope); + const [tableRows] = useAtom(tableRowsAtom, tableScope); + const updateField = useSetAtom(updateFieldAtom, tableScope); + const [cellValue, setCellValue] = useState(); + const [selectedCol, setSelectedCol] = useState(); + const [enableAction, setEnableAction] = useState(false); + + const handleCopy = useCallback(async () => { + try { + if (cellValue !== undefined && cellValue !== null && cellValue !== "") { + await navigator.clipboard.writeText( + typeof cellValue === "object" ? JSON.stringify(cellValue) : cellValue + ); + enqueueSnackbar("Copied"); + } else { + await navigator.clipboard.writeText(""); + } + } catch (error) { + enqueueSnackbar(`Failed to copy:${error}`, { variant: "error" }); + } + if (handleClose) handleClose(); + }, [cellValue, enqueueSnackbar, handleClose]); + + const handleCut = useCallback(async () => { + try { + if (!selectedCell || !selectedCol || !cellValue) return; + if (cellValue !== undefined && cellValue !== null && cellValue !== "") { + await navigator.clipboard.writeText( + typeof cellValue === "object" ? JSON.stringify(cellValue) : cellValue + ); + enqueueSnackbar("Copied"); + } else { + await navigator.clipboard.writeText(""); + } + if (cellValue !== undefined) + updateField({ + path: selectedCell.path, + fieldName: selectedCol.fieldName, + value: undefined, + deleteField: true, + }); + } catch (error) { + enqueueSnackbar(`Failed to cut: ${error}`, { variant: "error" }); + } + if (handleClose) handleClose(); + }, [ + cellValue, + selectedCell, + selectedCol, + updateField, + enqueueSnackbar, + handleClose, + ]); + + const handlePaste = useCallback(async () => { + try { + if (!selectedCell || !selectedCol) return; + const text = await navigator.clipboard.readText(); + const cellDataType = getFieldProp("dataType", getFieldType(selectedCol)); + let parsed; + switch (cellDataType) { + case "number": + parsed = Number(text); + if (isNaN(parsed)) throw new Error(`${text} is not a number`); + break; + case "string": + parsed = text; + break; + default: + parsed = JSON.parse(text); + break; + } + updateField({ + path: selectedCell.path, + fieldName: selectedCol.fieldName, + value: parsed, + }); + } catch (error) { + enqueueSnackbar(`Failed to paste: ${error}`, { variant: "error" }); + } + if (handleClose) handleClose(); + }, [selectedCell, selectedCol, updateField, enqueueSnackbar, handleClose]); + + useEffect(() => { + setEnableAction(SUPPORTED_TYPES.has(selectedCol?.type)); + }, [selectedCol]); + + useEffect(() => { + if (!selectedCell) return setCellValue(""); + const selectedCol = tableSchema.columns?.[selectedCell.columnKey]; + if (!selectedCol) return setCellValue(""); + setSelectedCol(selectedCol); + const selectedRow = find(tableRows, ["_rowy_ref.path", selectedCell.path]); + setCellValue(get(selectedRow, selectedCol.fieldName)); + }, [selectedCell, tableSchema, tableRows]); + + const checkEnabled = (func: Function) => { + return function () { + if (enableAction) { + return func(); + } else { + enqueueSnackbar(`Simple copy not supported with this type.`, { + variant: "info", + }); + } + }; + }; + + return { + handleCopy: checkEnabled(handleCopy), + handleCut: checkEnabled(handleCut), + handlePaste: handlePaste, + cellValue, + }; +} + +const TableKbShortcutContext = createContext(null); + +export function useTableKbShortcut() { + return useContext(TableKbShortcutContext); +} + +export function TableKbShortcutProvider(props: { children: any }) { + const [selectedCell] = useAtom(selectedCellAtom, tableScope); + const { handleCopy, handlePaste, handleCut } = useMenuAction(selectedCell); + + useHotkeys([ + ["mod+C", handleCopy], + ["mod+X", handleCut], + ["mod+V", handlePaste], + ]); + + return ( + + {props.children} + + ); +} diff --git a/yarn.lock b/yarn.lock index 003c37e7..61ea6da5 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2217,6 +2217,11 @@ resolved "https://registry.yarnpkg.com/@leichtgewicht/ip-codec/-/ip-codec-2.0.4.tgz#b2ac626d6cb9c8718ab459166d4bb405b8ffa78b" integrity sha512-Hcv+nVC0kZnQ3tD9GVu5xSMR4VVYOteQIr/hwFPVEvPdlXqgGEuRjiheChHgdM+JyqdgNcmzZOX/tnl0JOiI7A== +"@mantine/hooks@^5.10.0": + version "5.10.0" + resolved "https://registry.yarnpkg.com/@mantine/hooks/-/hooks-5.10.0.tgz#e7886025a11dfa25f99c8c7fb7186d6a065c9d5c" + integrity sha512-dAefxpvqjFtXNeKse+awkIa4U1XGnMMOqWg1+07Y2Ino2G6EiT8AEnYqQyTXgcPoNaWwG9533Q/DDadmyweqaQ== + "@mark.probst/unicode-properties@~1.1.0": version "1.1.0" resolved "https://registry.yarnpkg.com/@mark.probst/unicode-properties/-/unicode-properties-1.1.0.tgz#5caafeab4737df93163d6d288007df33f9939b80"