From 557d13eacbadcf42a2ee5ba532d03bb96eb234d2 Mon Sep 17 00:00:00 2001 From: Gibson Han Date: Tue, 1 Feb 2022 17:09:37 +0700 Subject: [PATCH 1/4] add cut and paste icons --- src/assets/icons/Cut.tsx | 10 ++++++++++ src/assets/icons/Paste.tsx | 10 ++++++++++ src/components/fields/Status/ConditionModalContent.tsx | 1 - 3 files changed, 20 insertions(+), 1 deletion(-) create mode 100644 src/assets/icons/Cut.tsx create mode 100644 src/assets/icons/Paste.tsx diff --git a/src/assets/icons/Cut.tsx b/src/assets/icons/Cut.tsx new file mode 100644 index 000000000..6442045f3 --- /dev/null +++ b/src/assets/icons/Cut.tsx @@ -0,0 +1,10 @@ +import SvgIcon, { SvgIconProps } from "@mui/material/SvgIcon"; +import { mdiContentCut } from "@mdi/js"; + +export default function Cut(props: SvgIconProps) { + return ( + + + + ); +} diff --git a/src/assets/icons/Paste.tsx b/src/assets/icons/Paste.tsx new file mode 100644 index 000000000..d86b008ab --- /dev/null +++ b/src/assets/icons/Paste.tsx @@ -0,0 +1,10 @@ +import SvgIcon, { SvgIconProps } from "@mui/material/SvgIcon"; +import { mdiContentPaste } from "@mdi/js"; + +export default function Paste(props: SvgIconProps) { + return ( + + + + ); +} diff --git a/src/components/fields/Status/ConditionModalContent.tsx b/src/components/fields/Status/ConditionModalContent.tsx index 197cbbcbc..cec386208 100644 --- a/src/components/fields/Status/ConditionModalContent.tsx +++ b/src/components/fields/Status/ConditionModalContent.tsx @@ -62,7 +62,6 @@ export default function ConditionModalContent({ {type === "number" && (
- {console.log(operatorOptions)} handleUpdate("operator")(v)} From 20fb3e9ca0e35e7b84d1b81732eb9a5953b0e64b Mon Sep 17 00:00:00 2001 From: Gibson Han Date: Tue, 1 Feb 2022 17:41:24 +0700 Subject: [PATCH 2/4] add context menu feature with cut, copy, and paste --- src/components/Table/ColumnMenu/index.tsx | 1 + .../Table/ContextMenu/MenuContent.tsx | 42 +++++++++++ src/components/Table/ContextMenu/MenuRow.tsx | 17 +++++ src/components/Table/ContextMenu/index.tsx | 56 +++++++++++++++ src/components/Table/TableRow.tsx | 23 +++++- src/components/Table/index.tsx | 10 +++ src/components/fields/ShortText/index.tsx | 2 + .../BasicCellContextMenuActions.tsx | 70 +++++++++++++++++++ src/components/fields/types.ts | 1 + src/contexts/ProjectContext.tsx | 5 ++ 10 files changed, 225 insertions(+), 2 deletions(-) create mode 100644 src/components/Table/ContextMenu/MenuContent.tsx create mode 100644 src/components/Table/ContextMenu/MenuRow.tsx create mode 100644 src/components/Table/ContextMenu/index.tsx create mode 100644 src/components/fields/_BasicCell/BasicCellContextMenuActions.tsx diff --git a/src/components/Table/ColumnMenu/index.tsx b/src/components/Table/ColumnMenu/index.tsx index 42f6c3cb5..093b19aaa 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< diff --git a/src/components/Table/ContextMenu/MenuContent.tsx b/src/components/Table/ContextMenu/MenuContent.tsx new file mode 100644 index 000000000..c8858e1c4 --- /dev/null +++ b/src/components/Table/ContextMenu/MenuContent.tsx @@ -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 ( + + {items.map((item, indx: number) => ( + + ))} + + ); +} diff --git a/src/components/Table/ContextMenu/MenuRow.tsx b/src/components/Table/ContextMenu/MenuRow.tsx new file mode 100644 index 000000000..c6c6fcd63 --- /dev/null +++ b/src/components/Table/ContextMenu/MenuRow.tsx @@ -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 ( + + {icon} + {label} + + ); +} diff --git a/src/components/Table/ContextMenu/index.tsx b/src/components/Table/ContextMenu/index.tsx new file mode 100644 index 000000000..5829ccfd1 --- /dev/null +++ b/src/components/Table/ContextMenu/index.tsx @@ -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>; + anchorEl: HTMLElement | null; + setAnchorEl: React.Dispatch< + React.SetStateAction + >; +}; + +export default function ContextMenu() { + const { contextMenuRef, tableState }: any = useProjectContext(); + const [anchorEl, setAnchorEl] = React.useState(null); + const [selectedCell, setSelectedCell] = React.useState(); + 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 ( + + ); +} diff --git a/src/components/Table/TableRow.tsx b/src/components/Table/TableRow.tsx index 3d5ad0a15..631886b6e 100644 --- a/src/components/Table/TableRow.tsx +++ b/src/components/Table/TableRow.tsx @@ -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) { return ( - + + + ); - return ; + return ( + + + + ); } + +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 handleClick(e)}>{props.children}; +}; diff --git a/src/components/Table/index.tsx b/src/components/Table/index.tsx index 737282b45..68e32209c 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"; @@ -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, + }); + }} /> ) : ( @@ -270,6 +279,7 @@ export default function Table() { + import( @@ -26,6 +27,7 @@ export const config: IFieldConfig = { initializable: true, icon: , description: "Text displayed on a single line.", + contextMenuActions: BasicContextMenuActions, TableCell: withBasicCell(BasicCell), TableEditor: TextEditor, SideDrawerField, diff --git a/src/components/fields/_BasicCell/BasicCellContextMenuActions.tsx b/src/components/fields/_BasicCell/BasicCellContextMenuActions.tsx new file mode 100644 index 000000000..ea1d1cfbd --- /dev/null +++ b/src/components/fields/_BasicCell/BasicCellContextMenuActions.tsx @@ -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: , onClick: handleCut }, + { label: "Copy", icon: , onClick: handleCopy }, + { label: "Paste", icon: , onClick: handlePaste }, + ]; + return cellMenuAction; +} diff --git a/src/components/fields/types.ts b/src/components/fields/types.ts index 57ce086c3..5a3c84c20 100644 --- a/src/components/fields/types.ts +++ b/src/components/fields/types.ts @@ -17,6 +17,7 @@ export interface IFieldConfig { icon?: React.ReactNode; description?: string; setupGuideLink?: string; + contextMenuActions?: () => void; TableCell: React.ComponentType>; TableEditor: React.ComponentType>; SideDrawerField: React.ComponentType; diff --git a/src/contexts/ProjectContext.tsx b/src/contexts/ProjectContext.tsx index 8c6e9d660..08d8448bc 100644 --- a/src/contexts/ProjectContext.tsx +++ b/src/contexts/ProjectContext.tsx @@ -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; // A ref to the side drawer state. Prevents unnecessary re-renders sideDrawerRef: React.MutableRefObject; + //A ref to the cell menu. Prevents unnecessary re-render + contextMenuRef: React.MutableRefObject; // A ref to the column menu. Prevents unnecessary re-renders columnMenuRef: React.MutableRefObject; // 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(null); const sideDrawerRef = useRef(); + const contextMenuRef = useRef(); const columnMenuRef = useRef(); const importWizardRef = useRef(); @@ -418,6 +422,7 @@ export const ProjectContextProvider: React.FC = ({ children }) => { table, dataGridRef, sideDrawerRef, + contextMenuRef, columnMenuRef, importWizardRef, rowyRun: _rowyRun, From 4a40996a0248f359eb923804cd53926b151066c8 Mon Sep 17 00:00:00 2001 From: Gibson Han Date: Tue, 1 Feb 2022 17:43:56 +0700 Subject: [PATCH 3/4] fix remove bug --- src/hooks/useTable/useTableConfig.ts | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/src/hooks/useTable/useTableConfig.ts b/src/hooks/useTable/useTableConfig.ts index 509238275..d8e462449 100644 --- a/src/hooks/useTable/useTableConfig.ts +++ b/src/hooks/useTable/useTableConfig.ts @@ -92,7 +92,7 @@ const useTableConfig = (tableId?: string) => { ...columns, [targetColumn.key]: { ...targetColumn, width }, }; - + documentDispatch({ action: DocActions.update, data: { columns: updatedColumns }, @@ -157,8 +157,14 @@ const useTableConfig = (tableId?: string) => { */ const remove = (key: string) => { const { columns } = tableConfigState; - let updatedColumns = columns; - updatedColumns[key] = deleteField(); + + let updatedColumns: any = Object.values(columns) + .filter((c: any) => c.key !== key) + .sort((c: any) => c.index) + .reduce((acc: any, curr: any, index: any) => { + acc[curr.key] = { ...curr, index }; + return acc; + }, {}); documentDispatch({ action: DocActions.update, data: { columns: updatedColumns }, From 8c9db63c54fb07ffd8237aa2c79a3277e50d17ce Mon Sep 17 00:00:00 2001 From: Gibson Han Date: Tue, 1 Feb 2022 17:57:41 +0700 Subject: [PATCH 4/4] enable context menu on all string field and only number field --- src/components/fields/Email/index.tsx | 3 +++ src/components/fields/LongText/index.tsx | 2 ++ src/components/fields/Number/index.tsx | 2 ++ src/components/fields/Phone/index.tsx | 2 ++ src/components/fields/RichText/index.tsx | 2 ++ src/components/fields/Url/index.tsx | 2 ++ 6 files changed, 13 insertions(+) diff --git a/src/components/fields/Email/index.tsx b/src/components/fields/Email/index.tsx index def22039a..6555d5938 100644 --- a/src/components/fields/Email/index.tsx +++ b/src/components/fields/Email/index.tsx @@ -6,6 +6,8 @@ import EmailIcon from "@mui/icons-material/MailOutlined"; import BasicCell from "../_BasicCell/BasicCellValue"; import TextEditor from "@src/components/Table/editors/TextEditor"; import { filterOperators } from "../ShortText/Filter"; +import BasicContextMenuActions from "../_BasicCell/BasicCellContextMenuActions"; + const SideDrawerField = lazy( () => import("./SideDrawerField" /* webpackChunkName: "SideDrawerField-Email" */) @@ -20,6 +22,7 @@ export const config: IFieldConfig = { initializable: true, icon: , description: "Email address. Not validated.", + contextMenuActions: BasicContextMenuActions, TableCell: withBasicCell(BasicCell), TableEditor: TextEditor, SideDrawerField, diff --git a/src/components/fields/LongText/index.tsx b/src/components/fields/LongText/index.tsx index 646642186..25b89913d 100644 --- a/src/components/fields/LongText/index.tsx +++ b/src/components/fields/LongText/index.tsx @@ -6,6 +6,7 @@ import LongTextIcon from "@mui/icons-material/Notes"; import BasicCell from "./BasicCell"; import TextEditor from "@src/components/Table/editors/TextEditor"; import { filterOperators } from "../ShortText/Filter"; +import BasicContextMenuActions from "../_BasicCell/BasicCellContextMenuActions"; const SideDrawerField = lazy( () => @@ -23,6 +24,7 @@ export const config: IFieldConfig = { initializable: true, icon: , description: "Text displayed on multiple lines.", + contextMenuActions: BasicContextMenuActions, TableCell: withBasicCell(BasicCell), TableEditor: TextEditor, SideDrawerField, diff --git a/src/components/fields/Number/index.tsx b/src/components/fields/Number/index.tsx index da8f42906..ea30cc3dd 100644 --- a/src/components/fields/Number/index.tsx +++ b/src/components/fields/Number/index.tsx @@ -6,6 +6,7 @@ import NumberIcon from "@src/assets/icons/Number"; import BasicCell from "./BasicCell"; import TextEditor from "@src/components/Table/editors/TextEditor"; import { filterOperators } from "./Filter"; +import BasicContextMenuActions from "../_BasicCell/BasicCellContextMenuActions"; const SideDrawerField = lazy( () => import("./SideDrawerField" /* webpackChunkName: "SideDrawerField-Number" */) @@ -20,6 +21,7 @@ export const config: IFieldConfig = { initializable: true, icon: , description: "Numeric value.", + contextMenuActions: BasicContextMenuActions, TableCell: withBasicCell(BasicCell), TableEditor: TextEditor, SideDrawerField, diff --git a/src/components/fields/Phone/index.tsx b/src/components/fields/Phone/index.tsx index 82b97f573..03e219bef 100644 --- a/src/components/fields/Phone/index.tsx +++ b/src/components/fields/Phone/index.tsx @@ -6,6 +6,7 @@ import PhoneIcon from "@mui/icons-material/PhoneOutlined"; import BasicCell from "../_BasicCell/BasicCellValue"; import TextEditor from "@src/components/Table/editors/TextEditor"; import { filterOperators } from "../ShortText/Filter"; +import BasicContextMenuActions from "../_BasicCell/BasicCellContextMenuActions"; const SideDrawerField = lazy( () => @@ -21,6 +22,7 @@ export const config: IFieldConfig = { initializable: true, icon: , description: "Phone number stored as text. Not validated.", + contextMenuActions: BasicContextMenuActions, TableCell: withBasicCell(BasicCell), TableEditor: TextEditor, SideDrawerField, diff --git a/src/components/fields/RichText/index.tsx b/src/components/fields/RichText/index.tsx index 325ff956b..e2e098311 100644 --- a/src/components/fields/RichText/index.tsx +++ b/src/components/fields/RichText/index.tsx @@ -5,6 +5,7 @@ import withHeavyCell from "../_withTableCell/withHeavyCell"; import RichTextIcon from "@mui/icons-material/TextFormat"; import BasicCell from "../_BasicCell/BasicCellNull"; import withSideDrawerEditor from "@src/components/Table/editors/withSideDrawerEditor"; +import BasicContextMenuActions from "../_BasicCell/BasicCellContextMenuActions"; const TableCell = lazy( () => import("./TableCell" /* webpackChunkName: "TableCell-RichText" */) @@ -25,6 +26,7 @@ export const config: IFieldConfig = { initializable: true, icon: , description: "HTML edited with a rich text editor.", + contextMenuActions: BasicContextMenuActions, TableCell: withHeavyCell(BasicCell, TableCell), TableEditor: withSideDrawerEditor(TableCell), SideDrawerField, diff --git a/src/components/fields/Url/index.tsx b/src/components/fields/Url/index.tsx index 6743b5e92..921a9ef06 100644 --- a/src/components/fields/Url/index.tsx +++ b/src/components/fields/Url/index.tsx @@ -6,6 +6,7 @@ import UrlIcon from "@mui/icons-material/Link"; import TableCell from "./TableCell"; import TextEditor from "@src/components/Table/editors/TextEditor"; import { filterOperators } from "../ShortText/Filter"; +import BasicContextMenuActions from "../_BasicCell/BasicCellContextMenuActions"; const SideDrawerField = lazy( () => @@ -21,6 +22,7 @@ export const config: IFieldConfig = { initializable: true, icon: , description: "Web address. Not validated.", + contextMenuActions: BasicContextMenuActions, TableCell: withBasicCell(TableCell), TableEditor: TextEditor, SideDrawerField,