mirror of
https://github.com/rowyio/rowy.git
synced 2025-12-28 16:06:41 +01:00
add ColumnMenu, independent ColumnModals
This commit is contained in:
@@ -1,8 +1,12 @@
|
||||
import { atom } from "jotai";
|
||||
import { atomWithStorage } from "jotai/utils";
|
||||
import { atomWithStorage, atomWithHash } from "jotai/utils";
|
||||
|
||||
import { DialogProps, ButtonProps } from "@mui/material";
|
||||
import { TableSettings, TableSchema } from "@src/types/table";
|
||||
import type { DialogProps, ButtonProps, PopoverProps } from "@mui/material";
|
||||
import type {
|
||||
TableSettings,
|
||||
TableSchema,
|
||||
ColumnConfig,
|
||||
} from "@src/types/table";
|
||||
import { getTableSchemaAtom } from "./project";
|
||||
|
||||
/** Nav open state stored in local storage. */
|
||||
@@ -154,6 +158,46 @@ export const tableDescriptionDismissedAtom = atomWithStorage<string[]>(
|
||||
[]
|
||||
);
|
||||
|
||||
/**
|
||||
* Open table column menu. Set to `null` to close.
|
||||
*
|
||||
* @example Basic usage:
|
||||
* ```
|
||||
* const openColumnMenu = useSetAtom(columnMenuAtom, globalScope);
|
||||
* openColumnMenu({ column, anchorEl: ... });
|
||||
* ```
|
||||
*
|
||||
* @example Close:
|
||||
* ```
|
||||
* openColumnMenu(null)
|
||||
* ```
|
||||
*/
|
||||
export const columnMenuAtom = atom<{
|
||||
column: ColumnConfig;
|
||||
anchorEl: PopoverProps["anchorEl"];
|
||||
} | null>(null);
|
||||
|
||||
/**
|
||||
* Opens a table column modal. Set to `null` to close.
|
||||
* Modals: new column, name change, type change, column settings.
|
||||
*
|
||||
* @example Basic usage:
|
||||
* ```
|
||||
* const openColumnModal = useSetAtom(columnModalAtom, globalScope);
|
||||
* openColumnModal({ type: "...", column });
|
||||
* ```
|
||||
*
|
||||
* @example Close:
|
||||
* ```
|
||||
* openColumnModal(null)
|
||||
* ```
|
||||
*/
|
||||
export const columnModalAtom = atomWithHash<{
|
||||
type: "new" | "name" | "type" | "config";
|
||||
columnKey?: string;
|
||||
index?: number;
|
||||
} | null>("columnModal", null, { replaceState: true });
|
||||
|
||||
/** Store current JSON editor view */
|
||||
export const jsonEditorAtom = atomWithStorage<"tree" | "code">(
|
||||
"__ROWY__JSON_EDITOR",
|
||||
|
||||
@@ -39,11 +39,18 @@ export const tableSchemaAtom = atom<TableSchema>({});
|
||||
export const updateTableSchemaAtom = atom<
|
||||
UpdateDocFunction<TableSchema> | undefined
|
||||
>(undefined);
|
||||
/** Store the table columns as an ordered array */
|
||||
/**
|
||||
* Store the table columns as an ordered array.
|
||||
* Puts frozen columns at the start, then sorts by ascending index.
|
||||
*/
|
||||
export const tableColumnsOrderedAtom = atom<ColumnConfig[]>((get) => {
|
||||
const tableSchema = get(tableSchemaAtom);
|
||||
if (!tableSchema || !tableSchema.columns) return [];
|
||||
return orderBy(Object.values(tableSchema?.columns ?? {}), "index");
|
||||
return orderBy(
|
||||
Object.values(tableSchema?.columns ?? {}),
|
||||
[(c) => Boolean(c.fixed), "index"],
|
||||
["desc", "asc"]
|
||||
);
|
||||
});
|
||||
/** Reducer function to convert from array of columns to columns object */
|
||||
export const tableColumnsReducer = (
|
||||
|
||||
319
src/components/ColumnMenu/ColumnMenu.tsx
Normal file
319
src/components/ColumnMenu/ColumnMenu.tsx
Normal file
@@ -0,0 +1,319 @@
|
||||
import { useAtom, useSetAtom } from "jotai";
|
||||
|
||||
import {
|
||||
Menu,
|
||||
ListItem,
|
||||
ListItemIcon,
|
||||
ListItemText,
|
||||
Typography,
|
||||
} 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 FreezeIcon from "@src/assets/icons/Freeze";
|
||||
import UnfreezeIcon from "@src/assets/icons/Unfreeze";
|
||||
import CellResizeIcon from "@src/assets/icons/CellResize";
|
||||
import ArrowDownwardIcon from "@mui/icons-material/ArrowDownward";
|
||||
import ArrowUpwardIcon from "@mui/icons-material/ArrowUpward";
|
||||
import EditIcon from "@mui/icons-material/EditOutlined";
|
||||
// import ReorderIcon from "@mui/icons-material/Reorder";
|
||||
import SettingsIcon from "@mui/icons-material/SettingsOutlined";
|
||||
import ColumnPlusBeforeIcon from "@src/assets/icons/ColumnPlusBefore";
|
||||
import ColumnPlusAfterIcon from "@src/assets/icons/ColumnPlusAfter";
|
||||
import ColumnRemoveIcon from "@src/assets/icons/ColumnRemove";
|
||||
|
||||
import MenuContents from "./MenuContents";
|
||||
import ColumnHeader from "@src/components/Table/Column";
|
||||
|
||||
import {
|
||||
globalScope,
|
||||
userSettingsAtom,
|
||||
updateUserSettingsAtom,
|
||||
confirmDialogAtom,
|
||||
columnMenuAtom,
|
||||
columnModalAtom,
|
||||
} from "@src/atoms/globalScope";
|
||||
import {
|
||||
tableScope,
|
||||
tableIdAtom,
|
||||
updateColumnAtom,
|
||||
deleteColumnAtom,
|
||||
tableOrdersAtom,
|
||||
} from "@src/atoms/tableScope";
|
||||
import { FieldType } from "@src/constants/fields";
|
||||
import { getFieldProp } from "@src/components/fields";
|
||||
import { analytics, logEvent } from "@src/analytics";
|
||||
import useKeyPress from "@src/hooks/useKeyPress";
|
||||
import { formatSubTableName } from "@src/utils/table";
|
||||
|
||||
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 default function ColumnMenu() {
|
||||
const [userSettings] = useAtom(userSettingsAtom, globalScope);
|
||||
const [updateUserSettings] = useAtom(updateUserSettingsAtom, globalScope);
|
||||
const [columnMenu, setColumnMenu] = useAtom(columnMenuAtom, globalScope);
|
||||
const openColumnModal = useSetAtom(columnModalAtom, globalScope);
|
||||
const confirm = useSetAtom(confirmDialogAtom, globalScope);
|
||||
const [tableId] = useAtom(tableIdAtom, tableScope);
|
||||
const updateColumn = useSetAtom(updateColumnAtom, tableScope);
|
||||
const deleteColumn = useSetAtom(deleteColumnAtom, tableScope);
|
||||
const [tableOrders, setTableOrders] = useAtom(tableOrdersAtom, tableScope);
|
||||
const altPress = useKeyPress("Alt");
|
||||
|
||||
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 = tableOrders[0]?.key === sortKey;
|
||||
const isAsc = isSorted && tableOrders[0]?.direction === "asc";
|
||||
|
||||
const userDocHiddenFields =
|
||||
userSettings.tables?.[formatSubTableName(tableId)]?.hiddenFields ?? [];
|
||||
|
||||
const handleDeleteColumn = () => {
|
||||
deleteColumn(column.key);
|
||||
logEvent(analytics, "delete_column", { type: column.type });
|
||||
handleClose();
|
||||
};
|
||||
|
||||
const menuItems = [
|
||||
{ type: "subheader", label: "Your view" },
|
||||
{
|
||||
label: "Sort: descending",
|
||||
activeLabel: "Remove sort: descending",
|
||||
icon: <ArrowDownwardIcon />,
|
||||
onClick: () => {
|
||||
setTableOrders(
|
||||
isSorted && !isAsc ? [] : [{ key: sortKey, direction: "desc" }]
|
||||
);
|
||||
handleClose();
|
||||
},
|
||||
active: isSorted && !isAsc,
|
||||
disabled: column.type === FieldType.id,
|
||||
},
|
||||
{
|
||||
label: "Sort: ascending",
|
||||
activeLabel: "Remove sort: ascending",
|
||||
icon: <ArrowUpwardIcon />,
|
||||
onClick: () => {
|
||||
setTableOrders(
|
||||
isSorted && isAsc ? [] : [{ key: sortKey, direction: "asc" }]
|
||||
);
|
||||
handleClose();
|
||||
},
|
||||
active: isSorted && isAsc,
|
||||
disabled: column.type === FieldType.id,
|
||||
},
|
||||
{
|
||||
label: "Hide",
|
||||
icon: <VisibilityIcon />,
|
||||
onClick: () => {
|
||||
if (updateUserSettings)
|
||||
updateUserSettings({
|
||||
tables: {
|
||||
[formatSubTableName(tableId)]: {
|
||||
hiddenFields: [...userDocHiddenFields, column.key],
|
||||
},
|
||||
},
|
||||
});
|
||||
handleClose();
|
||||
},
|
||||
disabled: !updateUserSettings,
|
||||
},
|
||||
{
|
||||
label: "Filter…",
|
||||
icon: <FilterIcon />,
|
||||
// FIXME: onClick: () => {
|
||||
// actions.update(column.key, { hidden: !column.hidden });
|
||||
// handleClose();
|
||||
// },
|
||||
active: column.hidden,
|
||||
disabled: true,
|
||||
},
|
||||
{ type: "subheader", label: "All users’ views" },
|
||||
{
|
||||
label: "Lock",
|
||||
activeLabel: "Unlock",
|
||||
icon: <LockOpenIcon />,
|
||||
activeIcon: <LockIcon />,
|
||||
onClick: () => {
|
||||
updateColumn({
|
||||
key: column.key,
|
||||
config: { editable: !column.editable },
|
||||
});
|
||||
handleClose();
|
||||
},
|
||||
active: !column.editable,
|
||||
},
|
||||
{
|
||||
label: "Disable resize",
|
||||
activeLabel: "Enable resize",
|
||||
icon: <CellResizeIcon />,
|
||||
onClick: () => {
|
||||
updateColumn({
|
||||
key: column.key,
|
||||
config: { resizable: !column.resizable },
|
||||
});
|
||||
handleClose();
|
||||
},
|
||||
active: !column.resizable,
|
||||
},
|
||||
{
|
||||
label: "Freeze",
|
||||
activeLabel: "Unfreeze",
|
||||
icon: <FreezeIcon />,
|
||||
activeIcon: <UnfreezeIcon />,
|
||||
onClick: () => {
|
||||
updateColumn({ key: column.key, config: { fixed: !column.fixed } });
|
||||
handleClose();
|
||||
},
|
||||
active: column.fixed,
|
||||
},
|
||||
{ type: "subheader", label: "Add column" },
|
||||
{
|
||||
label: "Add new to left…",
|
||||
icon: <ColumnPlusBeforeIcon />,
|
||||
onClick: () => {
|
||||
openColumnModal({ type: "new", index: column.index - 1 });
|
||||
handleClose();
|
||||
},
|
||||
},
|
||||
{
|
||||
label: "Add new to right…",
|
||||
icon: <ColumnPlusAfterIcon />,
|
||||
onClick: () => {
|
||||
openColumnModal({ type: "new", index: column.index + 1 });
|
||||
handleClose();
|
||||
},
|
||||
},
|
||||
{ type: "subheader", label: "Configure" },
|
||||
{
|
||||
label: "Rename…",
|
||||
icon: <EditIcon />,
|
||||
onClick: () => {
|
||||
openColumnModal({ type: "name", columnKey: column.key });
|
||||
handleClose();
|
||||
},
|
||||
},
|
||||
{
|
||||
label: `Edit type: ${getFieldProp("name", column.type)}…`,
|
||||
// This is based on the cell type
|
||||
icon: getFieldProp("icon", column.type),
|
||||
onClick: () => {
|
||||
openColumnModal({ type: "type", columnKey: column.key });
|
||||
handleClose();
|
||||
},
|
||||
},
|
||||
{
|
||||
label: `Column config…`,
|
||||
icon: <SettingsIcon />,
|
||||
onClick: () => {
|
||||
openColumnModal({ type: "config", columnKey: column.key });
|
||||
handleClose();
|
||||
},
|
||||
disabled: !isConfigurable,
|
||||
},
|
||||
// {
|
||||
// label: "Re-order",
|
||||
// icon: <ReorderIcon />,
|
||||
// onClick: () => alert("REORDER"),
|
||||
// },
|
||||
|
||||
// {
|
||||
// label: "Hide for everyone",
|
||||
// activeLabel: "Show",
|
||||
// icon: <VisibilityOffIcon />,
|
||||
// activeIcon: <VisibilityIcon />,
|
||||
// onClick: () => {
|
||||
// actions.update(column.key, { hidden: !column.hidden });
|
||||
// handleClose();
|
||||
// },
|
||||
// active: column.hidden,
|
||||
// color: "error" as "error",
|
||||
// },
|
||||
{
|
||||
label: `Delete column${altPress ? "" : "…"}`,
|
||||
icon: <ColumnRemoveIcon />,
|
||||
onClick: altPress
|
||||
? handleDeleteColumn
|
||||
: () =>
|
||||
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>
|
||||
</>
|
||||
),
|
||||
confirm: "Delete",
|
||||
confirmColor: "error",
|
||||
handleConfirm: handleDeleteColumn,
|
||||
}),
|
||||
color: "error" as "error",
|
||||
},
|
||||
];
|
||||
|
||||
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>
|
||||
);
|
||||
}
|
||||
51
src/components/ColumnMenu/MenuContents.tsx
Normal file
51
src/components/ColumnMenu/MenuContents.tsx
Normal file
@@ -0,0 +1,51 @@
|
||||
import { Fragment } from "react";
|
||||
|
||||
import { MenuItem, ListItemIcon, ListSubheader, Divider } from "@mui/material";
|
||||
|
||||
export interface IMenuContentsProps {
|
||||
menuItems: {
|
||||
type?: string;
|
||||
label?: string;
|
||||
activeLabel?: string;
|
||||
icon?: JSX.Element;
|
||||
activeIcon?: JSX.Element;
|
||||
onClick?: () => void;
|
||||
active?: boolean;
|
||||
color?: "error";
|
||||
disabled?: boolean;
|
||||
}[];
|
||||
}
|
||||
|
||||
export default function MenuContents({ menuItems }: IMenuContentsProps) {
|
||||
return (
|
||||
<>
|
||||
{menuItems.map((item, index) => {
|
||||
if (item.type === "subheader")
|
||||
return (
|
||||
<Fragment key={index}>
|
||||
<Divider variant="middle" sx={{ my: 0.5 }} />
|
||||
{item.label && (
|
||||
<ListSubheader disableSticky>{item.label}</ListSubheader>
|
||||
)}
|
||||
</Fragment>
|
||||
);
|
||||
|
||||
let icon: JSX.Element = item.icon ?? <></>;
|
||||
if (item.active && !!item.activeIcon) icon = item.activeIcon;
|
||||
|
||||
return (
|
||||
<MenuItem
|
||||
key={index}
|
||||
onClick={item.onClick}
|
||||
color={item.color}
|
||||
selected={item.active}
|
||||
disabled={item.disabled}
|
||||
>
|
||||
<ListItemIcon>{icon}</ListItemIcon>
|
||||
{item.active ? item.activeLabel : item.label}
|
||||
</MenuItem>
|
||||
);
|
||||
})}
|
||||
</>
|
||||
);
|
||||
}
|
||||
2
src/components/ColumnMenu/index.ts
Normal file
2
src/components/ColumnMenu/index.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export * from "./ColumnMenu";
|
||||
export { default } from "./ColumnMenu";
|
||||
218
src/components/ColumnModals/ColumnConfigModal/ColumnConfig.tsx
Normal file
218
src/components/ColumnModals/ColumnConfigModal/ColumnConfig.tsx
Normal file
@@ -0,0 +1,218 @@
|
||||
import { useState, Suspense, useMemo, createElement } from "react";
|
||||
import { useAtom, useSetAtom } from "jotai";
|
||||
import { set } from "lodash-es";
|
||||
import { ErrorBoundary } from "react-error-boundary";
|
||||
import { IColumnModalProps } from "@src/components/ColumnModals";
|
||||
|
||||
import { Typography, Stack } from "@mui/material";
|
||||
|
||||
import Modal from "@src/components/Modal";
|
||||
import { getFieldProp } from "@src/components/fields";
|
||||
import DefaultValueInput from "./DefaultValueInput";
|
||||
import { InlineErrorFallback } from "@src/components/ErrorFallback";
|
||||
import Loading from "@src/components/Loading";
|
||||
|
||||
import {
|
||||
globalScope,
|
||||
rowyRunAtom,
|
||||
confirmDialogAtom,
|
||||
} from "@src/atoms/globalScope";
|
||||
import {
|
||||
tableScope,
|
||||
tableSettingsAtom,
|
||||
updateColumnAtom,
|
||||
} from "@src/atoms/tableScope";
|
||||
import { useSnackLogContext } from "@src/contexts/SnackLogContext";
|
||||
import { FieldType } from "@src/constants/fields";
|
||||
import { runRoutes } from "@src/constants/runRoutes";
|
||||
import { useSnackbar } from "notistack";
|
||||
import { getSchemaPath } from "@src/utils/table";
|
||||
|
||||
export default function ColumnConfigModal({
|
||||
handleClose,
|
||||
column,
|
||||
}: IColumnModalProps) {
|
||||
const [rowyRun] = useAtom(rowyRunAtom, globalScope);
|
||||
const [tableSettings] = useAtom(tableSettingsAtom, tableScope);
|
||||
const updateColumn = useSetAtom(updateColumnAtom, tableScope);
|
||||
const confirm = useSetAtom(confirmDialogAtom, globalScope);
|
||||
const { enqueueSnackbar, closeSnackbar } = useSnackbar();
|
||||
const snackLogContext = useSnackLogContext();
|
||||
|
||||
const [showRebuildPrompt, setShowRebuildPrompt] = useState(false);
|
||||
const [newConfig, setNewConfig] = useState(column.config ?? {});
|
||||
const customFieldSettings = getFieldProp("settings", column.type);
|
||||
const settingsValidator = getFieldProp("settingsValidator", column.type);
|
||||
const initializable = getFieldProp("initializable", column.type);
|
||||
|
||||
const rendedFieldSettings = useMemo(
|
||||
() =>
|
||||
[FieldType.derivative, FieldType.aggregate].includes(column.type) &&
|
||||
newConfig.renderFieldType
|
||||
? getFieldProp("settings", newConfig.renderFieldType)
|
||||
: null,
|
||||
[newConfig.renderFieldType, column.type]
|
||||
);
|
||||
|
||||
const [errors, setErrors] = useState({});
|
||||
|
||||
const validateSettings = () => {
|
||||
if (settingsValidator) {
|
||||
const errors = settingsValidator(newConfig);
|
||||
setErrors(errors);
|
||||
return errors;
|
||||
}
|
||||
setErrors({});
|
||||
return {};
|
||||
};
|
||||
|
||||
const handleChange = (key: string) => (update: any) => {
|
||||
if (
|
||||
showRebuildPrompt === false &&
|
||||
(key.includes("defaultValue") || column.type === FieldType.derivative) &&
|
||||
column.config?.[key] !== update
|
||||
) {
|
||||
setShowRebuildPrompt(true);
|
||||
}
|
||||
const updatedConfig = set({ ...newConfig }, key, update);
|
||||
setNewConfig(updatedConfig);
|
||||
validateSettings();
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal
|
||||
maxWidth="md"
|
||||
onClose={handleClose}
|
||||
title={`${column.name}: Settings`}
|
||||
disableBackdropClick
|
||||
disableEscapeKeyDown
|
||||
children={
|
||||
<Suspense fallback={<Loading fullScreen={false} />}>
|
||||
<>
|
||||
{initializable && (
|
||||
<>
|
||||
<section style={{ marginTop: 1 }}>
|
||||
{/* top margin fixes visual bug */}
|
||||
<ErrorBoundary FallbackComponent={InlineErrorFallback}>
|
||||
<DefaultValueInput
|
||||
handleChange={handleChange}
|
||||
column={{ ...column, config: newConfig }}
|
||||
/>
|
||||
</ErrorBoundary>
|
||||
</section>
|
||||
</>
|
||||
)}
|
||||
|
||||
{customFieldSettings && (
|
||||
<Stack
|
||||
spacing={3}
|
||||
sx={{ borderTop: 1, borderColor: "divider", pt: 3 }}
|
||||
>
|
||||
{createElement(customFieldSettings, {
|
||||
config: newConfig,
|
||||
onChange: handleChange,
|
||||
fieldName: column.fieldName,
|
||||
onBlur: validateSettings,
|
||||
errors,
|
||||
})}
|
||||
</Stack>
|
||||
)}
|
||||
|
||||
{rendedFieldSettings && (
|
||||
<Stack
|
||||
spacing={3}
|
||||
sx={{ borderTop: 1, borderColor: "divider", pt: 3 }}
|
||||
>
|
||||
<Typography variant="subtitle1">
|
||||
Rendered field config
|
||||
</Typography>
|
||||
{createElement(rendedFieldSettings, {
|
||||
config: newConfig,
|
||||
onChange: handleChange,
|
||||
onBlur: validateSettings,
|
||||
errors,
|
||||
})}
|
||||
</Stack>
|
||||
)}
|
||||
{/* {
|
||||
<ConfigForm
|
||||
type={type}
|
||||
|
||||
config={newConfig}
|
||||
/>
|
||||
} */}
|
||||
</>
|
||||
</Suspense>
|
||||
}
|
||||
actions={{
|
||||
primary: {
|
||||
onClick: async () => {
|
||||
const errors = validateSettings();
|
||||
if (Object.keys(errors).length > 0) {
|
||||
confirm({
|
||||
title: "Invalid settings",
|
||||
body: (
|
||||
<>
|
||||
<Typography>Please fix the following settings:</Typography>
|
||||
<ul style={{ paddingLeft: "1.5em" }}>
|
||||
{Object.entries(errors).map(([key, message]) => (
|
||||
<li key={key}>
|
||||
<>
|
||||
<code>{key}</code>: {message}
|
||||
</>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</>
|
||||
),
|
||||
confirm: "Fix",
|
||||
hideCancel: true,
|
||||
handleConfirm: () => {},
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const savingSnack = enqueueSnackbar("Saving changes…");
|
||||
|
||||
await updateColumn({
|
||||
key: column.key,
|
||||
config: { config: newConfig },
|
||||
});
|
||||
|
||||
if (showRebuildPrompt) {
|
||||
confirm({
|
||||
title: "Deploy changes?",
|
||||
body: "You need to re-deploy this table’s cloud function to apply the changes you made. You can also re-deploy later.",
|
||||
confirm: "Deploy",
|
||||
cancel: "Later",
|
||||
handleConfirm: async () => {
|
||||
if (!rowyRun) return;
|
||||
snackLogContext.requestSnackLog();
|
||||
rowyRun({
|
||||
route: runRoutes.buildFunction,
|
||||
body: {
|
||||
tablePath: tableSettings.collection,
|
||||
pathname: window.location.pathname,
|
||||
tableConfigPath: getSchemaPath(tableSettings),
|
||||
},
|
||||
});
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
closeSnackbar(savingSnack);
|
||||
enqueueSnackbar("Changes saved");
|
||||
|
||||
handleClose();
|
||||
setShowRebuildPrompt(false);
|
||||
},
|
||||
children: "Update",
|
||||
},
|
||||
secondary: {
|
||||
onClick: handleClose,
|
||||
children: "Cancel",
|
||||
},
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,232 @@
|
||||
import { lazy, Suspense, createElement } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { useAtom } from "jotai";
|
||||
|
||||
import Checkbox from "@mui/material/Checkbox";
|
||||
import FormControlLabel from "@mui/material/FormControlLabel";
|
||||
import { Typography, TextField, MenuItem, ListItemText } from "@mui/material";
|
||||
|
||||
import { getFieldProp } from "@src/components/fields";
|
||||
import FieldSkeleton from "@src/components/SideDrawer/Form/FieldSkeleton";
|
||||
import CodeEditorHelper from "@src/components/CodeEditor/CodeEditorHelper";
|
||||
import FormAutosave from "./FormAutosave";
|
||||
import { FieldType } from "@src/constants/fields";
|
||||
import { WIKI_LINKS } from "@src/constants/externalLinks";
|
||||
|
||||
/* eslint-disable import/no-webpack-loader-syntax */
|
||||
import defaultValueDefs from "!!raw-loader!./defaultValue.d.ts";
|
||||
import {
|
||||
globalScope,
|
||||
compatibleRowyRunVersionAtom,
|
||||
projectSettingsAtom,
|
||||
} from "@src/atoms/globalScope";
|
||||
import { ColumnConfig } from "@src/types/table";
|
||||
|
||||
const CodeEditorComponent = lazy(
|
||||
() =>
|
||||
import("@src/components/CodeEditor" /* webpackChunkName: "CodeEditor" */)
|
||||
);
|
||||
|
||||
const diagnosticsOptions = {
|
||||
noSemanticValidation: false,
|
||||
noSyntaxValidation: false,
|
||||
noSuggestionDiagnostics: true,
|
||||
};
|
||||
|
||||
interface ICodeEditorProps {
|
||||
type: FieldType;
|
||||
column: ColumnConfig;
|
||||
handleChange: (key: string) => (update: string | undefined) => void;
|
||||
}
|
||||
|
||||
function CodeEditor({ type, column, handleChange }: ICodeEditorProps) {
|
||||
const [compatibleRowyRunVersion] = useAtom(
|
||||
compatibleRowyRunVersionAtom,
|
||||
globalScope
|
||||
);
|
||||
|
||||
const functionBodyOnly = compatibleRowyRunVersion!({ maxVersion: "1.3.10" });
|
||||
const returnType = getFieldProp("dataType", type) ?? "any";
|
||||
|
||||
let dynamicValueFn = "";
|
||||
if (functionBodyOnly) {
|
||||
dynamicValueFn = column.config?.defaultValue?.script || "";
|
||||
} else if (column.config?.defaultValue?.dynamicValueFn) {
|
||||
dynamicValueFn = column.config?.defaultValue?.dynamicValueFn;
|
||||
} else if (column.config?.defaultValue?.script) {
|
||||
dynamicValueFn = `const dynamicValueFn : DefaultValue = async ({row,ref,db,storage,auth})=>{
|
||||
${column.config?.defaultValue.script}
|
||||
}`;
|
||||
} else {
|
||||
dynamicValueFn = `const dynamicValueFn : DefaultValue = async ({row,ref,db,storage,auth})=>{
|
||||
// Write your default value code here
|
||||
// for example:
|
||||
// generate random hex color
|
||||
// const color = "#" + Math.floor(Math.random() * 16777215).toString(16);
|
||||
// return color;
|
||||
// checkout the documentation for more info: https://docs.rowy.io/how-to/default-values#dynamic
|
||||
}`;
|
||||
}
|
||||
|
||||
return (
|
||||
<CodeEditorComponent
|
||||
value={dynamicValueFn}
|
||||
diagnosticsOptions={functionBodyOnly ? undefined : diagnosticsOptions}
|
||||
extraLibs={[
|
||||
defaultValueDefs.replace(
|
||||
`"PLACEHOLDER_OUTPUT_TYPE"`,
|
||||
`${returnType} | Promise<${returnType}>`
|
||||
),
|
||||
]}
|
||||
onChange={handleChange(
|
||||
functionBodyOnly ? "defaultValue.script" : "defaultValue.dynamicValueFn"
|
||||
)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export interface IDefaultValueInputProps {
|
||||
handleChange: (key: string) => (update: any) => void;
|
||||
column: ColumnConfig;
|
||||
}
|
||||
|
||||
export default function DefaultValueInput({
|
||||
handleChange,
|
||||
column,
|
||||
}: IDefaultValueInputProps) {
|
||||
const [projectSettings] = useAtom(projectSettingsAtom, globalScope);
|
||||
|
||||
const _type =
|
||||
column.type !== FieldType.derivative
|
||||
? column.type
|
||||
: column.config?.renderFieldType ?? FieldType.shortText;
|
||||
const customFieldInput = getFieldProp("SideDrawerField", _type);
|
||||
const { control } = useForm({
|
||||
mode: "onBlur",
|
||||
defaultValues: {
|
||||
[column.fieldName]:
|
||||
column.config?.defaultValue?.value ??
|
||||
getFieldProp("initialValue", _type),
|
||||
},
|
||||
});
|
||||
|
||||
return (
|
||||
<>
|
||||
<TextField
|
||||
select
|
||||
label="Default value type"
|
||||
value={column.config?.defaultValue?.type ?? "undefined"}
|
||||
onChange={(e) => handleChange("defaultValue.type")(e.target.value)}
|
||||
fullWidth
|
||||
sx={{ mb: 1 }}
|
||||
>
|
||||
<MenuItem value="undefined">
|
||||
<ListItemText
|
||||
primary="Undefined"
|
||||
secondary="No default value. The field will not appear in the row’s corresponding Firestore document by default."
|
||||
/>
|
||||
</MenuItem>
|
||||
<MenuItem value="null">
|
||||
<ListItemText
|
||||
primary="Null"
|
||||
secondary={
|
||||
<>
|
||||
Initialise as <code>null</code>.
|
||||
</>
|
||||
}
|
||||
/>
|
||||
</MenuItem>
|
||||
<MenuItem value="static">
|
||||
<ListItemText
|
||||
primary="Static"
|
||||
secondary="Set a specific default value for all cells in this column."
|
||||
/>
|
||||
</MenuItem>
|
||||
<MenuItem
|
||||
value="dynamic"
|
||||
disabled={!projectSettings.rowyRunUrl}
|
||||
sx={{
|
||||
"&.Mui-disabled": { opacity: 1, color: "text.disabled" },
|
||||
"&.Mui-disabled .MuiListItemText-secondary": {
|
||||
color: "text.disabled",
|
||||
},
|
||||
}}
|
||||
>
|
||||
<ListItemText
|
||||
primary={
|
||||
projectSettings.rowyRunUrl ? (
|
||||
"Dynamic"
|
||||
) : (
|
||||
<>
|
||||
Dynamic —{" "}
|
||||
<Typography color="error" variant="inherit" component="span">
|
||||
Requires Rowy Run setup
|
||||
</Typography>
|
||||
</>
|
||||
)
|
||||
}
|
||||
secondary="Write code to set the default value using Rowy Run"
|
||||
/>
|
||||
</MenuItem>
|
||||
</TextField>
|
||||
{(!column.config?.defaultValue ||
|
||||
column.config?.defaultValue.type === "undefined") && (
|
||||
<>
|
||||
<FormControlLabel
|
||||
value="required"
|
||||
label={
|
||||
<>
|
||||
Make this column required
|
||||
<Typography
|
||||
variant="caption"
|
||||
color="text.secondary"
|
||||
display="block"
|
||||
>
|
||||
The row will not be created or updated unless all required
|
||||
values are set.
|
||||
</Typography>
|
||||
</>
|
||||
}
|
||||
control={
|
||||
<Checkbox
|
||||
checked={column.config?.required}
|
||||
onChange={(e) => handleChange("required")(e.target.checked)}
|
||||
name="required"
|
||||
/>
|
||||
}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
{column.config?.defaultValue?.type === "static" && customFieldInput && (
|
||||
<form>
|
||||
<FormAutosave
|
||||
control={control}
|
||||
handleSave={(values) =>
|
||||
handleChange("defaultValue.value")(values[column.fieldName])
|
||||
}
|
||||
/>
|
||||
|
||||
{createElement(customFieldInput, {
|
||||
column,
|
||||
control,
|
||||
docRef: {},
|
||||
disabled: false,
|
||||
})}
|
||||
</form>
|
||||
)}
|
||||
|
||||
{column.config?.defaultValue?.type === "dynamic" && (
|
||||
<>
|
||||
<CodeEditorHelper docLink={WIKI_LINKS.howToDefaultValues} />
|
||||
<Suspense fallback={<FieldSkeleton height={100} />}>
|
||||
<CodeEditor
|
||||
column={column}
|
||||
type={column.type}
|
||||
handleChange={handleChange}
|
||||
/>
|
||||
</Suspense>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
import { useEffect } from "react";
|
||||
import { useDebounce } from "use-debounce";
|
||||
import { isEqual } from "lodash-es";
|
||||
|
||||
import { Control, useWatch } from "react-hook-form";
|
||||
|
||||
export interface IAutosaveProps {
|
||||
control: Control;
|
||||
handleSave: (values: any) => void;
|
||||
debounce?: number;
|
||||
}
|
||||
|
||||
export default function FormAutosave({
|
||||
control,
|
||||
handleSave,
|
||||
debounce = 1000,
|
||||
}: IAutosaveProps) {
|
||||
const values = useWatch({ control });
|
||||
|
||||
const [debouncedValue] = useDebounce(values, debounce, {
|
||||
equalityFn: isEqual,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
handleSave(debouncedValue);
|
||||
}, [debouncedValue]);
|
||||
|
||||
return null;
|
||||
}
|
||||
8
src/components/ColumnModals/ColumnConfigModal/defaultValue.d.ts
vendored
Normal file
8
src/components/ColumnModals/ColumnConfigModal/defaultValue.d.ts
vendored
Normal file
@@ -0,0 +1,8 @@
|
||||
type DefaultValueContext = {
|
||||
row: Row;
|
||||
ref: FirebaseFirestore.DocumentReference;
|
||||
storage: firebasestorage.Storage;
|
||||
db: FirebaseFirestore.Firestore;
|
||||
auth: firebaseauth.BaseAuth;
|
||||
};
|
||||
type DefaultValue = (context: DefaultValueContext) => "PLACEHOLDER_OUTPUT_TYPE";
|
||||
2
src/components/ColumnModals/ColumnConfigModal/index.ts
Normal file
2
src/components/ColumnModals/ColumnConfigModal/index.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export * from "./ColumnConfig";
|
||||
export { default } from "./ColumnConfig";
|
||||
41
src/components/ColumnModals/ColumnModals.tsx
Normal file
41
src/components/ColumnModals/ColumnModals.tsx
Normal file
@@ -0,0 +1,41 @@
|
||||
import { useAtom } from "jotai";
|
||||
|
||||
import NewColumnModal from "./NewColumnModal";
|
||||
import NameChangeModal from "./NameChangeModal";
|
||||
import TypeChangeModal from "./TypeChangeModal";
|
||||
import ColumnConfigModal from "./ColumnConfigModal";
|
||||
|
||||
import { globalScope, columnModalAtom } from "@src/atoms/globalScope";
|
||||
import { tableScope, tableSchemaAtom } from "@src/atoms/tableScope";
|
||||
import { ColumnConfig } from "@src/types/table";
|
||||
|
||||
export interface IColumnModalProps {
|
||||
handleClose: () => void;
|
||||
column: ColumnConfig;
|
||||
}
|
||||
|
||||
export default function ColumnModals() {
|
||||
const [columnModal, setColumnModal] = useAtom(columnModalAtom, globalScope);
|
||||
const [tableSchema] = useAtom(tableSchemaAtom, tableScope);
|
||||
|
||||
if (!columnModal) return null;
|
||||
|
||||
const handleClose = () => setColumnModal(null);
|
||||
|
||||
if (columnModal.type === "new")
|
||||
return <NewColumnModal handleClose={handleClose} />;
|
||||
|
||||
const column = tableSchema.columns?.[columnModal.columnKey ?? ""];
|
||||
if (!column) return null;
|
||||
|
||||
if (columnModal.type === "name")
|
||||
return <NameChangeModal handleClose={handleClose} column={column} />;
|
||||
|
||||
if (columnModal.type === "type")
|
||||
return <TypeChangeModal handleClose={handleClose} column={column} />;
|
||||
|
||||
if (columnModal.type === "config")
|
||||
return <ColumnConfigModal handleClose={handleClose} column={column} />;
|
||||
|
||||
return null;
|
||||
}
|
||||
84
src/components/ColumnModals/FieldsDropdown.tsx
Normal file
84
src/components/ColumnModals/FieldsDropdown.tsx
Normal file
@@ -0,0 +1,84 @@
|
||||
import MultiSelect from "@rowy/multiselect";
|
||||
import { ListItemIcon } from "@mui/material";
|
||||
|
||||
import { FIELDS } from "@src/components/fields";
|
||||
import { FieldType } from "@src/constants/fields";
|
||||
import { getFieldProp } from "@src/components/fields";
|
||||
|
||||
export interface IFieldsDropdownProps {
|
||||
value: FieldType | "";
|
||||
onChange: (value: FieldType) => void;
|
||||
hideLabel?: boolean;
|
||||
label?: string;
|
||||
options?: FieldType[];
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns dropdown component of all available types
|
||||
*/
|
||||
export default function FieldsDropdown({
|
||||
value,
|
||||
onChange,
|
||||
hideLabel = false,
|
||||
label,
|
||||
options: optionsProp,
|
||||
...props
|
||||
}: IFieldsDropdownProps) {
|
||||
const fieldTypesToDisplay = optionsProp
|
||||
? FIELDS.filter((fieldConfig) => optionsProp.indexOf(fieldConfig.type) > -1)
|
||||
: FIELDS;
|
||||
const options = fieldTypesToDisplay.map((fieldConfig) => ({
|
||||
label: fieldConfig.name,
|
||||
value: fieldConfig.type,
|
||||
}));
|
||||
|
||||
return (
|
||||
<MultiSelect
|
||||
multiple={false}
|
||||
{...props}
|
||||
value={value ? value : ""}
|
||||
onChange={onChange}
|
||||
options={options}
|
||||
{...({
|
||||
AutocompleteProps: {
|
||||
groupBy: (option: typeof options[number]) =>
|
||||
getFieldProp("group", option.value),
|
||||
},
|
||||
} as any)}
|
||||
itemRenderer={(option) => (
|
||||
<>
|
||||
<ListItemIcon style={{ minWidth: 40 }}>
|
||||
{getFieldProp("icon", option.value as FieldType)}
|
||||
</ListItemIcon>
|
||||
{option.label}
|
||||
</>
|
||||
)}
|
||||
label={label || "Field type"}
|
||||
labelPlural="field types"
|
||||
TextFieldProps={{
|
||||
hiddenLabel: hideLabel,
|
||||
helperText: value && getFieldProp("description", value),
|
||||
...props.TextFieldProps,
|
||||
SelectProps: {
|
||||
displayEmpty: true,
|
||||
renderValue: () => (
|
||||
<>
|
||||
<ListItemIcon
|
||||
sx={{
|
||||
minWidth: 40,
|
||||
verticalAlign: "text-bottom",
|
||||
"& svg": { my: -0.5 },
|
||||
}}
|
||||
>
|
||||
{getFieldProp("icon", value as FieldType)}
|
||||
</ListItemIcon>
|
||||
{getFieldProp("name", value as FieldType)}
|
||||
</>
|
||||
),
|
||||
...props.TextFieldProps?.SelectProps,
|
||||
},
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
49
src/components/ColumnModals/NameChangeModal.tsx
Normal file
49
src/components/ColumnModals/NameChangeModal.tsx
Normal file
@@ -0,0 +1,49 @@
|
||||
import { useState } from "react";
|
||||
import { useSetAtom } from "jotai";
|
||||
import { IColumnModalProps } from ".";
|
||||
|
||||
import { TextField } from "@mui/material";
|
||||
import Modal from "@src/components/Modal";
|
||||
|
||||
import { tableScope, updateColumnAtom } from "@src/atoms/tableScope";
|
||||
|
||||
export default function NameChangeModal({
|
||||
handleClose,
|
||||
column,
|
||||
}: IColumnModalProps) {
|
||||
const updateColumn = useSetAtom(updateColumnAtom, tableScope);
|
||||
const [newName, setName] = useState(column.name);
|
||||
|
||||
return (
|
||||
<Modal
|
||||
onClose={handleClose}
|
||||
title="Rename column"
|
||||
maxWidth="xs"
|
||||
children={
|
||||
<TextField
|
||||
value={newName}
|
||||
autoFocus
|
||||
variant="filled"
|
||||
id="name"
|
||||
label="Column name"
|
||||
type="text"
|
||||
fullWidth
|
||||
onChange={(e) => setName(e.target.value)}
|
||||
/>
|
||||
}
|
||||
actions={{
|
||||
primary: {
|
||||
onClick: () => {
|
||||
updateColumn({ key: column.key, config: { name: newName } });
|
||||
handleClose();
|
||||
},
|
||||
children: "Update",
|
||||
},
|
||||
secondary: {
|
||||
onClick: handleClose,
|
||||
children: "Cancel",
|
||||
},
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
183
src/components/ColumnModals/NewColumnModal.tsx
Normal file
183
src/components/ColumnModals/NewColumnModal.tsx
Normal file
@@ -0,0 +1,183 @@
|
||||
import { useState } from "react";
|
||||
import { useAtom, useSetAtom } from "jotai";
|
||||
import { camelCase } from "lodash-es";
|
||||
import { IColumnModalProps } from ".";
|
||||
|
||||
import { TextField, Typography, Button } from "@mui/material";
|
||||
|
||||
import Modal from "@src/components/Modal";
|
||||
import FieldsDropdown from "./FieldsDropdown";
|
||||
|
||||
import {
|
||||
globalScope,
|
||||
columnModalAtom,
|
||||
updateTableAtom,
|
||||
} from "@src/atoms/globalScope";
|
||||
import {
|
||||
tableScope,
|
||||
tableSettingsAtom,
|
||||
addColumnAtom,
|
||||
} from "@src/atoms/tableScope";
|
||||
import { FieldType } from "@src/constants/fields";
|
||||
import { getFieldProp } from "@src/components/fields";
|
||||
import { analytics, logEvent } from "@src/analytics";
|
||||
|
||||
const AUDIT_FIELD_TYPES = [
|
||||
FieldType.createdBy,
|
||||
FieldType.createdAt,
|
||||
FieldType.updatedBy,
|
||||
FieldType.updatedAt,
|
||||
];
|
||||
|
||||
export default function NewColumnModal({
|
||||
handleClose,
|
||||
}: Pick<IColumnModalProps, "handleClose">) {
|
||||
const [columnModal, setColumnModal] = useAtom(columnModalAtom, globalScope);
|
||||
const [updateTable] = useAtom(updateTableAtom, globalScope);
|
||||
const [tableSettings] = useAtom(tableSettingsAtom, tableScope);
|
||||
const addColumn = useSetAtom(addColumnAtom, tableScope);
|
||||
|
||||
const [columnLabel, setColumnLabel] = useState("");
|
||||
const [fieldKey, setFieldKey] = useState("");
|
||||
const [type, setType] = useState(FieldType.shortText);
|
||||
const requireConfiguration = getFieldProp("requireConfiguration", type);
|
||||
|
||||
const isAuditField = AUDIT_FIELD_TYPES.includes(type);
|
||||
|
||||
const handleTypeChange = (type: FieldType) => {
|
||||
setType(type);
|
||||
switch (type) {
|
||||
case FieldType.id:
|
||||
setColumnLabel("ID");
|
||||
setFieldKey("id");
|
||||
break;
|
||||
case FieldType.createdBy:
|
||||
setColumnLabel("Created By");
|
||||
setFieldKey(tableSettings.auditFieldCreatedBy || "_createdBy");
|
||||
break;
|
||||
case FieldType.updatedBy:
|
||||
setColumnLabel("Updated By");
|
||||
setFieldKey(tableSettings.auditFieldUpdatedBy || "_updatedBy");
|
||||
break;
|
||||
case FieldType.createdAt:
|
||||
setColumnLabel("Created At");
|
||||
setFieldKey(
|
||||
(tableSettings.auditFieldCreatedBy || "_createdBy") + ".timestamp"
|
||||
);
|
||||
break;
|
||||
case FieldType.updatedAt:
|
||||
setColumnLabel("Updated At");
|
||||
setFieldKey(
|
||||
(tableSettings.auditFieldUpdatedBy || "_updatedBy") + ".timestamp"
|
||||
);
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal
|
||||
onClose={handleClose}
|
||||
title="Add new column"
|
||||
fullWidth
|
||||
maxWidth="xs"
|
||||
children={
|
||||
<>
|
||||
<section>
|
||||
<TextField
|
||||
value={columnLabel}
|
||||
autoFocus
|
||||
variant="filled"
|
||||
id="columnName"
|
||||
label="Column name"
|
||||
type="text"
|
||||
fullWidth
|
||||
onChange={(e) => {
|
||||
setColumnLabel(e.target.value);
|
||||
if (type !== FieldType.id && !isAuditField) {
|
||||
setFieldKey(camelCase(e.target.value));
|
||||
}
|
||||
}}
|
||||
helperText="Set the user-facing name for this column."
|
||||
/>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<TextField
|
||||
value={fieldKey}
|
||||
variant="filled"
|
||||
id="fieldKey"
|
||||
label="Field key"
|
||||
type="text"
|
||||
fullWidth
|
||||
onChange={(e) => setFieldKey(e.target.value)}
|
||||
disabled={
|
||||
(type === FieldType.id && fieldKey === "id") || isAuditField
|
||||
}
|
||||
helperText="Set the Firestore field key to link to this column. It will display any existing data for this field key."
|
||||
sx={{ "& .MuiInputBase-input": { fontFamily: "mono" } }}
|
||||
/>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<FieldsDropdown value={type} onChange={handleTypeChange} />
|
||||
</section>
|
||||
|
||||
{isAuditField && tableSettings.audit === false && (
|
||||
<section>
|
||||
<Typography gutterBottom>
|
||||
This field requires auditing to be enabled on this table.
|
||||
</Typography>
|
||||
|
||||
<Button
|
||||
variant="contained"
|
||||
color="primary"
|
||||
onClick={() => {
|
||||
if (updateTable)
|
||||
updateTable({
|
||||
id: tableSettings.id,
|
||||
tableType: tableSettings.tableType,
|
||||
audit: true,
|
||||
});
|
||||
}}
|
||||
>
|
||||
Enable auditing on this table
|
||||
</Button>
|
||||
</section>
|
||||
)}
|
||||
</>
|
||||
}
|
||||
actions={{
|
||||
primary: {
|
||||
onClick: () => {
|
||||
addColumn({
|
||||
config: {
|
||||
type,
|
||||
name: columnLabel,
|
||||
fieldName: fieldKey,
|
||||
key: fieldKey,
|
||||
config: {},
|
||||
},
|
||||
index: columnModal!.index,
|
||||
});
|
||||
if (requireConfiguration) {
|
||||
setColumnModal({ type: "config", columnKey: fieldKey });
|
||||
} else {
|
||||
handleClose();
|
||||
}
|
||||
logEvent(analytics, "create_column", { type });
|
||||
},
|
||||
disabled:
|
||||
!columnLabel ||
|
||||
!fieldKey ||
|
||||
!type ||
|
||||
(isAuditField && tableSettings.audit === false),
|
||||
children: requireConfiguration ? "Next" : "Add",
|
||||
},
|
||||
secondary: {
|
||||
onClick: handleClose,
|
||||
children: "Cancel",
|
||||
},
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
38
src/components/ColumnModals/TypeChangeModal.tsx
Normal file
38
src/components/ColumnModals/TypeChangeModal.tsx
Normal file
@@ -0,0 +1,38 @@
|
||||
import { useState } from "react";
|
||||
import { useSetAtom } from "jotai";
|
||||
import { IColumnModalProps } from ".";
|
||||
|
||||
import Modal from "@src/components/Modal";
|
||||
import FieldsDropdown from "./FieldsDropdown";
|
||||
|
||||
import { tableScope, updateColumnAtom } from "@src/atoms/tableScope";
|
||||
import { FieldType } from "@src/constants/fields";
|
||||
import { analytics, logEvent } from "analytics";
|
||||
|
||||
export default function TypeChangeModal({
|
||||
handleClose,
|
||||
column,
|
||||
}: IColumnModalProps) {
|
||||
const updateColumn = useSetAtom(updateColumnAtom, tableScope);
|
||||
const [newType, setType] = useState<FieldType>(column.type);
|
||||
|
||||
return (
|
||||
<Modal
|
||||
onClose={handleClose}
|
||||
title="Change column type"
|
||||
children={<FieldsDropdown value={newType} onChange={setType} />}
|
||||
actions={{
|
||||
primary: {
|
||||
onClick: () => {
|
||||
const prevType = column.type;
|
||||
updateColumn({ key: column.key, config: { type: newType } });
|
||||
handleClose();
|
||||
logEvent(analytics, "change_column_type", { newType, prevType });
|
||||
},
|
||||
children: "Update",
|
||||
},
|
||||
}}
|
||||
maxWidth="xs"
|
||||
/>
|
||||
);
|
||||
}
|
||||
2
src/components/ColumnModals/index.ts
Normal file
2
src/components/ColumnModals/index.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export * from "./ColumnModals";
|
||||
export { default } from "./ColumnModals";
|
||||
107
src/components/Table/Column.tsx
Normal file
107
src/components/Table/Column.tsx
Normal file
@@ -0,0 +1,107 @@
|
||||
import { Grid, GridProps, Typography } from "@mui/material";
|
||||
import { alpha } from "@mui/material/styles";
|
||||
|
||||
import { FieldType } from "@src/constants/fields";
|
||||
import { getFieldProp } from "@src/components/fields";
|
||||
|
||||
export interface IColumnProps extends Partial<GridProps> {
|
||||
label: string;
|
||||
type?: FieldType;
|
||||
secondaryItem?: React.ReactNode;
|
||||
|
||||
active?: boolean;
|
||||
}
|
||||
|
||||
export default function Column({
|
||||
label,
|
||||
type,
|
||||
secondaryItem,
|
||||
|
||||
active,
|
||||
...props
|
||||
}: IColumnProps) {
|
||||
return (
|
||||
<Grid
|
||||
container
|
||||
alignItems="center"
|
||||
wrap="nowrap"
|
||||
{...props}
|
||||
sx={[
|
||||
{
|
||||
width: "100%",
|
||||
height: 42,
|
||||
border: (theme) => `1px solid ${theme.palette.divider}`,
|
||||
backgroundColor: "background.default",
|
||||
|
||||
py: 0,
|
||||
px: 1,
|
||||
|
||||
color: "text.secondary",
|
||||
"&:hover": { color: "text.primary" },
|
||||
|
||||
"& svg": { display: "block" },
|
||||
},
|
||||
active
|
||||
? {
|
||||
backgroundColor: (theme) =>
|
||||
alpha(
|
||||
theme.palette.primary.main,
|
||||
theme.palette.action.selectedOpacity
|
||||
),
|
||||
color: (theme) =>
|
||||
theme.palette.mode === "dark"
|
||||
? theme.palette.text.primary
|
||||
: theme.palette.primary.dark,
|
||||
borderColor: (theme) =>
|
||||
alpha(
|
||||
theme.palette.primary.main,
|
||||
theme.palette.action.disabledOpacity
|
||||
),
|
||||
|
||||
"&:hover": {
|
||||
color: (theme) =>
|
||||
theme.palette.mode === "dark"
|
||||
? theme.palette.text.primary
|
||||
: theme.palette.primary.dark,
|
||||
},
|
||||
}
|
||||
: {},
|
||||
]}
|
||||
>
|
||||
{type && <Grid item>{getFieldProp("icon", type)}</Grid>}
|
||||
|
||||
<Grid
|
||||
item
|
||||
xs
|
||||
style={{
|
||||
flexShrink: 1,
|
||||
overflow: "hidden",
|
||||
}}
|
||||
>
|
||||
<Typography
|
||||
component={Grid}
|
||||
item
|
||||
variant="caption"
|
||||
noWrap
|
||||
sx={{
|
||||
fontWeight: "fontWeightMedium",
|
||||
lineHeight: "42px",
|
||||
display: "block",
|
||||
|
||||
userSelect: "none",
|
||||
|
||||
ml: 0.5,
|
||||
}}
|
||||
>
|
||||
{label}
|
||||
</Typography>
|
||||
</Grid>
|
||||
|
||||
{secondaryItem && (
|
||||
<Grid item sx={{ ml: 1 }}>
|
||||
{secondaryItem}
|
||||
</Grid>
|
||||
)}
|
||||
</Grid>
|
||||
);
|
||||
}
|
||||
@@ -18,12 +18,17 @@ import LockIcon from "@mui/icons-material/LockOutlined";
|
||||
|
||||
import ColumnHeaderSort from "./ColumnHeaderSort";
|
||||
|
||||
import { globalScope, userRolesAtom } from "@src/atoms/globalScope";
|
||||
import {
|
||||
globalScope,
|
||||
userRolesAtom,
|
||||
columnMenuAtom,
|
||||
} from "@src/atoms/globalScope";
|
||||
import { tableScope, updateColumnAtom } from "@src/atoms/tableScope";
|
||||
import { FieldType } from "@src/constants/fields";
|
||||
import { getFieldProp } from "@src/components/fields";
|
||||
import { DEFAULT_ROW_HEIGHT } from "@src/components/Table";
|
||||
import { ColumnConfig } from "@src/types/table";
|
||||
import useKeyPress from "@src/hooks/useKeyPress";
|
||||
|
||||
const LightTooltip = styled(({ className, ...props }: TooltipProps) => (
|
||||
<Tooltip {...props} classes={{ popper: className }} />
|
||||
@@ -47,6 +52,8 @@ export default function DraggableHeaderRenderer({
|
||||
}: IDraggableHeaderRendererProps) {
|
||||
const [userRoles] = useAtom(userRolesAtom, globalScope);
|
||||
const updateColumn = useSetAtom(updateColumnAtom, tableScope);
|
||||
const openColumnMenu = useSetAtom(columnMenuAtom, globalScope);
|
||||
const altPress = useKeyPress("Alt");
|
||||
|
||||
const [{ isDragging }, dragRef] = useDrag({
|
||||
type: "COLUMN_DRAG",
|
||||
@@ -59,7 +66,6 @@ export default function DraggableHeaderRenderer({
|
||||
const [{ isOver }, dropRef] = useDrop({
|
||||
accept: "COLUMN_DRAG",
|
||||
drop: ({ key }: { key: string }) => {
|
||||
console.log("drop", key, column.index);
|
||||
updateColumn({ key, config: {}, index: column.index });
|
||||
},
|
||||
collect: (monitor) => ({
|
||||
@@ -72,11 +78,7 @@ export default function DraggableHeaderRenderer({
|
||||
|
||||
const handleOpenMenu = (e: React.MouseEvent) => {
|
||||
e.preventDefault();
|
||||
// FIXME:
|
||||
// columnMenuRef?.current?.setSelectedColumnHeader({
|
||||
// column,
|
||||
// anchorEl: buttonRef.current,
|
||||
// });
|
||||
openColumnMenu({ column, anchorEl: buttonRef.current });
|
||||
};
|
||||
|
||||
return (
|
||||
@@ -186,7 +188,7 @@ export default function DraggableHeaderRenderer({
|
||||
component="div"
|
||||
color="inherit"
|
||||
>
|
||||
{column.name as string}
|
||||
{altPress ? `${column.index}: ${column.fieldName}` : column.name}
|
||||
</Typography>
|
||||
</LightTooltip>
|
||||
</Grid>
|
||||
|
||||
@@ -9,6 +9,10 @@ import { getFieldProp } from "@src/components/fields";
|
||||
|
||||
import { ColumnConfig } from "@src/types/table";
|
||||
|
||||
import { colord, extend } from "colord";
|
||||
import mixPlugin from "colord/plugins/lch";
|
||||
extend([mixPlugin]);
|
||||
|
||||
const SORT_STATES = ["none", "desc", "asc"] as const;
|
||||
|
||||
export interface IColumnHeaderSortProps {
|
||||
@@ -45,8 +49,19 @@ export default function ColumnHeaderSort({ column }: IColumnHeaderSortProps) {
|
||||
onClick={handleSortClick}
|
||||
color="inherit"
|
||||
sx={{
|
||||
bgcolor: "background.default",
|
||||
"&:hover": {
|
||||
backgroundColor: (theme) =>
|
||||
colord(theme.palette.background.default)
|
||||
.mix(
|
||||
theme.palette.action.hover,
|
||||
theme.palette.action.hoverOpacity
|
||||
)
|
||||
.alpha(1)
|
||||
.toHslString(),
|
||||
},
|
||||
|
||||
position: "relative",
|
||||
backgroundColor: "background.default",
|
||||
opacity: currentSort !== "none" ? 1 : 0,
|
||||
".column-header:hover &": { opacity: 1 },
|
||||
|
||||
@@ -59,7 +74,7 @@ export default function ColumnHeaderSort({ column }: IColumnHeaderSortProps) {
|
||||
),
|
||||
transform: currentSort === "asc" ? "rotate(180deg)" : "none",
|
||||
|
||||
"&:hover": {
|
||||
"&:hover svg": {
|
||||
transform:
|
||||
currentSort === "asc" || nextSort === "asc"
|
||||
? "rotate(180deg)"
|
||||
|
||||
@@ -1,30 +1,24 @@
|
||||
import { useAtom } from "jotai";
|
||||
import { useAtom, useSetAtom } from "jotai";
|
||||
import { Column } from "react-data-grid";
|
||||
|
||||
import { Button } from "@mui/material";
|
||||
import AddColumnIcon from "@src/assets/icons/AddColumn";
|
||||
|
||||
import { globalScope, userRolesAtom } from "@src/atoms/globalScope";
|
||||
import {
|
||||
globalScope,
|
||||
userRolesAtom,
|
||||
columnModalAtom,
|
||||
} from "@src/atoms/globalScope";
|
||||
|
||||
const FinalColumnHeader: Column<any>["headerRenderer"] = ({ column }) => {
|
||||
const [userRoles] = useAtom(userRolesAtom, globalScope);
|
||||
// FIXME: const { columnMenuRef } = useProjectContext();
|
||||
// if (!columnMenuRef) return null;
|
||||
const openColumnModal = useSetAtom(columnModalAtom, globalScope);
|
||||
|
||||
if (!userRoles.includes("ADMIN")) return null;
|
||||
|
||||
const handleClick = (
|
||||
event: React.MouseEvent<HTMLButtonElement, MouseEvent>
|
||||
) => {
|
||||
// columnMenuRef?.current?.setSelectedColumnHeader({
|
||||
// column,
|
||||
// anchorEl: event.currentTarget,
|
||||
// });
|
||||
};
|
||||
|
||||
return (
|
||||
<Button
|
||||
onClick={handleClick}
|
||||
onClick={(e) => openColumnModal({ type: "new" })}
|
||||
variant="contained"
|
||||
color="primary"
|
||||
startIcon={<AddColumnIcon />}
|
||||
|
||||
@@ -13,9 +13,7 @@ import DataGrid, {
|
||||
} from "react-data-grid";
|
||||
|
||||
import TableContainer, { OUT_OF_ORDER_MARGIN } from "./TableContainer";
|
||||
import TableToolbar from "@src/components/TableToolbar/TableToolbar";
|
||||
import ColumnHeader from "./ColumnHeader";
|
||||
// import ColumnMenu from "./ColumnMenu";
|
||||
// import ContextMenu from "./ContextMenu";
|
||||
import FinalColumnHeader from "./FinalColumnHeader";
|
||||
import FinalColumn from "./formatters/FinalColumn";
|
||||
@@ -138,21 +136,22 @@ export default function Table() {
|
||||
// Handle columns with field names that use dot notation (nested fields)
|
||||
const rows =
|
||||
useMemo(() => {
|
||||
const columnsWithNestedFieldNames = columns
|
||||
.map((col) => col.fieldName)
|
||||
.filter((fieldName) => fieldName.includes("."));
|
||||
// const columnsWithNestedFieldNames = columns
|
||||
// .map((col) => col.fieldName)
|
||||
// .filter((fieldName) => fieldName.includes("."));
|
||||
|
||||
if (columnsWithNestedFieldNames.length === 0) return tableRows;
|
||||
// if (columnsWithNestedFieldNames.length === 0)
|
||||
return tableRows;
|
||||
|
||||
return tableRows.map((row) =>
|
||||
columnsWithNestedFieldNames.reduce(
|
||||
(acc, fieldName) => ({
|
||||
...acc,
|
||||
[fieldName]: get(row, fieldName),
|
||||
}),
|
||||
{ ...row }
|
||||
)
|
||||
);
|
||||
// return tableRows.map((row) =>
|
||||
// columnsWithNestedFieldNames.reduce(
|
||||
// (acc, fieldName) => ({
|
||||
// ...acc,
|
||||
// [fieldName]: get(row, fieldName),
|
||||
// }),
|
||||
// { ...row }
|
||||
// )
|
||||
// );
|
||||
}, [columns, tableRows]) ?? [];
|
||||
|
||||
const rowsContainerRef = useRef<HTMLDivElement>(null);
|
||||
@@ -189,8 +188,6 @@ export default function Table() {
|
||||
<Hotkeys selectedCell={selectedCell} />
|
||||
</Suspense> */}
|
||||
<TableContainer ref={rowsContainerRef} rowHeight={rowHeight}>
|
||||
<TableToolbar />
|
||||
|
||||
<DndProvider backend={HTML5Backend}>
|
||||
<DataGrid
|
||||
onColumnResize={handleResize}
|
||||
@@ -276,7 +273,6 @@ export default function Table() {
|
||||
</DndProvider>
|
||||
</TableContainer>
|
||||
|
||||
{/* <ColumnMenu /> */}
|
||||
{/* <ContextMenu />
|
||||
<BulkActions
|
||||
selectedRows={selectedRows}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { styled, alpha, darken, lighten } from "@mui/material";
|
||||
import { APP_BAR_HEIGHT } from "@src/layouts/Navigation";
|
||||
import { TABLE_TOOLBAR_HEIGHT } from "@src/components/TableToolbar";
|
||||
// import { DRAWER_COLLAPSED_WIDTH } from "@src/components/SideDrawer";
|
||||
|
||||
import { colord, extend } from "colord";
|
||||
@@ -13,7 +14,7 @@ export const TableContainer = styled("div", {
|
||||
})<{ rowHeight: number }>(({ theme, rowHeight }) => ({
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
height: `calc(100vh - ${APP_BAR_HEIGHT}px)`,
|
||||
height: `calc(100vh - ${APP_BAR_HEIGHT}px - ${TABLE_TOOLBAR_HEIGHT}px)`,
|
||||
|
||||
"& > .rdg": {
|
||||
// FIXME:
|
||||
|
||||
@@ -72,7 +72,7 @@ export default function FinalColumn({ row }: FormatterProps<TableRow, any>) {
|
||||
<code
|
||||
style={{ userSelect: "all", wordBreak: "break-all" }}
|
||||
>
|
||||
{row.ref.path}
|
||||
{row._rowy_ref.path}
|
||||
</code>
|
||||
</>
|
||||
),
|
||||
@@ -94,6 +94,7 @@ export default function FinalColumn({ row }: FormatterProps<TableRow, any>) {
|
||||
),
|
||||
},
|
||||
}}
|
||||
disabled={!row._rowy_ref.path}
|
||||
>
|
||||
<DeleteIcon />
|
||||
</IconButton>
|
||||
|
||||
@@ -31,13 +31,10 @@ import { TableSettings } from "@src/types/table";
|
||||
import { analytics, logEvent } from "@src/analytics";
|
||||
|
||||
import { runRoutes } from "@src/constants/runRoutes";
|
||||
import {
|
||||
CONFIG,
|
||||
TABLE_GROUP_SCHEMAS,
|
||||
TABLE_SCHEMAS,
|
||||
} from "@src/config/dbPaths";
|
||||
import { CONFIG } from "@src/config/dbPaths";
|
||||
import { ROUTES } from "@src/constants/routes";
|
||||
import { useSnackLogContext } from "@src/contexts/SnackLogContext";
|
||||
import { getSchemaPath } from "@src/utils/table";
|
||||
|
||||
const customComponents = {
|
||||
tableName: {
|
||||
@@ -122,11 +119,7 @@ export default function TableSettingsDialog() {
|
||||
cancel: "Later",
|
||||
handleConfirm: async () => {
|
||||
const tablePath = data.collection;
|
||||
const tableConfigPath = `${
|
||||
data.tableType !== "collectionGroup"
|
||||
? TABLE_SCHEMAS
|
||||
: TABLE_GROUP_SCHEMAS
|
||||
}/${data.id}`;
|
||||
const tableConfigPath = getSchemaPath(data);
|
||||
|
||||
if (hasExtensions) {
|
||||
// find derivative, default value
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useRef, useState } from "react";
|
||||
import { useEffect, useRef, useMemo, useState } from "react";
|
||||
import { useAtom } from "jotai";
|
||||
import { isEqual } from "lodash-es";
|
||||
|
||||
@@ -7,8 +7,7 @@ import VisibilityOffIcon from "@mui/icons-material/VisibilityOffOutlined";
|
||||
|
||||
import MultiSelect from "@rowy/multiselect";
|
||||
import ButtonWithStatus from "@src/components/ButtonWithStatus";
|
||||
// FIXME:
|
||||
// import Column from "@src/components/Wizards/Column";
|
||||
import Column from "@src/components/Table/Column";
|
||||
|
||||
import {
|
||||
globalScope,
|
||||
@@ -18,6 +17,7 @@ import {
|
||||
import {
|
||||
tableScope,
|
||||
tableIdAtom,
|
||||
tableSchemaAtom,
|
||||
tableColumnsOrderedAtom,
|
||||
} from "@src/atoms/tableScope";
|
||||
import { formatSubTableName } from "@src/utils/table";
|
||||
@@ -27,16 +27,24 @@ export default function HiddenFields() {
|
||||
|
||||
const [userSettings] = useAtom(userSettingsAtom, globalScope);
|
||||
const [tableId] = useAtom(tableIdAtom, tableScope);
|
||||
const [tableSchema] = useAtom(tableSchemaAtom, tableScope);
|
||||
const [tableColumnsOrdered] = useAtom(tableColumnsOrderedAtom, tableScope);
|
||||
|
||||
const [open, setOpen] = useState(false);
|
||||
|
||||
// Store local selection here
|
||||
// Initialise hiddenFields from user doc
|
||||
const userDocHiddenFields =
|
||||
userSettings.tables?.[formatSubTableName(tableId)]?.hiddenFields ?? [];
|
||||
const userDocHiddenFields = useMemo(
|
||||
() =>
|
||||
userSettings.tables?.[formatSubTableName(tableId)]?.hiddenFields ?? [],
|
||||
[userSettings.tables, tableId]
|
||||
);
|
||||
|
||||
const [hiddenFields, setHiddenFields] =
|
||||
useState<string[]>(userDocHiddenFields);
|
||||
useEffect(() => {
|
||||
setHiddenFields(userDocHiddenFields);
|
||||
}, [userDocHiddenFields]);
|
||||
|
||||
const tableColumns = tableColumnsOrdered.map(({ key, name }) => ({
|
||||
value: key,
|
||||
@@ -61,12 +69,12 @@ export default function HiddenFields() {
|
||||
any
|
||||
>["renderOption"] = (props, option, { selected }) => (
|
||||
<li {...props}>
|
||||
{/* FIXME: <Column
|
||||
<Column
|
||||
label={option.label}
|
||||
type={tableState.columns[option.value]?.type}
|
||||
type={tableSchema.columns?.[option.value]?.type}
|
||||
secondaryItem={<VisibilityOffIcon className="hiddenIcon" />}
|
||||
active={selected}
|
||||
/> */}
|
||||
/>
|
||||
</li>
|
||||
);
|
||||
|
||||
@@ -89,49 +97,46 @@ export default function HiddenFields() {
|
||||
anchorEl: buttonRef.current,
|
||||
anchorOrigin: { vertical: "bottom", horizontal: "left" },
|
||||
transformOrigin: { vertical: "top", horizontal: "left" },
|
||||
},
|
||||
},
|
||||
}}
|
||||
{...({
|
||||
AutocompleteProps: {
|
||||
renderOption,
|
||||
sx: {
|
||||
"& .MuiAutocomplete-option": {
|
||||
padding: 0,
|
||||
paddingLeft: "0 !important",
|
||||
borderRadius: 0,
|
||||
marginBottom: "-1px",
|
||||
|
||||
"&::after": { content: "none" },
|
||||
sx: {
|
||||
"& .MuiAutocomplete-listbox .MuiAutocomplete-option": {
|
||||
padding: 0,
|
||||
paddingLeft: "0 !important",
|
||||
borderRadius: 0,
|
||||
marginBottom: "-1px",
|
||||
|
||||
"&:hover, &.Mui-focused, &.Mui-focusVisible": {
|
||||
backgroundColor: "transparent",
|
||||
"&::after": { content: "none" },
|
||||
|
||||
position: "relative",
|
||||
zIndex: 2,
|
||||
|
||||
"& > div": {
|
||||
color: "text.primary",
|
||||
borderColor: "currentColor",
|
||||
boxShadow: (theme: any) =>
|
||||
`0 0 0 1px ${theme.palette.text.primary} inset`,
|
||||
},
|
||||
"& .hiddenIcon": { opacity: 0.5 },
|
||||
},
|
||||
|
||||
'&[aria-selected="true"], &[aria-selected="true"].Mui-focused, &[aria-selected="true"].Mui-focusVisible':
|
||||
{
|
||||
"&:hover, &.Mui-focused, &.Mui-focusVisible": {
|
||||
backgroundColor: "transparent",
|
||||
|
||||
position: "relative",
|
||||
zIndex: 1,
|
||||
zIndex: 2,
|
||||
|
||||
"& .hiddenIcon": { opacity: 1 },
|
||||
"& > div": {
|
||||
color: "text.primary",
|
||||
borderColor: "currentColor",
|
||||
boxShadow: (theme: any) =>
|
||||
`0 0 0 1px ${theme.palette.text.primary} inset`,
|
||||
},
|
||||
"& .hiddenIcon": { opacity: 0.5 },
|
||||
},
|
||||
|
||||
'&[aria-selected="true"], &[aria-selected="true"].Mui-focused, &[aria-selected="true"].Mui-focusVisible':
|
||||
{
|
||||
backgroundColor: "transparent",
|
||||
|
||||
position: "relative",
|
||||
zIndex: 1,
|
||||
|
||||
"& .hiddenIcon": { opacity: 1 },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
} as any)}
|
||||
}}
|
||||
{...{ AutocompleteProps: { renderOption } }}
|
||||
label="Hidden fields"
|
||||
labelPlural="fields"
|
||||
options={tableColumns}
|
||||
|
||||
@@ -24,7 +24,7 @@ import {
|
||||
import { FieldType } from "@src/constants/fields";
|
||||
// import { useSnackLogContext } from "@src/contexts/SnackLogContext";
|
||||
|
||||
export const TABLE_HEADER_HEIGHT = 44;
|
||||
export const TABLE_TOOLBAR_HEIGHT = 44;
|
||||
|
||||
export default function TableToolbar() {
|
||||
const [userRoles] = useAtom(userRolesAtom, globalScope);
|
||||
@@ -49,7 +49,7 @@ export default function TableToolbar() {
|
||||
sx={{
|
||||
pl: (theme) => `max(env(safe-area-inset-left), ${theme.spacing(2)})`,
|
||||
pb: 1.5,
|
||||
height: TABLE_HEADER_HEIGHT,
|
||||
height: TABLE_TOOLBAR_HEIGHT,
|
||||
overflowX: "auto",
|
||||
overflowY: "hidden",
|
||||
"& > *": { flexShrink: 0 },
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
import { Fade, Stack, Button, Skeleton, SkeletonProps } from "@mui/material";
|
||||
import AddRowIcon from "@src/assets/icons/AddRow";
|
||||
|
||||
// FIXME:
|
||||
// import { TABLE_HEADER_HEIGHT } from "@src/components/TableToolbar";
|
||||
const TABLE_HEADER_HEIGHT = 44;
|
||||
import { TABLE_TOOLBAR_HEIGHT } from "@src/components/TableToolbar";
|
||||
|
||||
const ButtonSkeleton = (props: Partial<SkeletonProps>) => (
|
||||
<Skeleton
|
||||
@@ -26,7 +24,7 @@ export default function TableToolbarSkeleton() {
|
||||
pl: 2,
|
||||
pr: 2,
|
||||
pb: 1.5,
|
||||
height: TABLE_HEADER_HEIGHT,
|
||||
height: TABLE_TOOLBAR_HEIGHT,
|
||||
}}
|
||||
>
|
||||
<ButtonSkeleton>
|
||||
|
||||
@@ -19,7 +19,7 @@ import {
|
||||
import { tableScope, tableSettingsAtom } from "@src/atoms/tableScope";
|
||||
import { useActionParams } from "./FormDialog/Context";
|
||||
import { runRoutes } from "@src/constants/runRoutes";
|
||||
import { TABLE_SCHEMAS, TABLE_GROUP_SCHEMAS } from "@src/config/dbPaths";
|
||||
import { getSchemaPath } from "@src/utils/table";
|
||||
|
||||
const replacer = (data: any) => (m: string, key: string) => {
|
||||
const objKey = key.split(":")[0];
|
||||
@@ -80,11 +80,7 @@ export default function ActionFab({
|
||||
ref: { path: ref.path },
|
||||
column: { ...column, editor: undefined },
|
||||
action,
|
||||
schemaDocPath: `${
|
||||
tableSettings.tableType === "collectionGroup"
|
||||
? TABLE_GROUP_SCHEMAS
|
||||
: TABLE_SCHEMAS
|
||||
}/${tableSettings.id}`,
|
||||
schemaDocPath: getSchemaPath(tableSettings),
|
||||
actionParams,
|
||||
});
|
||||
|
||||
|
||||
@@ -27,7 +27,7 @@ import { getLabel } from "@src/components/fields/Connector/utils";
|
||||
import { useSnackbar } from "notistack";
|
||||
import { globalScope, rowyRunAtom } from "@src/atoms/globalScope";
|
||||
import { tableScope, tableSettingsAtom } from "@src/atoms/tableScope";
|
||||
import { TABLE_SCHEMAS, TABLE_GROUP_SCHEMAS } from "@src/config/dbPaths";
|
||||
import { getSchemaPath } from "@src/utils/table";
|
||||
|
||||
export interface IPopupContentsProps
|
||||
extends Omit<IConnectorSelectProps, "className" | "TextFieldProps"> {}
|
||||
@@ -74,11 +74,7 @@ export default function PopupContents({
|
||||
body: {
|
||||
columnKey: column.key,
|
||||
query: query,
|
||||
schemaDocPath: `${
|
||||
tableSettings.tableType === "collectionGroup"
|
||||
? TABLE_GROUP_SCHEMAS
|
||||
: TABLE_SCHEMAS
|
||||
}/${tableSettings.id}`,
|
||||
schemaDocPath: getSchemaPath(tableSettings),
|
||||
rowDocPath: docRef.path,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -5,7 +5,7 @@ import { useAtom, useSetAtom } from "jotai";
|
||||
import { Grid, InputLabel, FormHelperText } from "@mui/material";
|
||||
import MultiSelect from "@rowy/multiselect";
|
||||
import FieldSkeleton from "@src/components/SideDrawer/Form/FieldSkeleton";
|
||||
// FIXME: import FieldsDropdown from "@src/components/Table/ColumnMenu/FieldsDropdown";
|
||||
import FieldsDropdown from "@src/components/ColumnModals/FieldsDropdown";
|
||||
import CodeEditorHelper from "@src/components/CodeEditor/CodeEditorHelper";
|
||||
|
||||
import {
|
||||
@@ -108,7 +108,7 @@ export default function Settings({
|
||||
</Grid>
|
||||
|
||||
<Grid item xs={12} md={6}>
|
||||
{/* <FieldsDropdown
|
||||
<FieldsDropdown
|
||||
label="Output field type"
|
||||
value={config.renderFieldType}
|
||||
options={Object.values(FieldType).filter(
|
||||
@@ -129,7 +129,7 @@ export default function Settings({
|
||||
helperText: errors.renderFieldType,
|
||||
onBlur,
|
||||
}}
|
||||
/> */}
|
||||
/>
|
||||
</Grid>
|
||||
</Grid>
|
||||
|
||||
|
||||
@@ -8,25 +8,24 @@ import { Fade } from "@mui/material";
|
||||
import TableToolbarSkeleton from "@src/components/TableToolbar/TableToolbarSkeleton";
|
||||
import HeaderRowSkeleton from "@src/components/Table/HeaderRowSkeleton";
|
||||
import EmptyTable from "@src/components/Table/EmptyTable";
|
||||
import TableToolbar from "@src/components/TableToolbar";
|
||||
import Table from "@src/components/Table";
|
||||
import ColumnMenu from "@src/components/ColumnMenu";
|
||||
import ColumnModals from "@src/components/ColumnModals";
|
||||
|
||||
import { currentUserAtom, globalScope } from "@src/atoms/globalScope";
|
||||
import TableSourceFirestore from "@src/sources/TableSourceFirestore";
|
||||
import {
|
||||
tableScope,
|
||||
tableIdAtom,
|
||||
tableSettingsAtom,
|
||||
// tableSettingsAtom,
|
||||
tableSchemaAtom,
|
||||
} from "@src/atoms/tableScope";
|
||||
import ActionParamsProvider from "@src/components/fields/Action/FormDialog/Provider";
|
||||
|
||||
function TablePage() {
|
||||
const [tableId] = useAtom(tableIdAtom, tableScope);
|
||||
const [tableSettings] = useAtom(tableSettingsAtom, tableScope);
|
||||
const [tableSchema] = useAtom(tableSchemaAtom, tableScope);
|
||||
|
||||
console.log(tableSchema);
|
||||
|
||||
if (isEmpty(tableSchema.columns))
|
||||
return (
|
||||
<Fade style={{ transitionDelay: "500ms" }}>
|
||||
@@ -37,11 +36,20 @@ function TablePage() {
|
||||
);
|
||||
|
||||
return (
|
||||
// <Suspense fallback={<div>Loading rows…</div>}>
|
||||
<ActionParamsProvider>
|
||||
<Table />
|
||||
<Suspense fallback={<TableToolbarSkeleton />}>
|
||||
<TableToolbar />
|
||||
</Suspense>
|
||||
|
||||
<Suspense fallback={<HeaderRowSkeleton />}>
|
||||
<Table />
|
||||
</Suspense>
|
||||
|
||||
<Suspense>
|
||||
<ColumnMenu />
|
||||
<ColumnModals />
|
||||
</Suspense>
|
||||
</ActionParamsProvider>
|
||||
// </Suspense>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
8
src/types/table.d.ts
vendored
8
src/types/table.d.ts
vendored
@@ -85,9 +85,13 @@ export type ColumnConfig = {
|
||||
/** Set column width for all users */
|
||||
width?: number;
|
||||
/** If false (not undefined), locks the column for all users */
|
||||
editable?: boolean;
|
||||
editable?: boolean = true;
|
||||
/** Hide the column for all users */
|
||||
hidden?: boolean;
|
||||
hidden?: boolean = false;
|
||||
/** Freeze the column to the left */
|
||||
fixed?: boolean = false;
|
||||
/** Prevent column resizability */
|
||||
resizable?: boolean = true;
|
||||
|
||||
config?: {
|
||||
/** Set column to required */
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { mergeWith, isArray, get } from "lodash-es";
|
||||
import type { User } from "firebase/auth";
|
||||
import { TABLE_GROUP_SCHEMAS, TABLE_SCHEMAS } from "@src/config/dbPaths";
|
||||
import { TableSettings } from "@src/types/table";
|
||||
|
||||
/**
|
||||
* Creates a standard user object to write to table rows
|
||||
@@ -98,18 +99,26 @@ export const decrementId = (id: string = "zzzzzzzzzzzzzzzzzzzz") => {
|
||||
// Gets sub-table ID in $1
|
||||
const formatPathRegex = /\/[^\/]+\/([^\/]+)/g;
|
||||
|
||||
/** Format table path */
|
||||
export const formatPath = (
|
||||
tablePath: string,
|
||||
isCollectionGroup: boolean = false
|
||||
) => {
|
||||
return `${
|
||||
isCollectionGroup ? TABLE_GROUP_SCHEMAS : TABLE_SCHEMAS
|
||||
}/${tablePath.replace(formatPathRegex, "/subTables/$1")}`;
|
||||
};
|
||||
/**
|
||||
* Gets the path to the table’s schema doc, accounting for sub-tables
|
||||
* and collectionGroup tables
|
||||
* @param id - The table ID (could include sub-table ID)
|
||||
* @param tableType - primaryCollection (default) or collectionGroup
|
||||
* @returns Path to the table’s schema doc
|
||||
*/
|
||||
export const getSchemaPath = (
|
||||
tableSettings: Pick<TableSettings, "id" | "tableType">
|
||||
) =>
|
||||
(tableSettings.tableType === "collectionGroup"
|
||||
? TABLE_GROUP_SCHEMAS
|
||||
: TABLE_SCHEMAS) +
|
||||
"/" +
|
||||
tableSettings.id.replace(formatPathRegex, "/subTables/$1");
|
||||
|
||||
/** Format sub-table name to store settings in user settings */
|
||||
export const formatSubTableName = (tablePath: string) =>
|
||||
tablePath
|
||||
? tablePath.replace(formatPathRegex, "/subTables/$1").replace(/\//g, "_")
|
||||
: "";
|
||||
/**
|
||||
* Format sub-table name to store settings in user settings
|
||||
* @param id - Sub-table ID, including parent table ID
|
||||
* @returns Standardized sub-table name
|
||||
*/
|
||||
export const formatSubTableName = (id?: string) =>
|
||||
id ? id.replace(formatPathRegex, "/subTables/$1").replace(/\//g, "_") : "";
|
||||
|
||||
Reference in New Issue
Block a user