mirror of
https://github.com/rowyio/rowy.git
synced 2025-12-28 16:06:41 +01:00
Fix: copy/paste is picking up all fields in a subtable as subtable type
This commit is contained in:
@@ -10,7 +10,6 @@
|
||||
"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",
|
||||
|
||||
@@ -30,12 +30,14 @@ import {
|
||||
tableNextPageAtom,
|
||||
tablePageAtom,
|
||||
updateColumnAtom,
|
||||
selectedCellAtom,
|
||||
} from "@src/atoms/tableScope";
|
||||
import { useMenuAction } from "@src/contexts/TableKbShortcutContext";
|
||||
import { getFieldType, getFieldProp } from "@src/components/fields";
|
||||
import { useKeyboardNavigation } from "./useKeyboardNavigation";
|
||||
import { useSaveColumnSizing } from "./useSaveColumnSizing";
|
||||
import useHotKeys from "./useHotKeys";
|
||||
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;
|
||||
@@ -182,6 +184,13 @@ export default function Table({
|
||||
tableRows,
|
||||
leafColumns,
|
||||
});
|
||||
const [selectedCell] = useAtom(selectedCellAtom, tableScope);
|
||||
const { handleCopy, handlePaste, handleCut } = useMenuAction(selectedCell);
|
||||
const { handler } = useHotKeys([
|
||||
["mod+C", handleCopy],
|
||||
["mod+X", handleCut],
|
||||
["mod+V", handlePaste],
|
||||
]);
|
||||
|
||||
// Handle prompt to save local column sizes if user `canEditColumns`
|
||||
useSaveColumnSizing(columnSizing, canEditColumns);
|
||||
@@ -243,7 +252,10 @@ export default function Table({
|
||||
"--row-height": `${tableSchema.rowHeight || DEFAULT_ROW_HEIGHT}px`,
|
||||
} as any
|
||||
}
|
||||
onKeyDown={handleKeyDown}
|
||||
onKeyDown={(e) => {
|
||||
handleKeyDown(e);
|
||||
handler(e);
|
||||
}}
|
||||
>
|
||||
<div
|
||||
className="thead"
|
||||
@@ -268,17 +280,15 @@ export default function Table({
|
||||
{tableRows.length === 0 ? (
|
||||
emptyState ?? <EmptyState sx={{ py: 8 }} />
|
||||
) : (
|
||||
<TableKbShortcutProvider>
|
||||
<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}
|
||||
/>
|
||||
)}
|
||||
</StyledTable>
|
||||
|
||||
|
||||
99
src/components/Table/useHotKeys.tsx
Normal file
99
src/components/Table/useHotKeys.tsx
Normal file
@@ -0,0 +1,99 @@
|
||||
import { useCallback } from "react";
|
||||
|
||||
type HotKeysAction = [
|
||||
string,
|
||||
(event: React.KeyboardEvent<HTMLElement> | KeyboardEvent) => void
|
||||
];
|
||||
|
||||
export default function useHotKeys(actions: HotKeysAction[]) {
|
||||
// master event handler
|
||||
const handler = useCallback(
|
||||
(event: React.KeyboardEvent<HTMLDivElement>) => {
|
||||
const event_ = "nativeEvent" in event ? event.nativeEvent : event;
|
||||
actions.forEach(([hotkey, handler_]) => {
|
||||
if (getHotkeyMatcher(hotkey)(event_)) {
|
||||
event.preventDefault();
|
||||
handler_(event_);
|
||||
}
|
||||
});
|
||||
},
|
||||
[actions]
|
||||
);
|
||||
|
||||
return { handler };
|
||||
}
|
||||
|
||||
type KeyboardModifiers = {
|
||||
alt: boolean;
|
||||
ctrl: boolean;
|
||||
meta: boolean;
|
||||
mod: boolean;
|
||||
shift: boolean;
|
||||
};
|
||||
|
||||
export type Hotkey = KeyboardModifiers & {
|
||||
key?: string;
|
||||
};
|
||||
function isExactHotkey(hotkey: Hotkey, event: KeyboardEvent): boolean {
|
||||
const { alt, ctrl, meta, mod, shift, key } = hotkey;
|
||||
const { altKey, ctrlKey, metaKey, shiftKey, key: pressedKey } = event;
|
||||
|
||||
if (alt !== altKey) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (mod) {
|
||||
if (!ctrlKey && !metaKey) {
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
if (ctrl !== ctrlKey) {
|
||||
return false;
|
||||
}
|
||||
if (meta !== metaKey) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
if (shift !== shiftKey) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (
|
||||
key &&
|
||||
(pressedKey.toLowerCase() === key.toLowerCase() ||
|
||||
event.code.replace("Key", "").toLowerCase() === key.toLowerCase())
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
type CheckHotkeyMatch = (event: KeyboardEvent) => boolean;
|
||||
export function getHotkeyMatcher(hotkey: string): CheckHotkeyMatch {
|
||||
return (event) => isExactHotkey(parseHotkey(hotkey), event);
|
||||
}
|
||||
|
||||
function parseHotkey(hotkey: string): Hotkey {
|
||||
const keys = hotkey
|
||||
.toLowerCase()
|
||||
.split("+")
|
||||
.map((part) => part.trim());
|
||||
|
||||
const modifiers: KeyboardModifiers = {
|
||||
alt: keys.includes("alt"),
|
||||
ctrl: keys.includes("ctrl"),
|
||||
meta: keys.includes("meta"),
|
||||
mod: keys.includes("mod"),
|
||||
shift: keys.includes("shift"),
|
||||
};
|
||||
|
||||
const reservedKeys = ["alt", "ctrl", "meta", "shift", "mod"];
|
||||
|
||||
const freeKey = keys.find((key) => !reservedKeys.includes(key));
|
||||
|
||||
return {
|
||||
...modifiers,
|
||||
key: freeKey,
|
||||
};
|
||||
}
|
||||
@@ -1,21 +1,13 @@
|
||||
import {
|
||||
createContext,
|
||||
useContext,
|
||||
useCallback,
|
||||
useState,
|
||||
useEffect,
|
||||
} from "react";
|
||||
import { 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";
|
||||
@@ -45,7 +37,6 @@ export function useMenuAction(
|
||||
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 {
|
||||
@@ -134,10 +125,6 @@ export function useMenuAction(
|
||||
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];
|
||||
@@ -147,20 +134,23 @@ export function useMenuAction(
|
||||
setCellValue(get(selectedRow, selectedCol.fieldName));
|
||||
}, [selectedCell, tableSchema, tableRows]);
|
||||
|
||||
const checkEnabled = (func: Function) => {
|
||||
return function () {
|
||||
if (enableAction) {
|
||||
return func();
|
||||
} else {
|
||||
enqueueSnackbar(
|
||||
`${selectedCol?.type} field cannot be copied using keyboard shortcut`,
|
||||
{
|
||||
variant: "info",
|
||||
}
|
||||
);
|
||||
}
|
||||
};
|
||||
};
|
||||
const checkEnabled = useCallback(
|
||||
(func: Function) => {
|
||||
return function () {
|
||||
if (SUPPORTED_TYPES.has(selectedCol?.type)) {
|
||||
return func();
|
||||
} else {
|
||||
enqueueSnackbar(
|
||||
`${selectedCol?.type} field cannot be copied using keyboard shortcut`,
|
||||
{
|
||||
variant: "info",
|
||||
}
|
||||
);
|
||||
}
|
||||
};
|
||||
},
|
||||
[selectedCol]
|
||||
);
|
||||
|
||||
return {
|
||||
handleCopy: checkEnabled(handleCopy),
|
||||
@@ -169,26 +159,3 @@ export function useMenuAction(
|
||||
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,11 +2217,6 @@
|
||||
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