Merge pull request #632 from gibsonliketheguitar/righ-click-v2

Right click menu -v2
This commit is contained in:
Sidney Alcantara
2022-02-04 15:30:33 +11:00
committed by GitHub
10 changed files with 159 additions and 126 deletions

40
src/atoms/ContextMenu.ts Normal file
View File

@@ -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<SelectedCell | null>(null);
const anchorEleAtom = atomWithReset<HTMLElement | null>(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,
};
}

View File

@@ -1,11 +1,12 @@
import { Menu } from "@mui/material";
import MenuRow, { IMenuRow } from "./MenuRow";
import { default as MenuItem } from "./MenuItem";
import { IContextMenuItem } from "./MenuItem";
interface IMenuContents {
anchorEl: HTMLElement;
open: boolean;
handleClose: () => void;
items: IMenuRow[];
items: IContextMenuItem[];
}
export function MenuContents({
@@ -31,12 +32,17 @@ export function MenuContents({
vertical: "top",
horizontal: "left",
}}
sx={{ "& .MuiMenu-paper": { backgroundColor: "background.default" } }}
MenuListProps={{ disablePadding: true }}
sx={{
"& .MuiMenu-paper": {
backgroundColor: "background.default",
width: 200,
maxWidth: "100%",
},
}}
onContextMenu={handleContext}
>
{items.map((item, indx: number) => (
<MenuRow key={indx} {...item} />
<MenuItem key={indx} {...item} />
))}
</Menu>
);

View File

@@ -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 (
<MenuItem disabled={disabled} onClick={onClick}>
<ListItemIcon>{icon} </ListItemIcon>
<ListItemText> {label} </ListItemText>
{hotkeyLabel && (
<Typography variant="body2" color="text.secondary">
{hotkeyLabel}
</Typography>
)}
</MenuItem>
);
}

View File

@@ -1,17 +0,0 @@
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 (
<MenuItem disabled={disabled} onClick={onClick}>
<ListItemIcon>{icon} </ListItemIcon>
<ListItemText> {label} </ListItemText>
</MenuItem>
);
}

View File

@@ -1,56 +1,26 @@
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<React.SetStateAction<SelectedCell | null>>;
anchorEl: HTMLElement | null;
setAnchorEl: React.Dispatch<
React.SetStateAction<PopoverProps["anchorEl"] | null>
>;
};
import { useContextMenuAtom, useSetSelectedCell } from "@src/atoms/ContextMenu";
export default function ContextMenu() {
const { contextMenuRef, tableState }: any = useProjectContext();
const [anchorEl, setAnchorEl] = React.useState<any | null>(null);
const [selectedCell, setSelectedCell] = React.useState<any | null>();
const open = Boolean(anchorEl);
const handleClose = () => setAnchorEl(null);
if (contextMenuRef)
contextMenuRef.current = {
anchorEl,
setAnchorEl,
selectedCell,
setSelectedCell,
} as {};
const { tableState } = useProjectContext();
const { anchorEle, selectedCell, resetContextMenu } = useContextMenuAtom();
const columns = tableState?.columns;
const selectedColIndex = selectedCell?.colIndex;
const selectedCol = _find(tableState?.columns, { index: selectedColIndex });
const getActions =
const selectedCol = _find(columns, { index: selectedColIndex });
const configActions =
getFieldProp("contextMenuActions", selectedCol?.type) ||
function empty() {};
const actions = getActions() || [];
const hasNoActions = Boolean(actions.length === 0);
if (!contextMenuRef.current || hasNoActions) return null;
const actions = configActions(selectedCell, resetContextMenu) || [];
if (!anchorEle || actions.length === 0) return <></>;
return (
<MenuContents
anchorEl={anchorEl}
open={open}
handleClose={handleClose}
anchorEl={anchorEle}
open={Boolean(anchorEle)}
handleClose={resetContextMenu}
items={actions}
/>
);

View File

@@ -1,25 +1,24 @@
import { useProjectContext } from "@src/contexts/ProjectContext";
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<any>) {
const { contextMenuRef }: any = useProjectContext();
const { setAnchorEle } = useSetAnchorEle();
const handleContextMenu = (
e: React.MouseEvent<HTMLDivElement, MouseEvent>
) => {
e.preventDefault();
if (contextMenuRef?.current) contextMenuRef?.current?.setAnchorEl(e.target);
setAnchorEle?.(e?.target as HTMLElement);
};
if (props.row._rowy_outOfOrder)
return (
<Fragment key={props.row.id}>
<OutOfOrderIndicator top={props.top} height={props.height} />
<Row {...props} onContextMenu={handleContextMenu} />
<Row onContextMenu={handleContextMenu} {...props} />
</Fragment>
);
return <Row {...props} onContextMenu={handleContextMenu} />;
return <Row onContextMenu={handleContextMenu} {...props} />;
}

View File

@@ -32,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<any> & {
isNew?: boolean;
@@ -49,11 +50,11 @@ export default function Table() {
tableState,
tableActions,
dataGridRef,
contextMenuRef,
sideDrawerRef,
updateCell,
} = useProjectContext();
const { userDoc, userClaims } = useAppContext();
const { setSelectedCell } = useSetSelectedCell();
const userDocHiddenFields =
userDoc.state.doc?.tables?.[formatSubTableName(tableState?.config.id)]
@@ -264,12 +265,12 @@ export default function Table() {
});
}
}}
onSelectedCellChange={({ rowIdx, idx }) => {
contextMenuRef?.current?.setSelectedCell({
onSelectedCellChange={({ rowIdx, idx }) =>
setSelectedCell({
rowIndex: rowIdx,
colIndex: idx,
});
}}
})
}
/>
</DndProvider>
) : (

View File

@@ -5,69 +5,70 @@ import CopyCells from "@src/assets/icons/CopyCells";
import Paste from "@mui/icons-material/ContentPaste";
import { useProjectContext } from "@src/contexts/ProjectContext";
import { useSnackbar } from "notistack";
import { SelectedCell } from "@src/atoms/ContextMenu";
export default function BasicContextMenuActions() {
const { contextMenuRef, tableState, deleteCell, updateCell } =
useProjectContext();
export interface IContextMenuActions {
label: string;
icon: React.ReactNode;
onClick: () => void;
}
export default function BasicContextMenuActions(
selectedCell: SelectedCell,
reset: () => void | Promise<void>
): IContextMenuActions[] {
const { tableState, deleteCell, updateCell } = useProjectContext();
const { enqueueSnackbar } = useSnackbar();
const columns = tableState?.columns;
const rows = tableState?.rows;
const selectedRowIndex = contextMenuRef?.current?.selectedCell
.rowIndex as number;
const selectedColIndex = contextMenuRef?.current?.selectedCell?.colIndex;
const selectedRowIndex = selectedCell.rowIndex as number;
const selectedColIndex = selectedCell?.colIndex;
const selectedCol = _find(columns, { index: selectedColIndex });
const selectedRow = rows?.[selectedRowIndex];
const cell = selectedRow?.[selectedCol.key];
const handleClose = () => {
contextMenuRef?.current?.setSelectedCell(null);
contextMenuRef?.current?.setAnchorEl(null);
const handleClose = async () => await reset?.();
const handleCopy = async () => {
try {
await navigator.clipboard.writeText(JSON.stringify(cell));
enqueueSnackbar("Copied to clipboard", { variant: "success" });
} catch (error) {
enqueueSnackbar(`Failed to copy:${error}`, { variant: "error" });
}
handleClose();
};
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);
const handleCut = async () => {
try {
await navigator.clipboard.writeText(JSON.stringify(cell));
if (typeof cell !== "undefined")
deleteCell?.(selectedRow?.ref, selectedCol?.key);
} catch (error) {
enqueueSnackbar(`Failed to cut: ${error}`, { variant: "error" });
}
handleClose();
};
const handleCut = () => {
const cell = selectedRow?.[selectedCol.key];
const notUndefined = Boolean(typeof cell !== "undefined");
if (deleteCell && notUndefined)
deleteCell(selectedRow?.ref, selectedCol?.key);
const handlePaste = async () => {
try {
const text = await navigator.clipboard.readText();
const paste = await JSON.parse(text);
updateCell?.(selectedRow?.ref, selectedCol.key, paste);
} catch (error) {
enqueueSnackbar(`Failed to paste: ${error}`, { variant: "error" });
}
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 = [
const contextMenuActions = [
{ label: "Cut", icon: <Cut />, onClick: handleCut },
{ label: "Copy", icon: <CopyCells />, onClick: handleCopy },
{ label: "Paste", icon: <Paste />, onClick: handlePaste },
];
return cellMenuAction;
return contextMenuActions;
}

View File

@@ -1,8 +1,9 @@
import { FieldType } from "@src/constants/fields";
import { FormatterProps, EditorProps } from "react-data-grid";
import { Control, UseFormReturn } from "react-hook-form";
import { PopoverProps } from "@mui/material";
import { SelectedCell } from "@src/atoms/ContextMenu";
import { IContextMenuActions } from "./_BasicCell/BasicCellContextMenuActions";
export { FieldType };
@@ -17,7 +18,10 @@ export interface IFieldConfig {
icon?: React.ReactNode;
description?: string;
setupGuideLink?: string;
contextMenuActions?: () => void;
contextMenuActions?: (
selectedCell: SelectedCell,
reset: () => Promise<void>
) => IContextMenuActions[];
TableCell: React.ComponentType<FormatterProps<any>>;
TableEditor: React.ComponentType<EditorProps<any, any>>;
SideDrawerField: React.ComponentType<ISideDrawerFieldProps>;

View File

@@ -14,7 +14,6 @@ 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";
@@ -105,8 +104,6 @@ export interface IProjectContext {
dataGridRef: React.RefObject<DataGridHandle>;
// A ref to the side drawer state. Prevents unnecessary re-renders
sideDrawerRef: React.MutableRefObject<SideDrawerRef | undefined>;
//A ref to the cell menu. Prevents unnecessary re-render
contextMenuRef: React.MutableRefObject<ContextMenuRef | undefined>;
// A ref to the column menu. Prevents unnecessary re-renders
columnMenuRef: React.MutableRefObject<ColumnMenuRef | undefined>;
// A ref ot the import wizard. Prevents unnecessary re-renders
@@ -401,7 +398,6 @@ export const ProjectContextProvider: React.FC = ({ children }) => {
// A ref to the data grid. Contains data grid functions
const dataGridRef = useRef<DataGridHandle>(null);
const sideDrawerRef = useRef<SideDrawerRef>();
const contextMenuRef = useRef<ContextMenuRef>();
const columnMenuRef = useRef<ColumnMenuRef>();
const importWizardRef = useRef<ImportWizardRef>();
@@ -422,7 +418,6 @@ export const ProjectContextProvider: React.FC = ({ children }) => {
table,
dataGridRef,
sideDrawerRef,
contextMenuRef,
columnMenuRef,
importWizardRef,
rowyRun: _rowyRun,