Files
rowy/src/components/ColumnMenu/ColumnMenu.tsx
il3ven 91f2a4822f improvements such as:
1. Use generateId function instead of Math.random()

2. Remove `tablePage` from useEffect in `Table.tsx`

Upon an error in the firebase query this was causing an infinite loop
to increase page number.

3. Change the logic of when the clear and disable button are shown/disabled

The existing logic was preventing certain tasks to be performed.

For example, deleting all filters individually was disabling both apply
and clear all button. As a user I expect to delete all filters and
click on the apply button to save.
2023-08-21 09:42:31 +00:00

539 lines
16 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { useAtom, useSetAtom } from "jotai";
import { useSnackbar } from "notistack";
import {
Menu,
ListItem,
ListItemIcon,
ListItemText,
Typography,
Divider,
} from "@mui/material";
import FilterIcon from "@mui/icons-material/FilterList";
import LockOpenIcon from "@mui/icons-material/LockOpen";
import LockIcon from "@mui/icons-material/LockOutlined";
import VisibilityIcon from "@mui/icons-material/VisibilityOutlined";
import {
Freeze as FreezeIcon,
Unfreeze as UnfreezeIcon,
CellResize as CellResizeIcon,
ColumnPlusBefore as ColumnPlusBeforeIcon,
ColumnPlusAfter as ColumnPlusAfterIcon,
ColumnRemove as ColumnRemoveIcon,
CloudLogs as LogsIcon,
} from "@src/assets/icons";
import ArrowDownwardIcon from "@mui/icons-material/ArrowDownward";
import ArrowUpwardIcon from "@mui/icons-material/ArrowUpward";
import EditIcon from "@mui/icons-material/EditOutlined";
import SettingsIcon from "@mui/icons-material/SettingsOutlined";
import EvalIcon from "@mui/icons-material/PlayCircleOutline";
import MenuContents, { IMenuContentsProps } from "./MenuContents";
import ColumnHeader from "@src/components/Table/Mock/Column";
import {
projectScope,
userSettingsAtom,
updateUserSettingsAtom,
confirmDialogAtom,
rowyRunAtom,
altPressAtom,
} from "@src/atoms/projectScope";
import {
tableScope,
tableIdAtom,
tableSettingsAtom,
updateColumnAtom,
deleteColumnAtom,
tableSortsAtom,
columnMenuAtom,
columnModalAtom,
tableFiltersPopoverAtom,
tableNextPageAtom,
tableSchemaAtom,
cloudLogFiltersAtom,
tableModalAtom,
} from "@src/atoms/tableScope";
import { FieldType } from "@src/constants/fields";
import { getFieldProp } from "@src/components/fields";
import { analytics, logEvent } from "@src/analytics";
import {
formatSubTableName,
generateId,
getTableBuildFunctionPathname,
getTableSchemaPath,
} from "@src/utils/table";
import { runRoutes } from "@src/constants/runRoutes";
import { useSnackLogContext } from "@src/contexts/SnackLogContext";
import useSaveTableSorts from "@src/components/Table/ColumnHeader/useSaveTableSorts";
export interface IMenuModalProps {
name: string;
fieldName: string;
type: FieldType;
open: boolean;
config: Record<string, any>;
handleClose: () => void;
handleSave: (
fieldName: string,
config: Record<string, any>,
onSuccess?: Function
) => void;
}
export interface IColumnMenuProps {
canAddColumns: boolean;
canEditColumns: boolean;
canDeleteColumns: boolean;
}
export default function ColumnMenu({
canAddColumns,
canEditColumns,
canDeleteColumns,
}: IColumnMenuProps) {
const [userSettings] = useAtom(userSettingsAtom, projectScope);
const [updateUserSettings] = useAtom(updateUserSettingsAtom, projectScope);
const confirm = useSetAtom(confirmDialogAtom, projectScope);
const [rowyRun] = useAtom(rowyRunAtom, projectScope);
const [tableId] = useAtom(tableIdAtom, tableScope);
const [tableSettings] = useAtom(tableSettingsAtom, tableScope);
const updateColumn = useSetAtom(updateColumnAtom, tableScope);
const deleteColumn = useSetAtom(deleteColumnAtom, tableScope);
const [tableSorts, setTableSorts] = useAtom(tableSortsAtom, tableScope);
const [columnMenu, setColumnMenu] = useAtom(columnMenuAtom, tableScope);
const openColumnModal = useSetAtom(columnModalAtom, tableScope);
const openTableFiltersPopover = useSetAtom(
tableFiltersPopoverAtom,
tableScope
);
const [tableNextPage] = useAtom(tableNextPageAtom, tableScope);
const [tableSchema] = useAtom(tableSchemaAtom, tableScope);
const setModal = useSetAtom(tableModalAtom, tableScope);
const setCloudLogFilters = useSetAtom(cloudLogFiltersAtom, tableScope);
const snackLogContext = useSnackLogContext();
const [altPress] = useAtom(altPressAtom, projectScope);
const { enqueueSnackbar, closeSnackbar } = useSnackbar();
const triggerSaveTableSorts = useSaveTableSorts(canEditColumns);
if (!columnMenu) return null;
const { column, anchorEl } = columnMenu;
if (column.type === FieldType.last) return null;
const handleClose = () => {
setColumnMenu({ column, anchorEl: null });
setTimeout(() => setColumnMenu(null), 300);
};
const isConfigurable = Boolean(
getFieldProp("settings", column?.type) ||
getFieldProp("initializable", column?.type)
);
const _sortKey = getFieldProp("sortKey", (column as any).type);
const sortKey = _sortKey ? `${column.key}.${_sortKey}` : column.key;
const isSorted = tableSorts[0]?.key === sortKey;
const isAsc = isSorted && tableSorts[0]?.direction === "asc";
const userDocHiddenFields =
userSettings.tables?.[formatSubTableName(tableId)]?.hiddenFields ?? [];
let referencedColumns: string[] = [];
let referencedExtensions: string[] = [];
Object.entries(tableSchema?.columns ?? {}).forEach(([key, c], index) => {
if (
c.config?.listenerFields?.includes(column.key) ||
c.config?.requiredFields?.includes(column.key)
) {
referencedColumns.push(c.name);
}
});
tableSchema?.extensionObjects?.forEach((extension) => {
if (extension.requiredFields.includes(column.key)) {
referencedExtensions.push(extension.name);
}
});
const requireRebuild =
referencedColumns.length || referencedExtensions.length;
const handleDeleteColumn = () => {
deleteColumn(column.key);
if (requireRebuild) {
snackLogContext.requestSnackLog();
rowyRun({
route: runRoutes.buildFunction,
body: {
tablePath: tableSettings.collection,
// pathname must match old URL format
pathname: getTableBuildFunctionPathname(
tableSettings.id,
tableSettings.tableType
),
tableConfigPath: getTableSchemaPath(tableSettings),
},
});
logEvent(analytics, "deployed_extensions");
}
logEvent(analytics, "delete_column", { type: column.type });
handleClose();
};
const localViewActions: IMenuContentsProps["menuItems"] = [
{ type: "subheader", key: "subLocalView" },
{
key: "sortDesc",
label: "Sort: descending",
activeLabel: "Remove sort: descending",
icon: <ArrowDownwardIcon />,
onClick: () => {
setTableSorts(
isSorted && !isAsc ? [] : [{ key: sortKey, direction: "desc" }]
);
triggerSaveTableSorts(
isSorted && !isAsc ? [] : [{ key: sortKey, direction: "desc" }]
);
handleClose();
},
active: isSorted && !isAsc,
disabled: column.type === FieldType.id,
},
{
key: "sortAsc",
label: "Sort: ascending",
activeLabel: "Remove sort: ascending",
icon: <ArrowUpwardIcon />,
onClick: () => {
setTableSorts(
isSorted && isAsc ? [] : [{ key: sortKey, direction: "asc" }]
);
triggerSaveTableSorts(
isSorted && isAsc ? [] : [{ key: sortKey, direction: "asc" }]
);
handleClose();
},
active: isSorted && isAsc,
disabled: column.type === FieldType.id,
},
{
key: "hide",
label: "Hide",
icon: <VisibilityIcon />,
onClick: () => {
if (updateUserSettings)
updateUserSettings({
tables: {
[formatSubTableName(tableId)]: {
hiddenFields: [...userDocHiddenFields, column.key],
},
},
});
handleClose();
},
disabled: !updateUserSettings,
},
{
key: "filter",
label: "Filter…",
icon: <FilterIcon />,
onClick: () => {
openTableFiltersPopover({
defaultQuery: {
key: column.fieldName,
operator:
getFieldProp(
"filter",
column.type === FieldType.derivative
? column.config?.renderFieldType
: column.type
)!.operators[0]?.value || "==",
value: "",
id: generateId(),
},
});
handleClose();
},
active: column.hidden,
disabled: !getFieldProp(
"filter",
column.type === FieldType.derivative
? column.config?.renderFieldType
: column.type
),
},
];
const configActions: IMenuContentsProps["menuItems"] = [
{ type: "subheader", key: "subActionsConfig" },
{
label: "Lock",
key: "lock",
activeLabel: "Unlock",
icon: <LockOpenIcon />,
activeIcon: <LockIcon />,
onClick: () => {
updateColumn({
key: column.key,
config: { editable: !column.editable },
});
handleClose();
},
active: column.editable === false,
},
{
label: "Disable resize",
key: "disableResize",
activeLabel: "Enable resize",
icon: <CellResizeIcon />,
onClick: () => {
updateColumn({
key: column.key,
config: { resizable: !column.resizable },
});
handleClose();
},
active: !column.resizable,
},
{
label: "Freeze",
key: "freeze",
activeLabel: "Unfreeze",
icon: <FreezeIcon />,
activeIcon: <UnfreezeIcon />,
onClick: () => {
updateColumn({ key: column.key, config: { fixed: !column.fixed } });
handleClose();
},
active: column.fixed,
},
// { type: "subheader" },
{
label: "Rename…",
key: "rename",
icon: <EditIcon />,
onClick: () => {
openColumnModal({ type: "name", columnKey: column.key });
handleClose();
},
},
{
label: `Edit type: ${getFieldProp("name", column.type)}`,
key: "editType",
// This is based on the cell type
icon: getFieldProp("icon", column.type),
onClick: () => {
openColumnModal({ type: "type", columnKey: column.key });
handleClose();
},
},
{
label: `Column config…`,
key: "columConfig",
icon: <SettingsIcon />,
onClick: () => {
openColumnModal({ type: "config", columnKey: column.key });
handleClose();
},
disabled: !isConfigurable,
},
];
if (
column?.config?.defaultValue?.type === "dynamic" ||
[FieldType.action, FieldType.derivative, FieldType.connector].includes(
column.type
)
) {
configActions.push({
key: "logs",
label: altPress ? "Logs" : "Logs…",
icon: <LogsIcon />,
onClick: () => {
setModal("cloudLogs");
setCloudLogFilters({
type: "column",
timeRange: { type: "days", value: 7 },
column: [column.key],
});
},
});
}
// TODO: Generalize
const handleEvaluateAll = async () => {
try {
handleClose();
const evaluatingSnackKey = enqueueSnackbar(`Evaluating “${column.key}”…`);
const result = await rowyRun({
route: runRoutes.evaluateDerivative,
body: {
collectionPath: tableSettings.collection,
schemaDocPath: getTableSchemaPath(tableSettings),
columnKey: column.key,
},
});
closeSnackbar(evaluatingSnackKey);
if (result.success === false) {
enqueueSnackbar(result.message, { variant: "error" });
} else {
enqueueSnackbar(`Column “${column.key}” evaluated`, {
variant: "success",
});
}
} catch (error) {
enqueueSnackbar(`Failed: ${error}`, { variant: "error" });
}
};
const derivativeActions: IMenuContentsProps["menuItems"] = [
{ type: "subheader", key: "sub-derivative" },
{
key: "evaluateAll",
label: altPress ? "Evaluate all" : "Evaluate all…",
icon: <EvalIcon />,
onClick: altPress
? handleEvaluateAll
: () =>
confirm({
title: "Evaluate all?",
body: (
<>
All documents in {tableSettings.collection} collection will be
evaluated.
<br />{" "}
{tableNextPage.available &&
"For large collections this will take a while and might incur significant firestore costs."}
</>
),
handleConfirm: handleEvaluateAll,
confirm: "Evaluate",
}),
},
];
const columnActions: IMenuContentsProps["menuItems"] = [
{ type: "subheader", key: "subActions" },
{
label: "Insert to the left…",
key: "insertLeft",
icon: <ColumnPlusBeforeIcon />,
onClick: () => {
openColumnModal({ type: "new", index: column.index });
handleClose();
},
disabled: !canAddColumns,
},
{
label: "Insert to the right…",
key: "insertRight",
icon: <ColumnPlusAfterIcon />,
onClick: () => {
openColumnModal({ type: "new", index: column.index + 1 });
handleClose();
},
disabled: !canAddColumns,
},
{
label: `Delete column${altPress ? "" : "…"}`,
key: "delete",
icon: <ColumnRemoveIcon />,
onClick: altPress
? handleDeleteColumn
: () => {
return confirm({
title: "Delete column?",
body: (
<>
<Typography>
Only the column configuration will be deleted. No data will
be deleted. This cannot be undone.
</Typography>
<ColumnHeader type={column.type} label={column.name} />
<Typography sx={{ mt: 1 }}>
Key: <code style={{ userSelect: "all" }}>{column.key}</code>
</Typography>
{requireRebuild ? (
<>
<Divider sx={{ my: 2 }} />
{referencedColumns.length ? (
<Typography sx={{ mt: 1 }}>
This column will be removed as a dependency of the
following columns:{" "}
<Typography fontWeight="bold" component="span">
{referencedColumns.join(", ")}
</Typography>
</Typography>
) : null}
{referencedExtensions.length ? (
<Typography sx={{ mt: 1 }}>
This column will be removed as a dependency from the
following Extensions:{" "}
<Typography fontWeight="bold" component="span">
{referencedExtensions.join(", ")}
</Typography>
</Typography>
) : null}
<Typography sx={{ mt: 1, fontWeight: "bold" }}>
You need to re-deploy this tables cloud function.
</Typography>
</>
) : null}
</>
),
confirm: requireRebuild ? "Delete & re-deploy" : "Delete",
confirmColor: "error",
handleConfirm: handleDeleteColumn,
});
},
color: "error" as "error",
disabled: !canDeleteColumns,
},
];
let menuItems = [...localViewActions];
if (canEditColumns) {
menuItems.push.apply(menuItems, configActions);
if (column.type === FieldType.derivative) {
menuItems.push.apply(menuItems, derivativeActions);
}
}
if (canAddColumns || canDeleteColumns) {
menuItems.push.apply(menuItems, columnActions);
}
return (
<Menu
id="column-menu"
anchorEl={anchorEl}
open={Boolean(anchorEl)}
onClose={handleClose}
anchorOrigin={{ vertical: "bottom", horizontal: "right" }}
transformOrigin={{ vertical: "top", horizontal: "right" }}
MenuListProps={{ disablePadding: true }}
>
<ListItem>
<ListItemIcon style={{ minWidth: 36 }}>
{getFieldProp("icon", column.type)}
</ListItemIcon>
<ListItemText
primary={column.name as string}
secondary={
<>
Key: <code style={{ userSelect: "all" }}>{column.key}</code>
</>
}
primaryTypographyProps={{ variant: "subtitle2" }}
secondaryTypographyProps={{ variant: "caption" }}
sx={{ m: 0, minHeight: 40, "& > *": { userSelect: "none" } }}
/>
</ListItem>
<MenuContents menuItems={menuItems} />
</Menu>
);
}