add code comments & explanations

This commit is contained in:
Sidney Alcantara
2022-11-18 17:31:03 +11:00
parent be953da123
commit edb70164cf
15 changed files with 380 additions and 175 deletions

View File

@@ -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) => (
<Tooltip {...props} classes={{ popper: className }} />
))(({ 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<Omit<StackProps, "style" | "sx">> {
header: Header<TableRow, any>;
@@ -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({
</Tooltip>
)}
<LightTooltip
<StyledColumnHeaderNameTooltip
title={
<Typography
sx={{
@@ -261,7 +226,7 @@ export const ColumnHeader = memo(function ColumnHeader({
column.name
)}
</Typography>
</LightTooltip>
</StyledColumnHeaderNameTooltip>
{column.type !== FieldType.id && (
<ColumnHeaderSort

View File

@@ -18,6 +18,10 @@ export interface IColumnHeaderSortProps {
tabIndex?: number;
}
/**
* Renders button with current sort state.
* On click, updates `tableSortsAtom` in `tableScope`.
*/
export const ColumnHeaderSort = memo(function ColumnHeaderSort({
sortKey,
currentSort,

View File

@@ -0,0 +1,58 @@
import {
styled,
Tooltip,
TooltipProps,
tooltipClasses,
Stack,
} from "@mui/material";
import { COLUMN_HEADER_HEIGHT } from "@src/components/Table/Mock/Column";
export 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,
},
}));
export default StyledColumnHeader;
export const StyledColumnHeaderNameTooltip = styled(
({ className, ...props }: TooltipProps) => (
<Tooltip {...props} classes={{ popper: className }} />
)
)(({ 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),
},
}));

View File

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

View File

@@ -49,13 +49,34 @@ const columnHelper = createColumnHelper<TableRow>();
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<HTMLDivElement>(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 users hidden columns from props and memoize into a VisibilityState
// Get users 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(

View File

@@ -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<HTMLDivElement>;
/** Used in `useVirtualization` */
leafColumns: Column<TableRow, unknown>[];
/** Current table rows with context from TanStack Table state */
rows: Row<TableRow>[];
/** 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 (
<CellValidation
<TableCell
key={cell.id}
row={row}
cell={cell}

View File

@@ -0,0 +1,90 @@
import { useEffect, useLayoutEffect } from "react";
import useStateRef from "react-usestateref";
import { useSetAtom } from "jotai";
import { isEqual } from "lodash-es";
import { tableScope, updateFieldAtom } from "@src/atoms/tableScope";
import type {
IDisplayCellProps,
IEditorCellProps,
} from "@src/components/fields/types";
interface IEditorCellControllerProps extends IDisplayCellProps {
EditorCellComponent: React.ComponentType<IEditorCellProps>;
parentRef: IEditorCellProps["parentRef"];
saveOnUnmount: boolean;
}
/**
* Stores a local state for the cells value, so that `EditorCell` doesnt
* immediately update the database when the user quickly makes changes to the
* cells 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 havent 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 dont 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 hasnt saved yet
const [isDirty, setIsDirty, isDirtyRef] = useStateRef(false);
const updateField = useSetAtom(updateFieldAtom, tableScope);
// When this cells data has updated, update the local value if
// its 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 dont change across re-renders. We only want to run this on unmount
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
return (
<EditorCellComponent
{...props}
value={localValue}
onDirty={(dirty?: boolean) => setIsDirty(dirty ?? true)}
onChange={(v) => {
setIsDirty(true);
setLocalValue(v);
}}
onSubmit={handleSubmit}
/>
);
}

View File

@@ -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<TableRow>;
/** Current cell with context from TanStack Table state */
cell: Cell<TableRow, any>;
/** 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 dont 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
* dont 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 }) => <Dot onClick={openTooltip} />}
render={({ openTooltip }) => <StyledDot onClick={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 }) => <Dot onClick={openTooltip} />}
render={({ openTooltip }) => <StyledDot onClick={openTooltip} />}
/>
);
}

View File

@@ -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<PopoverProps>;
}
/** Received from `TableCell` */
export interface IRenderedTableCellProps<TValue = any>
extends CellContext<TableRow, TValue> {
value: TValue;
@@ -42,18 +35,61 @@ export interface IRenderedTableCellProps<TValue = any>
}
/**
* 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 types 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 its not defined in runtime
* - {@link ICellOptions}
*/
export default function withTableCell(
export default function withRenderTableCell(
DisplayCellComponent: React.ComponentType<IDisplayCellProps>,
EditorCellComponent: React.ComponentType<IEditorCellProps> | 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 ? (
<Suspense fallback={null}>
<EditorCellManager
<EditorCellController
{...basicCellProps}
EditorCellComponent={EditorCellComponent}
parentRef={parentRef}
@@ -201,6 +237,7 @@ export default function withTableCell(
// Should not reach this line
return null;
},
// Memo function
(prev, next) => {
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<IEditorCellProps>;
parentRef: IEditorCellProps["parentRef"];
saveOnUnmount: boolean;
}
function EditorCellManager({
EditorCellComponent,
saveOnUnmount,
value,
...props
}: IEditorCellManagerProps) {
// Store local value so we dont 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 hasnt saved yet
const [isDirty, setIsDirty, isDirtyRef] = useStateRef(false);
const updateField = useSetAtom(updateFieldAtom, tableScope);
// When this cells data has updated, update the local value if
// its 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 (
<EditorCellComponent
{...props}
value={localValue}
onDirty={(dirty?: boolean) => setIsDirty(dirty ?? true)}
onChange={(v) => {
setIsDirty(true);
setLocalValue(v);
}}
onSubmit={handleSubmit}
/>
);
}

View File

@@ -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<TableRow>[];
/** 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({
</Draggable>
);
})}
{/* Required by react-beautiful-dnd */}
{provided.placeholder}
</StyledRow>
)}

View File

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

View File

@@ -42,6 +42,7 @@ export interface IFieldConfig {
csvImportParser?: (value: string, config?: any) => any;
}
/** See {@link IRenderedTableCellProps | `withRenderTableCell` } for guidance */
export interface IDisplayCellProps<T = any> {
value: T;
type: FieldType;
@@ -51,11 +52,16 @@ export interface IDisplayCellProps<T = any> {
/** The rows _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<T = any> extends IDisplayCellProps<T> {
/** Call when the user has input but changes have not been saved */
onDirty: (dirty?: boolean) => void;

View File

@@ -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();

View File

@@ -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();

View File

@@ -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 its listening to URL state */
/**
* Disable modals on this table when a sub-table is open and its 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,