mirror of
https://github.com/rowyio/rowy.git
synced 2025-12-28 16:06:41 +01:00
worked on copy/paste feature
This commit is contained in:
@@ -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",
|
||||
|
||||
@@ -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: <Cut />, onClick: handleCut },
|
||||
|
||||
@@ -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 ?? <EmptyState sx={{ py: 8 }} />
|
||||
) : (
|
||||
<TableBody
|
||||
containerEl={containerEl}
|
||||
containerRef={containerRef}
|
||||
leafColumns={leafColumns}
|
||||
rows={rows}
|
||||
canEditCells={canEditCells}
|
||||
lastFrozen={lastFrozen}
|
||||
columnSizing={columnSizing}
|
||||
/>
|
||||
<TableKbShortcutProvider>
|
||||
<TableBody
|
||||
containerEl={containerEl}
|
||||
containerRef={containerRef}
|
||||
leafColumns={leafColumns}
|
||||
rows={rows}
|
||||
canEditCells={canEditCells}
|
||||
lastFrozen={lastFrozen}
|
||||
columnSizing={columnSizing}
|
||||
/>
|
||||
</TableKbShortcutProvider>
|
||||
)}
|
||||
</StyledTable>
|
||||
|
||||
|
||||
179
src/contexts/TableKbShortcutContext.tsx
Normal file
179
src/contexts/TableKbShortcutContext.tsx
Normal file
@@ -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<string | undefined>();
|
||||
const [selectedCol, setSelectedCol] = useState<ColumnConfig>();
|
||||
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>(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 (
|
||||
<TableKbShortcutContext.Provider value={null}>
|
||||
{props.children}
|
||||
</TableKbShortcutContext.Provider>
|
||||
);
|
||||
}
|
||||
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user