diff --git a/src/components/Table/ColumnHeader/ColumnHeader.tsx b/src/components/Table/ColumnHeader/ColumnHeader.tsx
index 84239a40..40e5c5a1 100644
--- a/src/components/Table/ColumnHeader/ColumnHeader.tsx
+++ b/src/components/Table/ColumnHeader/ColumnHeader.tsx
@@ -7,12 +7,8 @@ import type {
} from "react-beautiful-dnd";
import {
- styled,
Tooltip,
- TooltipProps,
- tooltipClasses,
Fade,
- Stack,
StackProps,
IconButton,
Typography,
@@ -20,6 +16,10 @@ import {
import DropdownIcon from "@mui/icons-material/MoreHoriz";
import LockIcon from "@mui/icons-material/LockOutlined";
+import {
+ StyledColumnHeader,
+ StyledColumnHeaderNameTooltip,
+} from "@src/components/Table/Styled/StyledColumnHeader";
import ColumnHeaderSort, { SORT_STATES } from "./ColumnHeaderSort";
import ColumnHeaderDragHandle from "./ColumnHeaderDragHandle";
import ColumnHeaderResizer from "./ColumnHeaderResizer";
@@ -39,53 +39,6 @@ import type { TableRow } from "@src/types/table";
export { COLUMN_HEADER_HEIGHT };
-const StyledColumnHeader = styled(Stack)(({ theme }) => ({
- position: "relative",
- height: "100%",
- border: `1px solid ${theme.palette.divider}`,
- "& + &": { borderLeftStyle: "none" },
-
- flexDirection: "row",
- alignItems: "center",
- padding: theme.spacing(0, 0.5, 0, 1),
- "& svg, & button": { display: "block", zIndex: 1 },
-
- backgroundColor: theme.palette.background.default,
- color: theme.palette.text.secondary,
- transition: theme.transitions.create("color", {
- duration: theme.transitions.duration.short,
- }),
- "&:hover": { color: theme.palette.text.primary },
-
- "& .MuiIconButton-root": {
- color: theme.palette.text.disabled,
- transition: theme.transitions.create(
- ["background-color", "opacity", "color"],
- { duration: theme.transitions.duration.short }
- ),
- },
- [`&:hover .MuiIconButton-root,
- &:focus .MuiIconButton-root,
- &:focus-within .MuiIconButton-root,
- .MuiIconButton-root:focus`]: {
- color: theme.palette.text.primary,
- opacity: 1,
- },
-}));
-
-const LightTooltip = styled(({ className, ...props }: TooltipProps) => (
-
-))(({ theme }) => ({
- [`& .${tooltipClasses.tooltip}`]: {
- backgroundColor: theme.palette.background.default,
- color: theme.palette.text.primary,
-
- margin: `-${COLUMN_HEADER_HEIGHT - 1 - 2}px 0 0 !important`,
- padding: 0,
- paddingRight: theme.spacing(1.5),
- },
-}));
-
export interface IColumnHeaderProps
extends Partial> {
header: Header;
@@ -101,6 +54,18 @@ export interface IColumnHeaderProps
isLastFrozen: boolean;
}
+/**
+ * Renders UI components for each column header, including accessibility
+ * attributes. Memoized to prevent re-render when resizing or reordering other
+ * columns.
+ *
+ * Renders:
+ * - Drag handle (accessible)
+ * - Field type icon + click to copy field key
+ * - Field name + hover to view full name if cut off
+ * - Sort button
+ * - Resize handle (not accessible)
+ */
export const ColumnHeader = memo(function ColumnHeader({
header,
column,
@@ -214,7 +179,7 @@ export const ColumnHeader = memo(function ColumnHeader({
)}
-
-
+
{column.type !== FieldType.id && (
({
+ position: "relative",
+ height: "100%",
+ border: `1px solid ${theme.palette.divider}`,
+ "& + &": { borderLeftStyle: "none" },
+
+ flexDirection: "row",
+ alignItems: "center",
+ padding: theme.spacing(0, 0.5, 0, 1),
+ "& svg, & button": { display: "block", zIndex: 1 },
+
+ backgroundColor: theme.palette.background.default,
+ color: theme.palette.text.secondary,
+ transition: theme.transitions.create("color", {
+ duration: theme.transitions.duration.short,
+ }),
+ "&:hover": { color: theme.palette.text.primary },
+
+ "& .MuiIconButton-root": {
+ color: theme.palette.text.disabled,
+ transition: theme.transitions.create(
+ ["background-color", "opacity", "color"],
+ { duration: theme.transitions.duration.short }
+ ),
+ },
+ [`&:hover .MuiIconButton-root,
+ &:focus .MuiIconButton-root,
+ &:focus-within .MuiIconButton-root,
+ .MuiIconButton-root:focus`]: {
+ color: theme.palette.text.primary,
+ opacity: 1,
+ },
+}));
+export default StyledColumnHeader;
+
+export const StyledColumnHeaderNameTooltip = styled(
+ ({ className, ...props }: TooltipProps) => (
+
+ )
+)(({ theme }) => ({
+ [`& .${tooltipClasses.tooltip}`]: {
+ backgroundColor: theme.palette.background.default,
+ color: theme.palette.text.primary,
+
+ margin: `-${COLUMN_HEADER_HEIGHT - 1 - 2}px 0 0 !important`,
+ padding: 0,
+ paddingRight: theme.spacing(1.5),
+ },
+}));
diff --git a/src/components/Table/Styled/StyledDot.tsx b/src/components/Table/Styled/StyledDot.tsx
new file mode 100644
index 00000000..bd942a8f
--- /dev/null
+++ b/src/components/Table/Styled/StyledDot.tsx
@@ -0,0 +1,22 @@
+import { styled } from "@mui/material";
+
+export const StyledDot = styled("div")(({ theme }) => ({
+ position: "absolute",
+ right: -5,
+ top: "50%",
+ transform: "translateY(-50%)",
+ zIndex: 1,
+
+ width: 12,
+ height: 12,
+
+ borderRadius: "50%",
+ backgroundColor: theme.palette.error.main,
+
+ boxShadow: `0 0 0 4px var(--cell-background-color)`,
+ "[role='row']:hover &": {
+ boxShadow: `0 0 0 4px var(--row-hover-background-color)`,
+ },
+}));
+
+export default StyledDot;
diff --git a/src/components/Table/Table.tsx b/src/components/Table/Table.tsx
index fe1447c4..2d412787 100644
--- a/src/components/Table/Table.tsx
+++ b/src/components/Table/Table.tsx
@@ -49,13 +49,34 @@ const columnHelper = createColumnHelper();
const getRowId = (row: TableRow) => row._rowy_ref.path || row._rowy_ref.id;
export interface ITableProps {
+ /** Determines if “Add column” button is displayed */
canAddColumns: boolean;
+ /** Determines if columns can be rearranged */
canEditColumns: boolean;
+ /**
+ * Determines if any cell can be edited.
+ * If false, `Table` only ever renders `EditorCell`.
+ */
canEditCells: boolean;
+ /** The hidden columns saved to user settings */
hiddenColumns?: string[];
+ /**
+ * Displayed when `tableRows` is empty.
+ * Loading state handled by Suspense in parent component.
+ */
emptyState?: React.ReactNode;
}
+/**
+ * Takes table schema and row data from `tableScope` and makes it compatible
+ * with TanStack Table. Renders table children and cell context menu.
+ *
+ * - Calls `useKeyboardNavigation` hook
+ * - Handles rearranging columns
+ * - Handles infinite scrolling
+ * - Stores local state for resizing columns, and asks admins if they want to
+ * save to table schema for all users
+ */
export default function Table({
canAddColumns,
canEditColumns,
@@ -75,7 +96,7 @@ export default function Table({
const gridRef = useRef(null);
// Get column defs from table schema
- // Also add end column for admins
+ // Also add end column for admins (canAddColumns || canEditCells)
const columns = useMemo(() => {
const _columns = tableColumnsOrdered
// Hide column for all users using table schema
@@ -103,13 +124,13 @@ export default function Table({
return _columns;
}, [tableColumnsOrdered, canAddColumns, canEditCells]);
- // Get user’s hidden columns from props and memoize into a VisibilityState
+ // Get user’s hidden columns from props and memoize into a `VisibilityState`
const columnVisibility = useMemo(() => {
if (!Array.isArray(hiddenColumns)) return {};
return hiddenColumns.reduce((a, c) => ({ ...a, [c]: false }), {});
}, [hiddenColumns]);
- // Get frozen columns
+ // Get frozen columns and memoize into a `ColumnPinningState`
const columnPinning = useMemo(
() => ({
left: columns.filter((c) => c.meta?.fixed && c.id).map((c) => c.id!),
@@ -128,7 +149,7 @@ export default function Table({
columnResizeMode: "onChange",
});
- // Store local columnSizing state so we can save it to table schema
+ // Store local `columnSizing` state so we can save it to table schema
// in `useSaveColumnSizing`. This could be generalized by storing the
// entire table state.
const [columnSizing, setColumnSizing] = useState(
@@ -139,16 +160,18 @@ export default function Table({
state: { ...prev.state, columnVisibility, columnPinning, columnSizing },
onColumnSizingChange: setColumnSizing,
}));
-
+ // Get rows and columns for virtualization
const { rows } = table.getRowModel();
const leafColumns = table.getVisibleLeafColumns();
+ // Handle keyboard navigation
const { handleKeyDown } = useKeyboardNavigation({
gridRef,
tableRows,
leafColumns,
});
+ // Handle prompt to save local column sizes if user `canEditColumns`
useSaveColumnSizing(columnSizing, canEditColumns);
const handleDropColumn = useCallback(
diff --git a/src/components/Table/TableBody.tsx b/src/components/Table/TableBody.tsx
index 674d8d96..45eef9a3 100644
--- a/src/components/Table/TableBody.tsx
+++ b/src/components/Table/TableBody.tsx
@@ -4,7 +4,7 @@ import type { Column, Row, ColumnSizingState } from "@tanstack/react-table";
import StyledRow from "./Styled/StyledRow";
import OutOfOrderIndicator from "./OutOfOrderIndicator";
-import CellValidation from "./TableCell";
+import TableCell from "./TableCell";
import { RowsSkeleton } from "./TableSkeleton";
import {
@@ -24,17 +24,28 @@ import {
} from "./Table";
export interface ITableBodyProps {
+ /** Used in `useVirtualization` */
containerRef: React.RefObject;
+ /** Used in `useVirtualization` */
leafColumns: Column[];
+ /** Current table rows with context from TanStack Table state */
rows: Row[];
-
+ /** Determines if EditorCell can be displayed */
canEditCells: boolean;
+ /** If specified, renders a shadow in the last frozen column */
lastFrozen?: string;
-
- /** Re-render when local column sizing changes */
+ /**
+ * Must pass this prop so that it re-renders when local column sizing changes */
columnSizing: ColumnSizingState;
}
+/**
+ * Renders table body & data rows.
+ * Handles virtualization of rows & columns via `useVirtualization`.
+ *
+ * - Renders row out of order indicator
+ * - Renders next page loading UI (`RowsSkeleton`)
+ */
export const TableBody = memo(function TableBody({
containerRef,
leafColumns,
@@ -98,7 +109,7 @@ export const TableBody = memo(function TableBody({
fieldTypeGroup === "Auditing" || fieldTypeGroup === "Metadata";
return (
- ;
+ parentRef: IEditorCellProps["parentRef"];
+ saveOnUnmount: boolean;
+}
+
+/**
+ * Stores a local state for the cell’s value, so that `EditorCell` doesn’t
+ * immediately update the database when the user quickly makes changes to the
+ * cell’s value (e.g. text input).
+ *
+ * Extracted from `withRenderTableCell()` so when the `DisplayCell` is
+ * rendered, an unnecessary extra state is not created.
+ *
+ * - Defines function to update the field in db
+ * - Tracks when the user has made the input “dirty”
+ * - By default, saves to db when the component is unmounted and the input
+ * is dirty
+ * - Has an effect to change the local value state when it receives an update
+ * from db and the field is not dirty. This is required to make inline
+ * `EditorCell` work when they haven’t been interacted with, but prevent the
+ * value changing while someone is editing a field, like Long Text.
+ */
+export default function EditorCellController({
+ EditorCellComponent,
+ saveOnUnmount,
+ value,
+ ...props
+}: IEditorCellControllerProps) {
+ // Store local value so we don’t immediately write to db when the user
+ // types in a textbox, for example
+ const [localValue, setLocalValue, localValueRef] = useStateRef(value);
+ // Mark if the user has interacted with this cell and hasn’t saved yet
+ const [isDirty, setIsDirty, isDirtyRef] = useStateRef(false);
+ const updateField = useSetAtom(updateFieldAtom, tableScope);
+
+ // When this cell’s data has updated, update the local value if
+ // it’s not dirty and the value is different
+ useEffect(() => {
+ if (!isDirty && !isEqual(value, localValueRef.current))
+ setLocalValue(value);
+ }, [isDirty, localValueRef, setLocalValue, value]);
+
+ // This is where we update the documents
+ const handleSubmit = () => {
+ // props.disabled should always be false as withRenderTableCell would
+ // render DisplayCell instead of EditorCell
+ if (props.disabled || !isDirtyRef.current) return;
+
+ updateField({
+ path: props._rowy_ref.path,
+ fieldName: props.column.fieldName,
+ value: localValueRef.current,
+ deleteField: localValueRef.current === undefined,
+ });
+ };
+
+ useLayoutEffect(() => {
+ return () => {
+ if (saveOnUnmount) handleSubmit();
+ };
+ // Warns that `saveOnUnmount` and `handleSubmit` should be included, but
+ // those don’t change across re-renders. We only want to run this on unmount
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, []);
+
+ return (
+ setIsDirty(dirty ?? true)}
+ onChange={(v) => {
+ setIsDirty(true);
+ setLocalValue(v);
+ }}
+ onSubmit={handleSubmit}
+ />
+ );
+}
diff --git a/src/components/Table/TableCell/TableCell.tsx b/src/components/Table/TableCell/TableCell.tsx
index 48fb7b93..1842602b 100644
--- a/src/components/Table/TableCell/TableCell.tsx
+++ b/src/components/Table/TableCell/TableCell.tsx
@@ -4,13 +4,13 @@ import { ErrorBoundary } from "react-error-boundary";
import { flexRender } from "@tanstack/react-table";
import type { Row, Cell } from "@tanstack/react-table";
-import { styled } from "@mui/material/styles";
import ErrorIcon from "@mui/icons-material/ErrorOutline";
import WarningIcon from "@mui/icons-material/WarningAmber";
import StyledCell from "@src/components/Table/Styled/StyledCell";
import { InlineErrorFallback } from "@src/components/ErrorFallback";
import RichTooltip from "@src/components/RichTooltip";
+import StyledDot from "@src/components/Table/Styled/StyledDot";
import {
tableScope,
@@ -20,39 +20,53 @@ import {
import type { TableRow } from "@src/types/table";
import type { IRenderedTableCellProps } from "./withRenderTableCell";
-const Dot = styled("div")(({ theme }) => ({
- position: "absolute",
- right: -5,
- top: "50%",
- transform: "translateY(-50%)",
- zIndex: 1,
-
- width: 12,
- height: 12,
-
- borderRadius: "50%",
- backgroundColor: theme.palette.error.main,
-
- boxShadow: `0 0 0 4px var(--cell-background-color)`,
- "[role='row']:hover &": {
- boxShadow: `0 0 0 4px var(--row-hover-background-color)`,
- },
-}));
-
export interface ITableCellProps {
+ /** Current row with context from TanStack Table state */
row: Row;
+ /** Current cell with context from TanStack Table state */
cell: Cell;
+ /** Virtual cell index (column index) */
index: number;
+ /** User has clicked or navigated to this cell */
isSelectedCell: boolean;
+ /** User has double-clicked or pressed Enter and this cell is selected */
focusInsideCell: boolean;
+ /**
+ * Used to disable `aria-description` that says “Press Enter to edit”
+ * for Auditing and Metadata cells. Need to find another way to do this.
+ */
isReadOnlyCell: boolean;
+ /** Determines if EditorCell can be displayed */
canEditCells: boolean;
+ /**
+ * Pass current row height as a prop so we don’t access `tableSchema` here.
+ * If that atom is listened to here, all table cells will re-render whenever
+ * `tableSchema` changes, which is unnecessary.
+ */
rowHeight: number;
+ /** If true, renders a shadow */
isLastFrozen: boolean;
+ /** Pass width as a prop to get local column sizing state */
width: number;
+ /**
+ * If provided, cell is pinned/frozen, and this value is used for
+ * `position: sticky`.
+ */
left?: number;
}
+/**
+ * Renders the container div for each cell with accessibility attributes for
+ * keyboard navigation.
+ *
+ * - Performs regex & missing value check and renders associated UI
+ * - Provides children with value from `cell.getValue()` so they can work with
+ * memoization
+ * - Provides helpers as props to aid with memoization, so children components
+ * don’t have to read atoms, which may cause unnecessary re-renders of many
+ * cell components
+ * - Renders `ErrorBoundary`
+ */
export const TableCell = memo(function TableCell({
row,
cell,
@@ -85,7 +99,7 @@ export const TableCell = memo(function TableCell({
title="Invalid data"
message="This row will not be saved until all the required fields contain valid data"
placement="right"
- render={({ openTooltip }) => }
+ render={({ openTooltip }) => }
/>
);
} else if (isMissing) {
@@ -95,7 +109,7 @@ export const TableCell = memo(function TableCell({
title="Required field"
message="This row will not be saved until all the required fields contain valid data"
placement="right"
- render={({ openTooltip }) => }
+ render={({ openTooltip }) => }
/>
);
}
diff --git a/src/components/Table/TableCell/withRenderTableCell.tsx b/src/components/Table/TableCell/withRenderTableCell.tsx
index a8f10f83..0d215b19 100644
--- a/src/components/Table/TableCell/withRenderTableCell.tsx
+++ b/src/components/Table/TableCell/withRenderTableCell.tsx
@@ -1,19 +1,11 @@
-import {
- memo,
- Suspense,
- useState,
- useEffect,
- useRef,
- useLayoutEffect,
-} from "react";
-import useStateRef from "react-usestateref";
-import { useSetAtom } from "jotai";
+import { memo, Suspense, useState, useEffect, useRef } from "react";
import { isEqual } from "lodash-es";
import type { CellContext } from "@tanstack/react-table";
import { Popover, PopoverProps } from "@mui/material";
-import { tableScope, updateFieldAtom } from "@src/atoms/tableScope";
+import EditorCellController from "./EditorCellController";
+
import { spreadSx } from "@src/utils/ui";
import type { TableRow } from "@src/types/table";
import type {
@@ -32,6 +24,7 @@ export interface ICellOptions {
popoverProps?: Partial;
}
+/** Received from `TableCell` */
export interface IRenderedTableCellProps
extends CellContext {
value: TValue;
@@ -42,18 +35,61 @@ export interface IRenderedTableCellProps
}
/**
- * HOC to render table cells.
- * Renders read-only DisplayCell while scrolling for scroll performance.
- * Defers render for inline EditorCell.
- * @param DisplayCellComponent - The lighter cell component to display values
- * @param EditorCellComponent - The heavier cell component to edit inline
- * @param editorMode - When to display the EditorCell
- * - "focus" (default) - when the cell is focused (Enter or double-click)
- * - "inline" - inline with deferred render
- * - "popover" - as a popover
- * @param options - {@link ICellOptions}
+ * Higher-order component to render each field type’s cell components.
+ * Handles when to render read-only `DisplayCell` and `EditorCell`.
+ *
+ * Memoized to re-render when value, column, focus, or disabled states change.
+ * Optionally re-renders when entire row updates.
+ *
+ * - Renders inline `EditorCell` after a timeout to improve scroll performance
+ * - Handles popovers
+ * - Renders Suspense for lazy-loaded `EditorCell`
+ * - Provides a `tabIndex` prop, so that interactive cell children (like
+ * buttons) cannot be interacted with unless the user has focused in the
+ * cell. Required for accessibility.
+ *
+ * @param DisplayCellComponent
+ * - The lighter cell component to display values. Also displayed when the
+ * column is disabled/read-only.
+ *
+ * - Keep these components lightweight, i.e. use base HTML or simple MUI
+ * components. Avoid `Tooltip`, which is heavy.
+ * - Avoid displaying disabled states (e.g. do not reduce opacity/grey out
+ * toggles). This improves the experience of read-only tables for non-admins
+ * - ⚠️ Make sure the disabled state does not render the buttons to open a
+ * popover `EditorCell` (like Single/Multi Select).
+ * - ⚠️ Make sure to use the `tabIndex` prop for buttons and other interactive
+ * elements.
+ * - {@link IDisplayCellProps}
+ *
+ * @param EditorCellComponent
+ * - The heavier cell component to edit values
+ *
+ * - `EditorCell` should use the `value` and `onChange` props for the
+ * rendered inputs. Avoid creating another local state here.
+ * - `onSubmit` is available if `saveOnUnmount` does not work or if you want
+ * to submit to the db before unmount.
+ * - ✨ You can reuse your `SideDrawerField` as they take the same props. It
+ * should probably be displayed in a popover.
+ * - You can pass `null` to `withRenderTableCell()` to always display the
+ * `DisplayCell`.
+ * - ⚠️ Make sure to use the `tabIndex` prop for buttons, text fields, and
+ * other interactive elements.
+ * - {@link IEditorCellProps}
+ *
+ * @param editorMode
+ * - When to display the `EditorCell`
+ * 1. **focus** (default): the user has focused on the cell by pressing Enter or
+ * double-clicking,
+ * 2. **inline**: always displayed if the cell is editable, or
+ * 3. **popover**: inside a popover when a user has focused on the cell
+ * (as above) or clicked a button rendered by `DisplayCell`
+ *
+ * @param options
+ * - Note this is OK to pass as an object since it’s not defined in runtime
+ * - {@link ICellOptions}
*/
-export default function withTableCell(
+export default function withRenderTableCell(
DisplayCellComponent: React.ComponentType,
EditorCellComponent: React.ComponentType | null,
editorMode: "focus" | "inline" | "popover" = "focus",
@@ -137,7 +173,7 @@ export default function withTableCell(
// Show displayCell as a fallback if intentionally null
const editorCell = EditorCellComponent ? (
- {
const valueEqual = isEqual(prev.value, next.value);
const columnEqual = isEqual(
@@ -220,65 +257,3 @@ export default function withTableCell(
}
);
}
-
-interface IEditorCellManagerProps extends IDisplayCellProps {
- EditorCellComponent: React.ComponentType;
- parentRef: IEditorCellProps["parentRef"];
- saveOnUnmount: boolean;
-}
-
-function EditorCellManager({
- EditorCellComponent,
- saveOnUnmount,
- value,
- ...props
-}: IEditorCellManagerProps) {
- // Store local value so we don’t immediately write to db when the user
- // types in a textbox, for example
- const [localValue, setLocalValue, localValueRef] = useStateRef(value);
- // Mark if the user has interacted with this cell and hasn’t saved yet
- const [isDirty, setIsDirty, isDirtyRef] = useStateRef(false);
- const updateField = useSetAtom(updateFieldAtom, tableScope);
-
- // When this cell’s data has updated, update the local value if
- // it’s not dirty and the value is different
- useEffect(() => {
- if (!isDirty && !isEqual(value, localValueRef.current))
- setLocalValue(value);
- }, [isDirty, localValueRef, setLocalValue, value]);
-
- // This is where we update the documents
- const handleSubmit = () => {
- if (props.disabled || !isDirtyRef.current) return;
-
- updateField({
- path: props._rowy_ref.path,
- fieldName: props.column.fieldName,
- value: localValueRef.current,
- deleteField: localValueRef.current === undefined,
- });
- };
-
- useLayoutEffect(() => {
- return () => {
- if (saveOnUnmount) {
- console.log("unmount", props._rowy_ref.path, props.column.fieldName);
- handleSubmit();
- }
- };
- // eslint-disable-next-line react-hooks/exhaustive-deps
- }, []);
-
- return (
- setIsDirty(dirty ?? true)}
- onChange={(v) => {
- setIsDirty(true);
- setLocalValue(v);
- }}
- onSubmit={handleSubmit}
- />
- );
-}
diff --git a/src/components/Table/TableHeader.tsx b/src/components/Table/TableHeader.tsx
index ac93d566..43eb8833 100644
--- a/src/components/Table/TableHeader.tsx
+++ b/src/components/Table/TableHeader.tsx
@@ -13,16 +13,27 @@ import { tableScope, selectedCellAtom } from "@src/atoms/tableScope";
import { DEFAULT_ROW_HEIGHT } from "@src/components/Table";
export interface ITableHeaderProps {
+ /** Headers with context from TanStack Table state */
headerGroups: HeaderGroup[];
+ /** Called when a header is dropped in a new position */
handleDropColumn: (result: DropResult) => void;
+ /** Passed to `FinalColumnHeader` */
canAddColumns: boolean;
+ /** Determines if columns can be re-ordered */
canEditColumns: boolean;
+ /** If specified, renders a shadow in the last frozen column */
lastFrozen?: string;
-
- /** Re-render when local column sizing changes */
+ /**
+ * Must pass this prop so that it re-renders when local column sizing changes */
columnSizing: ColumnSizingState;
}
+/**
+ * Renders table header row. Memoized to only re-render when column definitions
+ * and sizes change.
+ *
+ * - Renders drag & drop components
+ */
export const TableHeader = memo(function TableHeader({
headerGroups,
handleDropColumn,
@@ -93,6 +104,7 @@ export const TableHeader = memo(function TableHeader({
);
})}
+ {/* Required by react-beautiful-dnd */}
{provided.placeholder}
)}
diff --git a/src/components/Table/useSaveColumnSizing.tsx b/src/components/Table/useSaveColumnSizing.tsx
index 2bcf9c59..af1a6e37 100644
--- a/src/components/Table/useSaveColumnSizing.tsx
+++ b/src/components/Table/useSaveColumnSizing.tsx
@@ -17,7 +17,8 @@ import { DEBOUNCE_DELAY } from "./Table";
import { ColumnSizingState } from "@tanstack/react-table";
/**
- * Debounces columnSizing and asks admins if they want to save for all users
+ * Debounces `columnSizing` and asks user if they want to save for all users,
+ * if they have the `canEditColumns` permission
*/
export function useSaveColumnSizing(
columnSizing: ColumnSizingState,
diff --git a/src/components/fields/types.ts b/src/components/fields/types.ts
index b43b6d82..50a05f8a 100644
--- a/src/components/fields/types.ts
+++ b/src/components/fields/types.ts
@@ -42,6 +42,7 @@ export interface IFieldConfig {
csvImportParser?: (value: string, config?: any) => any;
}
+/** See {@link IRenderedTableCellProps | `withRenderTableCell` } for guidance */
export interface IDisplayCellProps {
value: T;
type: FieldType;
@@ -51,11 +52,16 @@ export interface IDisplayCellProps {
/** 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;
}
+/** 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;
diff --git a/src/pages/Table/ProvidedSubTablePage.tsx b/src/pages/Table/ProvidedSubTablePage.tsx
index 860748c2..e4659d33 100644
--- a/src/pages/Table/ProvidedSubTablePage.tsx
+++ b/src/pages/Table/ProvidedSubTablePage.tsx
@@ -28,7 +28,12 @@ import { TABLE_TOOLBAR_HEIGHT } from "@src/components/TableToolbar";
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();
diff --git a/src/pages/Table/ProvidedTablePage.tsx b/src/pages/Table/ProvidedTablePage.tsx
index 8f86be90..cdb7d19c 100644
--- a/src/pages/Table/ProvidedTablePage.tsx
+++ b/src/pages/Table/ProvidedTablePage.tsx
@@ -38,6 +38,13 @@ const TablePage = lazy(() => import("./TablePage" /* webpackChunkName: "TablePag
/**
* 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 3dce7445..6008c835 100644
--- a/src/pages/Table/TablePage.tsx
+++ b/src/pages/Table/TablePage.tsx
@@ -46,7 +46,10 @@ import { formatSubTableName } from "@src/utils/table";
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;
@@ -55,6 +58,15 @@ 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,