{documentCount} {column.name as string}: {label}
@@ -30,12 +34,12 @@ export default function SubTable({ column, row }: IHeavyCellProps) {
-
+
);
diff --git a/src/components/fields/SubTable/SideDrawerField.tsx b/src/components/fields/SubTable/SideDrawerField.tsx
index 32f2b1f8..abf8e1e0 100644
--- a/src/components/fields/SubTable/SideDrawerField.tsx
+++ b/src/components/fields/SubTable/SideDrawerField.tsx
@@ -6,7 +6,7 @@ import { ISideDrawerFieldProps } from "@src/components/fields/types";
import { Link } from "react-router-dom";
import { Box, Stack, IconButton } from "@mui/material";
-import LaunchIcon from "@mui/icons-material/Launch";
+import OpenIcon from "@mui/icons-material/OpenInBrowser";
import { tableScope, tableRowsAtom } from "@src/atoms/tableScope";
import { fieldSx, getFieldId } from "@src/components/SideDrawer/utils";
@@ -46,7 +46,7 @@ export default function SubTable({ column, _rowy_ref }: ISideDrawerFieldProps) {
sx={{ ml: 1 }}
disabled={!subTablePath}
>
-
+
);
diff --git a/src/components/fields/SubTable/index.tsx b/src/components/fields/SubTable/index.tsx
index b7ce171e..7e153771 100644
--- a/src/components/fields/SubTable/index.tsx
+++ b/src/components/fields/SubTable/index.tsx
@@ -1,14 +1,10 @@
import { lazy } from "react";
import { IFieldConfig, FieldType } from "@src/components/fields/types";
-import withHeavyCell from "@src/components/fields/_withTableCell/withHeavyCell";
+import withRenderTableCell from "@src/components/Table/TableCell/withRenderTableCell";
import { SubTable as SubTableIcon } from "@src/assets/icons";
-import BasicCell from "@src/components/fields/_BasicCell/BasicCellName";
-import NullEditor from "@src/components/Table/editors/NullEditor";
+import DisplayCell from "./DisplayCell";
-const TableCell = lazy(
- () => import("./TableCell" /* webpackChunkName: "TableCell-SubTable" */)
-);
const SideDrawerField = lazy(
() =>
import(
@@ -28,8 +24,10 @@ export const config: IFieldConfig = {
settings: Settings,
description:
"Connects to a sub-table in the current row. Also displays number of rows inside the sub-table. Max sub-table depth: 100.",
- TableCell: withHeavyCell(BasicCell, TableCell),
- TableEditor: NullEditor as any,
+ TableCell: withRenderTableCell(DisplayCell, null, "focus", {
+ usesRowData: true,
+ disablePadding: true,
+ }),
SideDrawerField,
initializable: false,
requireConfiguration: true,
diff --git a/src/components/fields/SubTable/utils.ts b/src/components/fields/SubTable/utils.ts
index e62e0a45..6f6c7746 100644
--- a/src/components/fields/SubTable/utils.ts
+++ b/src/components/fields/SubTable/utils.ts
@@ -6,7 +6,7 @@ import { ColumnConfig, TableRow, TableRowRef } from "@src/types/table";
export const useSubTableData = (
column: ColumnConfig,
row: TableRow,
- docRef: TableRowRef
+ _rowy_ref: TableRowRef
) => {
const label = (column.config?.parentLabel ?? []).reduce((acc, curr) => {
if (acc !== "") return `${acc} - ${row[curr]}`;
@@ -24,7 +24,7 @@ export const useSubTableData = (
let subTablePath = [
rootTablePath,
ROUTES.subTable,
- encodeURIComponent(docRef.path),
+ encodeURIComponent(_rowy_ref.path),
column.key,
].join("/");
diff --git a/src/components/fields/CreatedAt/TableCell.tsx b/src/components/fields/UpdatedAt/DisplayCell.tsx
similarity index 70%
rename from src/components/fields/CreatedAt/TableCell.tsx
rename to src/components/fields/UpdatedAt/DisplayCell.tsx
index b659618d..c6b29d9a 100644
--- a/src/components/fields/CreatedAt/TableCell.tsx
+++ b/src/components/fields/UpdatedAt/DisplayCell.tsx
@@ -1,9 +1,9 @@
-import { IHeavyCellProps } from "@src/components/fields/types";
+import { IDisplayCellProps } from "@src/components/fields/types";
import { format } from "date-fns";
import { DATE_TIME_FORMAT } from "@src/constants/dates";
-export default function CreatedAt({ column, value }: IHeavyCellProps) {
+export default function UpdatedAt({ column, value }: IDisplayCellProps) {
if (!value) return null;
const dateLabel = format(
value.toDate ? value.toDate() : value,
diff --git a/src/components/fields/UpdatedAt/index.tsx b/src/components/fields/UpdatedAt/index.tsx
index 3c2c3f28..d6e5eb92 100644
--- a/src/components/fields/UpdatedAt/index.tsx
+++ b/src/components/fields/UpdatedAt/index.tsx
@@ -1,14 +1,10 @@
import { lazy } from "react";
import { IFieldConfig, FieldType } from "@src/components/fields/types";
-import withHeavyCell from "@src/components/fields/_withTableCell/withHeavyCell";
+import withRenderTableCell from "@src/components/Table/TableCell/withRenderTableCell";
import { UpdatedAt as UpdatedAtIcon } from "@src/assets/icons";
-import BasicCell from "@src/components/fields/_BasicCell/BasicCellNull";
-import withSideDrawerEditor from "@src/components/Table/editors/withSideDrawerEditor";
+import DisplayCell from "./DisplayCell";
-const TableCell = lazy(
- () => import("./TableCell" /* webpackChunkName: "TableCell-UpdatedAt" */)
-);
const SideDrawerField = lazy(
() =>
import(
@@ -29,8 +25,7 @@ export const config: IFieldConfig = {
icon:
,
description:
"Displays the timestamp of the last update to the row. Read-only.",
- TableCell: withHeavyCell(BasicCell, TableCell),
- TableEditor: withSideDrawerEditor(TableCell),
+ TableCell: withRenderTableCell(DisplayCell, null),
SideDrawerField,
settings: Settings,
};
diff --git a/src/components/fields/UpdatedBy/TableCell.tsx b/src/components/fields/UpdatedBy/DisplayCell.tsx
similarity index 87%
rename from src/components/fields/UpdatedBy/TableCell.tsx
rename to src/components/fields/UpdatedBy/DisplayCell.tsx
index 5484626f..1bf71be3 100644
--- a/src/components/fields/UpdatedBy/TableCell.tsx
+++ b/src/components/fields/UpdatedBy/DisplayCell.tsx
@@ -1,11 +1,11 @@
-import { IHeavyCellProps } from "@src/components/fields/types";
+import { IDisplayCellProps } from "@src/components/fields/types";
import { Tooltip, Stack, Avatar } from "@mui/material";
import { format } from "date-fns";
import { DATE_TIME_FORMAT } from "@src/constants/dates";
-export default function UpdatedBy({ column, value }: IHeavyCellProps) {
+export default function UpdatedBy({ column, value }: IDisplayCellProps) {
if (!value || !value.displayName || !value.timestamp) return null;
const dateLabel = format(
value.timestamp.toDate ? value.timestamp.toDate() : value.timestamp,
diff --git a/src/components/fields/UpdatedBy/index.tsx b/src/components/fields/UpdatedBy/index.tsx
index 09ca1e2f..4b1c3a42 100644
--- a/src/components/fields/UpdatedBy/index.tsx
+++ b/src/components/fields/UpdatedBy/index.tsx
@@ -1,14 +1,10 @@
import { lazy } from "react";
import { IFieldConfig, FieldType } from "@src/components/fields/types";
-import withHeavyCell from "@src/components/fields/_withTableCell/withHeavyCell";
+import withRenderTableCell from "@src/components/Table/TableCell/withRenderTableCell";
import { UpdatedBy as UpdatedByIcon } from "@src/assets/icons";
-import BasicCell from "@src/components/fields/_BasicCell/BasicCellNull";
-import withSideDrawerEditor from "@src/components/Table/editors/withSideDrawerEditor";
+import DisplayCell from "./DisplayCell";
-const TableCell = lazy(
- () => import("./TableCell" /* webpackChunkName: "TableCell-UpdatedBy" */)
-);
const SideDrawerField = lazy(
() =>
import(
@@ -30,8 +26,7 @@ export const config: IFieldConfig = {
icon:
,
description:
"Displays the user that last updated the row, timestamp, and updated field key. Read-only.",
- TableCell: withHeavyCell(BasicCell, TableCell),
- TableEditor: withSideDrawerEditor(TableCell),
+ TableCell: withRenderTableCell(DisplayCell, null),
SideDrawerField,
settings: Settings,
};
diff --git a/src/components/fields/Url/TableCell.tsx b/src/components/fields/Url/DisplayCell.tsx
similarity index 75%
rename from src/components/fields/Url/TableCell.tsx
rename to src/components/fields/Url/DisplayCell.tsx
index 6c8affc3..20bd48dd 100644
--- a/src/components/fields/Url/TableCell.tsx
+++ b/src/components/fields/Url/DisplayCell.tsx
@@ -1,9 +1,9 @@
-import { IBasicCellProps } from "@src/components/fields/types";
+import { IDisplayCellProps } from "@src/components/fields/types";
import { Stack, IconButton } from "@mui/material";
import LaunchIcon from "@mui/icons-material/Launch";
-export default function Url({ value }: IBasicCellProps) {
+export default function Url({ value, tabIndex }: IDisplayCellProps) {
if (!value || typeof value !== "string") return null;
const href = value.includes("http") ? value : `https://${value}`;
@@ -13,8 +13,7 @@ export default function Url({ value }: IBasicCellProps) {
direction="row"
alignItems="center"
justifyContent="space-between"
- className="cell-collapse-padding"
- sx={{ p: "var(--cell-padding)", pr: 0.5 }}
+ sx={{ p: "var(--cell-padding)", pr: 0.5, width: "100%" }}
>
{value}
@@ -26,6 +25,7 @@ export default function Url({ value }: IBasicCellProps) {
size="small"
style={{ flexShrink: 0 }}
aria-label="Open in new tab"
+ tabIndex={tabIndex}
>
diff --git a/src/components/fields/Url/EditorCell.tsx b/src/components/fields/Url/EditorCell.tsx
new file mode 100644
index 00000000..d22c4880
--- /dev/null
+++ b/src/components/fields/Url/EditorCell.tsx
@@ -0,0 +1,6 @@
+import type { IEditorCellProps } from "@src/components/fields/types";
+import EditorCellTextField from "@src/components/Table/TableCell/EditorCellTextField";
+
+export default function Url(props: IEditorCellProps
) {
+ return ;
+}
diff --git a/src/components/fields/Url/index.tsx b/src/components/fields/Url/index.tsx
index 95376b06..be276388 100644
--- a/src/components/fields/Url/index.tsx
+++ b/src/components/fields/Url/index.tsx
@@ -1,12 +1,12 @@
import { lazy } from "react";
import { IFieldConfig, FieldType } from "@src/components/fields/types";
-import withBasicCell from "@src/components/fields/_withTableCell/withBasicCell";
+import withRenderTableCell from "@src/components/Table/TableCell/withRenderTableCell";
import UrlIcon from "@mui/icons-material/Link";
-import TableCell from "./TableCell";
-import TextEditor from "@src/components/Table/editors/TextEditor";
+import DisplayCell from "./DisplayCell";
+import EditorCell from "./EditorCell";
import { filterOperators } from "@src/components/fields/ShortText/Filter";
-import BasicContextMenuActions from "@src/components/fields/_BasicCell/BasicCellContextMenuActions";
+import BasicContextMenuActions from "@src/components/Table/ContextMenu/BasicCellContextMenuActions";
const SideDrawerField = lazy(
() =>
@@ -23,8 +23,9 @@ export const config: IFieldConfig = {
icon: ,
description: "Web address. Not validated.",
contextMenuActions: BasicContextMenuActions,
- TableCell: withBasicCell(TableCell),
- TableEditor: TextEditor,
+ TableCell: withRenderTableCell(DisplayCell, EditorCell, "focus", {
+ disablePadding: true,
+ }),
SideDrawerField,
filter: {
operators: filterOperators,
diff --git a/src/components/fields/User/TableCell.tsx b/src/components/fields/User/DisplayCell.tsx
similarity index 83%
rename from src/components/fields/User/TableCell.tsx
rename to src/components/fields/User/DisplayCell.tsx
index e1761826..d299b6c5 100644
--- a/src/components/fields/User/TableCell.tsx
+++ b/src/components/fields/User/DisplayCell.tsx
@@ -1,11 +1,11 @@
-import { IHeavyCellProps } from "@src/components/fields/types";
+import { IDisplayCellProps } from "@src/components/fields/types";
import { Tooltip, Stack, Avatar } from "@mui/material";
import { format } from "date-fns";
import { DATE_TIME_FORMAT } from "@src/constants/dates";
-export default function User({ value, column }: IHeavyCellProps) {
+export default function User({ value, column }: IDisplayCellProps) {
if (!value || !value.displayName) return null;
const chip = (
diff --git a/src/components/fields/User/index.tsx b/src/components/fields/User/index.tsx
index 70ec0a83..b06680df 100644
--- a/src/components/fields/User/index.tsx
+++ b/src/components/fields/User/index.tsx
@@ -1,14 +1,10 @@
import { lazy } from "react";
import { IFieldConfig, FieldType } from "@src/components/fields/types";
-import withHeavyCell from "@src/components/fields/_withTableCell/withHeavyCell";
+import withRenderTableCell from "@src/components/Table/TableCell/withRenderTableCell";
import UserIcon from "@mui/icons-material/PersonOutlined";
-import BasicCell from "@src/components/fields/_BasicCell/BasicCellNull";
-import withSideDrawerEditor from "@src/components/Table/editors/withSideDrawerEditor";
+import DisplayCell from "./DisplayCell";
-const TableCell = lazy(
- () => import("./TableCell" /* webpackChunkName: "TableCell-User" */)
-);
const SideDrawerField = lazy(
() =>
import("./SideDrawerField" /* webpackChunkName: "SideDrawerField-User" */)
@@ -27,8 +23,7 @@ export const config: IFieldConfig = {
initialValue: null,
icon: ,
description: "User information and optionally, timestamp. Read-only.",
- TableCell: withHeavyCell(BasicCell, TableCell),
- TableEditor: withSideDrawerEditor(TableCell),
+ TableCell: withRenderTableCell(DisplayCell, null),
SideDrawerField,
settings: Settings,
};
diff --git a/src/components/fields/_BasicCell/BasicCellName.tsx b/src/components/fields/_BasicCell/BasicCellName.tsx
deleted file mode 100644
index 5d93cd7b..00000000
--- a/src/components/fields/_BasicCell/BasicCellName.tsx
+++ /dev/null
@@ -1,5 +0,0 @@
-import { IBasicCellProps } from "@src/components/fields/types";
-
-export default function BasicCellName({ name }: IBasicCellProps) {
- return <>{name}>;
-}
diff --git a/src/components/fields/_BasicCell/BasicCellNull.tsx b/src/components/fields/_BasicCell/BasicCellNull.tsx
deleted file mode 100644
index 43d91ccb..00000000
--- a/src/components/fields/_BasicCell/BasicCellNull.tsx
+++ /dev/null
@@ -1,3 +0,0 @@
-export default function BasicCellNull() {
- return null;
-}
diff --git a/src/components/fields/_BasicCell/BasicCellValue.tsx b/src/components/fields/_BasicCell/BasicCellValue.tsx
deleted file mode 100644
index 8d8b0445..00000000
--- a/src/components/fields/_BasicCell/BasicCellValue.tsx
+++ /dev/null
@@ -1,6 +0,0 @@
-import { IBasicCellProps } from "@src/components/fields/types";
-
-export default function BasicCellValue({ value }: IBasicCellProps) {
- if (typeof value !== "string") return null;
- return <>{value}>;
-}
diff --git a/src/components/fields/_withTableCell/withBasicCell.tsx b/src/components/fields/_withTableCell/withBasicCell.tsx
deleted file mode 100644
index 09d0bcb9..00000000
--- a/src/components/fields/_withTableCell/withBasicCell.tsx
+++ /dev/null
@@ -1,41 +0,0 @@
-import { get } from "lodash-es";
-import { FormatterProps } from "react-data-grid";
-import { ErrorBoundary } from "react-error-boundary";
-import { IBasicCellProps } from "@src/components/fields/types";
-
-import { InlineErrorFallback } from "@src/components/ErrorFallback";
-import CellValidation from "@src/components/Table/CellValidation";
-import { FieldType } from "@src/constants/fields";
-import { TableRow } from "@src/types/table";
-
-/**
- * HOC to wrap around table cell components.
- * Renders read-only BasicCell only.
- * @param BasicCellComponent - The light cell component to display at all times
- */
-export default function withBasicCell(
- BasicCellComponent: React.ComponentType
-) {
- return function BasicCell(props: FormatterProps) {
- const { name, key } = props.column;
- const value = get(props.row, key);
-
- const { validationRegex, required } = (props.column as any).config;
-
- return (
-
-
-
-
-
- );
- };
-}
diff --git a/src/components/fields/_withTableCell/withHeavyCell.tsx b/src/components/fields/_withTableCell/withHeavyCell.tsx
deleted file mode 100644
index 29eaf370..00000000
--- a/src/components/fields/_withTableCell/withHeavyCell.tsx
+++ /dev/null
@@ -1,105 +0,0 @@
-import { Suspense, useState, useEffect } from "react";
-import { useSetAtom } from "jotai";
-import { get } from "lodash-es";
-import { FormatterProps } from "react-data-grid";
-import { ErrorBoundary } from "react-error-boundary";
-import { IBasicCellProps, IHeavyCellProps } from "@src/components/fields/types";
-
-import { InlineErrorFallback } from "@src/components/ErrorFallback";
-import CellValidation from "@src/components/Table/CellValidation";
-
-import { tableScope, updateFieldAtom } from "@src/atoms/tableScope";
-import { FieldType } from "@src/constants/fields";
-import { TableRow } from "@src/types/table";
-
-/**
- * HOC to wrap table cell components.
- * Renders read-only BasicCell while scrolling for better scroll performance.
- * @param BasicCellComponent - The lighter cell component to display while scrolling
- * @param HeavyCellComponent - The read/write cell component to display
- * @param readOnly - Prevent the component from updating the cell value
- */
-export default function withHeavyCell(
- BasicCellComponent: React.ComponentType,
- HeavyCellComponent: React.ComponentType,
- readOnly: boolean = false
-) {
- return function HeavyCell(props: FormatterProps) {
- const updateField = useSetAtom(updateFieldAtom, tableScope);
-
- const { validationRegex, required } = (props.column as any).config;
-
- // Initially display BasicCell to improve scroll performance
- const [displayedComponent, setDisplayedComponent] = useState<
- "basic" | "heavy"
- >("basic");
- // Then switch to HeavyCell once completed
- useEffect(() => {
- setTimeout(() => {
- setDisplayedComponent("heavy");
- });
- }, []);
-
- // TODO: Investigate if this still needs to be a state
- const value = get(props.row, props.column.key);
- const [localValue, setLocalValue] = useState(value);
- useEffect(() => {
- setLocalValue(value);
- }, [value]);
-
- // Declare basicCell here so props can be reused by HeavyCellComponent
- const basicCellProps = {
- value: localValue,
- name: props.column.name as string,
- type: (props.column as any).type as FieldType,
- };
- const basicCell = ;
-
- if (displayedComponent === "basic")
- return (
-
-
- {basicCell}
-
-
- );
-
- const handleSubmit = (value: any) => {
- if (readOnly) return;
- updateField({
- path: props.row._rowy_ref.path,
- fieldName: props.column.key,
- value,
- });
- setLocalValue(value);
- };
-
- if (displayedComponent === "heavy")
- return (
-
-
-
-
-
-
-
- );
-
- // Should not reach this line
- return null;
- };
-}
diff --git a/src/components/fields/_withTableCell/withPopoverCell.tsx b/src/components/fields/_withTableCell/withPopoverCell.tsx
deleted file mode 100644
index 5da116ca..00000000
--- a/src/components/fields/_withTableCell/withPopoverCell.tsx
+++ /dev/null
@@ -1,184 +0,0 @@
-import { Suspense, useState, useEffect, useRef } from "react";
-import { useSetAtom } from "jotai";
-import { find, get } from "lodash-es";
-import { FormatterProps } from "react-data-grid";
-import {
- IBasicCellProps,
- IPopoverInlineCellProps,
- IPopoverCellProps,
-} from "@src/components/fields/types";
-import { ErrorBoundary } from "react-error-boundary";
-
-import { Popover, PopoverProps } from "@mui/material";
-
-import { InlineErrorFallback } from "@src/components/ErrorFallback";
-import CellValidation from "@src/components/Table/CellValidation";
-
-import { tableScope, updateFieldAtom } from "@src/atoms/tableScope";
-import { FieldType } from "@src/constants/fields";
-
-export interface IPopoverCellOptions extends Partial {
- transparent?: boolean;
- readOnly?: boolean;
-}
-
-/**
- * HOC to wrap around table cell formatters.
- * Renders read-only BasicCell while scrolling for better scroll performance.
- * When the user clicks the heavier inline cell, it displays PopoverCell.
- * @param BasicCellComponent - The lighter cell component to display while scrolling
- * @param InlineCellComponent - The heavier cell component to display inline
- * @param PopoverCellComponent - The heavy read/write cell component to display in Popover
- * @param options - {@link IPopoverCellOptions}
- */
-export default function withPopoverCell(
- BasicCellComponent: React.ComponentType,
- InlineCellComponent: React.ForwardRefExoticComponent<
- IPopoverInlineCellProps & React.RefAttributes
- >,
- PopoverCellComponent: React.ComponentType,
- options?: IPopoverCellOptions
-) {
- return function PopoverCell(props: FormatterProps) {
- const { transparent, ...popoverProps } = options ?? {};
-
- const updateField = useSetAtom(updateFieldAtom, tableScope);
-
- const { validationRegex, required } = (props.column as any).config;
-
- // Initially display BasicCell to improve scroll performance
- const [displayedComponent, setDisplayedComponent] = useState<
- "basic" | "inline" | "popover"
- >("basic");
- // Then switch to heavier InlineCell once completed
- useEffect(() => {
- setTimeout(() => {
- setDisplayedComponent("inline");
- });
- }, []);
-
- // Store Popover open state here so we can add delay for close transition
- const [popoverOpen, setPopoverOpen] = useState(false);
-
- // Store ref to rendered InlineCell here to get positioning for PopoverCell
- const inlineCellRef = useRef(null);
-
- // TODO: Investigate if this still needs to be a state
- const value = get(props.row, props.column.key);
- const [localValue, setLocalValue] = useState(value);
- useEffect(() => {
- setLocalValue(value);
- }, [value]);
-
- // Declare basicCell here so props can be reused by HeavyCellComponent
- const basicCellProps = {
- value: localValue,
- name: props.column.name as string,
- type: (props.column as any).type as FieldType,
- };
-
- if (displayedComponent === "basic")
- return (
-
-
-
-
-
- );
-
- // This is where we update the documents
- const handleSubmit = (value: any) => {
- if (options?.readOnly) return;
- updateField({
- path: props.row._rowy_ref.path,
- fieldName: props.column.key,
- value,
- deleteField: value === undefined,
- });
- setLocalValue(value);
- };
- const showPopoverCell: any = (popover: boolean) => {
- if (popover) {
- setPopoverOpen(true);
- setDisplayedComponent("popover");
- } else {
- setPopoverOpen(false);
- setTimeout(() => setDisplayedComponent("inline"), 300);
- }
- };
-
- // Declare inlineCell and props here so it can be reused later
- const commonCellProps = {
- ...props,
- ...basicCellProps,
- column: props.column,
- onSubmit: handleSubmit,
- disabled: props.column.editable === false,
- docRef: props.row._rowy_ref,
- showPopoverCell,
- ref: inlineCellRef,
- };
- const inlineCell = (
-
- );
-
- if (displayedComponent === "inline")
- return (
-
-
- {inlineCell}
-
-
- );
-
- const parentRef = inlineCellRef.current?.parentElement;
-
- if (displayedComponent === "popover")
- return (
-
-
- {inlineCell}
-
-
-
- showPopoverCell(false)}
- {...popoverProps}
- sx={
- transparent
- ? {
- "& .MuiPopover-paper": { backgroundColor: "transparent" },
- }
- : {}
- }
- onClick={(e) => e.stopPropagation()}
- onDoubleClick={(e) => e.stopPropagation()}
- onKeyDown={(e) => e.stopPropagation()}
- >
-
-
-
-
- );
-
- // Should not reach this line
- return null;
- };
-}
diff --git a/src/components/fields/types.ts b/src/components/fields/types.ts
index 66890472..63374d81 100644
--- a/src/components/fields/types.ts
+++ b/src/components/fields/types.ts
@@ -1,15 +1,14 @@
import { FieldType } from "@src/constants/fields";
-import { FormatterProps, EditorProps } from "react-data-grid";
-import { Control, UseFormReturn } from "react-hook-form";
-import { PopoverProps } from "@mui/material";
-import {
+import { IRenderedTableCellProps } from "@src/components/Table/TableCell/withRenderTableCell";
+import type { PopoverProps } from "@mui/material";
+import type {
ColumnConfig,
TableRow,
TableRowRef,
TableFilter,
} from "@src/types/table";
-import { SelectedCell } from "@src/atoms/tableScope";
-import { IContextMenuItem } from "@src/components/Table/ContextMenu/ContextMenuItem";
+import type { SelectedCell } from "@src/atoms/tableScope";
+import type { IContextMenuItem } from "@src/components/Table/ContextMenu/ContextMenuItem";
export { FieldType };
@@ -28,8 +27,7 @@ export interface IFieldConfig {
selectedCell: SelectedCell,
reset: () => void
) => IContextMenuItem[];
- TableCell: React.ComponentType>;
- TableEditor: React.ComponentType>;
+ TableCell: React.ComponentType;
SideDrawerField: React.ComponentType;
settings?: React.ComponentType;
settingsValidator?: (config: Record) => Record;
@@ -44,52 +42,55 @@ export interface IFieldConfig {
csvImportParser?: (value: string, config?: any) => any;
}
-export interface IBasicCellProps {
- value: any;
+/** See {@link IRenderedTableCellProps | `withRenderTableCell` } for guidance */
+export interface IDisplayCellProps {
+ value: T;
type: FieldType;
name: string;
-}
-export interface IHeavyCellProps
- extends IBasicCellProps,
- FormatterProps {
- column: FormatterProps["column"] & { config?: Record };
- onSubmit: (value: any) => void;
- docRef: TableRowRef;
+ row: TableRow;
+ column: ColumnConfig;
+ /** The row’s _rowy_ref object */
+ _rowy_ref: TableRowRef;
disabled: boolean;
+ /**
+ * ⚠️ Make sure to use the `tabIndex` prop for buttons and other interactive
+ * elements.
+ */
+ tabIndex: number;
+ showPopoverCell: (value: boolean) => void;
+ setFocusInsideCell: (focusInside: boolean) => void;
+ rowHeight: number;
}
-
-export interface IPopoverInlineCellProps extends IHeavyCellProps {
- showPopoverCell: React.Dispatch>;
-}
-export interface IPopoverCellProps extends IPopoverInlineCellProps {
+/** See {@link IRenderedTableCellProps | `withRenderTableCell` } for guidance */
+export interface IEditorCellProps extends IDisplayCellProps {
+ /** Call when the user has input but changes have not been saved */
+ onDirty: (dirty?: boolean) => void;
+ /** Update the local value. Also calls onDirty */
+ onChange: (value: T) => void;
+ /** Call when user input is ready to be saved (e.g. onBlur) */
+ onSubmit: () => void;
+ /** Get parent element for popover positioning */
parentRef: PopoverProps["anchorEl"];
}
/** Props to be passed to all SideDrawerFields */
-export interface ISideDrawerFieldProps {
+export interface ISideDrawerFieldProps {
/** The column config */
- column: FormatterProps["column"] & ColumnConfig;
+ column: ColumnConfig;
/** The row’s _rowy_ref object */
_rowy_ref: TableRowRef;
/** The field’s local value – synced with db when field is not dirty */
- value: any;
+ value: T;
/** Call when the user has input but changes have not been saved */
- onDirty: () => void;
+ onDirty: (dirty?: boolean) => void;
/** Update the local value. Also calls onDirty */
- onChange: (value: any) => void;
+ onChange: (T: any) => void;
/** Call when user input is ready to be saved (e.g. onBlur) */
onSubmit: () => void;
/** Field locked. Do NOT check `column.locked` */
disabled: boolean;
-
- /** @deprecated */
- docRef: TableRowRef;
- /** @deprecated */
- control: Control;
- /** @deprecated */
- useFormMethods: UseFormReturn;
}
export interface ISettingsProps {
diff --git a/src/constants/routes.tsx b/src/constants/routes.tsx
index 4b59b687..af23e2cd 100644
--- a/src/constants/routes.tsx
+++ b/src/constants/routes.tsx
@@ -1,5 +1,5 @@
import Logo from "@src/assets/Logo";
-import BreadcrumbsTableRoot from "@src/components/Table/BreadcrumbsTableRoot";
+import BreadcrumbsTableRoot from "@src/components/Table/Breadcrumbs/BreadcrumbsTableRoot";
import { FadeProps, Typography } from "@mui/material";
export enum ROUTES {
diff --git a/src/hooks/useFirebaseStorageUploader.tsx b/src/hooks/useFirebaseStorageUploader.tsx
index bad0ff46..2be914fd 100644
--- a/src/hooks/useFirebaseStorageUploader.tsx
+++ b/src/hooks/useFirebaseStorageUploader.tsx
@@ -1,7 +1,6 @@
import { useReducer } from "react";
import { useAtom } from "jotai";
import { useSnackbar } from "notistack";
-import type { DocumentReference } from "firebase/firestore";
import {
ref,
uploadBytesResumable,
@@ -15,7 +14,7 @@ import InlineOpenInNewIcon from "@src/components/InlineOpenInNewIcon";
import { projectScope } from "@src/atoms/projectScope";
import { firebaseStorageAtom } from "@src/sources/ProjectSourceFirebase";
import { WIKI_LINKS } from "@src/constants/externalLinks";
-import { FileValue } from "@src/types/table";
+import type { FileValue, TableRowRef } from "@src/types/table";
import { generateId } from "@src/utils/table";
export type UploadState = {
@@ -50,7 +49,7 @@ const uploadReducer = (
};
export type UploadProps = {
- docRef: DocumentReference;
+ docRef: TableRowRef;
fieldName: string;
files: File[];
onComplete?: ({
diff --git a/src/hooks/useSaveOnUnmount.ts b/src/hooks/useSaveOnUnmount.ts
new file mode 100644
index 00000000..7807ce23
--- /dev/null
+++ b/src/hooks/useSaveOnUnmount.ts
@@ -0,0 +1,19 @@
+/* eslint-disable react-hooks/exhaustive-deps */
+import { useLayoutEffect } from "react";
+import useState from "react-usestateref";
+
+export function useSaveOnUnmount(
+ initialValue: T,
+ onSave: (value: T) => void
+) {
+ const [localValue, setLocalValue, localValueRef] = useState(initialValue);
+
+ useLayoutEffect(() => {
+ return () => {
+ onSave(localValueRef.current);
+ };
+ }, []);
+
+ return [localValue, setLocalValue, localValueRef] as const;
+}
+export default useSaveOnUnmount;
diff --git a/src/pages/Table/ProvidedSubTablePage.tsx b/src/pages/Table/ProvidedSubTablePage.tsx
index 2b3e090b..e4659d33 100644
--- a/src/pages/Table/ProvidedSubTablePage.tsx
+++ b/src/pages/Table/ProvidedSubTablePage.tsx
@@ -1,4 +1,4 @@
-import { Suspense, useMemo } from "react";
+import { lazy, Suspense, useMemo } from "react";
import { useAtom, Provider } from "jotai";
import { selectAtom } from "jotai/utils";
import { DebugAtoms } from "@src/atoms/utils";
@@ -7,10 +7,9 @@ import { useLocation, useNavigate, useParams } from "react-router-dom";
import { find, isEqual } from "lodash-es";
import Modal from "@src/components/Modal";
-import BreadcrumbsSubTable from "@src/components/Table/BreadcrumbsSubTable";
+import BreadcrumbsSubTable from "@src/components/Table/Breadcrumbs/BreadcrumbsSubTable";
import ErrorFallback from "@src/components/ErrorFallback";
import TableSourceFirestore from "@src/sources/TableSourceFirestore";
-import TablePage from "./TablePage";
import TableToolbarSkeleton from "@src/components/TableToolbar/TableToolbarSkeleton";
import TableSkeleton from "@src/components/Table/TableSkeleton";
@@ -25,8 +24,16 @@ import { ROUTES } from "@src/constants/routes";
import { TOP_BAR_HEIGHT } from "@src/layouts/Navigation/TopBar";
import { TABLE_TOOLBAR_HEIGHT } from "@src/components/TableToolbar";
+// prettier-ignore
+const TablePage = lazy(() => import("./TablePage" /* webpackChunkName: "TablePage" */));
+
/**
- * Wraps `TablePage` with the data for a top-level table.
+ * Wraps `TablePage` with the data for a sub-table.
+ *
+ * Differences to `ProvidedTablePage`:
+ * - Renders a `Modal`
+ * - When this is a child of `ProvidedTablePage`, the `TablePage` rendered for
+ * the root table has its modals disabled
*/
export default function ProvidedSubTablePage() {
const location = useLocation();
@@ -108,6 +115,7 @@ export default function ProvidedSubTablePage() {
disableBottomDivider: true,
style: { "--dialog-spacing": 0, "--dialog-contents-spacing": 0 } as any,
}}
+ BackdropProps={{ key: "sub-table-modal-backdrop" }}
>
import("./TablePage" /* webpackChunkName: "TablePage" */));
+
/**
* Wraps `TablePage` with the data for a top-level table.
* `SubTablePage` is inserted in the outlet, alongside `TablePage`.
+ *
+ * Interfaces with `projectScope` atoms to find the correct table (or sub-table)
+ * settings and schema.
+ *
+ * - Renders the Jotai `Provider` with `tableScope`
+ * - Renders `TableSourceFirestore`, which queries Firestore and stores data in
+ * atoms in `tableScope`
*/
export default function ProvidedTablePage() {
const { id } = useParams();
diff --git a/src/pages/Table/TablePage.tsx b/src/pages/Table/TablePage.tsx
index 20671792..75f1fc28 100644
--- a/src/pages/Table/TablePage.tsx
+++ b/src/pages/Table/TablePage.tsx
@@ -1,10 +1,9 @@
-import { useRef, Suspense, lazy } from "react";
+import { Suspense, lazy } from "react";
import { useAtom } from "jotai";
-import { DataGridHandle } from "react-data-grid";
import { ErrorBoundary } from "react-error-boundary";
-import { isEmpty } from "lodash-es";
+import { isEmpty, intersection } from "lodash-es";
-import { Fade } from "@mui/material";
+import { Box, Fade } from "@mui/material";
import ErrorFallback, {
InlineErrorFallback,
} from "@src/components/ErrorFallback";
@@ -14,13 +13,23 @@ import TableSkeleton from "@src/components/Table/TableSkeleton";
import EmptyTable from "@src/components/Table/EmptyTable";
import TableToolbar from "@src/components/TableToolbar";
import Table from "@src/components/Table";
-import SideDrawer from "@src/components/SideDrawer";
+import SideDrawer, { DRAWER_WIDTH } from "@src/components/SideDrawer";
import ColumnMenu from "@src/components/ColumnMenu";
import ColumnModals from "@src/components/ColumnModals";
import TableModals from "@src/components/TableModals";
+import EmptyState from "@src/components/EmptyState";
+import AddRow from "@src/components/TableToolbar/AddRow";
+import { AddRow as AddRowIcon } from "@src/assets/icons";
+import {
+ projectScope,
+ userRolesAtom,
+ userSettingsAtom,
+} from "@src/atoms/projectScope";
import {
tableScope,
+ tableIdAtom,
+ tableSettingsAtom,
tableSchemaAtom,
columnModalAtom,
tableModalAtom,
@@ -28,12 +37,19 @@ import {
import useBeforeUnload from "@src/hooks/useBeforeUnload";
import ActionParamsProvider from "@src/components/fields/Action/FormDialog/Provider";
import { useSnackLogContext } from "@src/contexts/SnackLogContext";
+import { TOP_BAR_HEIGHT } from "@src/layouts/Navigation/TopBar";
+import { TABLE_TOOLBAR_HEIGHT } from "@src/components/TableToolbar";
+import { DRAWER_COLLAPSED_WIDTH } from "@src/components/SideDrawer";
+import { formatSubTableName } from "@src/utils/table";
// prettier-ignore
const BuildLogsSnack = lazy(() => import("@src/components/TableModals/CloudLogsModal/BuildLogs/BuildLogsSnack" /* webpackChunkName: "TableModals-BuildLogsSnack" */));
export interface ITablePageProps {
- /** Disable modals on this table when a sub-table is open and it’s listening to URL state */
+ /**
+ * Disable modals on this table when a sub-table is open and it’s listening
+ * to URL state
+ */
disableModals?: boolean;
/** Disable side drawer */
disableSideDrawer?: boolean;
@@ -42,21 +58,42 @@ export interface ITablePageProps {
/**
* TablePage renders all the UI for the table.
* Must be wrapped by either `ProvidedTablePage` or `ProvidedSubTablePage`.
+ *
+ * Renders `Table`, `TableToolbar`, `SideDrawer`, `TableModals`, `ColumnMenu`,
+ * Suspense fallback UI. These components are all independent of each other.
+ *
+ * - Renders empty state if no columns
+ * - Defines empty state if no rows
+ * - Defines permissions `canAddColumns`, `canEditColumns`, `canEditCells`
+ * for `Table` using `userRolesAtom` in `projectScope`
+ * - Provides `Table` with hidden columns array from user settings
*/
export default function TablePage({
disableModals,
disableSideDrawer,
}: ITablePageProps) {
+ const [userRoles] = useAtom(userRolesAtom, projectScope);
+ const [userSettings] = useAtom(userSettingsAtom, projectScope);
+ const [tableId] = useAtom(tableIdAtom, tableScope);
+ const [tableSettings] = useAtom(tableSettingsAtom, tableScope);
const [tableSchema] = useAtom(tableSchemaAtom, tableScope);
const snackLogContext = useSnackLogContext();
+ // Set permissions here so we can pass them to the `Table` component, which
+ // shouldn’t access `projectScope` at all, to separate concerns.
+ const canAddColumns =
+ userRoles.includes("ADMIN") || userRoles.includes("OPS");
+ const canEditColumns = canAddColumns;
+ const canDeleteColumns = canAddColumns;
+ const canEditCells =
+ userRoles.includes("ADMIN") ||
+ (!tableSettings.readOnly &&
+ intersection(userRoles, tableSettings.roles).length > 0);
+
// Warn user about leaving when they have a table modal open
useBeforeUnload(columnModalAtom, tableScope);
useBeforeUnload(tableModalAtom, tableScope);
- // A ref to the data grid. Contains data grid functions
- const dataGridRef = useRef(null);
-
if (!(tableSchema as any)._rowy_ref)
return (
<>
@@ -94,13 +131,49 @@ export default function TablePage({
}>
-
+
+
+
+
+
+ }
+ style={{ position: "absolute", inset: 0 }}
+ />
+ }
+ />
+
- {!disableSideDrawer && }
+ {!disableSideDrawer && }
@@ -113,7 +186,11 @@ export default function TablePage({
{!disableModals && (
-
+
diff --git a/src/sources/TableSourceFirestore/TableSourceFirestore.tsx b/src/sources/TableSourceFirestore/TableSourceFirestore.tsx
index 87949768..557fb75e 100644
--- a/src/sources/TableSourceFirestore/TableSourceFirestore.tsx
+++ b/src/sources/TableSourceFirestore/TableSourceFirestore.tsx
@@ -14,7 +14,7 @@ import {
_updateRowDbAtom,
_deleteRowDbAtom,
tableNextPageAtom,
- serverDocCountAtom
+ serverDocCountAtom,
} from "@src/atoms/tableScope";
import useFirestoreDocWithAtom from "@src/hooks/useFirestoreDocWithAtom";
import useFirestoreCollectionWithAtom from "@src/hooks/useFirestoreCollectionWithAtom";
@@ -78,7 +78,7 @@ export const TableSourceFirestore = memo(function TableSourceFirestore() {
updateDocAtom: _updateRowDbAtom,
deleteDocAtom: _deleteRowDbAtom,
nextPageAtom: tableNextPageAtom,
- serverDocCountAtom: serverDocCountAtom
+ serverDocCountAtom: serverDocCountAtom,
}
);
diff --git a/src/theme/components.tsx b/src/theme/components.tsx
index 9224d46d..374dc25b 100644
--- a/src/theme/components.tsx
+++ b/src/theme/components.tsx
@@ -680,6 +680,12 @@ export const components = (theme: Theme): ThemeOptions => {
},
},
+ MuiButtonBase: {
+ defaultProps: {
+ focusRipple: true,
+ },
+ },
+
MuiButton: {
defaultProps: {
variant: "outlined",
diff --git a/src/types/json-stable-stringify-without-jsonify.d.ts b/src/types/json-stable-stringify-without-jsonify.d.ts
index 3e1639a6..a0f5dc4b 100644
--- a/src/types/json-stable-stringify-without-jsonify.d.ts
+++ b/src/types/json-stable-stringify-without-jsonify.d.ts
@@ -1,4 +1,4 @@
declare module "json-stable-stringify-without-jsonify" {
- const stringify: any;
+ const stringify: (...args: any) => string;
export default stringify;
}
diff --git a/src/types/table.d.ts b/src/types/table.d.ts
index f4e3bb96..b9753c41 100644
--- a/src/types/table.d.ts
+++ b/src/types/table.d.ts
@@ -137,16 +137,18 @@ export type ColumnConfig = {
/** Prevent column resizability */
resizable?: boolean = true;
- config?: {
+ config?: Partial<{
/** Set column to required */
- required?: boolean;
+ required: boolean;
/** Set column default value */
- defaultValue?: {
+ defaultValue: {
type: "undefined" | "null" | "static" | "dynamic";
value?: any;
script?: string;
dynamicValueFn?: string;
};
+ /** Regex used in CellValidation */
+ validationRegex: string;
/** FieldType to render for Derivative fields */
renderFieldType?: FieldType;
/** Used in Derivative fields */
@@ -154,13 +156,13 @@ export type ColumnConfig = {
/** Used in Derivative and Action fields */
requiredFields?: string[];
/** For sub-table fields */
- parentLabel?: string[];
+ parentLabel: string[];
- primaryKeys?: string[];
+ primaryKeys: string[];
/** Column-specific config */
[key: string]: any;
- };
+ }>;
};
export type TableFilter = {
diff --git a/yarn.lock b/yarn.lock
index 12293512..00c14283 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -2373,6 +2373,11 @@
resolved "https://registry.yarnpkg.com/@protobufjs/utf8/-/utf8-1.1.0.tgz#a777360b5b39a1a2e5106f8e858f2fd2d060c570"
integrity sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw==
+"@reach/observe-rect@^1.1.0":
+ version "1.2.0"
+ resolved "https://registry.yarnpkg.com/@reach/observe-rect/-/observe-rect-1.2.0.tgz#d7a6013b8aafcc64c778a0ccb83355a11204d3b2"
+ integrity sha512-Ba7HmkFgfQxZqqaeIWWkNK0rEhpxVQHIoVyW1YDSkGsGIXzcaW4deC8B0pZrNSSyLTdIk7y+5olKt5+g0GmFIQ==
+
"@react-dnd/asap@^4.0.0":
version "4.0.1"
resolved "https://registry.yarnpkg.com/@react-dnd/asap/-/asap-4.0.1.tgz#5291850a6b58ce6f2da25352a64f1b0674871aab"
@@ -2698,6 +2703,18 @@
dependencies:
"@jest/create-cache-key-function" "^27.4.2"
+"@tanstack/react-table@^8.5.15":
+ version "8.5.15"
+ resolved "https://registry.yarnpkg.com/@tanstack/react-table/-/react-table-8.5.15.tgz#8179d24d7fdf909799a517e8897501c44e51284d"
+ integrity sha512-9rSvhIFeMpfXksFgQNTWnVoJbkae/U8CkHnHYGWAIB/O0Ca51IKap0Rjp5WkIUVBWxJ7Wfl2y13oY+aWcyM6Rg==
+ dependencies:
+ "@tanstack/table-core" "8.5.15"
+
+"@tanstack/table-core@8.5.15":
+ version "8.5.15"
+ resolved "https://registry.yarnpkg.com/@tanstack/table-core/-/table-core-8.5.15.tgz#e1e674135cd6c36f29a1562a2b846f824861149b"
+ integrity sha512-k+BcCOAYD610Cij6p1BPyEqjMQjZIdAnVDoIUKVnA/tfHbF4JlDP7pKAftXPBxyyX5Z1yQPurPnOdEY007Snyg==
+
"@testing-library/dom@^8.5.0":
version "8.13.0"
resolved "https://registry.yarnpkg.com/@testing-library/dom/-/dom-8.13.0.tgz#bc00bdd64c7d8b40841e27a70211399ad3af46f5"
@@ -9947,13 +9964,6 @@ react-color-palette@^6.2.0:
resolved "https://registry.yarnpkg.com/react-color-palette/-/react-color-palette-6.2.0.tgz#aa3be88f6953d57502c00f4433692129ffbad3e7"
integrity sha512-9rIboaRJNoeF8aCI2f3J8wgMyhl74SnGmZLDjor3bKf0iDBhP2EBv0/jGmm0hrj6OackGCqtWl5ZvM89XUc3sg==
-react-data-grid@7.0.0-beta.5:
- version "7.0.0-beta.5"
- resolved "https://registry.yarnpkg.com/react-data-grid/-/react-data-grid-7.0.0-beta.5.tgz#bc39ce45b7a7f42ebfb66840e0ec1c8619d60f10"
- integrity sha512-rtN4wnePrQ80UN6lYF/zUQqVVJMT3HW5bTLx9nR5XOKQiG72cGzX2d2+b+e82vUh23zTFBicEnuWSlN9Fa/83Q==
- dependencies:
- clsx "^1.1.1"
-
react-detect-offline@^2.4.5:
version "2.4.5"
resolved "https://registry.yarnpkg.com/react-detect-offline/-/react-detect-offline-2.4.5.tgz#3c242516c37b6789cf89102881031f87e70b80e6"
@@ -10259,6 +10269,13 @@ react-usestateref@^1.0.8:
resolved "https://registry.yarnpkg.com/react-usestateref/-/react-usestateref-1.0.8.tgz#b40519af0d6f3b3822c70eb5db80f7d47f1b1ff5"
integrity sha512-whaE6H0XGarFKwZ3EYbpHBsRRCLZqdochzg/C7e+b6VFMTA3LS3K4ZfpI4NT40iy83jG89rGXrw70P9iDfOdsA==
+react-virtual@^2.10.4:
+ version "2.10.4"
+ resolved "https://registry.yarnpkg.com/react-virtual/-/react-virtual-2.10.4.tgz#08712f0acd79d7d6f7c4726f05651a13b24d8704"
+ integrity sha512-Ir6+oPQZTVHfa6+JL9M7cvMILstFZH/H3jqeYeKI4MSUX+rIruVwFC6nGVXw9wqAw8L0Kg2KvfXxI85OvYQdpQ==
+ dependencies:
+ "@reach/observe-rect" "^1.1.0"
+
react@^18.0.0:
version "18.0.0"
resolved "https://registry.yarnpkg.com/react/-/react-18.0.0.tgz#b468736d1f4a5891f38585ba8e8fb29f91c3cb96"