diff --git a/src/assets/icons/Clear.tsx b/src/assets/icons/Clear.tsx
new file mode 100644
index 00000000..44baee02
--- /dev/null
+++ b/src/assets/icons/Clear.tsx
@@ -0,0 +1,9 @@
+import SvgIcon, { SvgIconProps } from "@mui/material/SvgIcon";
+
+export function Clear(props: SvgIconProps) {
+ return (
+
+
+
+ );
+}
diff --git a/src/assets/icons/index.ts b/src/assets/icons/index.ts
index 60bee5a4..db947a38 100644
--- a/src/assets/icons/index.ts
+++ b/src/assets/icons/index.ts
@@ -83,6 +83,7 @@ import { CarBrakeAlert } from "mdi-material-ui";
export { CarBrakeAlert as Critical };
export * from "./AddRow";
+export * from "./Clear";
export * from "./ConnectTable";
export * from "./Copy";
export * from "./CopyCells";
diff --git a/src/atoms/tableScope/ui.ts b/src/atoms/tableScope/ui.ts
index a247c177..6fcb15dd 100644
--- a/src/atoms/tableScope/ui.ts
+++ b/src/atoms/tableScope/ui.ts
@@ -100,11 +100,12 @@ export const tableModalAtom = atomWithHash<
/** Store side drawer open state */
export const sideDrawerOpenAtom = atom(false);
-/** Store selected cell in table */
-export const selectedCellAtom = atom<{
- path: string;
- columnKey: string;
-} | null>(null);
+export type SelectedCell = { path: string; columnKey: string };
+/** Store selected cell in table. Used in side drawer and context menu */
+export const selectedCellAtom = atom(null);
+
+/** Store context menu target atom for positioning. If not null, menu open. */
+export const contextMenuTargetAtom = atom(null);
export type CloudLogFilters = {
type: "webhook" | "functions" | "audit" | "build";
diff --git a/src/components/Table/ContextMenu/ContextMenu.tsx b/src/components/Table/ContextMenu/ContextMenu.tsx
new file mode 100644
index 00000000..10383d28
--- /dev/null
+++ b/src/components/Table/ContextMenu/ContextMenu.tsx
@@ -0,0 +1,34 @@
+import { useAtom } from "jotai";
+
+import { Menu } from "@mui/material";
+import MenuContents from "./MenuContents";
+
+import { tableScope, contextMenuTargetAtom } from "@src/atoms/tableScope";
+
+export default function ContextMenu() {
+ const [contextMenuTarget, setContextMenuTarget] = useAtom(
+ contextMenuTargetAtom,
+ tableScope
+ );
+
+ const handleClose = () => setContextMenuTarget(null);
+
+ return (
+
+ );
+}
diff --git a/src/components/Table/ContextMenu/MenuContents.tsx b/src/components/Table/ContextMenu/MenuContents.tsx
new file mode 100644
index 00000000..bf4261a1
--- /dev/null
+++ b/src/components/Table/ContextMenu/MenuContents.tsx
@@ -0,0 +1,160 @@
+import { useAtom, useSetAtom } from "jotai";
+import { getFieldProp } from "@src/components/fields";
+import { find } from "lodash-es";
+
+import { Divider } from "@mui/material";
+import {
+ CopyCells as DuplicateIcon,
+ Clear as ClearIcon,
+} from "@src/assets/icons";
+import DeleteIcon from "@mui/icons-material/DeleteOutlined";
+
+import MenuItem from "./MenuItem";
+
+import {
+ globalScope,
+ altPressAtom,
+ tableAddRowIdTypeAtom,
+ confirmDialogAtom,
+} from "@src/atoms/globalScope";
+import {
+ tableScope,
+ tableSettingsAtom,
+ tableSchemaAtom,
+ tableRowsAtom,
+ selectedCellAtom,
+ addRowAtom,
+ deleteRowAtom,
+ updateFieldAtom,
+} from "@src/atoms/tableScope";
+import { IContextMenuItem } from "./MenuItem";
+import { FieldType } from "@src/constants/fields";
+
+interface IMenuContentsProps {
+ onClose: () => void;
+}
+
+export default function MenuContents({ onClose }: IMenuContentsProps) {
+ const [altPress] = useAtom(altPressAtom, globalScope);
+ const [addRowIdType] = useAtom(tableAddRowIdTypeAtom, globalScope);
+ const confirm = useSetAtom(confirmDialogAtom, globalScope);
+ const [tableSettings] = useAtom(tableSettingsAtom, tableScope);
+ const [tableSchema] = useAtom(tableSchemaAtom, tableScope);
+ const [tableRows] = useAtom(tableRowsAtom, tableScope);
+ const [selectedCell] = useAtom(selectedCellAtom, tableScope);
+ const addRow = useSetAtom(addRowAtom, tableScope);
+ const deleteRow = useSetAtom(deleteRowAtom, tableScope);
+ const updateField = useSetAtom(updateFieldAtom, tableScope);
+
+ if (!tableSchema.columns || !selectedCell) return null;
+
+ const selectedColumn = tableSchema.columns[selectedCell.columnKey];
+ const menuActions = getFieldProp("contextMenuActions", selectedColumn.type);
+
+ const actionGroups: IContextMenuItem[][] = [];
+
+ // Field type actions
+ const fieldTypeActions = menuActions
+ ? menuActions(selectedCell, onClose)
+ : [];
+ if (fieldTypeActions.length > 0) actionGroups.push(fieldTypeActions);
+
+ if (selectedColumn.type === FieldType.derivative) {
+ const renderedFieldMenuActions = getFieldProp(
+ "contextMenuActions",
+ selectedColumn.config?.renderFieldType
+ );
+ if (renderedFieldMenuActions) {
+ actionGroups.push(renderedFieldMenuActions(selectedCell, onClose));
+ }
+ }
+
+ // Cell actions
+ // TODO: Add copy and paste here
+ const handleClearValue = () =>
+ updateField({
+ path: selectedCell.path,
+ fieldName: selectedColumn.fieldName,
+ value: null,
+ deleteField: true,
+ });
+ const cellActions = [
+ {
+ label: altPress ? "Clear value" : "Clear value…",
+ color: "error",
+ icon: ,
+ onClick: altPress
+ ? handleClearValue
+ : () => {
+ confirm({
+ title: "Clear cell value?",
+ body: "The cell’s value cannot be recovered after",
+ confirm: "Delete",
+ confirmColor: "error",
+ handleConfirm: handleClearValue,
+ });
+ onClose();
+ },
+ },
+ ];
+ actionGroups.push(cellActions);
+
+ // Row actions
+ const row = find(tableRows, ["_rowy_ref.path", selectedCell.path]);
+ if (row) {
+ const handleDelete = () => deleteRow(row._rowy_ref.path);
+ const rowActions = [
+ {
+ label: "Duplicate row",
+ icon: ,
+ disabled: tableSettings.tableType === "collectionGroup",
+ onClick: () => {
+ addRow({
+ row,
+ setId: addRowIdType === "custom" ? "decrement" : addRowIdType,
+ });
+ onClose();
+ },
+ },
+ {
+ label: altPress ? "Delete row" : "Delete row…",
+ color: "error",
+ icon: ,
+ onClick: altPress
+ ? handleDelete
+ : () => {
+ confirm({
+ title: "Delete row?",
+ body: (
+ <>
+ Row path:
+
+
+ {row._rowy_ref.path}
+
+ >
+ ),
+ confirm: "Delete",
+ confirmColor: "error",
+ handleConfirm: handleDelete,
+ });
+ onClose();
+ },
+ },
+ ];
+ actionGroups.push(rowActions);
+ }
+
+ return (
+ <>
+ {actionGroups.map((items, groupIndex) => (
+ <>
+ {groupIndex > 0 && }
+ {items.map((item, index: number) => (
+
+ ))}
+ >
+ ))}
+ >
+ );
+}
diff --git a/src/components/Table/ContextMenu/MenuItem.tsx b/src/components/Table/ContextMenu/MenuItem.tsx
new file mode 100644
index 00000000..d502d235
--- /dev/null
+++ b/src/components/Table/ContextMenu/MenuItem.tsx
@@ -0,0 +1,36 @@
+import {
+ ListItemIcon,
+ ListItemText,
+ MenuItem,
+ MenuItemProps,
+ Typography,
+} from "@mui/material";
+
+export interface IContextMenuItem extends Partial {
+ onClick: () => void;
+ icon?: React.ReactNode;
+ label: string;
+ disabled?: boolean;
+ hotkeyLabel?: string;
+}
+
+export default function ContextMenuItem({
+ onClick,
+ icon,
+ label,
+ disabled,
+ hotkeyLabel,
+ ...props
+}: IContextMenuItem) {
+ return (
+
+ );
+}
diff --git a/src/components/Table/ContextMenu/index.ts b/src/components/Table/ContextMenu/index.ts
new file mode 100644
index 00000000..0b83f120
--- /dev/null
+++ b/src/components/Table/ContextMenu/index.ts
@@ -0,0 +1,2 @@
+export * from "./ContextMenu";
+export { default } from "./ContextMenu";
diff --git a/src/components/Table/EmptyTable.tsx b/src/components/Table/EmptyTable.tsx
index e3033e1a..31fa90c4 100644
--- a/src/components/Table/EmptyTable.tsx
+++ b/src/components/Table/EmptyTable.tsx
@@ -13,7 +13,6 @@ import {
import { APP_BAR_HEIGHT } from "@src/layouts/Navigation";
// FIXME:
-// import ColumnMenu from "./ColumnMenu";
// import ImportWizard from "@src/components/Wizards/ImportWizard";
// import ImportCSV from "@src/components/TableToolbar/ImportCsv";
diff --git a/src/components/Table/Table.tsx b/src/components/Table/Table.tsx
index 287dfaa6..d433500f 100644
--- a/src/components/Table/Table.tsx
+++ b/src/components/Table/Table.tsx
@@ -1,4 +1,4 @@
-import React, { useMemo, useState } from "react";
+import React, { useMemo, useState, Suspense } from "react";
import { useAtom, useSetAtom } from "jotai";
import { useDebouncedCallback, useThrottledCallback } from "use-debounce";
import { DndProvider } from "react-dnd";
@@ -15,7 +15,6 @@ import { LinearProgress } from "@mui/material";
import TableContainer, { OUT_OF_ORDER_MARGIN } from "./TableContainer";
import ColumnHeader, { COLUMN_HEADER_HEIGHT } from "./ColumnHeader";
-// import ContextMenu from "./ContextMenu";
import FinalColumnHeader from "./FinalColumnHeader";
import FinalColumn from "./formatters/FinalColumn";
import TableRow from "./TableRow";
@@ -23,6 +22,8 @@ import EmptyState from "@src/components/EmptyState";
// import BulkActions from "./BulkActions";
import AddRow from "@src/components/TableToolbar/AddRow";
import { AddRow as AddRowIcon } from "@src/assets/icons";
+import Loading from "@src/components/Loading";
+import ContextMenu from "./ContextMenu";
import {
globalScope,
@@ -211,10 +212,8 @@ export default function Table({
);
return (
- <>
- {/* }>
-
- */}
+ }>
+ {/* */}
{showLeftScrollDivider && }
@@ -268,7 +267,7 @@ export default function Table({
// onRowsChange={() => {
//console.log('onRowsChange',rows)
// }}
- // FIXME: onFill={(e) => {
+ // TODO: onFill={(e) => {
// console.log("onFill", e);
// const { columnKey, sourceRow, targetRows } = e;
// if (updateCell)
@@ -277,7 +276,8 @@ export default function Table({
// );
// return [];
// }}
- onPaste={(e) => {
+ onPaste={(e, ...args) => {
+ console.log("onPaste", e, ...args);
const value = e.sourceRow[e.sourceColumnKey];
updateField({
path: e.targetRow._rowy_ref.path,
@@ -324,7 +324,8 @@ export default function Table({
{tableNextPage.loading && }
- {/*
+
+ {/*
*/}
- >
+
);
}
diff --git a/src/components/Table/TableRow.tsx b/src/components/Table/TableRow.tsx
index d52b6b08..0ecd229a 100644
--- a/src/components/Table/TableRow.tsx
+++ b/src/components/Table/TableRow.tsx
@@ -1,17 +1,16 @@
-// FIXME:
-// import { useSetAnchorEle } from "@src/atoms/ContextMenu";
import { Fragment } from "react";
+import { useSetAtom } from "jotai";
import { Row, RowRendererProps } from "react-data-grid";
import OutOfOrderIndicator from "./OutOfOrderIndicator";
+import { tableScope, contextMenuTargetAtom } from "@src/atoms/tableScope";
+
export default function TableRow(props: RowRendererProps) {
- // const { setAnchorEle } = useSetAnchorEle();
- const handleContextMenu = (
- e: React.MouseEvent
- ) => {
+ const setContextMenuTarget = useSetAtom(contextMenuTargetAtom, tableScope);
+ const handleContextMenu = (e: React.MouseEvent) => {
e.preventDefault();
- // setAnchorEle?.(e?.target as HTMLElement);
+ setContextMenuTarget(e?.target as HTMLElement);
};
if (props.row._rowy_outOfOrder)
diff --git a/src/components/fields/Derivative/ContextMenuActions.tsx.disabled b/src/components/fields/Derivative/ContextMenuActions.tsx
similarity index 54%
rename from src/components/fields/Derivative/ContextMenuActions.tsx.disabled
rename to src/components/fields/Derivative/ContextMenuActions.tsx
index 5ee24459..194185f6 100644
--- a/src/components/fields/Derivative/ContextMenuActions.tsx.disabled
+++ b/src/components/fields/Derivative/ContextMenuActions.tsx
@@ -1,11 +1,19 @@
+import { useAtom } from "jotai";
import { find, get } from "lodash-es";
+import { useSnackbar } from "notistack";
import ReEvalIcon from "@mui/icons-material/Replay";
import EvalIcon from "@mui/icons-material/PlayCircle";
-import { useProjectContext } from "@src/contexts/ProjectContext";
-import { useSnackbar } from "notistack";
-import { SelectedCell } from "@src/atoms/ContextMenu";
+import { globalScope, rowyRunAtom } from "@src/atoms/globalScope";
+import {
+ tableScope,
+ tableSettingsAtom,
+ tableSchemaAtom,
+ tableRowsAtom,
+} from "@src/atoms/tableScope";
+import { getTableSchemaPath } from "@src/utils/table";
+import { IFieldConfig } from "@src/components/fields/types";
import { runRoutes } from "@src/constants/runRoutes";
export interface IContextMenuActions {
@@ -14,40 +22,41 @@ export interface IContextMenuActions {
onClick: () => void;
}
-export default function ContextMenuActions(
- selectedCell: SelectedCell,
- reset: () => void | Promise
-): IContextMenuActions[] {
- const { tableState, rowyRun } = useProjectContext();
+export const ContextMenuActions: IFieldConfig["contextMenuActions"] = (
+ selectedCell,
+ reset
+) => {
+ const [rowyRun] = useAtom(rowyRunAtom, globalScope);
+ const [tableSettings] = useAtom(tableSettingsAtom, tableScope);
+ const [tableSchema] = useAtom(tableSchemaAtom, tableScope);
+ const [tableRows] = useAtom(tableRowsAtom, tableScope);
const { enqueueSnackbar, closeSnackbar } = useSnackbar();
- const columns = tableState?.columns;
- const rows = tableState?.rows;
- const selectedRowIndex = selectedCell.rowIndex as number;
- const selectedColIndex = selectedCell?.colIndex;
- const selectedCol = find(columns, { index: selectedColIndex });
+
+ const selectedCol = tableSchema.columns?.[selectedCell.columnKey];
if (!selectedCol) return [];
- // don't show evalute button if function has external dependency
+
+ const selectedRow = find(tableRows, ["_rowy_ref.path", selectedCell.path]);
+ const cellValue = get(selectedRow, selectedCol.fieldName);
+
+ if (!selectedCol) return [];
+
+ // don't show evaluate button if function has external dependency
const code =
- selectedCol.config.derivativeFn ?? selectedCol.config.script ?? "";
+ selectedCol.config?.derivativeFn ?? selectedCol.config?.script ?? "";
if (code.includes("require(")) return [];
- const selectedRow = rows?.[selectedRowIndex];
- const cellValue = get(selectedRow, selectedCol.key);
- const handleClose = async () => await reset?.();
const handleEvaluate = async () => {
try {
if (!selectedCol || !rowyRun || !selectedRow) return;
- handleClose();
+ reset();
const evaluatingSnackKey = enqueueSnackbar("Evaluating...", {
variant: "info",
});
const result = await rowyRun({
route: runRoutes.evaluateDerivative,
body: {
- ref: {
- path: selectedRow.ref.path,
- },
- schemaDocPath: tableState?.config.tableConfig.path,
+ ref: { path: selectedCell.path },
+ schemaDocPath: getTableSchemaPath(tableSettings),
columnKey: selectedCol.key,
},
});
@@ -70,4 +79,6 @@ export default function ContextMenuActions(
];
return contextMenuActions;
-}
+};
+
+export default ContextMenuActions;
diff --git a/src/components/fields/Derivative/index.tsx b/src/components/fields/Derivative/index.tsx
index 467fb8bc..a42f2f8c 100644
--- a/src/components/fields/Derivative/index.tsx
+++ b/src/components/fields/Derivative/index.tsx
@@ -5,7 +5,7 @@ import { Derivative as DerivativeIcon } from "@src/assets/icons";
import BasicCell from "@src/components/fields/_BasicCell/BasicCellNull";
import NullEditor from "@src/components/Table/editors/NullEditor";
import Settings, { settingsValidator } from "./Settings";
-// import ContextMenuActions from "./ContextMenuActions";
+import ContextMenuActions from "./ContextMenuActions";
export const config: IFieldConfig = {
type: FieldType.derivative,
@@ -20,7 +20,7 @@ export const config: IFieldConfig = {
TableCell: withBasicCell(BasicCell),
TableEditor: NullEditor as any,
SideDrawerField: BasicCell as any,
- // FIXME: contextMenuActions: ContextMenuActions,
+ contextMenuActions: ContextMenuActions,
settings: Settings,
settingsValidator,
requireConfiguration: true,
diff --git a/src/components/fields/_BasicCell/BasicCellContextMenuActions.tsx b/src/components/fields/_BasicCell/BasicCellContextMenuActions.tsx
index fe0b2bdf..bb0ae81c 100644
--- a/src/components/fields/_BasicCell/BasicCellContextMenuActions.tsx
+++ b/src/components/fields/_BasicCell/BasicCellContextMenuActions.tsx
@@ -1,42 +1,36 @@
import { useAtom, useSetAtom } from "jotai";
-import { get } from "lodash-es";
+import { useSnackbar } from "notistack";
+import { get, find } from "lodash-es";
-import Cut from "@mui/icons-material/ContentCut";
+// import Cut from "@mui/icons-material/ContentCut";
import { CopyCells } from "@src/assets/icons";
import Paste from "@mui/icons-material/ContentPaste";
import {
tableScope,
- tableColumnsOrderedAtom,
+ tableSchemaAtom,
tableRowsAtom,
updateFieldAtom,
} from "@src/atoms/tableScope";
-import { useSnackbar } from "notistack";
-// import { SelectedCell } from "@src/atoms/ContextMenu";
import { getFieldProp, getFieldType } from "@src/components/fields";
+import { IFieldConfig } from "@src/components/fields/types";
-export interface IContextMenuActions {
- label: string;
- icon: React.ReactNode;
- onClick: () => void;
-}
-
-export default function BasicContextMenuActions(
- selectedCell: any,
- reset: () => void | Promise
-): IContextMenuActions[] {
+// TODO: Remove this and add `handlePaste` function to column config
+export const BasicContextMenuActions: IFieldConfig["contextMenuActions"] = (
+ selectedCell,
+ reset
+) => {
const { enqueueSnackbar } = useSnackbar();
- // TODO: Remove these useAtom calls that cause re-render
- const [tableColumnsOrdered] = useAtom(tableColumnsOrderedAtom, tableScope);
+ const [tableSchema] = useAtom(tableSchemaAtom, tableScope);
const [tableRows] = useAtom(tableRowsAtom, tableScope);
const updateField = useSetAtom(updateFieldAtom, tableScope);
- const selectedCol = tableColumnsOrdered[selectedCell?.colIndex];
+ const selectedCol = tableSchema.columns?.[selectedCell.columnKey];
if (!selectedCol) return [];
- const selectedRow = tableRows[selectedCell.rowIndex];
- const cellValue = get(selectedRow, selectedCol.key);
+ const selectedRow = find(tableRows, ["_rowy_ref.path", selectedCell.path]);
+ const cellValue = get(selectedRow, selectedCol.fieldName);
const handleClose = async () => await reset?.();
@@ -50,21 +44,21 @@ export default function BasicContextMenuActions(
handleClose();
};
- const handleCut = async () => {
- try {
- await navigator.clipboard.writeText(cellValue);
- if (typeof cellValue !== "undefined")
- updateField({
- path: selectedRow._rowy_ref.path,
- fieldName: selectedCol.fieldName,
- value: undefined,
- deleteField: true,
- });
- } catch (error) {
- enqueueSnackbar(`Failed to cut: ${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 {
@@ -85,7 +79,7 @@ export default function BasicContextMenuActions(
break;
}
updateField({
- path: selectedRow._rowy_ref.path,
+ path: selectedCell.path,
fieldName: selectedCol.fieldName,
value: parsed,
});
@@ -97,10 +91,12 @@ export default function BasicContextMenuActions(
};
const contextMenuActions = [
- { label: "Cut", icon: , onClick: handleCut },
+ // { label: "Cut", icon: , onClick: handleCut },
{ label: "Copy", icon: , onClick: handleCopy },
{ label: "Paste", icon: , onClick: handlePaste },
];
return contextMenuActions;
-}
+};
+
+export default BasicContextMenuActions;
diff --git a/src/components/fields/types.ts b/src/components/fields/types.ts
index f231b12d..4fd75acf 100644
--- a/src/components/fields/types.ts
+++ b/src/components/fields/types.ts
@@ -4,8 +4,8 @@ import { Control, UseFormReturn } from "react-hook-form";
import { PopoverProps } from "@mui/material";
import { WhereFilterOp } from "firebase/firestore";
import { ColumnConfig, TableRow, TableRowRef } from "@src/types/table";
-import { selectedCellAtom } from "@src/atoms/tableScope";
-import { IContextMenuActions } from "./_BasicCell/BasicCellContextMenuActions";
+import { SelectedCell } from "@src/atoms/tableScope";
+import { IContextMenuItem } from "@src/components/Table/ContextMenu/MenuItem";
export { FieldType };
@@ -21,9 +21,9 @@ export interface IFieldConfig {
description?: string;
setupGuideLink?: string;
contextMenuActions?: (
- selectedCell: ReturnType,
- reset: () => Promise
- ) => IContextMenuActions[];
+ selectedCell: SelectedCell,
+ reset: () => void
+ ) => IContextMenuItem[];
TableCell: React.ComponentType>;
TableEditor: React.ComponentType>;
SideDrawerField: React.ComponentType;
diff --git a/src/index.tsx b/src/index.tsx
index f4d7b2c9..5d545f47 100644
--- a/src/index.tsx
+++ b/src/index.tsx
@@ -8,11 +8,11 @@ import reportWebVitals from "./reportWebVitals";
const container = document.getElementById("root")!;
const root = createRoot(container);
root.render(
- //
-
-
-
- //
+
+
+
+
+
);
// If you want to start measuring performance in your app, pass a function