diff --git a/package.json b/package.json index 71a462cb..e0974557 100644 --- a/package.json +++ b/package.json @@ -39,6 +39,7 @@ "jszip": "^3.6.0", "jwt-decode": "^3.1.2", "lodash": "^4.17.21", + "match-sorter": "^6.3.1", "notistack": "^2.0.2", "pb-util": "^1.0.1", "query-string": "^6.8.3", @@ -56,7 +57,6 @@ "react-helmet": "^6.1.0", "react-hook-form": "^7.21.2", "react-image": "^4.0.3", - "react-joyride": "^2.3.0", "react-json-view": "^1.19.1", "react-markdown": "^8.0.0", "react-router-dom": "^5.0.1", diff --git a/src/atoms/ContextMenu.ts b/src/atoms/ContextMenu.ts new file mode 100644 index 00000000..77f88a92 --- /dev/null +++ b/src/atoms/ContextMenu.ts @@ -0,0 +1,40 @@ +import { useAtom } from "jotai"; +import { atomWithReset, useResetAtom, useUpdateAtom } from "jotai/utils"; + +export type SelectedCell = { + rowIndex: number; + colIndex: number; +}; + +export type anchorEl = HTMLElement; + +const selectedCellAtom = atomWithReset(null); +const anchorEleAtom = atomWithReset(null); + +export function useSetAnchorEle() { + const setAnchorEle = useUpdateAtom(anchorEleAtom); + return { setAnchorEle }; +} + +export function useSetSelectedCell() { + const setSelectedCell = useUpdateAtom(selectedCellAtom); + return { setSelectedCell }; +} + +export function useContextMenuAtom() { + const [anchorEle] = useAtom(anchorEleAtom); + const [selectedCell] = useAtom(selectedCellAtom); + const resetAnchorEle = useResetAtom(anchorEleAtom); + const resetSelectedCell = useResetAtom(selectedCellAtom); + + const resetContextMenu = async () => { + await resetAnchorEle(); + await resetSelectedCell(); + }; + + return { + anchorEle, + selectedCell, + resetContextMenu, + }; +} diff --git a/src/components/Table/ColumnMenu/NewColumn.tsx b/src/components/Table/ColumnMenu/NewColumn.tsx index b59c0cc8..e23ec00e 100644 --- a/src/components/Table/ColumnMenu/NewColumn.tsx +++ b/src/components/Table/ColumnMenu/NewColumn.tsx @@ -27,10 +27,8 @@ export default function NewColumn({ data, openSettings, handleClose, - handleSave, }: INewColumnProps) { - const { table, settingsActions } = useProjectContext(); - + const { settingsActions, table, tableActions } = useProjectContext(); const [columnLabel, setColumnLabel] = useState(""); const [fieldKey, setFieldKey] = useState(""); const [type, setType] = useState(FieldType.shortText); @@ -139,14 +137,19 @@ export default function NewColumn({ actions={{ primary: { onClick: () => { - handleSave(fieldKey, { - type, - name: columnLabel, - fieldName: fieldKey, - key: fieldKey, - config: {}, - ...data.initializeColumn, - }); + tableActions?.column.insert( + { + type, + name: columnLabel, + fieldName: fieldKey, + key: fieldKey, + config: {}, + }, + { + insert: data.insert, + index: data.sourceIndex, + } + ); if (requireConfiguration) { openSettings({ type, @@ -154,7 +157,6 @@ export default function NewColumn({ fieldName: fieldKey, key: fieldKey, config: {}, - ...data.initializeColumn, }); } else handleClose(); analytics.logEvent("create_column", { diff --git a/src/components/Table/ColumnMenu/index.tsx b/src/components/Table/ColumnMenu/index.tsx index dd620e32..a5dcaa19 100644 --- a/src/components/Table/ColumnMenu/index.tsx +++ b/src/components/Table/ColumnMenu/index.tsx @@ -51,6 +51,7 @@ type SelectedColumnHeader = { column: Column & { [key: string]: any }; anchorEl: PopoverProps["anchorEl"]; }; + export type ColumnMenuRef = { selectedColumnHeader: SelectedColumnHeader | null; setSelectedColumnHeader: React.Dispatch< @@ -92,9 +93,7 @@ export default function ColumnMenu() { if (column && column.type === FieldType.last) { setModal({ type: ModalStates.new, - data: { - initializeColumn: { index: column.index ? column.index + 1 : 0 }, - }, + data: {}, }); } }, [column]); @@ -209,7 +208,8 @@ export default function ColumnMenu() { setModal({ type: ModalStates.new, data: { - initializeColumn: { index: column.index ? column.index - 1 : 0 }, + insert: "left", + sourceIndex: column.index, }, }), }, @@ -220,7 +220,8 @@ export default function ColumnMenu() { setModal({ type: ModalStates.new, data: { - initializeColumn: { index: column.index ? column.index + 1 : 0 }, + insert: "right", + sourceIndex: column.index, }, }), }, @@ -351,6 +352,7 @@ export default function ColumnMenu() { open={modal.type === ModalStates.typeChange} /> diff --git a/src/components/Table/ContextMenu/MenuContent.tsx b/src/components/Table/ContextMenu/MenuContent.tsx new file mode 100644 index 00000000..f092ef2d --- /dev/null +++ b/src/components/Table/ContextMenu/MenuContent.tsx @@ -0,0 +1,49 @@ +import { Menu } from "@mui/material"; +import { default as MenuItem } from "./MenuItem"; +import { IContextMenuItem } from "./MenuItem"; + +interface IMenuContents { + anchorEl: HTMLElement; + open: boolean; + handleClose: () => void; + items: IContextMenuItem[]; +} + +export function MenuContents({ + anchorEl, + open, + handleClose, + items, +}: IMenuContents) { + const handleContext = (e: React.MouseEvent) => e.preventDefault(); + + return ( + + {items.map((item, indx: number) => ( + + ))} + + ); +} diff --git a/src/components/Table/ContextMenu/MenuItem.tsx b/src/components/Table/ContextMenu/MenuItem.tsx new file mode 100644 index 00000000..cabf5e18 --- /dev/null +++ b/src/components/Table/ContextMenu/MenuItem.tsx @@ -0,0 +1,34 @@ +import { + ListItemIcon, + ListItemText, + MenuItem, + Typography, +} from "@mui/material"; + +export interface IContextMenuItem { + onClick: () => void; + icon: JSX.Element; + label: string; + disabled?: boolean; + hotkeyLabel?: string; +} + +export default function ContextMenuItem({ + onClick, + icon, + label, + disabled, + hotkeyLabel, +}: IContextMenuItem) { + return ( + + {icon} + {label} + {hotkeyLabel && ( + + {hotkeyLabel} + + )} + + ); +} diff --git a/src/components/Table/ContextMenu/index.tsx b/src/components/Table/ContextMenu/index.tsx new file mode 100644 index 00000000..bce4b02c --- /dev/null +++ b/src/components/Table/ContextMenu/index.tsx @@ -0,0 +1,27 @@ +import _find from "lodash/find"; +import { getFieldProp } from "@src/components/fields"; +import { useProjectContext } from "@src/contexts/ProjectContext"; +import { MenuContents } from "./MenuContent"; +import { useContextMenuAtom, useSetSelectedCell } from "@src/atoms/ContextMenu"; + +export default function ContextMenu() { + const { tableState } = useProjectContext(); + const { anchorEle, selectedCell, resetContextMenu } = useContextMenuAtom(); + const columns = tableState?.columns; + const selectedColIndex = selectedCell?.colIndex; + const selectedCol = _find(columns, { index: selectedColIndex }); + const configActions = + getFieldProp("contextMenuActions", selectedCol?.type) || + function empty() {}; + const actions = configActions(selectedCell, resetContextMenu) || []; + + if (!anchorEle || actions.length === 0) return <>; + return ( + + ); +} diff --git a/src/components/Table/TableRow.tsx b/src/components/Table/TableRow.tsx index 3d5ad0a1..29d6b15e 100644 --- a/src/components/Table/TableRow.tsx +++ b/src/components/Table/TableRow.tsx @@ -1,16 +1,24 @@ +import { useSetAnchorEle } from "@src/atoms/ContextMenu"; import { Fragment } from "react"; import { Row, RowRendererProps } from "react-data-grid"; import OutOfOrderIndicator from "./OutOfOrderIndicator"; export default function TableRow(props: RowRendererProps) { + const { setAnchorEle } = useSetAnchorEle(); + const handleContextMenu = ( + e: React.MouseEvent + ) => { + e.preventDefault(); + setAnchorEle?.(e?.target as HTMLElement); + }; if (props.row._rowy_outOfOrder) return ( - + ); - return ; + return ; } diff --git a/src/components/Table/index.tsx b/src/components/Table/index.tsx index 737282b4..3976a8ff 100644 --- a/src/components/Table/index.tsx +++ b/src/components/Table/index.tsx @@ -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"; @@ -31,6 +32,7 @@ import { formatSubTableName } from "@src/utils/fns"; import { useAppContext } from "@src/contexts/AppContext"; import { useProjectContext } from "@src/contexts/ProjectContext"; import useWindowSize from "@src/hooks/useWindowSize"; +import { useSetSelectedCell } from "@src/atoms/ContextMenu"; export type TableColumn = Column & { isNew?: boolean; @@ -52,6 +54,7 @@ export default function Table() { updateCell, } = useProjectContext(); const { userDoc, userClaims } = useAppContext(); + const { setSelectedCell } = useSetSelectedCell(); const userDocHiddenFields = userDoc.state.doc?.tables?.[formatSubTableName(tableState?.config.id)] @@ -262,6 +265,12 @@ export default function Table() { }); } }} + onSelectedCellChange={({ rowIdx, idx }) => + setSelectedCell({ + rowIndex: rowIdx, + colIndex: idx, + }) + } /> ) : ( @@ -270,6 +279,7 @@ export default function Table() { + Clear - {overrideTableFilters - ? " (ignore table filter)" - : " (use table filter)"} + {hasTableFilters && + (overrideTableFilters + ? " (ignore table filter)" + : " (use table filter)")}