mirror of
https://github.com/rowyio/rowy.git
synced 2025-12-28 16:06:41 +01:00
unify withTableCell HOCs into single HOC with memo
This commit is contained in:
@@ -133,6 +133,7 @@ export const sideDrawerOpenAtom = atom(false);
|
||||
export type SelectedCell = {
|
||||
path: string | "_rowy_header";
|
||||
columnKey: string | "_rowy_row_actions";
|
||||
focusInside: boolean;
|
||||
};
|
||||
/** Store selected cell in table. Used in side drawer and context menu */
|
||||
export const selectedCellAtom = atom<SelectedCell | null>(null);
|
||||
|
||||
@@ -79,7 +79,11 @@ export default function SideDrawer({
|
||||
if (direction === "down" && rowIndex < tableRows.length - 1) rowIndex += 1;
|
||||
const newPath = tableRows[rowIndex]._rowy_ref.path;
|
||||
|
||||
setCell((cell) => ({ columnKey: cell!.columnKey, path: newPath }));
|
||||
setCell((cell) => ({
|
||||
columnKey: cell!.columnKey,
|
||||
path: newPath,
|
||||
focusInside: false,
|
||||
}));
|
||||
|
||||
const columnIndex = visibleColumnKeys.indexOf(cell!.columnKey || "");
|
||||
dataGridRef?.current?.selectCell(
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { memo } from "react";
|
||||
import { styled } from "@mui/material/styles";
|
||||
import ErrorIcon from "@mui/icons-material/ErrorOutline";
|
||||
import WarningIcon from "@mui/icons-material/WarningAmber";
|
||||
@@ -18,7 +19,7 @@ const Dot = styled("div")(({ theme }) => ({
|
||||
borderRadius: "50%",
|
||||
backgroundColor: theme.palette.error.main,
|
||||
|
||||
boxShadow: `0 0 0 4px var(--background-color)`,
|
||||
boxShadow: `0 0 0 4px var(--cell-background-color)`,
|
||||
"[role='row']:hover &": {
|
||||
boxShadow: `0 0 0 4px var(--row-hover-background-color)`,
|
||||
},
|
||||
@@ -34,7 +35,7 @@ export interface ICellValidationProps
|
||||
validationRegex?: string;
|
||||
}
|
||||
|
||||
export default function CellValidation({
|
||||
export const CellValidation = memo(function MemoizedCellValidation({
|
||||
value,
|
||||
required,
|
||||
validationRegex,
|
||||
@@ -73,4 +74,6 @@ export default function CellValidation({
|
||||
);
|
||||
|
||||
return <StyledCell {...props}>{children}</StyledCell>;
|
||||
}
|
||||
});
|
||||
|
||||
export default CellValidation;
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { memo } from "react";
|
||||
import { useAtom, useSetAtom } from "jotai";
|
||||
import type { TableCellProps } from "@src/components/Table";
|
||||
|
||||
@@ -21,7 +22,10 @@ import {
|
||||
contextMenuTargetAtom,
|
||||
} from "@src/atoms/tableScope";
|
||||
|
||||
export default function FinalColumn({ row, focusInsideCell }: TableCellProps) {
|
||||
export const FinalColumn = memo(function FinalColumn({
|
||||
row,
|
||||
focusInsideCell,
|
||||
}: TableCellProps) {
|
||||
const [userRoles] = useAtom(userRolesAtom, projectScope);
|
||||
const [addRowIdType] = useAtom(tableAddRowIdTypeAtom, projectScope);
|
||||
const confirm = useSetAtom(confirmDialogAtom, projectScope);
|
||||
@@ -47,7 +51,7 @@ export default function FinalColumn({ row, focusInsideCell }: TableCellProps) {
|
||||
<Stack
|
||||
direction="row"
|
||||
alignItems="center"
|
||||
style={{ height: "100%" }}
|
||||
className="cell-contents"
|
||||
gap={0.5}
|
||||
>
|
||||
<Tooltip title="Row menu">
|
||||
@@ -145,4 +149,5 @@ export default function FinalColumn({ row, focusInsideCell }: TableCellProps) {
|
||||
</Tooltip>
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
});
|
||||
export default FinalColumn;
|
||||
|
||||
@@ -1,31 +1,29 @@
|
||||
import { colord } from "colord";
|
||||
import { styled } from "@mui/material";
|
||||
|
||||
export const StyledCell = styled("div")(({ theme }) => ({
|
||||
position: "relative",
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
lineHeight: "calc(var(--row-height) - 1px)",
|
||||
whiteSpace: "nowrap",
|
||||
"--cell-padding": theme.spacing(10 / 8),
|
||||
|
||||
"& > .cell-contents": {
|
||||
padding: "0 var(--cell-padding)",
|
||||
lineHeight: "calc(var(--row-height) - 1px)",
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
contain: "strict",
|
||||
overflow: "hidden",
|
||||
},
|
||||
|
||||
overflow: "visible",
|
||||
contain: "strict",
|
||||
position: "relative",
|
||||
|
||||
backgroundColor: theme.palette.background.paper,
|
||||
backgroundColor: "var(--cell-background-color)",
|
||||
|
||||
border: `1px solid ${theme.palette.divider}`,
|
||||
borderTop: "none",
|
||||
"& + &": { borderLeft: "none" },
|
||||
|
||||
"[role='row']:hover &": {
|
||||
backgroundColor: colord(theme.palette.background.paper)
|
||||
.mix(theme.palette.action.hover, theme.palette.action.hoverOpacity)
|
||||
.alpha(1)
|
||||
.toHslString(),
|
||||
backgroundColor: "var(--row-hover-background-color)",
|
||||
},
|
||||
|
||||
"[data-out-of-order='true'] + [role='row'] &": {
|
||||
|
||||
@@ -1,10 +1,7 @@
|
||||
import { styled, alpha } from "@mui/material";
|
||||
|
||||
import { DEFAULT_ROW_HEIGHT } from "@src/components/Table";
|
||||
|
||||
export const StyledRow = styled("div")(({ theme }) => ({
|
||||
display: "flex",
|
||||
height: DEFAULT_ROW_HEIGHT,
|
||||
position: "relative",
|
||||
|
||||
"& > *": {
|
||||
@@ -31,19 +28,24 @@ export const StyledRow = styled("div")(({ theme }) => ({
|
||||
},
|
||||
},
|
||||
|
||||
"& .MuiIconButton-root.row-hover-iconButton, .MuiIconButton-root.row-hover-iconButton:focus":
|
||||
{
|
||||
color: theme.palette.text.disabled,
|
||||
transitionDuration: "0s",
|
||||
},
|
||||
"&:hover .MuiIconButton-root.row-hover-iconButton, .MuiIconButton-root.row-hover-iconButton:focus":
|
||||
{
|
||||
color: theme.palette.text.primary,
|
||||
backgroundColor: alpha(
|
||||
theme.palette.action.hover,
|
||||
theme.palette.action.hoverOpacity * 1.5
|
||||
),
|
||||
},
|
||||
"& .row-hover-iconButton, .row-hover-iconButton:focus": {
|
||||
color: theme.palette.text.disabled,
|
||||
transitionDuration: "0s",
|
||||
|
||||
flexShrink: 0,
|
||||
borderRadius: theme.shape.borderRadius,
|
||||
padding: (32 - 20) / 2,
|
||||
boxSizing: "content-box",
|
||||
|
||||
"&.end": { marginRight: theme.spacing(0.5) },
|
||||
},
|
||||
"&:hover .row-hover-iconButton, .row-hover-iconButton:focus": {
|
||||
color: theme.palette.text.primary,
|
||||
backgroundColor: alpha(
|
||||
theme.palette.action.hover,
|
||||
theme.palette.action.hoverOpacity * 1.5
|
||||
),
|
||||
},
|
||||
}));
|
||||
StyledRow.displayName = "StyledRow";
|
||||
|
||||
|
||||
@@ -1,6 +1,19 @@
|
||||
import { styled } from "@mui/material";
|
||||
import { colord } from "colord";
|
||||
|
||||
export const StyledTable = styled("div")(({ theme }) => ({
|
||||
"--cell-background-color":
|
||||
theme.palette.mode === "light"
|
||||
? theme.palette.background.paper
|
||||
: colord(theme.palette.background.paper)
|
||||
.mix("#fff", 0.04)
|
||||
.alpha(1)
|
||||
.toHslString(),
|
||||
"--row-hover-background-color": colord(theme.palette.background.paper)
|
||||
.mix(theme.palette.action.hover, theme.palette.action.hoverOpacity)
|
||||
.alpha(1)
|
||||
.toHslString(),
|
||||
|
||||
...(theme.typography.caption as any),
|
||||
lineHeight: "inherit !important",
|
||||
|
||||
|
||||
@@ -14,6 +14,7 @@ import {
|
||||
Droppable,
|
||||
Draggable,
|
||||
} from "react-beautiful-dnd";
|
||||
import { get } from "lodash-es";
|
||||
import { Portal } from "@mui/material";
|
||||
import { ErrorBoundary } from "react-error-boundary";
|
||||
|
||||
@@ -63,6 +64,8 @@ export const DEBOUNCE_DELAY = 500;
|
||||
|
||||
export type TableCellProps = CellContext<TableRow, any> & {
|
||||
focusInsideCell: boolean;
|
||||
setFocusInsideCell: (focusInside: boolean) => void;
|
||||
disabled: boolean;
|
||||
};
|
||||
|
||||
declare module "@tanstack/table-core" {
|
||||
@@ -95,6 +98,7 @@ export default function Table({
|
||||
const [tablePage, setTablePage] = useAtom(tablePageAtom, tableScope);
|
||||
const [selectedCell, setSelectedCell] = useAtom(selectedCellAtom, tableScope);
|
||||
const setContextMenuTarget = useSetAtom(contextMenuTargetAtom, tableScope);
|
||||
const focusInsideCell = selectedCell?.focusInside ?? false;
|
||||
|
||||
const updateColumn = useSetAtom(updateColumnAtom, tableScope);
|
||||
const updateField = useSetAtom(updateFieldAtom, tableScope);
|
||||
@@ -109,7 +113,7 @@ export default function Table({
|
||||
// Hide column for all users using table schema
|
||||
.filter((column) => !column.hidden)
|
||||
.map((columnConfig) =>
|
||||
columnHelper.accessor(columnConfig.fieldName, {
|
||||
columnHelper.accessor((row) => get(row, columnConfig.fieldName), {
|
||||
id: columnConfig.fieldName,
|
||||
meta: columnConfig,
|
||||
size: columnConfig.width,
|
||||
@@ -119,7 +123,7 @@ export default function Table({
|
||||
})
|
||||
);
|
||||
|
||||
if (canAddColumn || !tableSettings.readOnly) {
|
||||
if (canAddColumn || canEditCell) {
|
||||
_columns.push(
|
||||
columnHelper.display({
|
||||
id: "_rowy_column_actions",
|
||||
@@ -129,7 +133,7 @@ export default function Table({
|
||||
}
|
||||
|
||||
return _columns;
|
||||
}, [tableColumnsOrdered, canAddColumn, tableSettings.readOnly]);
|
||||
}, [tableColumnsOrdered, canAddColumn, canEditCell]);
|
||||
|
||||
// Get user’s hidden columns from props and memoize into a VisibilityState
|
||||
const columnVisibility = useMemo(() => {
|
||||
@@ -171,7 +175,7 @@ export default function Table({
|
||||
const { rows } = table.getRowModel();
|
||||
const leafColumns = table.getVisibleLeafColumns();
|
||||
|
||||
const { handleKeyDown, focusInsideCell } = useKeyboardNavigation({
|
||||
const { handleKeyDown } = useKeyboardNavigation({
|
||||
gridRef,
|
||||
tableRows,
|
||||
leafColumns,
|
||||
@@ -228,7 +232,7 @@ export default function Table({
|
||||
<StyledTable
|
||||
ref={gridRef}
|
||||
role="grid"
|
||||
aria-readonly={tableSettings.readOnly}
|
||||
aria-readonly={!canEditCell}
|
||||
aria-colcount={columns.length}
|
||||
aria-rowcount={tableRows.length + 1}
|
||||
style={
|
||||
@@ -320,15 +324,24 @@ export default function Table({
|
||||
zIndex: header.column.getIsPinned() ? 11 : 10,
|
||||
}}
|
||||
width={header.getSize()}
|
||||
sx={[
|
||||
sx={
|
||||
snapshot.isDragging
|
||||
? {}
|
||||
: { "& + &": { borderLeft: "none" } },
|
||||
]}
|
||||
? undefined
|
||||
: { "& + &": { borderLeft: "none" } }
|
||||
}
|
||||
onClick={(e) => {
|
||||
setSelectedCell({
|
||||
path: "_rowy_header",
|
||||
columnKey: header.id,
|
||||
focusInside: false,
|
||||
});
|
||||
(e.target as HTMLDivElement).focus();
|
||||
}}
|
||||
onDoubleClick={(e) => {
|
||||
setSelectedCell({
|
||||
path: "_rowy_header",
|
||||
columnKey: header.id,
|
||||
focusInside: true,
|
||||
});
|
||||
(e.target as HTMLDivElement).focus();
|
||||
}}
|
||||
@@ -425,11 +438,11 @@ export default function Table({
|
||||
tabIndex={isSelectedCell && !focusInsideCell ? 0 : -1}
|
||||
aria-colindex={cellIndex + 1}
|
||||
aria-readonly={
|
||||
!canEditCell &&
|
||||
!canEditCell ||
|
||||
cell.column.columnDef.meta?.editable === false
|
||||
}
|
||||
aria-required={Boolean(
|
||||
cell.column.columnDef.meta!.config?.required
|
||||
cell.column.columnDef.meta?.config?.required
|
||||
)}
|
||||
aria-selected={isSelectedCell}
|
||||
aria-describedby="rowy-table-cell-description"
|
||||
@@ -456,6 +469,15 @@ export default function Table({
|
||||
setSelectedCell({
|
||||
path: row.original._rowy_ref.path,
|
||||
columnKey: cell.column.id,
|
||||
focusInside: false,
|
||||
});
|
||||
(e.target as HTMLDivElement).focus();
|
||||
}}
|
||||
onDoubleClick={(e) => {
|
||||
setSelectedCell({
|
||||
path: row.original._rowy_ref.path,
|
||||
columnKey: cell.column.id,
|
||||
focusInside: true,
|
||||
});
|
||||
(e.target as HTMLDivElement).focus();
|
||||
}}
|
||||
@@ -464,27 +486,32 @@ export default function Table({
|
||||
setSelectedCell({
|
||||
path: row.original._rowy_ref.path,
|
||||
columnKey: cell.column.id,
|
||||
focusInside: false,
|
||||
});
|
||||
(e.target as HTMLDivElement).focus();
|
||||
setContextMenuTarget(e.target as HTMLElement);
|
||||
}}
|
||||
value={cell.getValue()}
|
||||
required={cell.column.columnDef.meta!.config?.required}
|
||||
required={cell.column.columnDef.meta?.config?.required}
|
||||
validationRegex={
|
||||
cell.column.columnDef.meta!.config?.validationRegex
|
||||
cell.column.columnDef.meta?.config?.validationRegex
|
||||
}
|
||||
>
|
||||
<div
|
||||
className="cell-contents"
|
||||
style={{ height: tableSchema.rowHeight }}
|
||||
>
|
||||
<ErrorBoundary fallbackRender={InlineErrorFallback}>
|
||||
{flexRender(cell.column.columnDef.cell, {
|
||||
...cell.getContext(),
|
||||
focusInsideCell: isSelectedCell && focusInsideCell,
|
||||
})}
|
||||
</ErrorBoundary>
|
||||
</div>
|
||||
<ErrorBoundary fallbackRender={InlineErrorFallback}>
|
||||
{flexRender(cell.column.columnDef.cell, {
|
||||
...cell.getContext(),
|
||||
focusInsideCell: isSelectedCell && focusInsideCell,
|
||||
setFocusInsideCell: (focusInside: boolean) =>
|
||||
setSelectedCell({
|
||||
path: row.original._rowy_ref.path,
|
||||
columnKey: cell.column.id,
|
||||
focusInside,
|
||||
}),
|
||||
disabled:
|
||||
!canEditCell ||
|
||||
cell.column.columnDef.meta?.editable === false,
|
||||
})}
|
||||
</ErrorBoundary>
|
||||
</CellValidation>
|
||||
);
|
||||
})}
|
||||
|
||||
@@ -43,7 +43,7 @@ export const TableContainer = styled("div", {
|
||||
"--color": theme.palette.text.primary,
|
||||
"--border-color": theme.palette.divider,
|
||||
// "--summary-border-color": "#aaa",
|
||||
"--background-color":
|
||||
"--cell-background-color":
|
||||
theme.palette.mode === "light"
|
||||
? theme.palette.background.paper
|
||||
: colord(theme.palette.background.paper)
|
||||
|
||||
@@ -81,7 +81,7 @@ export default function TextEditor({ row, column }: EditorProps<any>) {
|
||||
sx={{
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
backgroundColor: "var(--background-color)",
|
||||
backgroundColor: "var(--cell-background-color)",
|
||||
|
||||
"& .MuiInputBase-root": {
|
||||
height: "100%",
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { useState } from "react";
|
||||
import { useSetAtom } from "jotai";
|
||||
import { Column } from "@tanstack/react-table";
|
||||
|
||||
@@ -23,7 +22,6 @@ export function useKeyboardNavigation({
|
||||
leafColumns,
|
||||
}: IUseKeyboardNavigationProps) {
|
||||
const setSelectedCell = useSetAtom(selectedCellAtom, tableScope);
|
||||
const [focusInsideCell, setFocusInsideCell] = useState(false);
|
||||
|
||||
const handleKeyDown = (e: React.KeyboardEvent<HTMLDivElement>) => {
|
||||
// Block default browser behavior for arrow keys (scroll) and other keys
|
||||
@@ -43,7 +41,7 @@ export function useKeyboardNavigation({
|
||||
|
||||
// Esc: exit cell
|
||||
if (e.key === "Escape") {
|
||||
setFocusInsideCell(false);
|
||||
setSelectedCell((c) => ({ ...c!, focusInside: false }));
|
||||
(
|
||||
gridRef.current?.querySelector("[aria-selected=true]") as HTMLDivElement
|
||||
)?.focus();
|
||||
@@ -63,7 +61,7 @@ export function useKeyboardNavigation({
|
||||
|
||||
// Enter: enter cell
|
||||
if (e.key === "Enter") {
|
||||
setFocusInsideCell(true);
|
||||
setSelectedCell((c) => ({ ...c!, focusInside: true }));
|
||||
(target.querySelector("[tabindex]") as HTMLElement)?.focus();
|
||||
return;
|
||||
}
|
||||
@@ -125,6 +123,8 @@ export function useKeyboardNavigation({
|
||||
? tableRows[newRowIndex]._rowy_ref.path
|
||||
: "_rowy_header",
|
||||
columnKey: leafColumns[newColIndex].id! || leafColumns[0].id!,
|
||||
// When selected cell changes, exit current cell
|
||||
focusInside: false,
|
||||
};
|
||||
|
||||
// Store in selectedCellAtom
|
||||
@@ -139,12 +139,9 @@ export function useKeyboardNavigation({
|
||||
|
||||
// Focus the cell
|
||||
if (newCellEl) setTimeout(() => (newCellEl as HTMLDivElement).focus());
|
||||
|
||||
// When selected cell changes, exit current cell
|
||||
setFocusInsideCell(false);
|
||||
};
|
||||
|
||||
return { handleKeyDown, focusInsideCell } as const;
|
||||
return { handleKeyDown } as const;
|
||||
}
|
||||
|
||||
export default useKeyboardNavigation;
|
||||
|
||||
@@ -39,7 +39,7 @@ export function useVirtualization(
|
||||
} = useVirtual({
|
||||
parentRef: containerRef,
|
||||
size: tableRows.length,
|
||||
overscan: 10,
|
||||
overscan: 5,
|
||||
paddingEnd: TABLE_PADDING,
|
||||
estimateSize: useCallback(
|
||||
(index: number) =>
|
||||
@@ -58,7 +58,7 @@ export function useVirtualization(
|
||||
parentRef: containerRef,
|
||||
horizontal: true,
|
||||
size: leafColumns.length,
|
||||
overscan: 10,
|
||||
overscan: 5,
|
||||
paddingStart: TABLE_PADDING,
|
||||
paddingEnd: TABLE_PADDING,
|
||||
estimateSize: useCallback(
|
||||
|
||||
198
src/components/Table/withTableCell.tsx
Normal file
198
src/components/Table/withTableCell.tsx
Normal file
@@ -0,0 +1,198 @@
|
||||
import { memo, Suspense, useState, useEffect, useRef } from "react";
|
||||
import { useSetAtom } from "jotai";
|
||||
import { get, isEqual } from "lodash-es";
|
||||
import type { TableCellProps } from "@src/components/Table";
|
||||
import {
|
||||
IDisplayCellProps,
|
||||
IEditorCellProps,
|
||||
} from "@src/components/fields/types";
|
||||
|
||||
import { Popover, PopoverProps } from "@mui/material";
|
||||
|
||||
import { tableScope, updateFieldAtom } from "@src/atoms/tableScope";
|
||||
|
||||
export interface ICellOptions {
|
||||
/** If the rest of the row’s data is used, set this to true for memoization */
|
||||
usesRowData?: boolean;
|
||||
/** Handle padding inside the cell component */
|
||||
disablePadding?: boolean;
|
||||
/** Set popover background to be transparent */
|
||||
transparent?: boolean;
|
||||
/** Props to pass to MUI Popover component */
|
||||
popoverProps?: Partial<PopoverProps>;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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}
|
||||
*/
|
||||
export default function withTableCell(
|
||||
DisplayCellComponent: React.ComponentType<IDisplayCellProps>,
|
||||
EditorCellComponent: React.ComponentType<IEditorCellProps>,
|
||||
editorMode: "focus" | "inline" | "popover" = "focus",
|
||||
options: ICellOptions = {}
|
||||
) {
|
||||
return memo(
|
||||
function TableCell({
|
||||
row,
|
||||
column,
|
||||
getValue,
|
||||
focusInsideCell,
|
||||
setFocusInsideCell,
|
||||
disabled,
|
||||
}: TableCellProps) {
|
||||
const value = getValue();
|
||||
const updateField = useSetAtom(updateFieldAtom, tableScope);
|
||||
|
||||
// Store ref to rendered DisplayCell to get positioning for PopoverCell
|
||||
const displayCellRef = useRef<HTMLDivElement>(null);
|
||||
const parentRef = displayCellRef.current?.parentElement;
|
||||
|
||||
// Store Popover open state here so we can add delay for close transition
|
||||
const [popoverOpen, setPopoverOpen] = useState(false);
|
||||
useEffect(() => {
|
||||
if (focusInsideCell) setPopoverOpen(true);
|
||||
}, [focusInsideCell]);
|
||||
const showPopoverCell = (popover: boolean) => {
|
||||
if (popover) {
|
||||
setPopoverOpen(true);
|
||||
// Need to call this after a timeout, since the cell’s `onClick`
|
||||
// event is fired, which sets focusInsideCell false
|
||||
setTimeout(() => setFocusInsideCell(true));
|
||||
} else {
|
||||
setPopoverOpen(false);
|
||||
// Call after a timeout to allow the close transition to finish
|
||||
setTimeout(() => {
|
||||
setFocusInsideCell(false);
|
||||
// Focus the cell. Otherwise, it focuses the body.
|
||||
parentRef?.focus();
|
||||
}, 300);
|
||||
}
|
||||
};
|
||||
|
||||
// Declare basicCell here so props can be reused by HeavyCellComponent
|
||||
const basicCellProps: IDisplayCellProps = {
|
||||
value,
|
||||
name: column.columnDef.meta!.name,
|
||||
type: column.columnDef.meta!.type,
|
||||
row: row.original,
|
||||
column: column.columnDef.meta!,
|
||||
docRef: row.original._rowy_ref,
|
||||
disabled: column.columnDef.meta!.editable === false,
|
||||
showPopoverCell,
|
||||
setFocusInsideCell,
|
||||
};
|
||||
|
||||
// Show display cell, unless if editorMode is inline
|
||||
const displayCell = (
|
||||
<div
|
||||
className="cell-contents"
|
||||
style={options.disablePadding ? { padding: 0 } : undefined}
|
||||
ref={displayCellRef}
|
||||
>
|
||||
<DisplayCellComponent {...basicCellProps} />
|
||||
</div>
|
||||
);
|
||||
if (disabled || (editorMode !== "inline" && !focusInsideCell))
|
||||
return displayCell;
|
||||
|
||||
// This is where we update the documents
|
||||
const handleSubmit = (value: any) => {
|
||||
if (disabled) return;
|
||||
updateField({
|
||||
path: row.original._rowy_ref.path,
|
||||
fieldName: column.id,
|
||||
value,
|
||||
deleteField: value === undefined,
|
||||
});
|
||||
};
|
||||
|
||||
const editorCell = (
|
||||
<EditorCellComponent
|
||||
{...basicCellProps}
|
||||
tabIndex={focusInsideCell ? 0 : -1}
|
||||
onSubmit={handleSubmit}
|
||||
parentRef={parentRef}
|
||||
/>
|
||||
);
|
||||
|
||||
if (editorMode === "focus" && focusInsideCell) {
|
||||
return editorCell;
|
||||
}
|
||||
|
||||
if (editorMode === "inline") {
|
||||
return (
|
||||
<div
|
||||
className="cell-contents"
|
||||
style={options.disablePadding ? { padding: 0 } : undefined}
|
||||
ref={displayCellRef}
|
||||
>
|
||||
{editorCell}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (editorMode === "popover")
|
||||
return (
|
||||
<>
|
||||
{displayCell}
|
||||
|
||||
<Suspense fallback={null}>
|
||||
<Popover
|
||||
open={popoverOpen}
|
||||
anchorEl={parentRef}
|
||||
onClose={() => showPopoverCell(false)}
|
||||
anchorOrigin={{ horizontal: "left", vertical: "bottom" }}
|
||||
{...options.popoverProps}
|
||||
sx={
|
||||
options.transparent
|
||||
? {
|
||||
"& .MuiPopover-paper": {
|
||||
backgroundColor: "transparent",
|
||||
},
|
||||
}
|
||||
: {}
|
||||
}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
onDoubleClick={(e) => e.stopPropagation()}
|
||||
onKeyDown={(e) => e.stopPropagation()}
|
||||
>
|
||||
{editorCell}
|
||||
</Popover>
|
||||
</Suspense>
|
||||
</>
|
||||
);
|
||||
|
||||
// Should not reach this line
|
||||
return null;
|
||||
},
|
||||
(prev, next) => {
|
||||
const valueEqual = isEqual(
|
||||
get(prev.row.original, prev.column.columnDef.meta!.fieldName),
|
||||
get(next.row.original, next.column.columnDef.meta!.fieldName)
|
||||
);
|
||||
const columnEqual = isEqual(
|
||||
prev.column.columnDef.meta,
|
||||
next.column.columnDef.meta
|
||||
);
|
||||
const rowEqual = isEqual(prev.row.original, next.row.original);
|
||||
const focusInsideCellEqual =
|
||||
prev.focusInsideCell === next.focusInsideCell;
|
||||
const disabledEqual = prev.disabled === next.disabled;
|
||||
|
||||
const baseEqualities =
|
||||
valueEqual && columnEqual && focusInsideCellEqual && disabledEqual;
|
||||
|
||||
if (options?.usesRowData) return baseEqualities && rowEqual;
|
||||
else return baseEqualities;
|
||||
}
|
||||
);
|
||||
}
|
||||
@@ -12,7 +12,7 @@ import {
|
||||
} from "@src/atoms/tableScope";
|
||||
import { DEFAULT_ROW_HEIGHT } from "@src/components/Table";
|
||||
|
||||
const ROW_HEIGHTS = [32, 40, 64, 96, 128, 160];
|
||||
const ROW_HEIGHTS = [32, 40, 64, 96, 128, 160].map((x) => x + 1);
|
||||
|
||||
export default function RowHeight() {
|
||||
const theme = useTheme();
|
||||
@@ -63,7 +63,7 @@ export default function RowHeight() {
|
||||
<ListSubheader>Row height</ListSubheader>
|
||||
{ROW_HEIGHTS.map((height) => (
|
||||
<MenuItem key={height} value={height}>
|
||||
{height}px
|
||||
{height - 1}px
|
||||
</MenuItem>
|
||||
))}
|
||||
</TextField>
|
||||
|
||||
30
src/components/fields/Checkbox/DisplayCell.tsx
Normal file
30
src/components/fields/Checkbox/DisplayCell.tsx
Normal file
@@ -0,0 +1,30 @@
|
||||
import { IDisplayCellProps } from "@src/components/fields/types";
|
||||
|
||||
import { FormControlLabel, Switch } from "@mui/material";
|
||||
|
||||
export default function Checkbox({ column, value }: IDisplayCellProps) {
|
||||
return (
|
||||
<FormControlLabel
|
||||
control={
|
||||
<Switch checked={!!value} disabled color="success" tabIndex={-1} />
|
||||
}
|
||||
label={column.name as string}
|
||||
labelPlacement="start"
|
||||
sx={{
|
||||
m: 0,
|
||||
width: "100%",
|
||||
alignItems: "center",
|
||||
|
||||
"& .MuiFormControlLabel-label": {
|
||||
font: "inherit",
|
||||
letterSpacing: "inherit",
|
||||
flexGrow: 1,
|
||||
overflowX: "hidden",
|
||||
mt: "0 !important",
|
||||
},
|
||||
|
||||
"& .MuiSwitch-root": { mr: -0.75 },
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
import { IHeavyCellProps } from "@src/components/fields/types";
|
||||
import { IEditorCellProps } from "@src/components/fields/types";
|
||||
import { useSetAtom } from "jotai";
|
||||
import { get } from "lodash-es";
|
||||
|
||||
@@ -17,7 +17,8 @@ export default function Checkbox({
|
||||
value,
|
||||
onSubmit,
|
||||
disabled,
|
||||
}: IHeavyCellProps) {
|
||||
tabIndex,
|
||||
}: IEditorCellProps) {
|
||||
const confirm = useSetAtom(confirmDialogAtom, projectScope);
|
||||
|
||||
const handleChange = () => {
|
||||
@@ -43,6 +44,7 @@ export default function Checkbox({
|
||||
onChange={handleChange}
|
||||
disabled={disabled}
|
||||
color="success"
|
||||
tabIndex={tabIndex}
|
||||
/>
|
||||
}
|
||||
label={column.name as string}
|
||||
@@ -1,13 +1,12 @@
|
||||
import { lazy } from "react";
|
||||
import { IFieldConfig, FieldType } from "@src/components/fields/types";
|
||||
import withHeavyCell from "@src/components/fields/_withTableCell/withHeavyCell";
|
||||
import withTableCell from "@src/components/Table/withTableCell";
|
||||
|
||||
import CheckboxIcon from "@mui/icons-material/ToggleOnOutlined";
|
||||
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-Checkbox" */)
|
||||
const EditorCell = lazy(
|
||||
() => import("./EditorCell" /* webpackChunkName: "EditorCell-Checkbox" */)
|
||||
);
|
||||
const SideDrawerField = lazy(
|
||||
() =>
|
||||
@@ -25,8 +24,9 @@ export const config: IFieldConfig = {
|
||||
initializable: true,
|
||||
icon: <CheckboxIcon />,
|
||||
description: "True/false value. Default: false.",
|
||||
TableCell: withHeavyCell(BasicCell, TableCell),
|
||||
TableEditor: NullEditor as any,
|
||||
TableCell: withTableCell(DisplayCell, EditorCell, "inline", {
|
||||
usesRowData: true,
|
||||
}),
|
||||
csvImportParser: (value: string) => {
|
||||
if (["YES", "TRUE", "1"].includes(value.toUpperCase())) return true;
|
||||
else if (["NO", "FALSE", "0"].includes(value.toUpperCase())) return false;
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { forwardRef } from "react";
|
||||
import { IPopoverInlineCellProps } from "@src/components/fields/types";
|
||||
import { IDisplayCellProps } from "@src/components/fields/types";
|
||||
|
||||
import { ButtonBase, Grid } from "@mui/material";
|
||||
import { ChevronDown } from "@src/assets/icons";
|
||||
@@ -7,22 +6,21 @@ import { ChevronDown } from "@src/assets/icons";
|
||||
import { sanitiseValue } from "./utils";
|
||||
import ChipList from "@src/components/Table/formatters/ChipList";
|
||||
import FormattedChip from "@src/components/FormattedChip";
|
||||
import { ConvertStringToArray } from "./ConvertStringToArray";
|
||||
|
||||
export const MultiSelect = forwardRef(function MultiSelect(
|
||||
{ value, showPopoverCell, disabled, onSubmit }: IPopoverInlineCellProps,
|
||||
ref: React.Ref<any>
|
||||
) {
|
||||
if (typeof value === "string" && value !== "")
|
||||
return <ConvertStringToArray value={value} onSubmit={onSubmit} />;
|
||||
export default function MultiSelect({
|
||||
value,
|
||||
showPopoverCell,
|
||||
disabled,
|
||||
}: IDisplayCellProps) {
|
||||
// if (typeof value === "string" && value !== "")
|
||||
// return <ConvertStringToArray value={value} onSubmit={onSubmit} />;
|
||||
|
||||
return (
|
||||
<ButtonBase
|
||||
onClick={() => showPopoverCell(true)}
|
||||
ref={ref}
|
||||
disabled={disabled}
|
||||
className="cell-collapse-padding"
|
||||
sx={{
|
||||
style={{
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
font: "inherit",
|
||||
color: "inherit !important",
|
||||
@@ -42,20 +40,7 @@ export const MultiSelect = forwardRef(function MultiSelect(
|
||||
)}
|
||||
</ChipList>
|
||||
|
||||
{!disabled && (
|
||||
<ChevronDown
|
||||
className="row-hover-iconButton"
|
||||
sx={{
|
||||
flexShrink: 0,
|
||||
mr: 0.5,
|
||||
borderRadius: 1,
|
||||
p: (32 - 20) / 2 / 8,
|
||||
boxSizing: "content-box !important",
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{!disabled && <ChevronDown className="row-hover-iconButton end" />}
|
||||
</ButtonBase>
|
||||
);
|
||||
});
|
||||
|
||||
export default MultiSelect;
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
import { IPopoverCellProps } from "@src/components/fields/types";
|
||||
import { IEditorCellProps } from "@src/components/fields/types";
|
||||
|
||||
import MultiSelectComponent from "@rowy/multiselect";
|
||||
|
||||
@@ -11,7 +11,7 @@ export default function MultiSelect({
|
||||
parentRef,
|
||||
showPopoverCell,
|
||||
disabled,
|
||||
}: IPopoverCellProps) {
|
||||
}: IEditorCellProps) {
|
||||
const config = column.config ?? {};
|
||||
|
||||
return (
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
import { lazy } from "react";
|
||||
import { IFieldConfig, FieldType } from "@src/components/fields/types";
|
||||
import withPopoverCell from "@src/components/fields/_withTableCell/withPopoverCell";
|
||||
import withTableCell from "@src/components/Table/withTableCell";
|
||||
|
||||
import { MultiSelect as MultiSelectIcon } from "@src/assets/icons";
|
||||
import BasicCell from "@src/components/fields/_BasicCell/BasicCellNull";
|
||||
import InlineCell from "./InlineCell";
|
||||
import DisplayCell from "./DisplayCell";
|
||||
import NullEditor from "@src/components/Table/editors/NullEditor";
|
||||
import { filterOperators } from "./Filter";
|
||||
const PopoverCell = lazy(
|
||||
@@ -34,9 +33,8 @@ export const config: IFieldConfig = {
|
||||
icon: <MultiSelectIcon />,
|
||||
description:
|
||||
"Multiple values from predefined options. Options are searchable and users can optionally input custom values.",
|
||||
TableCell: withPopoverCell(BasicCell, InlineCell, PopoverCell, {
|
||||
anchorOrigin: { horizontal: "left", vertical: "bottom" },
|
||||
transparent: true,
|
||||
TableCell: withTableCell(DisplayCell, PopoverCell, "popover", {
|
||||
disablePadding: true,
|
||||
}),
|
||||
TableEditor: NullEditor as any,
|
||||
SideDrawerField,
|
||||
|
||||
@@ -61,7 +61,7 @@ export default function TextEditor({ row, column }: EditorProps<any>) {
|
||||
sx={{
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
backgroundColor: "var(--background-color)",
|
||||
backgroundColor: "var(--cell-background-color)",
|
||||
|
||||
"& .MuiInputBase-root": {
|
||||
height: "100%",
|
||||
|
||||
61
src/components/fields/ShortText/EditorCell.tsx
Normal file
61
src/components/fields/ShortText/EditorCell.tsx
Normal file
@@ -0,0 +1,61 @@
|
||||
import type { IEditorCellProps } from "@src/components/fields/types";
|
||||
import { useSaveOnUnmount } from "@src/hooks/useSaveOnUnmount";
|
||||
|
||||
import { InputBase } from "@mui/material";
|
||||
|
||||
export default function ShortText({
|
||||
column,
|
||||
value,
|
||||
onSubmit,
|
||||
setFocusInsideCell,
|
||||
}: IEditorCellProps<string>) {
|
||||
const [localValue, setLocalValue] = useSaveOnUnmount(value, onSubmit);
|
||||
const maxLength = column.config?.maxLength;
|
||||
|
||||
return (
|
||||
<InputBase
|
||||
value={localValue}
|
||||
onChange={(e) => setLocalValue(e.target.value)}
|
||||
fullWidth
|
||||
inputProps={{ maxLength }}
|
||||
sx={{
|
||||
width: "100%",
|
||||
height: "calc(100% - 1px)",
|
||||
marginTop: "1px",
|
||||
paddingBottom: "1px",
|
||||
|
||||
backgroundColor: "var(--cell-background-color)",
|
||||
outline: "inherit",
|
||||
outlineOffset: "inherit",
|
||||
|
||||
font: "inherit", // Prevent text jumping
|
||||
letterSpacing: "inherit", // Prevent text jumping
|
||||
|
||||
"& .MuiInputBase-input": { p: "var(--cell-padding)" },
|
||||
|
||||
"& textarea.MuiInputBase-input": {
|
||||
lineHeight: (theme) => theme.typography.body2.lineHeight,
|
||||
maxHeight: "100%",
|
||||
boxSizing: "border-box",
|
||||
py: 3 / 8,
|
||||
},
|
||||
}}
|
||||
autoFocus
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === "ArrowLeft" || e.key === "ArrowRight") {
|
||||
e.stopPropagation();
|
||||
}
|
||||
if (e.key === "Escape") {
|
||||
// Escape removes focus inside cell, this runs before save on unmount
|
||||
setLocalValue(value);
|
||||
}
|
||||
if (e.key === "Enter") {
|
||||
// Removes focus from inside cell, triggering save on unmount
|
||||
setFocusInsideCell(false);
|
||||
}
|
||||
}}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
onDoubleClick={(e) => e.stopPropagation()}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -1,10 +1,10 @@
|
||||
import { lazy } from "react";
|
||||
import { IFieldConfig, FieldType } from "@src/components/fields/types";
|
||||
import withBasicCell from "@src/components/fields/_withTableCell/withBasicCell";
|
||||
import withTableCell from "@src/components/Table/withTableCell";
|
||||
|
||||
import ShortTextIcon from "@mui/icons-material/ShortText";
|
||||
import BasicCell from "@src/components/fields/_BasicCell/BasicCellValue";
|
||||
import TextEditor from "@src/components/Table/editors/TextEditor";
|
||||
import EditorCell from "./EditorCell";
|
||||
|
||||
import { filterOperators } from "./Filter";
|
||||
import BasicContextMenuActions from "@src/components/fields/_BasicCell/BasicCellContextMenuActions";
|
||||
@@ -30,8 +30,7 @@ export const config: IFieldConfig = {
|
||||
icon: <ShortTextIcon />,
|
||||
description: "Text displayed on a single line.",
|
||||
contextMenuActions: BasicContextMenuActions,
|
||||
TableCell: withBasicCell(BasicCell),
|
||||
TableEditor: TextEditor,
|
||||
TableCell: withTableCell(BasicCell, EditorCell),
|
||||
SideDrawerField,
|
||||
settings: Settings,
|
||||
filter: {
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
export default function BasicCellNull() {
|
||||
return null;
|
||||
export default function BasicCellNull(props: any) {
|
||||
return <div {...props} style={{ position: "absolute", inset: 0 }} />;
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Suspense, useState, useEffect } from "react";
|
||||
import { Suspense, useState, useEffect, startTransition } from "react";
|
||||
import { useSetAtom } from "jotai";
|
||||
import { get } from "lodash-es";
|
||||
import type { TableCellProps } from "@src/components/Table";
|
||||
@@ -22,13 +22,14 @@ export default function withHeavyCell(
|
||||
return function HeavyCell({ row, column, getValue }: TableCellProps) {
|
||||
const updateField = useSetAtom(updateFieldAtom, tableScope);
|
||||
|
||||
// const displayedComponent = "heavy";
|
||||
// Initially display BasicCell to improve scroll performance
|
||||
const [displayedComponent, setDisplayedComponent] = useState<
|
||||
"basic" | "heavy"
|
||||
>("basic");
|
||||
// Then switch to HeavyCell once completed
|
||||
useEffect(() => {
|
||||
setTimeout(() => {
|
||||
startTransition(() => {
|
||||
setDisplayedComponent("heavy");
|
||||
});
|
||||
}, []);
|
||||
@@ -45,6 +46,8 @@ export default function withHeavyCell(
|
||||
value: localValue,
|
||||
name: column.columnDef.meta!.name,
|
||||
type: column.columnDef.meta!.type,
|
||||
onMouseOver: () => setDisplayedComponent("heavy"),
|
||||
onMouseLeave: () => setDisplayedComponent("basic"),
|
||||
};
|
||||
const basicCell = <BasicCellComponent {...basicCellProps} />;
|
||||
|
||||
|
||||
@@ -43,13 +43,13 @@ export default function withPopoverCell(
|
||||
// Initially display BasicCell to improve scroll performance
|
||||
const [displayedComponent, setDisplayedComponent] = useState<
|
||||
"basic" | "inline" | "popover"
|
||||
>("basic");
|
||||
>("inline");
|
||||
// Then switch to heavier InlineCell once completed
|
||||
useEffect(() => {
|
||||
setTimeout(() => {
|
||||
setDisplayedComponent("inline");
|
||||
});
|
||||
}, []);
|
||||
// useEffect(() => {
|
||||
// setTimeout(() => {
|
||||
// setDisplayedComponent("inline");
|
||||
// });
|
||||
// }, []);
|
||||
|
||||
// Store Popover open state here so we can add delay for close transition
|
||||
const [popoverOpen, setPopoverOpen] = useState(false);
|
||||
|
||||
@@ -30,7 +30,8 @@ export interface IFieldConfig {
|
||||
reset: () => void
|
||||
) => IContextMenuItem[];
|
||||
TableCell: React.ComponentType<TableCellProps>;
|
||||
TableEditor: React.ComponentType<EditorProps<TableRow, any>>;
|
||||
/** @deprecated TODO: REMOVE */
|
||||
TableEditor?: React.ComponentType<EditorProps<TableRow, any>>;
|
||||
SideDrawerField: React.ComponentType<ISideDrawerFieldProps>;
|
||||
settings?: React.ComponentType<ISettingsProps>;
|
||||
settingsValidator?: (config: Record<string, any>) => Record<string, string>;
|
||||
@@ -45,11 +46,13 @@ export interface IFieldConfig {
|
||||
csvImportParser?: (value: string, config?: any) => any;
|
||||
}
|
||||
|
||||
/** @deprecated TODO: REMOVE */
|
||||
export interface IBasicCellProps {
|
||||
value: any;
|
||||
type: FieldType;
|
||||
name: string;
|
||||
}
|
||||
/** @deprecated TODO: REMOVE */
|
||||
export interface IHeavyCellProps extends IBasicCellProps {
|
||||
row: TableRow;
|
||||
column: ColumnConfig;
|
||||
@@ -57,14 +60,32 @@ export interface IHeavyCellProps extends IBasicCellProps {
|
||||
docRef: TableRowRef;
|
||||
disabled: boolean;
|
||||
}
|
||||
|
||||
/** @deprecated TODO: REMOVE */
|
||||
export interface IPopoverInlineCellProps extends IHeavyCellProps {
|
||||
showPopoverCell: React.Dispatch<React.SetStateAction<boolean>>;
|
||||
}
|
||||
/** @deprecated TODO: REMOVE */
|
||||
export interface IPopoverCellProps extends IPopoverInlineCellProps {
|
||||
parentRef: PopoverProps["anchorEl"];
|
||||
}
|
||||
|
||||
export interface IDisplayCellProps<T = any> {
|
||||
value: T;
|
||||
type: FieldType;
|
||||
name: string;
|
||||
row: TableRow;
|
||||
column: ColumnConfig;
|
||||
docRef: TableRowRef;
|
||||
disabled: boolean;
|
||||
showPopoverCell: (value: boolean) => void;
|
||||
setFocusInsideCell: (focusInside: boolean) => void;
|
||||
}
|
||||
export interface IEditorCellProps<T = any> extends IDisplayCellProps<T> {
|
||||
onSubmit: (value: T) => void;
|
||||
tabIndex: number;
|
||||
parentRef: PopoverProps["anchorEl"];
|
||||
}
|
||||
|
||||
/** Props to be passed to all SideDrawerFields */
|
||||
export interface ISideDrawerFieldProps {
|
||||
/** The column config */
|
||||
|
||||
19
src/hooks/useSaveOnUnmount.ts
Normal file
19
src/hooks/useSaveOnUnmount.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
/* eslint-disable react-hooks/exhaustive-deps */
|
||||
import { useLayoutEffect } from "react";
|
||||
import useState from "react-usestateref";
|
||||
|
||||
export function useSaveOnUnmount<T>(
|
||||
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;
|
||||
@@ -8,11 +8,11 @@ import reportWebVitals from "./reportWebVitals";
|
||||
const container = document.getElementById("root")!;
|
||||
const root = createRoot(container);
|
||||
root.render(
|
||||
// <StrictMode>
|
||||
<Providers>
|
||||
<App />
|
||||
</Providers>
|
||||
// </StrictMode>
|
||||
<StrictMode>
|
||||
<Providers>
|
||||
<App />
|
||||
</Providers>
|
||||
</StrictMode>
|
||||
);
|
||||
|
||||
// If you want to start measuring performance in your app, pass a function
|
||||
|
||||
@@ -2,7 +2,7 @@ import { useRef, 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 { Box, Fade } from "@mui/material";
|
||||
import ErrorFallback, {
|
||||
@@ -72,7 +72,10 @@ export default function TablePage({
|
||||
// shouldn’t access projectScope at all, to separate concerns.
|
||||
const canAddColumn = userRoles.includes("ADMIN");
|
||||
const canEditColumn = userRoles.includes("ADMIN");
|
||||
const canEditCell = userRoles.includes("ADMIN") || !tableSettings.readOnly;
|
||||
const canEditCell =
|
||||
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);
|
||||
|
||||
Reference in New Issue
Block a user