re-enable table cells

This commit is contained in:
Sidney Alcantara
2022-11-02 16:36:10 +11:00
parent 564b4c8b3b
commit e436e2083a
10 changed files with 107 additions and 194 deletions

View File

@@ -2,30 +2,9 @@ import { styled } from "@mui/material/styles";
import ErrorIcon from "@mui/icons-material/ErrorOutline";
import WarningIcon from "@mui/icons-material/WarningAmber";
import StyledCell from "./Styled/StyledCell";
import RichTooltip from "@src/components/RichTooltip";
const Root = styled("div", { shouldForwardProp: (prop) => prop !== "error" })(
({ theme, ...props }) => ({
width: "100%",
height: "100%",
padding: "var(--cell-padding)",
position: "relative",
overflow: "hidden",
contain: "strict",
display: "flex",
alignItems: "center",
...((props as any).error
? {
".rdg-cell:not([aria-selected=true]) &": {
boxShadow: `inset 0 0 0 2px ${theme.palette.error.main}`,
},
}
: {}),
})
);
const Dot = styled("div")(({ theme }) => ({
position: "absolute",
right: -5,
@@ -40,7 +19,7 @@ const Dot = styled("div")(({ theme }) => ({
backgroundColor: theme.palette.error.main,
boxShadow: `0 0 0 4px var(--background-color)`,
".rdg-row:hover &": {
"[role='row']:hover &": {
boxShadow: `0 0 0 4px var(--row-hover-background-color)`,
},
}));
@@ -60,13 +39,14 @@ export default function CellValidation({
required,
validationRegex,
children,
...props
}: ICellValidationProps) {
const isInvalid = validationRegex && !new RegExp(validationRegex).test(value);
const isMissing = required && value === undefined;
if (isInvalid)
return (
<>
<StyledCell aria-invalid="true" {...props}>
<RichTooltip
icon={<ErrorIcon fontSize="inherit" color="error" />}
title="Invalid data"
@@ -74,14 +54,13 @@ export default function CellValidation({
placement="right"
render={({ openTooltip }) => <Dot onClick={openTooltip} />}
/>
<Root {...({ error: true } as any)}>{children}</Root>
</>
{children}
</StyledCell>
);
if (isMissing)
return (
<>
<StyledCell aria-invalid="true" {...props}>
<RichTooltip
icon={<WarningIcon fontSize="inherit" color="warning" />}
title="Required field"
@@ -89,10 +68,9 @@ export default function CellValidation({
placement="right"
render={({ openTooltip }) => <Dot onClick={openTooltip} />}
/>
<Root {...({ error: true } as any)}>{children}</Root>
</>
{children}
</StyledCell>
);
return <Root>{children}</Root>;
return <StyledCell {...props}>{children}</StyledCell>;
}

View File

@@ -1,5 +1,5 @@
import { useAtom, useSetAtom } from "jotai";
import type { CellContext } from "@tanstack/table-core";
import type { TableCellProps } from "@src/components/Table";
import { Stack, Tooltip, IconButton, alpha } from "@mui/material";
import { CopyCells as CopyCellsIcon } from "@src/assets/icons";
@@ -20,12 +20,8 @@ import {
deleteRowAtom,
contextMenuTargetAtom,
} from "@src/atoms/tableScope";
import { TableRow } from "@src/types/table";
export default function FinalColumn({
row,
focusInsideCell,
}: CellContext<TableRow, any> & { focusInsideCell: boolean }) {
export default function FinalColumn({ row, focusInsideCell }: TableCellProps) {
const [userRoles] = useAtom(userRolesAtom, projectScope);
const [addRowIdType] = useAtom(tableAddRowIdTypeAtom, projectScope);
const confirm = useSetAtom(confirmDialogAtom, projectScope);

View File

@@ -12,7 +12,7 @@ export const StyledCell = styled("div")(({ theme }) => ({
},
overflow: "visible",
contain: "none",
contain: "strict",
position: "relative",
backgroundColor: theme.palette.background.paper,
@@ -31,5 +31,11 @@ export const StyledCell = styled("div")(({ theme }) => ({
"[data-out-of-order='true'] + [role='row'] &": {
borderTop: `1px solid ${theme.palette.divider}`,
},
"&[aria-invalid='true']": {
boxShadow: `inset 0 0 0 2px ${theme.palette.error.main}`,
},
}));
StyledCell.displayName = "StyledCell";
export default StyledCell;

View File

@@ -6,6 +6,7 @@ import {
flexRender,
getCoreRowModel,
useReactTable,
CellContext,
} from "@tanstack/react-table";
import {
DragDropContext,
@@ -14,6 +15,7 @@ import {
Draggable,
} from "react-beautiful-dnd";
import { Portal } from "@mui/material";
import { ErrorBoundary } from "react-error-boundary";
import StyledTable from "./Styled/StyledTable";
import StyledRow from "./Styled/StyledRow";
@@ -23,8 +25,10 @@ import FinalColumnHeader from "./FinalColumn/FinalColumnHeader";
import FinalColumn from "./FinalColumn/FinalColumn";
import OutOfOrderIndicator from "./OutOfOrderIndicator";
import ContextMenu from "./ContextMenu";
import CellValidation from "./CellValidation";
import EmptyState from "@src/components/EmptyState";
import { InlineErrorFallback } from "@src/components/ErrorFallback";
// import BulkActions from "./BulkActions";
import {
@@ -57,6 +61,10 @@ export const TABLE_PADDING = 16;
export const OUT_OF_ORDER_MARGIN = 8;
export const DEBOUNCE_DELAY = 500;
export type TableCellProps = CellContext<TableRow, any> & {
focusInsideCell: boolean;
};
declare module "@tanstack/table-core" {
interface ColumnMeta<TData, TValue> extends ColumnConfig {}
}
@@ -107,21 +115,7 @@ export default function Table({
size: columnConfig.width,
enableResizing: columnConfig.resizable !== false,
minSize: MIN_COL_WIDTH,
// formatter:
// getFieldProp("TableCell", getFieldType(columnConfig)) ??
// function InDev() {
// return null;
// },
// editor:
// getFieldProp("TableEditor", getFieldType(columnConfig)) ??
// function InDev() {
// return null;
// },
// ...columnConfig,
// editable:
// tableSettings.readOnly && !userRoles.includes("ADMIN")
// ? false
// : columnConfig.editable ?? true,
cell: getFieldProp("TableCell", getFieldType(columnConfig)),
})
);
@@ -419,7 +413,7 @@ export default function Table({
selectedCell?.columnKey === cell.column.id;
return (
<StyledCell
<CellValidation
key={cell.id}
data-row-id={row.id}
data-col-id={cell.column.id}
@@ -431,12 +425,17 @@ export default function Table({
tabIndex={isSelectedCell && !focusInsideCell ? 0 : -1}
aria-colindex={cellIndex + 1}
aria-readonly={
!canEditCell &&
cell.column.columnDef.meta?.editable === false
}
aria-required={Boolean(
cell.column.columnDef.meta!.config?.required
)}
aria-selected={isSelectedCell}
aria-describedby="rowy-table-cell-description"
style={{
width: cell.column.getSize(),
height: tableSchema.rowHeight,
left: cell.column.getIsPinned()
? virtualCell.start - TABLE_PADDING
: undefined,
@@ -469,22 +468,24 @@ export default function Table({
(e.target as HTMLDivElement).focus();
setContextMenuTarget(e.target as HTMLElement);
}}
value={cell.getValue()}
required={cell.column.columnDef.meta!.config?.required}
validationRegex={
cell.column.columnDef.meta!.config?.validationRegex
}
>
<div
className="cell-contents"
style={{ height: tableSchema.rowHeight }}
>
{flexRender(cell.column.columnDef.cell, {
...cell.getContext(),
focusInsideCell: isSelectedCell && focusInsideCell,
})}
<ErrorBoundary fallbackRender={InlineErrorFallback}>
{flexRender(cell.column.columnDef.cell, {
...cell.getContext(),
focusInsideCell: isSelectedCell && focusInsideCell,
})}
</ErrorBoundary>
</div>
{/* <button
tabIndex={isSelectedCell && focusInsideCell ? 0 : -1}
>
{isSelectedCell ? "f" : "x"}
</button> */}
</StyledCell>
</CellValidation>
);
})}

View File

@@ -32,7 +32,7 @@ export const ConnectTable = forwardRef(function ConnectTable(
value.map((item: any) => (
<Grid item key={item.docPath}>
<Chip
label={config.primaryKeys
label={(config.primaryKeys ?? [])
.map((key: string) => item.snapshot[key])
.join(" ")}
/>
@@ -41,7 +41,7 @@ export const ConnectTable = forwardRef(function ConnectTable(
) : value ? (
<Grid item>
<Chip
label={config.primaryKeys
label={(config.primaryKeys ?? [])
.map((key: string) => value.snapshot[key])
.join(" ")}
/>

View File

@@ -1,12 +1,9 @@
import { get } from "lodash-es";
import { FormatterProps } from "react-data-grid";
import type { TableCellProps } from "@src/components/Table";
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.
@@ -16,26 +13,17 @@ import { TableRow } from "@src/types/table";
export default function withBasicCell(
BasicCellComponent: React.ComponentType<IBasicCellProps>
) {
return function BasicCell(props: FormatterProps<TableRow>) {
const { name, key } = props.column;
const value = get(props.row, key);
const { validationRegex, required } = (props.column as any).config;
return function BasicCell({ row, column, getValue }: TableCellProps) {
const columnConfig = column.columnDef.meta!;
const { name } = columnConfig;
const value = getValue();
return (
<ErrorBoundary FallbackComponent={InlineErrorFallback}>
<CellValidation
value={value}
required={required}
validationRegex={validationRegex}
>
<BasicCellComponent
value={value}
name={name as string}
type={(props.column as any).type as FieldType}
/>
</CellValidation>
</ErrorBoundary>
<BasicCellComponent
value={value}
name={name as string}
type={columnConfig.type}
/>
);
};
}

View File

@@ -1,16 +1,11 @@
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 type { TableCellProps } from "@src/components/Table";
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.
@@ -24,11 +19,9 @@ export default function withHeavyCell(
HeavyCellComponent: React.ComponentType<IHeavyCellProps>,
readOnly: boolean = false
) {
return function HeavyCell(props: FormatterProps<TableRow>) {
return function HeavyCell({ row, column, getValue }: TableCellProps) {
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"
@@ -41,7 +34,7 @@ export default function withHeavyCell(
}, []);
// TODO: Investigate if this still needs to be a state
const value = get(props.row, props.column.key);
const value = getValue();
const [localValue, setLocalValue] = useState(value);
useEffect(() => {
setLocalValue(value);
@@ -50,29 +43,18 @@ export default function withHeavyCell(
// 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,
name: column.columnDef.meta!.name,
type: column.columnDef.meta!.type,
};
const basicCell = <BasicCellComponent {...basicCellProps} />;
if (displayedComponent === "basic")
return (
<ErrorBoundary FallbackComponent={InlineErrorFallback}>
<CellValidation
value={value}
required={required}
validationRegex={validationRegex}
>
{basicCell}
</CellValidation>
</ErrorBoundary>
);
if (displayedComponent === "basic") return basicCell;
const handleSubmit = (value: any) => {
if (readOnly) return;
updateField({
path: props.row._rowy_ref.path,
fieldName: props.column.key,
path: row.original._rowy_ref.path,
fieldName: column.id,
value,
});
setLocalValue(value);
@@ -80,23 +62,16 @@ export default function withHeavyCell(
if (displayedComponent === "heavy")
return (
<ErrorBoundary FallbackComponent={InlineErrorFallback}>
<Suspense fallback={basicCell}>
<CellValidation
value={value}
required={required}
validationRegex={validationRegex}
>
<HeavyCellComponent
{...props}
{...basicCellProps}
docRef={props.row._rowy_ref}
onSubmit={handleSubmit}
disabled={props.column.editable === false}
/>
</CellValidation>
</Suspense>
</ErrorBoundary>
<Suspense fallback={basicCell}>
<HeavyCellComponent
{...basicCellProps}
row={row.original}
column={column.columnDef.meta!}
docRef={row.original._rowy_ref}
onSubmit={handleSubmit}
disabled={column.columnDef.meta!.editable === false}
/>
</Suspense>
);
// Should not reach this line

View File

@@ -1,19 +1,15 @@
import { Suspense, useState, useEffect, useRef } from "react";
import { useSetAtom } from "jotai";
import { find, get } from "lodash-es";
import { FormatterProps } from "react-data-grid";
import { get } from "lodash-es";
import type { TableCellProps } from "@src/components/Table";
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";
@@ -39,13 +35,11 @@ export default function withPopoverCell(
PopoverCellComponent: React.ComponentType<IPopoverCellProps>,
options?: IPopoverCellOptions
) {
return function PopoverCell(props: FormatterProps<any>) {
return function PopoverCell({ row, column, getValue }: TableCellProps) {
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"
@@ -64,7 +58,7 @@ export default function withPopoverCell(
const inlineCellRef = useRef<any>(null);
// TODO: Investigate if this still needs to be a state
const value = get(props.row, props.column.key);
const value = getValue();
const [localValue, setLocalValue] = useState(value);
useEffect(() => {
setLocalValue(value);
@@ -73,29 +67,19 @@ export default function withPopoverCell(
// 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,
name: column.columnDef.meta!.name,
type: column.columnDef.meta!.type,
};
if (displayedComponent === "basic")
return (
<ErrorBoundary FallbackComponent={InlineErrorFallback}>
<CellValidation
value={value}
required={required}
validationRegex={validationRegex}
>
<BasicCellComponent {...basicCellProps} />
</CellValidation>
</ErrorBoundary>
);
return <BasicCellComponent {...basicCellProps} />;
// 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,
path: row.original._rowy_ref.path,
fieldName: column.id,
value,
deleteField: value === undefined,
});
@@ -113,12 +97,12 @@ export default function withPopoverCell(
// Declare inlineCell and props here so it can be reused later
const commonCellProps = {
...props,
...basicCellProps,
column: props.column,
row: row.original,
column: column.columnDef.meta!,
onSubmit: handleSubmit,
disabled: props.column.editable === false,
docRef: props.row._rowy_ref,
disabled: column.columnDef.meta!.editable === false,
docRef: row.original._rowy_ref,
showPopoverCell,
ref: inlineCellRef,
};
@@ -126,31 +110,14 @@ export default function withPopoverCell(
<InlineCellComponent {...commonCellProps} ref={inlineCellRef} />
);
if (displayedComponent === "inline")
return (
<ErrorBoundary FallbackComponent={InlineErrorFallback}>
<CellValidation
value={value}
required={required}
validationRegex={validationRegex}
>
{inlineCell}
</CellValidation>
</ErrorBoundary>
);
if (displayedComponent === "inline") return inlineCell;
const parentRef = inlineCellRef.current?.parentElement;
if (displayedComponent === "popover")
return (
<ErrorBoundary FallbackComponent={InlineErrorFallback}>
<CellValidation
value={value}
required={required}
validationRegex={validationRegex}
>
{inlineCell}
</CellValidation>
<>
{inlineCell}
<Suspense fallback={null}>
<Popover
@@ -175,7 +142,7 @@ export default function withPopoverCell(
/>
</Popover>
</Suspense>
</ErrorBoundary>
</>
);
// Should not reach this line

View File

@@ -1,4 +1,5 @@
import { FieldType } from "@src/constants/fields";
import type { TableCellProps } from "@src/components/Table";
import { FormatterProps, EditorProps } from "react-data-grid";
import { Control, UseFormReturn } from "react-hook-form";
import { PopoverProps } from "@mui/material";
@@ -28,7 +29,7 @@ export interface IFieldConfig {
selectedCell: SelectedCell,
reset: () => void
) => IContextMenuItem[];
TableCell: React.ComponentType<FormatterProps<TableRow>>;
TableCell: React.ComponentType<TableCellProps>;
TableEditor: React.ComponentType<EditorProps<TableRow, any>>;
SideDrawerField: React.ComponentType<ISideDrawerFieldProps>;
settings?: React.ComponentType<ISettingsProps>;
@@ -49,10 +50,9 @@ export interface IBasicCellProps {
type: FieldType;
name: string;
}
export interface IHeavyCellProps
extends IBasicCellProps,
FormatterProps<TableRow> {
column: FormatterProps<TableRow>["column"] & { config?: Record<string, any> };
export interface IHeavyCellProps extends IBasicCellProps {
row: TableRow;
column: ColumnConfig;
onSubmit: (value: any) => void;
docRef: TableRowRef;
disabled: boolean;

16
src/types/table.d.ts vendored
View File

@@ -133,26 +133,28 @@ 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;
renderFieldType: FieldType;
/** For sub-table fields */
parentLabel?: string[];
parentLabel: string[];
primaryKeys?: string[];
primaryKeys: string[];
/** Column-specific config */
[key: string]: any;
};
}>;
};
export type TableFilter = {