Merge branch 'data-layer-rewrite' of https://github.com/rowyio/rowy into data-layer-rewrite

This commit is contained in:
shamsmosowi
2022-06-06 15:59:35 +10:00
15 changed files with 346 additions and 97 deletions

View File

@@ -0,0 +1,9 @@
import SvgIcon, { SvgIconProps } from "@mui/material/SvgIcon";
export function Clear(props: SvgIconProps) {
return (
<SvgIcon {...props}>
<path d="M20 6v12a2 2 0 0 1-2 2H7l-6-8 6-8h11a2 2 0 0 1 2 2Zm-2.001-.001h-10l-4.5 6 4.5 6h10v-12ZM9.12 7.71 7.71 9.12 10.59 12l-2.88 2.88 1.41 1.41L12 13.41l2.88 2.88 1.41-1.41L13.41 12l2.88-2.88-1.41-1.41L12 10.59" />
</SvgIcon>
);
}

View File

@@ -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";

View File

@@ -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<SelectedCell | null>(null);
/** Store context menu target atom for positioning. If not null, menu open. */
export const contextMenuTargetAtom = atom<HTMLElement | null>(null);
export type CloudLogFilters = {
type: "webhook" | "functions" | "audit" | "build";

View File

@@ -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 (
<Menu
id="cell-context-menu"
aria-label="Cell context menu"
anchorEl={contextMenuTarget as any}
open={Boolean(contextMenuTarget)}
onClose={handleClose}
anchorOrigin={{ vertical: "bottom", horizontal: "left" }}
transformOrigin={{ vertical: "top", horizontal: "left" }}
// sx={{
// "& .MuiMenu-paper": {
// backgroundColor: "background.default",
// },
// }}
>
<MenuContents onClose={handleClose} />
</Menu>
);
}

View File

@@ -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: <ClearIcon />,
onClick: altPress
? handleClearValue
: () => {
confirm({
title: "Clear cell value?",
body: "The cells 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: <DuplicateIcon />,
disabled: tableSettings.tableType === "collectionGroup",
onClick: () => {
addRow({
row,
setId: addRowIdType === "custom" ? "decrement" : addRowIdType,
});
onClose();
},
},
{
label: altPress ? "Delete row" : "Delete row…",
color: "error",
icon: <DeleteIcon />,
onClick: altPress
? handleDelete
: () => {
confirm({
title: "Delete row?",
body: (
<>
Row path:
<br />
<code style={{ userSelect: "all", wordBreak: "break-all" }}>
{row._rowy_ref.path}
</code>
</>
),
confirm: "Delete",
confirmColor: "error",
handleConfirm: handleDelete,
});
onClose();
},
},
];
actionGroups.push(rowActions);
}
return (
<>
{actionGroups.map((items, groupIndex) => (
<>
{groupIndex > 0 && <Divider variant="middle" />}
{items.map((item, index: number) => (
<MenuItem key={`contextMenu-${groupIndex}-${index}`} {...item} />
))}
</>
))}
</>
);
}

View File

@@ -0,0 +1,36 @@
import {
ListItemIcon,
ListItemText,
MenuItem,
MenuItemProps,
Typography,
} from "@mui/material";
export interface IContextMenuItem extends Partial<MenuItemProps> {
onClick: () => void;
icon?: React.ReactNode;
label: string;
disabled?: boolean;
hotkeyLabel?: string;
}
export default function ContextMenuItem({
onClick,
icon,
label,
disabled,
hotkeyLabel,
...props
}: IContextMenuItem) {
return (
<MenuItem {...props} disabled={disabled} onClick={onClick}>
<ListItemIcon>{icon}</ListItemIcon>
<ListItemText>{label}</ListItemText>
{hotkeyLabel && (
<Typography variant="body2" color="text.secondary">
{hotkeyLabel}
</Typography>
)}
</MenuItem>
);
}

View File

@@ -0,0 +1,2 @@
export * from "./ContextMenu";
export { default } from "./ContextMenu";

View File

@@ -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";

View File

@@ -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 (
<>
{/* <Suspense fallback={<Loading message="Loading header" />}>
<Hotkeys selectedCell={selectedCell} />
</Suspense> */}
<Suspense fallback={<Loading message="Loading fields" />}>
{/* <Hotkeys selectedCell={selectedCell} /> */}
<TableContainer rowHeight={rowHeight}>
<DndProvider backend={HTML5Backend}>
{showLeftScrollDivider && <div className="left-scroll-divider" />}
@@ -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 && <LinearProgress />}
</TableContainer>
{/* <ContextMenu />
<ContextMenu />
{/*
<BulkActions
selectedRows={selectedRows}
columns={columns}
@@ -333,6 +334,6 @@ export default function Table({
setSelectedRows([]);
}}
/> */}
</>
</Suspense>
);
}

View File

@@ -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<any>) {
// const { setAnchorEle } = useSetAnchorEle();
const handleContextMenu = (
e: React.MouseEvent<HTMLDivElement, 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)

View File

@@ -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<void>
): 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;

View File

@@ -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,

View File

@@ -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<void>
): 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: <Cut />, onClick: handleCut },
// { label: "Cut", icon: <Cut />, onClick: handleCut },
{ label: "Copy", icon: <CopyCells />, onClick: handleCopy },
{ label: "Paste", icon: <Paste />, onClick: handlePaste },
];
return contextMenuActions;
}
};
export default BasicContextMenuActions;

View File

@@ -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<typeof selectedCellAtom["read"]>,
reset: () => Promise<void>
) => IContextMenuActions[];
selectedCell: SelectedCell,
reset: () => void
) => IContextMenuItem[];
TableCell: React.ComponentType<FormatterProps<TableRow>>;
TableEditor: React.ComponentType<EditorProps<TableRow, any>>;
SideDrawerField: React.ComponentType<ISideDrawerFieldProps>;

View File

@@ -8,11 +8,11 @@ import reportWebVitals from "./reportWebVitals";
const container = document.getElementById("root")!;
const root = createRoot(container);
root.render(
// <StrictMode>
<Providers>
<App />
</Providers>
// </StrictMode>
<StrictMode>
<Providers>
<App />
</Providers>
</StrictMode>
);
// If you want to start measuring performance in your app, pass a function