From 8dddfcd5335f5bd68a9053ff14c57f5c4690e051 Mon Sep 17 00:00:00 2001 From: Sidney Alcantara Date: Fri, 14 Oct 2022 16:15:41 +1100 Subject: [PATCH 01/66] add basic read-only table --- package.json | 1 + src/components/Table/Styled/StyledCell.tsx | 18 + src/components/Table/Styled/StyledRow.tsx | 11 + src/components/Table/Styled/StyledTable.tsx | 7 + src/components/Table/Table.tsx | 409 +++++++----------- .../fields/SingleSelect/Settings.tsx | 2 +- src/layouts/Navigation/Navigation.tsx | 2 +- src/pages/Table/TablePage.tsx | 26 +- yarn.lock | 12 + 9 files changed, 221 insertions(+), 267 deletions(-) create mode 100644 src/components/Table/Styled/StyledCell.tsx create mode 100644 src/components/Table/Styled/StyledRow.tsx create mode 100644 src/components/Table/Styled/StyledTable.tsx diff --git a/package.json b/package.json index fb029fc7..e743edda 100644 --- a/package.json +++ b/package.json @@ -18,6 +18,7 @@ "@mui/x-date-pickers": "^5.0.0-alpha.4", "@rowy/form-builder": "^0.7.0", "@rowy/multiselect": "^0.4.1", + "@tanstack/react-table": "^8.5.15", "@tinymce/tinymce-react": "^3", "@uiw/react-md-editor": "^3.14.1", "algoliasearch": "^4.13.1", diff --git a/src/components/Table/Styled/StyledCell.tsx b/src/components/Table/Styled/StyledCell.tsx new file mode 100644 index 00000000..0676daa2 --- /dev/null +++ b/src/components/Table/Styled/StyledCell.tsx @@ -0,0 +1,18 @@ +import { styled } from "@mui/material"; + +export const StyledCell = styled("div")(({ theme }) => ({ + display: "flex", + alignItems: "center", + "--cell-padding": theme.spacing(1.5), + padding: "0 var(--cell-padding)", + + overflow: "visible", + contain: "none", + position: "relative", + + lineHeight: "calc(var(--row-height) - 1px)", + + borderBottom: `1px solid ${theme.palette.divider}`, + borderLeft: `1px solid ${theme.palette.divider}`, +})); +StyledCell.displayName = "StyledCell"; diff --git a/src/components/Table/Styled/StyledRow.tsx b/src/components/Table/Styled/StyledRow.tsx new file mode 100644 index 00000000..4e76b1d8 --- /dev/null +++ b/src/components/Table/Styled/StyledRow.tsx @@ -0,0 +1,11 @@ +import { styled } from "@mui/material"; + +import { DEFAULT_ROW_HEIGHT } from "@src/components/Table"; + +export const StyledRow = styled("div")(({ theme }) => ({ + display: "flex", + height: DEFAULT_ROW_HEIGHT, + + backgroundColor: theme.palette.background.paper, +})); +StyledRow.displayName = "StyledRow"; diff --git a/src/components/Table/Styled/StyledTable.tsx b/src/components/Table/Styled/StyledTable.tsx new file mode 100644 index 00000000..fc855036 --- /dev/null +++ b/src/components/Table/Styled/StyledTable.tsx @@ -0,0 +1,7 @@ +import { styled } from "@mui/material"; + +export const StyledTable = styled("div")(({ theme }) => ({ + ...(theme.typography.caption as any), + lineHeight: "inherit !important", +})); +StyledTable.displayName = "StyledTable"; diff --git a/src/components/Table/Table.tsx b/src/components/Table/Table.tsx index 308e8767..6dbddcaf 100644 --- a/src/components/Table/Table.tsx +++ b/src/components/Table/Table.tsx @@ -5,19 +5,26 @@ import { DndProvider } from "react-dnd"; import { HTML5Backend } from "react-dnd-html5-backend"; import { findIndex } from "lodash-es"; -// import "react-data-grid/dist/react-data-grid.css"; -import DataGrid, { - Column, - DataGridHandle, - // SelectColumn as _SelectColumn, -} from "react-data-grid"; +import { + createColumnHelper, + flexRender, + getCoreRowModel, + useReactTable, +} from "@tanstack/react-table"; + +import { TOP_BAR_HEIGHT } from "@src/layouts/Navigation/TopBar"; +import { TABLE_TOOLBAR_HEIGHT } from "@src/components/TableToolbar"; +import { StyledTable } from "./Styled/StyledTable"; +import { StyledRow } from "./Styled/StyledRow"; +import ColumnHeaderComponent from "./Column"; + import { LinearProgress } from "@mui/material"; import TableContainer, { OUT_OF_ORDER_MARGIN } from "./TableContainer"; import ColumnHeader, { COLUMN_HEADER_HEIGHT } from "./ColumnHeader"; import FinalColumnHeader from "./FinalColumnHeader"; import FinalColumn from "./formatters/FinalColumn"; -import TableRow from "./TableRow"; +// import TableRow from "./TableRow"; import EmptyState from "@src/components/EmptyState"; // import BulkActions from "./BulkActions"; import AddRow from "@src/components/TableToolbar/AddRow"; @@ -47,22 +54,21 @@ import { import { getFieldType, getFieldProp } from "@src/components/fields"; import { FieldType } from "@src/constants/fields"; import { formatSubTableName } from "@src/utils/table"; -import { ColumnConfig } from "@src/types/table"; +import { TableRow, ColumnConfig } from "@src/types/table"; +import { StyledCell } from "./Styled/StyledCell"; -export type DataGridColumn = ColumnConfig & Column & { isNew?: true }; export const DEFAULT_ROW_HEIGHT = 41; export const DEFAULT_COL_WIDTH = 150; export const MAX_COL_WIDTH = 380; -const rowKeyGetter = (row: any) => row.id; -const rowClass = (row: any) => (row._rowy_outOfOrder ? "out-of-order" : ""); -//const SelectColumn = { ..._SelectColumn, width: 42, maxWidth: 42 }; +declare module "@tanstack/table-core" { + interface ColumnMeta extends ColumnConfig {} +} -export default function Table({ - dataGridRef, -}: { - dataGridRef?: React.MutableRefObject; -}) { +const columnHelper = createColumnHelper(); +const getRowId = (row: TableRow) => row._rowy_ref.path || row._rowy_ref.id; + +export default function TableComponent() { const [userRoles] = useAtom(userRolesAtom, projectScope); const [userSettings] = useAtom(userSettingsAtom, projectScope); @@ -78,266 +84,143 @@ export default function Table({ const updateColumn = useSetAtom(updateColumnAtom, tableScope); const updateField = useSetAtom(updateFieldAtom, tableScope); + const canAddColumn = userRoles.includes("ADMIN"); const userDocHiddenFields = userSettings.tables?.[formatSubTableName(tableId)]?.hiddenFields; - // Get column configs from table schema and map them to DataGridColumns - // Also filter out hidden columns and add end column + // Get column defs from table schema + // Also add end column for admins const columns = useMemo(() => { - const _columns: DataGridColumn[] = tableColumnsOrdered - .filter((column) => { - if (column.hidden) return false; - if ( - Array.isArray(userDocHiddenFields) && - userDocHiddenFields.includes(column.key) - ) - return false; - return true; - }) - .map((column: any) => ({ - draggable: true, - resizable: true, - frozen: column.fixed, - headerRenderer: ColumnHeader, - formatter: - getFieldProp("TableCell", getFieldType(column)) ?? - function InDev() { - return null; - }, - editor: - getFieldProp("TableEditor", getFieldType(column)) ?? - function InDev() { - return null; - }, - ...column, - editable: - tableSettings.readOnly && !userRoles.includes("ADMIN") - ? false - : column.editable ?? true, - width: column.width ?? DEFAULT_COL_WIDTH, - })); + const _columns = tableColumnsOrdered + // .filter((column) => { + // if (column.hidden) return false; + // if ( + // Array.isArray(userDocHiddenFields) && + // userDocHiddenFields.includes(column.key) + // ) + // return false; + // return true; + // }) + .map((columnConfig) => + columnHelper.accessor(columnConfig.fieldName, { + meta: columnConfig, + // draggable: true, + // resizable: true, + // frozen: columnConfig.fixed, + // headerRenderer: ColumnHeader, + // 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, + // width: columnConfig.width ?? DEFAULT_COL_WIDTH, + }) + ); - if (userRoles.includes("ADMIN") || !tableSettings.readOnly) { - _columns.push({ - isNew: true, - key: "new", - fieldName: "_rowy_new", - name: "Add column", - type: FieldType.last, - index: _columns.length ?? 0, - width: 154, - headerRenderer: FinalColumnHeader, - headerCellClass: "final-column-header", - cellClass: "final-column-cell", - formatter: FinalColumn, - editable: false, - }); - } + // if (canAddColumn || !tableSettings.readOnly) { + // _columns.push({ + // isNew: true, + // key: "new", + // fieldName: "_rowy_new", + // name: "Add column", + // type: FieldType.last, + // index: _columns.length ?? 0, + // width: 154, + // headerRenderer: FinalColumnHeader, + // headerCellClass: "final-column-header", + // cellClass: "final-column-cell", + // formatter: FinalColumn, + // editable: false, + // }); + // } return _columns; }, [ tableColumnsOrdered, - userDocHiddenFields, - tableSettings.readOnly, - userRoles, + // userDocHiddenFields, + // tableSettings.readOnly, + // canAddColumn, ]); - const selectedColumnIndex = useMemo(() => { - if (!selectedCell?.columnKey) return -1; - return findIndex(columns, ["key", selectedCell.columnKey]); - }, [selectedCell?.columnKey, columns]); - // Handle columns with field names that use dot notation (nested fields) - const rows = - useMemo(() => { - // const columnsWithNestedFieldNames = columns - // .map((col) => col.fieldName) - // .filter((fieldName) => fieldName.includes(".")); + const table = useReactTable({ + data: tableRows, + columns, + getCoreRowModel: getCoreRowModel(), + getRowId, + columnResizeMode: "onChange", + // debugRows: true, + }); + console.log(table); - // if (columnsWithNestedFieldNames.length === 0) - return tableRows; - - // return tableRows.map((row) => - // columnsWithNestedFieldNames.reduce( - // (acc, fieldName) => ({ - // ...acc, - // [fieldName]: get(row, fieldName), - // }), - // { ...row } - // ) - // ); - }, [tableRows]) ?? []; - - // const [selectedRowsSet, setSelectedRowsSet] = useState>(); - // const [selectedRows, setSelectedRows] = useState([]); - - // Gets more rows when scrolled down. - // https://github.com/adazzle/react-data-grid/blob/ead05032da79d7e2b86e37cdb9af27f2a4d80b90/stories/demos/AllFeatures.tsx#L60 - const handleScroll = useThrottledCallback( - (event: React.UIEvent) => { - // Select corresponding header cell when scrolled to prevent jumping - dataGridRef?.current?.selectCell({ - idx: - selectedColumnIndex > -1 ? selectedColumnIndex : columns.length - 1, - rowIdx: -1, - }); - // console.log( - // "scroll", - // dataGridRef?.current, - // selectedColumnIndex, - // columns.length - // ); - - const target = event.target as HTMLDivElement; - - // TODO: - // if (navPinned && !columns[0].fixed) - // setShowLeftScrollDivider(target.scrollLeft > 16); - - const offset = 800; - const isAtBottom = - target.clientHeight + target.scrollTop >= target.scrollHeight - offset; - if (!isAtBottom) return; - // Call for the next page - setTablePage((p) => p + 1); - }, - 250 - ); - - const [showLeftScrollDivider, setShowLeftScrollDivider] = useState(false); - - const rowHeight = tableSchema.rowHeight ?? DEFAULT_ROW_HEIGHT; - const handleResize = useDebouncedCallback( - (colIndex: number, width: number) => { - const column = columns[colIndex]; - if (!column.key) return; - updateColumn({ key: column.key, config: { width } }); - }, - 1000 - ); + const handleKeyDown = (e: React.KeyboardEvent) => {}; return ( - }> - {/* */} - - - {showLeftScrollDivider &&
} + <> + +
+ {table.getHeaderGroups().map((headerGroup) => ( + + {headerGroup.headers.map((header) => ( + + {/*
*/} + + ))} + + ))} +
- { - if (dataGridRef) dataGridRef.current = handle; - }} - rows={rows} - columns={columns} - // Increase row height of out of order rows to add margins - rowHeight={({ row }) => { - if (row._rowy_outOfOrder) - return rowHeight + OUT_OF_ORDER_MARGIN + 1; - - return rowHeight; - }} - headerRowHeight={DEFAULT_ROW_HEIGHT + 1} - className="rdg-light" // Handle dark mode in MUI theme - cellNavigationMode="LOOP_OVER_ROW" - rowRenderer={TableRow} - rowKeyGetter={rowKeyGetter} - rowClass={rowClass} - // selectedRows={selectedRowsSet} - // onSelectedRowsChange={(newSelectedSet) => { - // const newSelectedArray = newSelectedSet - // ? [...newSelectedSet] - // : []; - // const prevSelectedRowsArray = selectedRowsSet - // ? [...selectedRowsSet] - // : []; - // const addedSelections = difference( - // newSelectedArray, - // prevSelectedRowsArray - // ); - // const removedSelections = difference( - // prevSelectedRowsArray, - // newSelectedArray - // ); - // addedSelections.forEach((id) => { - // const newRow = find(rows, { id }); - // setSelectedRows([...selectedRows, newRow]); - // }); - // removedSelections.forEach((rowId) => { - // setSelectedRows(selectedRows.filter((row) => row.id !== rowId)); - // }); - // setSelectedRowsSet(newSelectedSet); - // }} - // onRowsChange={() => { - //console.log('onRowsChange',rows) - // }} - // TODO: onFill={(e) => { - // console.log("onFill", e); - // const { columnKey, sourceRow, targetRows } = e; - // if (updateCell) - // targetRows.forEach((row) => - // updateCell(row._rowy_ref, columnKey, sourceRow[columnKey]) - // ); - // return []; - // }} - onPaste={(e, ...args) => { - console.log("onPaste", e, ...args); - const value = e.sourceRow[e.sourceColumnKey]; - updateField({ - path: e.targetRow._rowy_ref.path, - fieldName: e.targetColumnKey, - value, - }); - }} - onSelectedCellChange={({ rowIdx, idx }) => { - if (!rows[rowIdx]?._rowy_ref) return; // May be the header row - - const path = rows[rowIdx]._rowy_ref.path; - if (!path) return; - - const columnKey = tableColumnsOrdered.filter((col) => - userDocHiddenFields - ? !userDocHiddenFields.includes(col.key) - : true - )[idx]?.key; - if (!columnKey) return; // May be the final column - - setSelectedCell({ path, columnKey }); - }} - /> - - - {tableRows.length === 0 && ( - -
- -
- } - style={{ - position: "absolute", - inset: 0, - top: COLUMN_HEADER_HEIGHT, - height: "auto", - }} - /> - )} - {tableNextPage.loading && } - - - - {/* - { - setSelectedRowsSet(new Set()); - setSelectedRows([]); - }} - /> */} - +
+ {table.getRowModel().rows.map((row) => ( + + {row.getVisibleCells().map((cell) => ( + + {flexRender(cell.column.columnDef.cell, cell.getContext())} + + + ))} + + ))} +
+
+ ); } diff --git a/src/components/fields/SingleSelect/Settings.tsx b/src/components/fields/SingleSelect/Settings.tsx index 103bb2cd..2fc908b9 100644 --- a/src/components/fields/SingleSelect/Settings.tsx +++ b/src/components/fields/SingleSelect/Settings.tsx @@ -93,7 +93,7 @@ export default function Settings({ onChange, config }: ISettingsProps) { onChange={(e) => { setNewOption(e.target.value); }} - onKeyPress={(e: any) => { + onKeyDown={(e: any) => { if (e.key === "Enter") { handleAdd(); } diff --git a/src/layouts/Navigation/Navigation.tsx b/src/layouts/Navigation/Navigation.tsx index f17053a1..3f967301 100644 --- a/src/layouts/Navigation/Navigation.tsx +++ b/src/layouts/Navigation/Navigation.tsx @@ -62,7 +62,7 @@ export default function Navigation({ children }: React.PropsWithChildren<{}>) { } > -
+
{children}
diff --git a/src/pages/Table/TablePage.tsx b/src/pages/Table/TablePage.tsx index 4d682fc3..17e8213a 100644 --- a/src/pages/Table/TablePage.tsx +++ b/src/pages/Table/TablePage.tsx @@ -4,7 +4,7 @@ import { DataGridHandle } from "react-data-grid"; import { ErrorBoundary } from "react-error-boundary"; import { isEmpty } from "lodash-es"; -import { Fade } from "@mui/material"; +import { Box, Fade } from "@mui/material"; import ErrorFallback, { InlineErrorFallback, } from "@src/components/ErrorFallback"; @@ -27,6 +27,12 @@ import { import useBeforeUnload from "@src/hooks/useBeforeUnload"; import ActionParamsProvider from "@src/components/fields/Action/FormDialog/Provider"; import { useSnackLogContext } from "@src/contexts/SnackLogContext"; +import { TOP_BAR_HEIGHT } from "@src/layouts/Navigation/TopBar"; +import { TABLE_TOOLBAR_HEIGHT } from "@src/components/TableToolbar"; +import { + DRAWER_COLLAPSED_WIDTH, + DRAWER_WIDTH, +} from "@src/components/SideDrawer"; // prettier-ignore const BuildLogsSnack = lazy(() => import("@src/components/TableModals/CloudLogsModal/BuildLogs/BuildLogsSnack" /* webpackChunkName: "TableModals-BuildLogsSnack" */)); @@ -93,7 +99,23 @@ export default function TablePage({ }> - + + `max(env(safe-area-inset-bottom), ${theme.spacing(2)})`, + }, + }} + > +
+ diff --git a/yarn.lock b/yarn.lock index 4d281370..b6e7cfa3 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2688,6 +2688,18 @@ dependencies: "@jest/create-cache-key-function" "^27.4.2" +"@tanstack/react-table@^8.5.15": + version "8.5.15" + resolved "https://registry.yarnpkg.com/@tanstack/react-table/-/react-table-8.5.15.tgz#8179d24d7fdf909799a517e8897501c44e51284d" + integrity sha512-9rSvhIFeMpfXksFgQNTWnVoJbkae/U8CkHnHYGWAIB/O0Ca51IKap0Rjp5WkIUVBWxJ7Wfl2y13oY+aWcyM6Rg== + dependencies: + "@tanstack/table-core" "8.5.15" + +"@tanstack/table-core@8.5.15": + version "8.5.15" + resolved "https://registry.yarnpkg.com/@tanstack/table-core/-/table-core-8.5.15.tgz#e1e674135cd6c36f29a1562a2b846f824861149b" + integrity sha512-k+BcCOAYD610Cij6p1BPyEqjMQjZIdAnVDoIUKVnA/tfHbF4JlDP7pKAftXPBxyyX5Z1yQPurPnOdEY007Snyg== + "@testing-library/dom@^8.5.0": version "8.13.0" resolved "https://registry.yarnpkg.com/@testing-library/dom/-/dom-8.13.0.tgz#bc00bdd64c7d8b40841e27a70211399ad3af46f5" From 4f49503e7a20bd54649b6b8fface6aeb88f26747 Mon Sep 17 00:00:00 2001 From: Sidney Alcantara Date: Mon, 17 Oct 2022 17:57:26 +1100 Subject: [PATCH 02/66] add cell navigation --- src/atoms/tableScope/ui.ts | 5 +- src/components/Table/Styled/StyledTable.tsx | 11 + src/components/Table/Table.tsx | 218 +++++++++++++++++--- 3 files changed, 207 insertions(+), 27 deletions(-) diff --git a/src/atoms/tableScope/ui.ts b/src/atoms/tableScope/ui.ts index fa987118..a26ea065 100644 --- a/src/atoms/tableScope/ui.ts +++ b/src/atoms/tableScope/ui.ts @@ -124,7 +124,10 @@ export const importAirtableAtom = atom<{ /** Store side drawer open state */ export const sideDrawerOpenAtom = atom(false); -export type SelectedCell = { path: string; columnKey: string }; +export type SelectedCell = { + path: string | "_rowy_header"; + columnKey: string | "_rowy_row_actions"; +}; /** Store selected cell in table. Used in side drawer and context menu */ export const selectedCellAtom = atom(null); diff --git a/src/components/Table/Styled/StyledTable.tsx b/src/components/Table/Styled/StyledTable.tsx index fc855036..ab65a84c 100644 --- a/src/components/Table/Styled/StyledTable.tsx +++ b/src/components/Table/Styled/StyledTable.tsx @@ -3,5 +3,16 @@ import { styled } from "@mui/material"; export const StyledTable = styled("div")(({ theme }) => ({ ...(theme.typography.caption as any), lineHeight: "inherit !important", + + "& [role='columnheader'], & [role='gridcell']": { + "&[aria-selected='true']": { + outline: `1px solid ${theme.palette.primary.main}`, + outlineOffset: "-1px", + }, + "&:focus": { + outline: `2px solid ${theme.palette.primary.main}`, + outlineOffset: "-2px", + }, + }, })); StyledTable.displayName = "StyledTable"; diff --git a/src/components/Table/Table.tsx b/src/components/Table/Table.tsx index 6dbddcaf..547016a7 100644 --- a/src/components/Table/Table.tsx +++ b/src/components/Table/Table.tsx @@ -1,4 +1,4 @@ -import React, { useMemo, useState, Suspense } from "react"; +import React, { useMemo, useState, Suspense, useRef } from "react"; import { useAtom, useSetAtom } from "jotai"; import { useDebouncedCallback, useThrottledCallback } from "use-debounce"; import { DndProvider } from "react-dnd"; @@ -49,7 +49,9 @@ import { updateColumnAtom, updateFieldAtom, selectedCellAtom, + SelectedCell, } from "@src/atoms/tableScope"; +import { COLLECTION_PAGE_SIZE } from "@src/config/db"; import { getFieldType, getFieldProp } from "@src/components/fields"; import { FieldType } from "@src/constants/fields"; @@ -84,7 +86,11 @@ export default function TableComponent() { const updateColumn = useSetAtom(updateColumnAtom, tableScope); const updateField = useSetAtom(updateFieldAtom, tableScope); + const gridRef = useRef(null); + const [focusInsideCell, setFocusInsideCell] = useState(false); + const canAddColumn = userRoles.includes("ADMIN"); + const canEditColumn = userRoles.includes("ADMIN"); const userDocHiddenFields = userSettings.tables?.[formatSubTableName(tableId)]?.hiddenFields; @@ -103,6 +109,7 @@ export default function TableComponent() { // }) .map((columnConfig) => columnHelper.accessor(columnConfig.fieldName, { + id: columnConfig.fieldName, meta: columnConfig, // draggable: true, // resizable: true, @@ -160,28 +167,162 @@ export default function TableComponent() { columnResizeMode: "onChange", // debugRows: true, }); - console.log(table); + console.log(table, selectedCell); - const handleKeyDown = (e: React.KeyboardEvent) => {}; + const handleKeyDown = (e: React.KeyboardEvent) => { + console.log( + "keydown", + // e.target, + e.key, + e.ctrlKey ? "ctrl" : "", + e.altKey ? "alt" : "", + e.metaKey ? "meta" : "", + e.shiftKey ? "shift" : "" + ); + const LISTENED_KEYS = [ + "ArrowUp", + "ArrowDown", + "ArrowLeft", + "ArrowRight", + "Enter", + "Escape", + "Home", + "End", + "PageUp", + "PageDown", + ]; + if (LISTENED_KEYS.includes(e.key)) e.preventDefault(); + + const target = e.target as HTMLDivElement; + if ( + target.getAttribute("role") !== "columnheader" && + target.getAttribute("role") !== "gridcell" + ) + return; + + const colIndex = Number(target.getAttribute("aria-colindex")) - 1; + const rowIndex = + Number(target.parentElement!.getAttribute("aria-rowindex")) - 2; + + const rowId = target.getAttribute("data-rowId")!; + const colId = target.getAttribute("data-colId")!; + + const isHeader = rowId === "_rowy_header"; + + let newColIndex = colIndex; + let newRowIndex = rowIndex; + + // const newSelectedCell: SelectedCell = selectedCell + // ? { ...selectedCell } + // : { path: rowId, columnKey: colId }; + + switch (e.key) { + case "ArrowUp": + if (rowIndex > -1) newRowIndex = rowIndex - 1; + break; + + case "ArrowDown": + if (rowIndex < tableRows.length - 1) newRowIndex = rowIndex + 1; + break; + + case "ArrowLeft": + if (colIndex > 0) newColIndex = colIndex - 1; + break; + + case "ArrowRight": + if (colIndex < columns.length - 1) newColIndex = colIndex + 1; + break; + + case "PageUp": + newRowIndex = Math.max(0, rowIndex - COLLECTION_PAGE_SIZE); + break; + + case "PageDown": + newRowIndex = Math.min( + tableRows.length - 1, + rowIndex + COLLECTION_PAGE_SIZE + ); + break; + + case "Home": + newColIndex = 0; + if (e.ctrlKey || e.metaKey) newRowIndex = -1; + break; + + case "End": + newColIndex = columns.length - 1; + if (e.ctrlKey || e.metaKey) newRowIndex = tableRows.length - 1; + break; + } + + const newSelectedCell = { + path: + newRowIndex > -1 + ? tableRows[newRowIndex]._rowy_ref.path + : "_rowy_header", + columnKey: columns[newColIndex].id! || columns[0].id!, + }; + console.log(newRowIndex, newColIndex, newSelectedCell); + + setSelectedCell(newSelectedCell); + + // Find matching DOM element for the cell + const newCellEl = gridRef.current?.querySelector( + `[aria-rowindex="${newRowIndex + 2}"] [aria-colindex="${ + newColIndex + 1 + }"]` + ); + // Focus either the cell or the first focusable element in the cell + if (newCellEl) (newCellEl as HTMLDivElement).focus(); + }; return ( <> -
+
{table.getHeaderGroups().map((headerGroup) => ( - - {headerGroup.headers.map((header) => ( - - {/*
+ {headerGroup.headers.map((header) => { + const isFocusable = + (!selectedCell && header.index === 0) || + (selectedCell?.path === "_rowy_header" && + selectedCell?.columnKey === header.id); + + return ( + { + setSelectedCell({ + path: "_rowy_header", + columnKey: header.id, + }); + (e.target as HTMLDivElement).focus(); + }} + > + {/*
*/} - - ))} + + ); + })} ))}
-
+
{table.getRowModel().rows.map((row) => ( - - {row.getVisibleCells().map((cell) => ( - - {flexRender(cell.column.columnDef.cell, cell.getContext())} - - - ))} + + {row.getVisibleCells().map((cell, cellIndex) => { + const isFocusable = + selectedCell?.path === row.original._rowy_ref.path && + selectedCell?.columnKey === cell.column.id; + + return ( + { + setSelectedCell({ + path: row.original._rowy_ref.path, + columnKey: cell.column.id, + }); + (e.target as HTMLDivElement).focus(); + }} + > + {flexRender(cell.column.columnDef.cell, cell.getContext())} + + + ); + })} ))}
From 0e7a02bf08717030d699e930e4fad319dd7be718 Mon Sep 17 00:00:00 2001 From: Sidney Alcantara Date: Tue, 18 Oct 2022 14:49:17 +1100 Subject: [PATCH 03/66] add in-cell focus with Enter/Esc --- src/components/Table/Styled/StyledTable.tsx | 8 ++-- src/components/Table/Table.tsx | 49 +++++++++++++++------ 2 files changed, 39 insertions(+), 18 deletions(-) diff --git a/src/components/Table/Styled/StyledTable.tsx b/src/components/Table/Styled/StyledTable.tsx index ab65a84c..2f08622b 100644 --- a/src/components/Table/Styled/StyledTable.tsx +++ b/src/components/Table/Styled/StyledTable.tsx @@ -6,13 +6,13 @@ export const StyledTable = styled("div")(({ theme }) => ({ "& [role='columnheader'], & [role='gridcell']": { "&[aria-selected='true']": { - outline: `1px solid ${theme.palette.primary.main}`, - outlineOffset: "-1px", - }, - "&:focus": { outline: `2px solid ${theme.palette.primary.main}`, outlineOffset: "-2px", }, + "&:focus": { + outlineWidth: "3px", + outlineOffset: "-3px", + }, }, })); StyledTable.displayName = "StyledTable"; diff --git a/src/components/Table/Table.tsx b/src/components/Table/Table.tsx index 547016a7..ecca81de 100644 --- a/src/components/Table/Table.tsx +++ b/src/components/Table/Table.tsx @@ -167,7 +167,7 @@ export default function TableComponent() { columnResizeMode: "onChange", // debugRows: true, }); - console.log(table, selectedCell); + // console.log(table, selectedCell); const handleKeyDown = (e: React.KeyboardEvent) => { console.log( @@ -193,6 +193,14 @@ export default function TableComponent() { ]; if (LISTENED_KEYS.includes(e.key)) e.preventDefault(); + if (e.key === "Escape") { + setFocusInsideCell(false); + ( + gridRef.current?.querySelector("[aria-selected=true]") as HTMLDivElement + )?.focus(); + return; + } + const target = e.target as HTMLDivElement; if ( target.getAttribute("role") !== "columnheader" && @@ -200,6 +208,12 @@ export default function TableComponent() { ) return; + if (e.key === "Enter") { + setFocusInsideCell(true); + (target.querySelector("[tabindex]") as HTMLElement)?.focus(); + return; + } + const colIndex = Number(target.getAttribute("aria-colindex")) - 1; const rowIndex = Number(target.parentElement!.getAttribute("aria-rowindex")) - 2; @@ -218,19 +232,23 @@ export default function TableComponent() { switch (e.key) { case "ArrowUp": - if (rowIndex > -1) newRowIndex = rowIndex - 1; + if (e.ctrlKey || e.metaKey) newRowIndex = -1; + else if (rowIndex > -1) newRowIndex = rowIndex - 1; break; case "ArrowDown": - if (rowIndex < tableRows.length - 1) newRowIndex = rowIndex + 1; + if (e.ctrlKey || e.metaKey) newRowIndex = tableRows.length - 1; + else if (rowIndex < tableRows.length - 1) newRowIndex = rowIndex + 1; break; case "ArrowLeft": - if (colIndex > 0) newColIndex = colIndex - 1; + if (e.ctrlKey || e.metaKey) newColIndex = 0; + else if (colIndex > 0) newColIndex = colIndex - 1; break; case "ArrowRight": - if (colIndex < columns.length - 1) newColIndex = colIndex + 1; + if (e.ctrlKey || e.metaKey) newColIndex = columns.length - 1; + else if (colIndex < columns.length - 1) newColIndex = colIndex + 1; break; case "PageUp": @@ -272,8 +290,9 @@ export default function TableComponent() { newColIndex + 1 }"]` ); - // Focus either the cell or the first focusable element in the cell + // Focus the cell if (newCellEl) (newCellEl as HTMLDivElement).focus(); + setFocusInsideCell(false); }; return ( @@ -295,7 +314,7 @@ export default function TableComponent() { {table.getHeaderGroups().map((headerGroup) => ( {headerGroup.headers.map((header) => { - const isFocusable = + const isSelectedCell = (!selectedCell && header.index === 0) || (selectedCell?.path === "_rowy_header" && selectedCell?.columnKey === header.id); @@ -306,11 +325,11 @@ export default function TableComponent() { data-rowId={"_rowy_header"} data-colId={header.id} role="columnheader" - tabIndex={isFocusable ? 0 : -1} + tabIndex={isSelectedCell ? 0 : -1} aria-colindex={header.index + 1} aria-readonly={canEditColumn} // TODO: aria-sort={"none" | "ascending" | "descending" | "other" | undefined} - aria-selected={isFocusable} + aria-selected={isSelectedCell} label={header.column.columnDef.meta?.name || header.id} type={header.column.columnDef.meta?.type} style={{ width: header.getSize() }} @@ -351,7 +370,7 @@ export default function TableComponent() { {table.getRowModel().rows.map((row) => ( {row.getVisibleCells().map((cell, cellIndex) => { - const isFocusable = + const isSelectedCell = selectedCell?.path === row.original._rowy_ref.path && selectedCell?.columnKey === cell.column.id; @@ -361,12 +380,12 @@ export default function TableComponent() { data-rowId={row.id} data-colId={cell.column.id} role="gridcell" - tabIndex={isFocusable ? 0 : -1} + tabIndex={isSelectedCell && !focusInsideCell ? 0 : -1} aria-colindex={cellIndex + 1} aria-readonly={ cell.column.columnDef.meta?.editable === false } - aria-selected={isFocusable} + aria-selected={isSelectedCell} style={{ width: cell.column.getSize() }} onClick={(e) => { setSelectedCell({ @@ -377,8 +396,10 @@ export default function TableComponent() { }} > {flexRender(cell.column.columnDef.cell, cell.getContext())} - ); From bc23b93b428af17cb84afbd2b806e1e8159f7369 Mon Sep 17 00:00:00 2001 From: Sidney Alcantara Date: Tue, 18 Oct 2022 15:05:11 +1100 Subject: [PATCH 04/66] SideDrawer: add aria-labels to buttons --- src/components/SideDrawer/SideDrawer.tsx | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/components/SideDrawer/SideDrawer.tsx b/src/components/SideDrawer/SideDrawer.tsx index eafdcdfa..c4a02119 100644 --- a/src/components/SideDrawer/SideDrawer.tsx +++ b/src/components/SideDrawer/SideDrawer.tsx @@ -141,6 +141,7 @@ export default function SideDrawer({ {!!cell && (
{ if (setOpen) From c7d97d26986eed19bb00d57b38e27747a793467b Mon Sep 17 00:00:00 2001 From: Sidney Alcantara Date: Tue, 18 Oct 2022 17:53:47 +1100 Subject: [PATCH 05/66] add virtualization + infinite scrolll --- package.json | 1 + src/components/Table/Table.tsx | 320 +++++++++--------- .../Table/useKeyboardNavigation.tsx | 140 ++++++++ src/pages/Table/TablePage.tsx | 10 +- yarn.lock | 12 + 5 files changed, 316 insertions(+), 167 deletions(-) create mode 100644 src/components/Table/useKeyboardNavigation.tsx diff --git a/package.json b/package.json index e743edda..baf6cda0 100644 --- a/package.json +++ b/package.json @@ -65,6 +65,7 @@ "react-router-hash-link": "^2.4.3", "react-scripts": "^5.0.0", "react-usestateref": "^1.0.8", + "react-virtual": "^2.10.4", "remark-gfm": "^3.0.1", "seedrandom": "^3.0.5", "stream-browserify": "^3.0.0", diff --git a/src/components/Table/Table.tsx b/src/components/Table/Table.tsx index ecca81de..153b5576 100644 --- a/src/components/Table/Table.tsx +++ b/src/components/Table/Table.tsx @@ -1,4 +1,4 @@ -import React, { useMemo, useState, Suspense, useRef } from "react"; +import { useMemo, useRef, useCallback, Suspense, useEffect } from "react"; import { useAtom, useSetAtom } from "jotai"; import { useDebouncedCallback, useThrottledCallback } from "use-debounce"; import { DndProvider } from "react-dnd"; @@ -11,6 +11,7 @@ import { getCoreRowModel, useReactTable, } from "@tanstack/react-table"; +import { useVirtual } from "react-virtual"; import { TOP_BAR_HEIGHT } from "@src/layouts/Navigation/TopBar"; import { TABLE_TOOLBAR_HEIGHT } from "@src/components/TableToolbar"; @@ -58,6 +59,7 @@ import { FieldType } from "@src/constants/fields"; import { formatSubTableName } from "@src/utils/table"; import { TableRow, ColumnConfig } from "@src/types/table"; import { StyledCell } from "./Styled/StyledCell"; +import { useKeyboardNavigation } from "./useKeyboardNavigation"; export const DEFAULT_ROW_HEIGHT = 41; export const DEFAULT_COL_WIDTH = 150; @@ -86,8 +88,8 @@ export default function TableComponent() { const updateColumn = useSetAtom(updateColumnAtom, tableScope); const updateField = useSetAtom(updateFieldAtom, tableScope); + const containerRef = useRef(null); const gridRef = useRef(null); - const [focusInsideCell, setFocusInsideCell] = useState(false); const canAddColumn = userRoles.includes("ADMIN"); const canEditColumn = userRoles.includes("ADMIN"); @@ -167,149 +169,111 @@ export default function TableComponent() { columnResizeMode: "onChange", // debugRows: true, }); + const { rows } = table.getRowModel(); // console.log(table, selectedCell); - const handleKeyDown = (e: React.KeyboardEvent) => { - console.log( - "keydown", - // e.target, - e.key, - e.ctrlKey ? "ctrl" : "", - e.altKey ? "alt" : "", - e.metaKey ? "meta" : "", - e.shiftKey ? "shift" : "" - ); - const LISTENED_KEYS = [ - "ArrowUp", - "ArrowDown", - "ArrowLeft", - "ArrowRight", - "Enter", - "Escape", - "Home", - "End", - "PageUp", - "PageDown", - ]; - if (LISTENED_KEYS.includes(e.key)) e.preventDefault(); + const { + virtualItems: virtualRows, + totalSize: totalHeight, + scrollToIndex: scrollToRowIndex, + } = useVirtual({ + parentRef: containerRef, + size: tableRows.length, + overscan: 10, + estimateSize: useCallback( + () => tableSchema.rowHeight || DEFAULT_ROW_HEIGHT, + [tableSchema.rowHeight] + ), + }); - if (e.key === "Escape") { - setFocusInsideCell(false); - ( - gridRef.current?.querySelector("[aria-selected=true]") as HTMLDivElement - )?.focus(); - return; + const { + virtualItems: virtualCols, + totalSize: totalWidth, + scrollToIndex: scrollToColIndex, + } = useVirtual({ + parentRef: containerRef, + horizontal: true, + size: columns.length, + overscan: 1, + estimateSize: useCallback( + (index: number) => columns[index].size || DEFAULT_COL_WIDTH, + [columns] + ), + }); + + console.log(totalHeight); + + useEffect(() => { + if (!selectedCell) return; + if (selectedCell.path) { + const rowIndex = tableRows.findIndex( + (row) => row._rowy_ref.path === selectedCell.path + ); + if (rowIndex === -1) return; + scrollToRowIndex(rowIndex); } - - const target = e.target as HTMLDivElement; - if ( - target.getAttribute("role") !== "columnheader" && - target.getAttribute("role") !== "gridcell" - ) - return; - - if (e.key === "Enter") { - setFocusInsideCell(true); - (target.querySelector("[tabindex]") as HTMLElement)?.focus(); - return; + if (selectedCell.columnKey) { + const colIndex = columns.findIndex( + (col) => col.id === selectedCell.columnKey + ); + if (colIndex === -1) return; + scrollToColIndex(colIndex); } + }, [selectedCell, tableRows, columns, scrollToRowIndex, scrollToColIndex]); - const colIndex = Number(target.getAttribute("aria-colindex")) - 1; - const rowIndex = - Number(target.parentElement!.getAttribute("aria-rowindex")) - 2; + const { handleKeyDown, focusInsideCell } = useKeyboardNavigation({ + gridRef, + tableRows, + columns, + }); - const rowId = target.getAttribute("data-rowId")!; - const colId = target.getAttribute("data-colId")!; + const paddingTop = virtualRows.length > 0 ? virtualRows?.[0]?.start || 0 : 0; + const paddingBottom = + virtualRows.length > 0 + ? totalHeight - (virtualRows?.[virtualRows.length - 1]?.end || 0) + : 0; - const isHeader = rowId === "_rowy_header"; + const paddingLeft = virtualCols.length > 0 ? virtualCols?.[0]?.start || 0 : 0; + const paddingRight = + virtualCols.length > 0 + ? totalWidth - (virtualCols?.[virtualCols.length - 1]?.end || 0) + : 0; - let newColIndex = colIndex; - let newRowIndex = rowIndex; + const fetchMoreOnBottomReached = useThrottledCallback( + (containerElement?: HTMLDivElement | null) => { + console.log("fetchMoreOnBottomReached", containerElement); + if (!containerElement) return; - // const newSelectedCell: SelectedCell = selectedCell - // ? { ...selectedCell } - // : { path: rowId, columnKey: colId }; - - switch (e.key) { - case "ArrowUp": - if (e.ctrlKey || e.metaKey) newRowIndex = -1; - else if (rowIndex > -1) newRowIndex = rowIndex - 1; - break; - - case "ArrowDown": - if (e.ctrlKey || e.metaKey) newRowIndex = tableRows.length - 1; - else if (rowIndex < tableRows.length - 1) newRowIndex = rowIndex + 1; - break; - - case "ArrowLeft": - if (e.ctrlKey || e.metaKey) newColIndex = 0; - else if (colIndex > 0) newColIndex = colIndex - 1; - break; - - case "ArrowRight": - if (e.ctrlKey || e.metaKey) newColIndex = columns.length - 1; - else if (colIndex < columns.length - 1) newColIndex = colIndex + 1; - break; - - case "PageUp": - newRowIndex = Math.max(0, rowIndex - COLLECTION_PAGE_SIZE); - break; - - case "PageDown": - newRowIndex = Math.min( - tableRows.length - 1, - rowIndex + COLLECTION_PAGE_SIZE - ); - break; - - case "Home": - newColIndex = 0; - if (e.ctrlKey || e.metaKey) newRowIndex = -1; - break; - - case "End": - newColIndex = columns.length - 1; - if (e.ctrlKey || e.metaKey) newRowIndex = tableRows.length - 1; - break; - } - - const newSelectedCell = { - path: - newRowIndex > -1 - ? tableRows[newRowIndex]._rowy_ref.path - : "_rowy_header", - columnKey: columns[newColIndex].id! || columns[0].id!, - }; - console.log(newRowIndex, newColIndex, newSelectedCell); - - setSelectedCell(newSelectedCell); - - // Find matching DOM element for the cell - const newCellEl = gridRef.current?.querySelector( - `[aria-rowindex="${newRowIndex + 2}"] [aria-colindex="${ - newColIndex + 1 - }"]` - ); - // Focus the cell - if (newCellEl) (newCellEl as HTMLDivElement).focus(); - setFocusInsideCell(false); - }; + const { scrollHeight, scrollTop, clientHeight } = containerElement; + if (scrollHeight - scrollTop - clientHeight < 300) { + setTablePage((p) => p + 1); + } + }, + 250 + ); return ( - <> +
fetchMoreOnBottomReached(e.target as HTMLDivElement)} + style={{ overflow: "auto", width: "100%", height: "100%" }} + >
{table.getHeaderGroups().map((headerGroup) => ( @@ -322,8 +286,8 @@ export default function TableComponent() { return (
- {table.getRowModel().rows.map((row) => ( - - {row.getVisibleCells().map((cell, cellIndex) => { - const isSelectedCell = - selectedCell?.path === row.original._rowy_ref.path && - selectedCell?.columnKey === cell.column.id; + {paddingTop > 0 && ( +
+ )} - return ( - { - setSelectedCell({ - path: row.original._rowy_ref.path, - columnKey: cell.column.id, - }); - (e.target as HTMLDivElement).focus(); - }} - > - {flexRender(cell.column.columnDef.cell, cell.getContext())} - - - ); - })} - - ))} + {flexRender( + cell.column.columnDef.cell, + cell.getContext() + )} + + + ); + })} + + {paddingRight > 0 && ( +
+ )} + + ); + })} + + {paddingBottom > 0 && ( +
+ )}
- +
); } diff --git a/src/components/Table/useKeyboardNavigation.tsx b/src/components/Table/useKeyboardNavigation.tsx new file mode 100644 index 00000000..be41e37f --- /dev/null +++ b/src/components/Table/useKeyboardNavigation.tsx @@ -0,0 +1,140 @@ +import { useState } from "react"; +import { useSetAtom } from "jotai"; +import { ColumnDef } from "@tanstack/react-table"; + +import { tableScope, selectedCellAtom } from "@src/atoms/tableScope"; +import { TableRow } from "@src/types/table"; +import { COLLECTION_PAGE_SIZE } from "@src/config/db"; + +export interface IUseKeyboardNavigationProps { + gridRef: React.RefObject; + tableRows: TableRow[]; + columns: ColumnDef[]; +} + +export function useKeyboardNavigation({ + gridRef, + tableRows, + columns, +}: IUseKeyboardNavigationProps) { + const setSelectedCell = useSetAtom(selectedCellAtom, tableScope); + const [focusInsideCell, setFocusInsideCell] = useState(false); + + const handleKeyDown = (e: React.KeyboardEvent) => { + // Block default browser behavior for arrow keys (scroll) and other keys + const LISTENED_KEYS = [ + "ArrowUp", + "ArrowDown", + "ArrowLeft", + "ArrowRight", + "Enter", + "Escape", + "Home", + "End", + "PageUp", + "PageDown", + ]; + if (LISTENED_KEYS.includes(e.key)) e.preventDefault(); + + // Esc: exit cell + if (e.key === "Escape") { + setFocusInsideCell(false); + ( + gridRef.current?.querySelector("[aria-selected=true]") as HTMLDivElement + )?.focus(); + return; + } + + // If event target is not a cell, ignore + const target = e.target as HTMLDivElement; + if ( + target.getAttribute("role") !== "columnheader" && + target.getAttribute("role") !== "gridcell" + ) + return; + + // Enter: enter cell + if (e.key === "Enter") { + setFocusInsideCell(true); + (target.querySelector("[tabindex]") as HTMLElement)?.focus(); + return; + } + + const colIndex = Number(target.getAttribute("aria-colindex")) - 1; + const rowIndex = + Number(target.parentElement!.getAttribute("aria-rowindex")) - 2; + + let newColIndex = colIndex; + let newRowIndex = rowIndex; + + switch (e.key) { + case "ArrowUp": + if (e.ctrlKey || e.metaKey) newRowIndex = -1; + else if (rowIndex > -1) newRowIndex = rowIndex - 1; + break; + + case "ArrowDown": + if (e.ctrlKey || e.metaKey) newRowIndex = tableRows.length - 1; + else if (rowIndex < tableRows.length - 1) newRowIndex = rowIndex + 1; + break; + + case "ArrowLeft": + if (e.ctrlKey || e.metaKey) newColIndex = 0; + else if (colIndex > 0) newColIndex = colIndex - 1; + break; + + case "ArrowRight": + if (e.ctrlKey || e.metaKey) newColIndex = columns.length - 1; + else if (colIndex < columns.length - 1) newColIndex = colIndex + 1; + break; + + case "PageUp": + newRowIndex = Math.max(0, rowIndex - COLLECTION_PAGE_SIZE); + break; + + case "PageDown": + newRowIndex = Math.min( + tableRows.length - 1, + rowIndex + COLLECTION_PAGE_SIZE + ); + break; + + case "Home": + newColIndex = 0; + if (e.ctrlKey || e.metaKey) newRowIndex = -1; + break; + + case "End": + newColIndex = columns.length - 1; + if (e.ctrlKey || e.metaKey) newRowIndex = tableRows.length - 1; + break; + } + + // Get `path` and `columnKey` from `tableRows` and `columns` respectively + const newSelectedCell = { + path: + newRowIndex > -1 + ? tableRows[newRowIndex]._rowy_ref.path + : "_rowy_header", + columnKey: columns[newColIndex].id! || columns[0].id!, + }; + + // Store in selectedCellAtom + setSelectedCell(newSelectedCell); + + // Find matching DOM element for the cell + const newCellEl = gridRef.current?.querySelector( + `[aria-rowindex="${newRowIndex + 2}"] [aria-colindex="${ + newColIndex + 1 + }"]` + ); + + // Focus the cell + if (newCellEl) setTimeout(() => (newCellEl as HTMLDivElement).focus()); + + // When selected cell changes, exit current cell + setFocusInsideCell(false); + }; + + return { handleKeyDown, focusInsideCell } as const; +} diff --git a/src/pages/Table/TablePage.tsx b/src/pages/Table/TablePage.tsx index 17e8213a..b82a411f 100644 --- a/src/pages/Table/TablePage.tsx +++ b/src/pages/Table/TablePage.tsx @@ -101,12 +101,12 @@ export default function TablePage({ }> diff --git a/yarn.lock b/yarn.lock index b6e7cfa3..aa25b7ed 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2363,6 +2363,11 @@ resolved "https://registry.yarnpkg.com/@protobufjs/utf8/-/utf8-1.1.0.tgz#a777360b5b39a1a2e5106f8e858f2fd2d060c570" integrity sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw== +"@reach/observe-rect@^1.1.0": + version "1.2.0" + resolved "https://registry.yarnpkg.com/@reach/observe-rect/-/observe-rect-1.2.0.tgz#d7a6013b8aafcc64c778a0ccb83355a11204d3b2" + integrity sha512-Ba7HmkFgfQxZqqaeIWWkNK0rEhpxVQHIoVyW1YDSkGsGIXzcaW4deC8B0pZrNSSyLTdIk7y+5olKt5+g0GmFIQ== + "@react-dnd/asap@^4.0.0": version "4.0.1" resolved "https://registry.yarnpkg.com/@react-dnd/asap/-/asap-4.0.1.tgz#5291850a6b58ce6f2da25352a64f1b0674871aab" @@ -10277,6 +10282,13 @@ react-usestateref@^1.0.8: resolved "https://registry.yarnpkg.com/react-usestateref/-/react-usestateref-1.0.8.tgz#b40519af0d6f3b3822c70eb5db80f7d47f1b1ff5" integrity sha512-whaE6H0XGarFKwZ3EYbpHBsRRCLZqdochzg/C7e+b6VFMTA3LS3K4ZfpI4NT40iy83jG89rGXrw70P9iDfOdsA== +react-virtual@^2.10.4: + version "2.10.4" + resolved "https://registry.yarnpkg.com/react-virtual/-/react-virtual-2.10.4.tgz#08712f0acd79d7d6f7c4726f05651a13b24d8704" + integrity sha512-Ir6+oPQZTVHfa6+JL9M7cvMILstFZH/H3jqeYeKI4MSUX+rIruVwFC6nGVXw9wqAw8L0Kg2KvfXxI85OvYQdpQ== + dependencies: + "@reach/observe-rect" "^1.1.0" + react@^18.0.0: version "18.0.0" resolved "https://registry.yarnpkg.com/react/-/react-18.0.0.tgz#b468736d1f4a5891f38585ba8e8fb29f91c3cb96" From c9b1cf98e05bcd6eec96acc32aee0621c8c50866 Mon Sep 17 00:00:00 2001 From: Sidney Alcantara Date: Wed, 19 Oct 2022 12:41:20 +1100 Subject: [PATCH 06/66] support hidden columns --- src/components/Table/Table.tsx | 92 ++++++++++--------- .../Table/useKeyboardNavigation.tsx | 16 ++-- src/pages/Table/TablePage.tsx | 8 +- 3 files changed, 62 insertions(+), 54 deletions(-) diff --git a/src/components/Table/Table.tsx b/src/components/Table/Table.tsx index 153b5576..d3cbc0a6 100644 --- a/src/components/Table/Table.tsx +++ b/src/components/Table/Table.tsx @@ -93,22 +93,13 @@ export default function TableComponent() { const canAddColumn = userRoles.includes("ADMIN"); const canEditColumn = userRoles.includes("ADMIN"); - const userDocHiddenFields = - userSettings.tables?.[formatSubTableName(tableId)]?.hiddenFields; // Get column defs from table schema // Also add end column for admins const columns = useMemo(() => { const _columns = tableColumnsOrdered - // .filter((column) => { - // if (column.hidden) return false; - // if ( - // Array.isArray(userDocHiddenFields) && - // userDocHiddenFields.includes(column.key) - // ) - // return false; - // return true; - // }) + // Hide column for all users using table schema + .filter((column) => !column.hidden) .map((columnConfig) => columnHelper.accessor(columnConfig.fieldName, { id: columnConfig.fieldName, @@ -136,30 +127,41 @@ export default function TableComponent() { }) ); - // if (canAddColumn || !tableSettings.readOnly) { - // _columns.push({ - // isNew: true, - // key: "new", - // fieldName: "_rowy_new", - // name: "Add column", - // type: FieldType.last, - // index: _columns.length ?? 0, - // width: 154, - // headerRenderer: FinalColumnHeader, - // headerCellClass: "final-column-header", - // cellClass: "final-column-cell", - // formatter: FinalColumn, - // editable: false, - // }); - // } + if (canAddColumn || !tableSettings.readOnly) { + _columns.push( + columnHelper.display({ + id: "_rowy_column_actions", + header: () => "Actions", + cell: () => <>Menu | Duplicate | Delete, + }) + // { + // isNew: true, + // key: "new", + // fieldName: "_rowy_new", + // name: "Add column", + // type: FieldType.last, + // index: _columns.length ?? 0, + // width: 154, + // headerRenderer: FinalColumnHeader, + // headerCellClass: "final-column-header", + // cellClass: "final-column-cell", + // formatter: FinalColumn, + // editable: false, + // } + ); + } return _columns; - }, [ - tableColumnsOrdered, - // userDocHiddenFields, - // tableSettings.readOnly, - // canAddColumn, - ]); + }, [tableColumnsOrdered, canAddColumn, tableSettings.readOnly]); + + // Get user’s hidden columns from user document + const userDocHiddenFields = + userSettings.tables?.[formatSubTableName(tableId)]?.hiddenFields; + // Memoize into a VisibilityState + const columnVisibility = useMemo(() => { + if (!Array.isArray(userDocHiddenFields)) return {}; + return userDocHiddenFields.reduce((a, c) => ({ ...a, [c]: false }), {}); + }, [userDocHiddenFields]); const table = useReactTable({ data: tableRows, @@ -167,9 +169,11 @@ export default function TableComponent() { getCoreRowModel: getCoreRowModel(), getRowId, columnResizeMode: "onChange", + state: { columnVisibility }, // debugRows: true, }); const { rows } = table.getRowModel(); + const leafColumns = table.getVisibleLeafColumns(); // console.log(table, selectedCell); const { @@ -193,16 +197,14 @@ export default function TableComponent() { } = useVirtual({ parentRef: containerRef, horizontal: true, - size: columns.length, + size: leafColumns.length, overscan: 1, estimateSize: useCallback( - (index: number) => columns[index].size || DEFAULT_COL_WIDTH, - [columns] + (index: number) => leafColumns[index].columnDef.size || DEFAULT_COL_WIDTH, + [leafColumns] ), }); - console.log(totalHeight); - useEffect(() => { if (!selectedCell) return; if (selectedCell.path) { @@ -213,18 +215,24 @@ export default function TableComponent() { scrollToRowIndex(rowIndex); } if (selectedCell.columnKey) { - const colIndex = columns.findIndex( + const colIndex = leafColumns.findIndex( (col) => col.id === selectedCell.columnKey ); if (colIndex === -1) return; scrollToColIndex(colIndex); } - }, [selectedCell, tableRows, columns, scrollToRowIndex, scrollToColIndex]); + }, [ + selectedCell, + tableRows, + leafColumns, + scrollToRowIndex, + scrollToColIndex, + ]); const { handleKeyDown, focusInsideCell } = useKeyboardNavigation({ gridRef, tableRows, - columns, + leafColumns, }); const paddingTop = virtualRows.length > 0 ? virtualRows?.[0]?.start || 0 : 0; @@ -296,7 +304,7 @@ export default function TableComponent() { aria-selected={isSelectedCell} label={header.column.columnDef.meta?.name || header.id} type={header.column.columnDef.meta?.type} - style={{ width: header.getSize() }} + style={{ width: header.getSize(), borderRight: "none" }} onClick={(e) => { setSelectedCell({ path: "_rowy_header", diff --git a/src/components/Table/useKeyboardNavigation.tsx b/src/components/Table/useKeyboardNavigation.tsx index be41e37f..f814f15f 100644 --- a/src/components/Table/useKeyboardNavigation.tsx +++ b/src/components/Table/useKeyboardNavigation.tsx @@ -1,6 +1,6 @@ import { useState } from "react"; import { useSetAtom } from "jotai"; -import { ColumnDef } from "@tanstack/react-table"; +import { Column } from "@tanstack/react-table"; import { tableScope, selectedCellAtom } from "@src/atoms/tableScope"; import { TableRow } from "@src/types/table"; @@ -9,13 +9,13 @@ import { COLLECTION_PAGE_SIZE } from "@src/config/db"; export interface IUseKeyboardNavigationProps { gridRef: React.RefObject; tableRows: TableRow[]; - columns: ColumnDef[]; + leafColumns: Column[]; } export function useKeyboardNavigation({ gridRef, tableRows, - columns, + leafColumns, }: IUseKeyboardNavigationProps) { const setSelectedCell = useSetAtom(selectedCellAtom, tableScope); const [focusInsideCell, setFocusInsideCell] = useState(false); @@ -84,8 +84,8 @@ export function useKeyboardNavigation({ break; case "ArrowRight": - if (e.ctrlKey || e.metaKey) newColIndex = columns.length - 1; - else if (colIndex < columns.length - 1) newColIndex = colIndex + 1; + if (e.ctrlKey || e.metaKey) newColIndex = leafColumns.length - 1; + else if (colIndex < leafColumns.length - 1) newColIndex = colIndex + 1; break; case "PageUp": @@ -105,18 +105,18 @@ export function useKeyboardNavigation({ break; case "End": - newColIndex = columns.length - 1; + newColIndex = leafColumns.length - 1; if (e.ctrlKey || e.metaKey) newRowIndex = tableRows.length - 1; break; } - // Get `path` and `columnKey` from `tableRows` and `columns` respectively + // Get `path` and `columnKey` from `tableRows` and `leafColumns` respectively const newSelectedCell = { path: newRowIndex > -1 ? tableRows[newRowIndex]._rowy_ref.path : "_rowy_header", - columnKey: columns[newColIndex].id! || columns[0].id!, + columnKey: leafColumns[newColIndex].id! || leafColumns[0].id!, }; // Store in selectedCellAtom diff --git a/src/pages/Table/TablePage.tsx b/src/pages/Table/TablePage.tsx index b82a411f..41d8c003 100644 --- a/src/pages/Table/TablePage.tsx +++ b/src/pages/Table/TablePage.tsx @@ -103,14 +103,14 @@ export default function TablePage({ sx={{ height: `calc(100vh - ${TOP_BAR_HEIGHT}px - ${TABLE_TOOLBAR_HEIGHT}px)`, width: `calc(100% - ${DRAWER_COLLAPSED_WIDTH}px)`, - // width: { - // xs: "100%", - // sm: `calc(100% - ${DRAWER_COLLAPSED_WIDTH}px)`, - // }, '& [role="grid"]': { paddingBottom: (theme) => `max(env(safe-area-inset-bottom), ${theme.spacing(2)})`, + paddingLeft: (theme) => + `max(env(safe-area-inset-left), ${theme.spacing(2)})`, + paddingRight: (theme) => + `max(env(safe-area-inset-right), ${theme.spacing(2)})`, }, }} > From f9b1a35445f5f653446760bd8863210c98b6b73e Mon Sep 17 00:00:00 2001 From: Sidney Alcantara Date: Wed, 19 Oct 2022 12:54:31 +1100 Subject: [PATCH 07/66] fix rowHeight & virtualized rows jumping --- src/components/Table/Table.tsx | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/src/components/Table/Table.tsx b/src/components/Table/Table.tsx index d3cbc0a6..99fccb45 100644 --- a/src/components/Table/Table.tsx +++ b/src/components/Table/Table.tsx @@ -249,7 +249,6 @@ export default function TableComponent() { const fetchMoreOnBottomReached = useThrottledCallback( (containerElement?: HTMLDivElement | null) => { - console.log("fetchMoreOnBottomReached", containerElement); if (!containerElement) return; const { scrollHeight, scrollTop, clientHeight } = containerElement; @@ -263,7 +262,7 @@ export default function TableComponent() { return (
fetchMoreOnBottomReached(e.target as HTMLDivElement)} + onScroll={(e) => fetchMoreOnBottomReached(e.target as HTMLDivElement)} style={{ overflow: "auto", width: "100%", height: "100%" }} >
+ {paddingLeft > 0 && (
{isSelectedCell ? "f" : "x"} - + */} ); })} From f32a290345493fc50e868d6225bb51d23f037103 Mon Sep 17 00:00:00 2001 From: Sidney Alcantara Date: Fri, 21 Oct 2022 14:58:25 +1100 Subject: [PATCH 08/66] add frozen columns --- src/components/Table/Column.tsx | 2 + src/components/Table/Styled/StyledCell.tsx | 17 +++- src/components/Table/Styled/StyledRow.tsx | 24 ++++- src/components/Table/Styled/StyledTable.tsx | 15 +++ src/components/Table/Table.tsx | 100 ++++++++++++++------ src/pages/Table/TablePage.tsx | 9 +- 6 files changed, 126 insertions(+), 41 deletions(-) diff --git a/src/components/Table/Column.tsx b/src/components/Table/Column.tsx index 71c73894..eb32c945 100644 --- a/src/components/Table/Column.tsx +++ b/src/components/Table/Column.tsx @@ -3,6 +3,7 @@ import { alpha } from "@mui/material/styles"; import { FieldType } from "@src/constants/fields"; import { getFieldProp } from "@src/components/fields"; +import { spreadSx } from "@src/utils/ui"; export const COLUMN_HEADER_HEIGHT = 42; @@ -68,6 +69,7 @@ export default function Column({ }, } : {}, + ...spreadSx(props.sx), ]} > {type && {getFieldProp("icon", type)}} diff --git a/src/components/Table/Styled/StyledCell.tsx b/src/components/Table/Styled/StyledCell.tsx index 0676daa2..af2e4646 100644 --- a/src/components/Table/Styled/StyledCell.tsx +++ b/src/components/Table/Styled/StyledCell.tsx @@ -1,9 +1,10 @@ +import { colord } from "colord"; import { styled } from "@mui/material"; export const StyledCell = styled("div")(({ theme }) => ({ display: "flex", alignItems: "center", - "--cell-padding": theme.spacing(1.5), + "--cell-padding": theme.spacing(10 / 8), padding: "0 var(--cell-padding)", overflow: "visible", @@ -12,7 +13,17 @@ export const StyledCell = styled("div")(({ theme }) => ({ lineHeight: "calc(var(--row-height) - 1px)", - borderBottom: `1px solid ${theme.palette.divider}`, - borderLeft: `1px solid ${theme.palette.divider}`, + backgroundColor: theme.palette.background.paper, + + 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(), + }, })); StyledCell.displayName = "StyledCell"; diff --git a/src/components/Table/Styled/StyledRow.tsx b/src/components/Table/Styled/StyledRow.tsx index 4e76b1d8..e49abeb7 100644 --- a/src/components/Table/Styled/StyledRow.tsx +++ b/src/components/Table/Styled/StyledRow.tsx @@ -6,6 +6,28 @@ export const StyledRow = styled("div")(({ theme }) => ({ display: "flex", height: DEFAULT_ROW_HEIGHT, - backgroundColor: theme.palette.background.paper, + "& > *": { + flexGrow: 0, + flexShrink: 0, + minWidth: 0, + }, + + "& [role='columnheader']": { + "&:first-of-type": { + borderTopLeftRadius: theme.shape.borderRadius, + }, + "&:last-of-type": { + borderTopRightRadius: theme.shape.borderRadius, + }, + }, + + "&:last-of-type": { + "& [role='gridcell']:first-of-type": { + borderBottomLeftRadius: theme.shape.borderRadius, + }, + "& [role='gridcell']:last-of-type": { + borderBottomRightRadius: theme.shape.borderRadius, + }, + }, })); StyledRow.displayName = "StyledRow"; diff --git a/src/components/Table/Styled/StyledTable.tsx b/src/components/Table/Styled/StyledTable.tsx index 2f08622b..0033ee68 100644 --- a/src/components/Table/Styled/StyledTable.tsx +++ b/src/components/Table/Styled/StyledTable.tsx @@ -14,5 +14,20 @@ export const StyledTable = styled("div")(({ theme }) => ({ outlineOffset: "-3px", }, }, + + "& [data-frozen='left']": { + position: "sticky", + left: 0, + zIndex: 2, + + "&[data-frozen-last='true']": { + boxShadow: theme.shadows[2] + .replace(/, 0 (\d+px)/g, ", $1 0") + .split("),") + .slice(1) + .join("),"), + clipPath: "inset(0 -4px 0 0)", + }, + }, })); StyledTable.displayName = "StyledTable"; diff --git a/src/components/Table/Table.tsx b/src/components/Table/Table.tsx index 99fccb45..90359284 100644 --- a/src/components/Table/Table.tsx +++ b/src/components/Table/Table.tsx @@ -19,7 +19,7 @@ import { StyledTable } from "./Styled/StyledTable"; import { StyledRow } from "./Styled/StyledRow"; import ColumnHeaderComponent from "./Column"; -import { LinearProgress } from "@mui/material"; +import { IconButton, LinearProgress } from "@mui/material"; import TableContainer, { OUT_OF_ORDER_MARGIN } from "./TableContainer"; import ColumnHeader, { COLUMN_HEADER_HEIGHT } from "./ColumnHeader"; @@ -63,7 +63,8 @@ import { useKeyboardNavigation } from "./useKeyboardNavigation"; export const DEFAULT_ROW_HEIGHT = 41; export const DEFAULT_COL_WIDTH = 150; -export const MAX_COL_WIDTH = 380; +export const TABLE_PADDING = 16; +export const TABLE_GUTTER = 8; declare module "@tanstack/table-core" { interface ColumnMeta extends ColumnConfig {} @@ -132,22 +133,14 @@ export default function TableComponent() { columnHelper.display({ id: "_rowy_column_actions", header: () => "Actions", - cell: () => <>Menu | Duplicate | Delete, + cell: () => ( + <> + M + D + X + + ), }) - // { - // isNew: true, - // key: "new", - // fieldName: "_rowy_new", - // name: "Add column", - // type: FieldType.last, - // index: _columns.length ?? 0, - // width: 154, - // headerRenderer: FinalColumnHeader, - // headerCellClass: "final-column-header", - // cellClass: "final-column-cell", - // formatter: FinalColumn, - // editable: false, - // } ); } @@ -163,13 +156,23 @@ export default function TableComponent() { return userDocHiddenFields.reduce((a, c) => ({ ...a, [c]: false }), {}); }, [userDocHiddenFields]); + // Get frozen columns + const columnPinning = useMemo( + () => ({ + left: columns.filter((c) => c.meta?.fixed && c.id).map((c) => c.id!), + }), + [columns] + ); + const lastFrozen: string | undefined = + columnPinning.left[columnPinning.left.length - 1]; + const table = useReactTable({ data: tableRows, columns, getCoreRowModel: getCoreRowModel(), getRowId, columnResizeMode: "onChange", - state: { columnVisibility }, + state: { columnVisibility, columnPinning }, // debugRows: true, }); const { rows } = table.getRowModel(); @@ -184,6 +187,7 @@ export default function TableComponent() { parentRef: containerRef, size: tableRows.length, overscan: 10, + paddingEnd: TABLE_PADDING, estimateSize: useCallback( () => tableSchema.rowHeight || DEFAULT_ROW_HEIGHT, [tableSchema.rowHeight] @@ -198,7 +202,9 @@ export default function TableComponent() { parentRef: containerRef, horizontal: true, size: leafColumns.length, - overscan: 1, + overscan: 10, + paddingStart: TABLE_PADDING, + paddingEnd: TABLE_PADDING, estimateSize: useCallback( (index: number) => leafColumns[index].columnDef.size || DEFAULT_COL_WIDTH, [leafColumns] @@ -211,15 +217,13 @@ export default function TableComponent() { const rowIndex = tableRows.findIndex( (row) => row._rowy_ref.path === selectedCell.path ); - if (rowIndex === -1) return; - scrollToRowIndex(rowIndex); + if (rowIndex > -1) scrollToRowIndex(rowIndex); } if (selectedCell.columnKey) { const colIndex = leafColumns.findIndex( (col) => col.id === selectedCell.columnKey ); - if (colIndex === -1) return; - scrollToColIndex(colIndex); + if (colIndex > -1) scrollToColIndex(colIndex); } }, [ selectedCell, @@ -277,7 +281,12 @@ export default function TableComponent() {
{table.getHeaderGroups().map((headerGroup) => ( @@ -290,17 +299,25 @@ export default function TableComponent() { return ( { setSelectedCell({ path: "_rowy_header", @@ -367,8 +384,12 @@ export default function TableComponent() { return ( { setSelectedCell({ path: row.original._rowy_ref.path, diff --git a/src/pages/Table/TablePage.tsx b/src/pages/Table/TablePage.tsx index 41d8c003..526f40fc 100644 --- a/src/pages/Table/TablePage.tsx +++ b/src/pages/Table/TablePage.tsx @@ -105,12 +105,9 @@ export default function TablePage({ width: `calc(100% - ${DRAWER_COLLAPSED_WIDTH}px)`, '& [role="grid"]': { - paddingBottom: (theme) => - `max(env(safe-area-inset-bottom), ${theme.spacing(2)})`, - paddingLeft: (theme) => - `max(env(safe-area-inset-left), ${theme.spacing(2)})`, - paddingRight: (theme) => - `max(env(safe-area-inset-right), ${theme.spacing(2)})`, + marginBottom: `env(safe-area-inset-bottom)`, + marginLeft: `env(safe-area-inset-left)`, + marginRight: `env(safe-area-inset-right)`, }, }} > From 202883e1fd462d697aaccd09b5d5c22582a95ebb Mon Sep 17 00:00:00 2001 From: Sidney Alcantara Date: Mon, 24 Oct 2022 13:12:53 +1100 Subject: [PATCH 09/66] add resize columns & ask admins if they want to save to scheam --- src/components/Table/Column.tsx | 5 + src/components/Table/Styled/StyledResizer.tsx | 53 +++++++++ src/components/Table/Table.tsx | 101 +++++++++++++----- 3 files changed, 134 insertions(+), 25 deletions(-) create mode 100644 src/components/Table/Styled/StyledResizer.tsx diff --git a/src/components/Table/Column.tsx b/src/components/Table/Column.tsx index eb32c945..35b42500 100644 --- a/src/components/Table/Column.tsx +++ b/src/components/Table/Column.tsx @@ -11,6 +11,7 @@ export interface IColumnProps extends Partial { label: string; type?: FieldType; secondaryItem?: React.ReactNode; + children?: React.ReactNode; active?: boolean; } @@ -19,6 +20,7 @@ export default function Column({ label, type, secondaryItem, + children, active, ...props @@ -35,6 +37,7 @@ export default function Column({ height: COLUMN_HEADER_HEIGHT, border: (theme) => `1px solid ${theme.palette.divider}`, backgroundColor: "background.default", + position: "relative", py: 0, px: 1, @@ -106,6 +109,8 @@ export default function Column({ {secondaryItem} )} + + {children} ); } diff --git a/src/components/Table/Styled/StyledResizer.tsx b/src/components/Table/Styled/StyledResizer.tsx new file mode 100644 index 00000000..526dd8b6 --- /dev/null +++ b/src/components/Table/Styled/StyledResizer.tsx @@ -0,0 +1,53 @@ +import { styled } from "@mui/material"; + +export interface IStyledResizerProps { + isResizing: boolean; +} + +export const StyledResizer = styled("div", { + name: "StyledResizer", + shouldForwardProp: (prop) => prop !== "isResizing", +})(({ theme, isResizing }) => ({ + position: "absolute", + right: 0, + top: 0, + height: "100%", + width: 10, + + cursor: "col-resize", + userSelect: "none", + touchAction: "none", + + display: "flex", + justifyContent: "flex-end", + alignItems: "center", + + transition: theme.transitions.create("opacity", { + duration: theme.transitions.duration.shortest, + }), + opacity: isResizing ? 1 : 0, + "[role='columnheader']:hover &": { opacity: 0.33 }, + "[role='columnheader'] &:hover, [role='columnheader'] &:active": { + opacity: 1, + "&::before": { transform: "scaleY(1.25)" }, + }, + + "&::before": { + content: "''", + display: "block", + + height: "50%", + width: 4, + borderRadius: 2, + marginRight: 2, + + background: isResizing + ? theme.palette.primary.main + : theme.palette.action.active, + transition: theme.transitions.create("transform", { + duration: theme.transitions.duration.shortest, + }), + transform: isResizing ? "scaleY(1.5) !important" : undefined, + }, +})); +StyledResizer.displayName = "StyledResizer"; diff --git a/src/components/Table/Table.tsx b/src/components/Table/Table.tsx index 90359284..c922bfa9 100644 --- a/src/components/Table/Table.tsx +++ b/src/components/Table/Table.tsx @@ -1,9 +1,15 @@ -import { useMemo, useRef, useCallback, Suspense, useEffect } from "react"; +import { useMemo, useRef, useCallback, useState, useEffect } from "react"; import { useAtom, useSetAtom } from "jotai"; -import { useDebouncedCallback, useThrottledCallback } from "use-debounce"; +import { + useDebounce, + useDebouncedCallback, + useThrottledCallback, +} from "use-debounce"; +import { useSnackbar } from "notistack"; +import useMemoValue from "use-memo-value"; import { DndProvider } from "react-dnd"; import { HTML5Backend } from "react-dnd-html5-backend"; -import { findIndex } from "lodash-es"; +import { isEmpty, isEqual } from "lodash-es"; import { createColumnHelper, @@ -17,9 +23,11 @@ import { TOP_BAR_HEIGHT } from "@src/layouts/Navigation/TopBar"; import { TABLE_TOOLBAR_HEIGHT } from "@src/components/TableToolbar"; import { StyledTable } from "./Styled/StyledTable"; import { StyledRow } from "./Styled/StyledRow"; +import { StyledResizer } from "./Styled/StyledResizer"; import ColumnHeaderComponent from "./Column"; -import { IconButton, LinearProgress } from "@mui/material"; +import { IconButton, LinearProgress, Button } from "@mui/material"; +import { LoadingButton } from "@mui/lab"; import TableContainer, { OUT_OF_ORDER_MARGIN } from "./TableContainer"; import ColumnHeader, { COLUMN_HEADER_HEIGHT } from "./ColumnHeader"; @@ -63,8 +71,10 @@ import { useKeyboardNavigation } from "./useKeyboardNavigation"; export const DEFAULT_ROW_HEIGHT = 41; export const DEFAULT_COL_WIDTH = 150; +export const MIN_COL_WIDTH = 32; export const TABLE_PADDING = 16; export const TABLE_GUTTER = 8; +export const DEBOUNCE_DELAY = 500; declare module "@tanstack/table-core" { interface ColumnMeta extends ColumnConfig {} @@ -74,6 +84,8 @@ const columnHelper = createColumnHelper(); const getRowId = (row: TableRow) => row._rowy_ref.path || row._rowy_ref.id; export default function TableComponent() { + const { enqueueSnackbar, closeSnackbar } = useSnackbar(); + const [userRoles] = useAtom(userRolesAtom, projectScope); const [userSettings] = useAtom(userSettingsAtom, projectScope); @@ -105,6 +117,9 @@ export default function TableComponent() { columnHelper.accessor(columnConfig.fieldName, { id: columnConfig.fieldName, meta: columnConfig, + size: columnConfig.width, + enableResizing: columnConfig.resizable !== false, + minSize: MIN_COL_WIDTH, // draggable: true, // resizable: true, // frozen: columnConfig.fixed, @@ -166,15 +181,62 @@ export default function TableComponent() { const lastFrozen: string | undefined = columnPinning.left[columnPinning.left.length - 1]; + // Call TanStack Table const table = useReactTable({ data: tableRows, columns, getCoreRowModel: getCoreRowModel(), getRowId, columnResizeMode: "onChange", - state: { columnVisibility, columnPinning }, - // debugRows: true, }); + + const [columnSizing, setColumnSizing] = useState( + table.initialState.columnSizing + ); + table.setOptions((prev) => ({ + ...prev, + state: { ...prev.state, columnVisibility, columnPinning, columnSizing }, + onColumnSizingChange: setColumnSizing, + })); + + // Debounce for saving to schema + const [debouncedColumnSizing] = useDebounce(columnSizing, DEBOUNCE_DELAY, { + equalityFn: isEqual, + }); + // Offer to save when column sizing changes + useEffect(() => { + if (!canEditColumn || isEmpty(debouncedColumnSizing)) return; + + const snackbarId = enqueueSnackbar("Save column sizes for all users?", { + action: ( + + Save + + ), + anchorOrigin: { horizontal: "center", vertical: "top" }, + }); + + async function handleSaveToSchema() { + const promises = Object.entries(debouncedColumnSizing).map( + ([key, value]) => updateColumn({ key, config: { width: value } }) + ); + await Promise.all(promises); + closeSnackbar(snackbarId); + } + + return () => closeSnackbar(snackbarId); + }, [ + debouncedColumnSizing, + canEditColumn, + enqueueSnackbar, + closeSnackbar, + updateColumn, + ]); + const { rows } = table.getRowModel(); const leafColumns = table.getVisibleLeafColumns(); // console.log(table, selectedCell); @@ -260,7 +322,7 @@ export default function TableComponent() { setTablePage((p) => p + 1); } }, - 250 + DEBOUNCE_DELAY ); return ( @@ -326,24 +388,13 @@ export default function TableComponent() { (e.target as HTMLDivElement).focus(); }} > - {/*
*/} + {header.column.getCanResize() && ( + + )} ); })} From 6d1c6b01001f45920aba10b09a52976d4dede98a Mon Sep 17 00:00:00 2001 From: Sidney Alcantara Date: Mon, 24 Oct 2022 13:22:46 +1100 Subject: [PATCH 10/66] fix table not loading more pages when window height can show all of first page --- src/components/Table/Table.tsx | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/components/Table/Table.tsx b/src/components/Table/Table.tsx index c922bfa9..8e1814cd 100644 --- a/src/components/Table/Table.tsx +++ b/src/components/Table/Table.tsx @@ -95,7 +95,7 @@ export default function TableComponent() { const [tableColumnsOrdered] = useAtom(tableColumnsOrderedAtom, tableScope); const [tableRows] = useAtom(tableRowsAtom, tableScope); const [tableNextPage] = useAtom(tableNextPageAtom, tableScope); - const setTablePage = useSetAtom(tablePageAtom, tableScope); + const [tablePage, setTablePage] = useAtom(tablePageAtom, tableScope); const [selectedCell, setSelectedCell] = useAtom(selectedCellAtom, tableScope); const updateColumn = useSetAtom(updateColumnAtom, tableScope); @@ -324,6 +324,11 @@ export default function TableComponent() { }, DEBOUNCE_DELAY ); + // Check on mount and after fetch to see if the table is at the bottom + // for large screen heights + useEffect(() => { + fetchMoreOnBottomReached(containerRef.current); + }, [fetchMoreOnBottomReached, tablePage, tableNextPage.loading]); return (
Date: Mon, 24 Oct 2022 14:59:29 +1100 Subject: [PATCH 11/66] show save state for column sizing --- src/components/Table/Table.tsx | 43 +------- .../Table/useKeyboardNavigation.tsx | 2 + src/components/Table/useSaveColumnSizing.tsx | 102 ++++++++++++++++++ 3 files changed, 107 insertions(+), 40 deletions(-) create mode 100644 src/components/Table/useSaveColumnSizing.tsx diff --git a/src/components/Table/Table.tsx b/src/components/Table/Table.tsx index 8e1814cd..04f5fb35 100644 --- a/src/components/Table/Table.tsx +++ b/src/components/Table/Table.tsx @@ -68,10 +68,11 @@ import { formatSubTableName } from "@src/utils/table"; import { TableRow, ColumnConfig } from "@src/types/table"; import { StyledCell } from "./Styled/StyledCell"; import { useKeyboardNavigation } from "./useKeyboardNavigation"; +import { useSaveColumnSizing } from "./useSaveColumnSizing"; export const DEFAULT_ROW_HEIGHT = 41; export const DEFAULT_COL_WIDTH = 150; -export const MIN_COL_WIDTH = 32; +export const MIN_COL_WIDTH = 80; export const TABLE_PADDING = 16; export const TABLE_GUTTER = 8; export const DEBOUNCE_DELAY = 500; @@ -84,8 +85,6 @@ const columnHelper = createColumnHelper(); const getRowId = (row: TableRow) => row._rowy_ref.path || row._rowy_ref.id; export default function TableComponent() { - const { enqueueSnackbar, closeSnackbar } = useSnackbar(); - const [userRoles] = useAtom(userRolesAtom, projectScope); const [userSettings] = useAtom(userSettingsAtom, projectScope); @@ -199,43 +198,7 @@ export default function TableComponent() { onColumnSizingChange: setColumnSizing, })); - // Debounce for saving to schema - const [debouncedColumnSizing] = useDebounce(columnSizing, DEBOUNCE_DELAY, { - equalityFn: isEqual, - }); - // Offer to save when column sizing changes - useEffect(() => { - if (!canEditColumn || isEmpty(debouncedColumnSizing)) return; - - const snackbarId = enqueueSnackbar("Save column sizes for all users?", { - action: ( - - Save - - ), - anchorOrigin: { horizontal: "center", vertical: "top" }, - }); - - async function handleSaveToSchema() { - const promises = Object.entries(debouncedColumnSizing).map( - ([key, value]) => updateColumn({ key, config: { width: value } }) - ); - await Promise.all(promises); - closeSnackbar(snackbarId); - } - - return () => closeSnackbar(snackbarId); - }, [ - debouncedColumnSizing, - canEditColumn, - enqueueSnackbar, - closeSnackbar, - updateColumn, - ]); + useSaveColumnSizing(columnSizing, canEditColumn); const { rows } = table.getRowModel(); const leafColumns = table.getVisibleLeafColumns(); diff --git a/src/components/Table/useKeyboardNavigation.tsx b/src/components/Table/useKeyboardNavigation.tsx index f814f15f..d71c9f8e 100644 --- a/src/components/Table/useKeyboardNavigation.tsx +++ b/src/components/Table/useKeyboardNavigation.tsx @@ -138,3 +138,5 @@ export function useKeyboardNavigation({ return { handleKeyDown, focusInsideCell } as const; } + +export default useKeyboardNavigation; diff --git a/src/components/Table/useSaveColumnSizing.tsx b/src/components/Table/useSaveColumnSizing.tsx new file mode 100644 index 00000000..2ea940ac --- /dev/null +++ b/src/components/Table/useSaveColumnSizing.tsx @@ -0,0 +1,102 @@ +import { useEffect, useState } from "react"; +import { useSetAtom } from "jotai"; +import { useSnackbar } from "notistack"; +import { useDebounce } from "use-debounce"; +import { isEqual, isEmpty } from "lodash-es"; + +import LoadingButton from "@mui/lab/LoadingButton"; +import CheckIcon from "@mui/icons-material/Check"; +import CircularProgressOptical from "@src/components/CircularProgressOptical"; + +import { + tableScope, + updateColumnAtom, + IUpdateColumnOptions, +} from "@src/atoms/tableScope"; +import { DEBOUNCE_DELAY } from "./Table"; +import { ColumnSizingState } from "@tanstack/react-table"; + +/** Debounces columnSizing and asks admins if they want to save for all users */ +export function useSaveColumnSizing( + columnSizing: ColumnSizingState, + canEditColumn: boolean +) { + const { enqueueSnackbar, closeSnackbar } = useSnackbar(); + const updateColumn = useSetAtom(updateColumnAtom, tableScope); + + // Debounce for saving to schema + const [debouncedColumnSizing] = useDebounce(columnSizing, DEBOUNCE_DELAY, { + equalityFn: isEqual, + }); + // Offer to save when column sizing changes + useEffect(() => { + if (!canEditColumn || isEmpty(debouncedColumnSizing)) return; + + const snackbarId = enqueueSnackbar("Save column sizes for all users?", { + action: ( + + ), + anchorOrigin: { horizontal: "center", vertical: "top" }, + }); + + return () => closeSnackbar(snackbarId); + }, [ + debouncedColumnSizing, + canEditColumn, + enqueueSnackbar, + closeSnackbar, + updateColumn, + ]); + + return null; +} + +interface ISaveColumnSizingButtonProps { + debouncedColumnSizing: ColumnSizingState; + updateColumn: (update: IUpdateColumnOptions) => Promise; +} + +/** + * Make the button a component so it can have its own state, + * so we can display the loading state without showing a new snackbar + */ +function SaveColumnSizingButton({ + debouncedColumnSizing, + updateColumn, +}: ISaveColumnSizingButtonProps) { + const [state, setState] = useState<"" | "loading" | "success">(""); + + const handleSaveToSchema = async () => { + setState("loading"); + // Do this one by one for now to prevent race conditions. + // Need to support updating multiple columns in updateColumnAtom + // in the future. + for (const [key, value] of Object.entries(debouncedColumnSizing)) { + await updateColumn({ key, config: { width: value } }); + } + setState("success"); + }; + + return ( + + ) : ( + + ) + } + > + Save + + ); +} + +export default useSaveColumnSizing; From 0ec112106c1f59f6a95320206a6de258eb643afc Mon Sep 17 00:00:00 2001 From: Sidney Alcantara Date: Mon, 24 Oct 2022 15:19:28 +1100 Subject: [PATCH 12/66] display rows out of order --- src/components/Table/Styled/StyledCell.tsx | 4 ++++ src/components/Table/Table.tsx | 22 +++++++++++++--------- 2 files changed, 17 insertions(+), 9 deletions(-) diff --git a/src/components/Table/Styled/StyledCell.tsx b/src/components/Table/Styled/StyledCell.tsx index af2e4646..b445607e 100644 --- a/src/components/Table/Styled/StyledCell.tsx +++ b/src/components/Table/Styled/StyledCell.tsx @@ -25,5 +25,9 @@ export const StyledCell = styled("div")(({ theme }) => ({ .alpha(1) .toHslString(), }, + + "[data-out-of-order='true'] + [role='row'] &": { + borderTop: `1px solid ${theme.palette.divider}`, + }, })); StyledCell.displayName = "StyledCell"; diff --git a/src/components/Table/Table.tsx b/src/components/Table/Table.tsx index 04f5fb35..8c148649 100644 --- a/src/components/Table/Table.tsx +++ b/src/components/Table/Table.tsx @@ -19,17 +19,13 @@ import { } from "@tanstack/react-table"; import { useVirtual } from "react-virtual"; -import { TOP_BAR_HEIGHT } from "@src/layouts/Navigation/TopBar"; -import { TABLE_TOOLBAR_HEIGHT } from "@src/components/TableToolbar"; import { StyledTable } from "./Styled/StyledTable"; import { StyledRow } from "./Styled/StyledRow"; import { StyledResizer } from "./Styled/StyledResizer"; import ColumnHeaderComponent from "./Column"; -import { IconButton, LinearProgress, Button } from "@mui/material"; -import { LoadingButton } from "@mui/lab"; +import { IconButton } from "@mui/material"; -import TableContainer, { OUT_OF_ORDER_MARGIN } from "./TableContainer"; import ColumnHeader, { COLUMN_HEADER_HEIGHT } from "./ColumnHeader"; import FinalColumnHeader from "./FinalColumnHeader"; import FinalColumn from "./formatters/FinalColumn"; @@ -74,7 +70,7 @@ export const DEFAULT_ROW_HEIGHT = 41; export const DEFAULT_COL_WIDTH = 150; export const MIN_COL_WIDTH = 80; export const TABLE_PADDING = 16; -export const TABLE_GUTTER = 8; +export const OUT_OF_ORDER_MARGIN = 8; export const DEBOUNCE_DELAY = 500; declare module "@tanstack/table-core" { @@ -214,8 +210,10 @@ export default function TableComponent() { overscan: 10, paddingEnd: TABLE_PADDING, estimateSize: useCallback( - () => tableSchema.rowHeight || DEFAULT_ROW_HEIGHT, - [tableSchema.rowHeight] + (index: number) => + (tableSchema.rowHeight || DEFAULT_ROW_HEIGHT) + + (tableRows[index]._rowy_outOfOrder ? OUT_OF_ORDER_MARGIN : 0), + [tableSchema.rowHeight, tableRows] ), }); @@ -383,7 +381,13 @@ export default function TableComponent() { key={row.id} role="row" aria-rowindex={row.index + 2} - style={{ height: tableSchema.rowHeight }} + style={{ + height: tableSchema.rowHeight, + marginBottom: row.original._rowy_outOfOrder + ? OUT_OF_ORDER_MARGIN + : 0, + }} + data-out-of-order={row.original._rowy_outOfOrder || undefined} > {paddingLeft > 0 && (
Date: Mon, 24 Oct 2022 15:38:35 +1100 Subject: [PATCH 13/66] add out of order rows --- src/components/Table/OutOfOrderIndicator.tsx | 20 +++------- src/components/Table/Styled/StyledCell.tsx | 8 ++-- src/components/Table/Styled/StyledRow.tsx | 1 + src/components/Table/Table.tsx | 40 ++++++++++++++------ src/components/Table/TableRow.tsx | 2 +- 5 files changed, 42 insertions(+), 29 deletions(-) diff --git a/src/components/Table/OutOfOrderIndicator.tsx b/src/components/Table/OutOfOrderIndicator.tsx index 337b3d50..907ec2e3 100644 --- a/src/components/Table/OutOfOrderIndicator.tsx +++ b/src/components/Table/OutOfOrderIndicator.tsx @@ -3,7 +3,6 @@ import { useAtom } from "jotai"; import { styled } from "@mui/material/styles"; import RichTooltip from "@src/components/RichTooltip"; import WarningIcon from "@mui/icons-material/WarningAmber"; -import { OUT_OF_ORDER_MARGIN } from "./TableContainer"; import { projectScope, @@ -24,15 +23,7 @@ const Dot = styled("div")(({ theme }) => ({ backgroundColor: theme.palette.warning.main, })); -export interface IOutOfOrderIndicatorProps { - top: number; - height: number; -} - -export default function OutOfOrderIndicator({ - top, - height, -}: IOutOfOrderIndicatorProps) { +export default function OutOfOrderIndicator() { const [dismissed, setDismissed] = useAtom( tableOutOfOrderDismissedAtom, projectScope @@ -42,11 +33,12 @@ export default function OutOfOrderIndicator({
({ display: "flex", alignItems: "center", "--cell-padding": theme.spacing(10 / 8), - padding: "0 var(--cell-padding)", + + "& > .cell-contents": { + padding: "0 var(--cell-padding)", + lineHeight: "calc(var(--row-height) - 1px)", + }, overflow: "visible", contain: "none", position: "relative", - lineHeight: "calc(var(--row-height) - 1px)", - backgroundColor: theme.palette.background.paper, border: `1px solid ${theme.palette.divider}`, diff --git a/src/components/Table/Styled/StyledRow.tsx b/src/components/Table/Styled/StyledRow.tsx index e49abeb7..da2e11a6 100644 --- a/src/components/Table/Styled/StyledRow.tsx +++ b/src/components/Table/Styled/StyledRow.tsx @@ -5,6 +5,7 @@ import { DEFAULT_ROW_HEIGHT } from "@src/components/Table"; export const StyledRow = styled("div")(({ theme }) => ({ display: "flex", height: DEFAULT_ROW_HEIGHT, + position: "relative", "& > *": { flexGrow: 0, diff --git a/src/components/Table/Table.tsx b/src/components/Table/Table.tsx index 8c148649..a8b5a6da 100644 --- a/src/components/Table/Table.tsx +++ b/src/components/Table/Table.tsx @@ -23,6 +23,7 @@ import { StyledTable } from "./Styled/StyledTable"; import { StyledRow } from "./Styled/StyledRow"; import { StyledResizer } from "./Styled/StyledResizer"; import ColumnHeaderComponent from "./Column"; +import OutOfOrderIndicator from "./OutOfOrderIndicator"; import { IconButton } from "@mui/material"; @@ -303,7 +304,13 @@ export default function TableComponent() { aria-readonly={tableSettings.readOnly} aria-colcount={columns.length} aria-rowcount={tableRows.length + 1} - style={{ width: table.getTotalSize(), userSelect: "none" }} + style={ + { + width: table.getTotalSize(), + userSelect: "none", + "--row-height": `${tableSchema.rowHeight || DEFAULT_ROW_HEIGHT}px`, + } as any + } onKeyDown={handleKeyDown} >
{table.getHeaderGroups().map((headerGroup) => ( - + {headerGroup.headers.map((header) => { const isSelectedCell = (!selectedCell && header.index === 0) || @@ -375,6 +387,7 @@ export default function TableComponent() { {virtualRows.map((virtualRow) => { const row = rows[virtualRow.index]; + const outOfOrder = row.original._rowy_outOfOrder; return ( {paddingLeft > 0 && (
)} + {outOfOrder && } + {virtualCols.map((virtualCell) => { const cellIndex = virtualCell.index; const cell = row.getVisibleCells()[cellIndex]; @@ -446,10 +459,15 @@ export default function TableComponent() { (e.target as HTMLDivElement).focus(); }} > - {flexRender( - cell.column.columnDef.cell, - cell.getContext() - )} +
+ {flexRender( + cell.column.columnDef.cell, + cell.getContext() + )} +
{/*
); } From d2261167b767a3b16c52f11fa45fc86994bf0f63 Mon Sep 17 00:00:00 2001 From: Sidney Alcantara Date: Fri, 28 Oct 2022 12:06:14 +1100 Subject: [PATCH 18/66] fix TableInformationDrawer crashing because of imports --- .../TableInformationDrawer/TableInformationDrawer.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/TableInformationDrawer/TableInformationDrawer.tsx b/src/components/TableInformationDrawer/TableInformationDrawer.tsx index f2074049..25b9a5e8 100644 --- a/src/components/TableInformationDrawer/TableInformationDrawer.tsx +++ b/src/components/TableInformationDrawer/TableInformationDrawer.tsx @@ -17,7 +17,7 @@ import CloseIcon from "@mui/icons-material/Close"; import { sideDrawerAtom, tableScope } from "@src/atoms/tableScope"; import { TOP_BAR_HEIGHT } from "@src/layouts/Navigation/TopBar"; -import { TABLE_TOOLBAR_HEIGHT } from "@src/components/TableToolbar/TableToolbar"; +import { TABLE_TOOLBAR_HEIGHT } from "@src/components/TableToolbar"; import ErrorFallback from "@src/components/ErrorFallback"; import Details from "./Details"; From 2355ff7dfc3778972cc5a8d3214d2d3455bf8e37 Mon Sep 17 00:00:00 2001 From: Sidney Alcantara Date: Mon, 31 Oct 2022 16:17:21 +1100 Subject: [PATCH 19/66] add context menu for rows --- src/components/Table/Table.tsx | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/src/components/Table/Table.tsx b/src/components/Table/Table.tsx index 2f04d79c..4f2f51c3 100644 --- a/src/components/Table/Table.tsx +++ b/src/components/Table/Table.tsx @@ -51,6 +51,7 @@ import { updateColumnAtom, updateFieldAtom, selectedCellAtom, + contextMenuTargetAtom, } from "@src/atoms/tableScope"; import { COLLECTION_PAGE_SIZE } from "@src/config/db"; @@ -89,6 +90,7 @@ export default function TableComponent() { const [tableNextPage] = useAtom(tableNextPageAtom, tableScope); const [tablePage, setTablePage] = useAtom(tablePageAtom, tableScope); const [selectedCell, setSelectedCell] = useAtom(selectedCellAtom, tableScope); + const setContextMenuTarget = useSetAtom(contextMenuTargetAtom, tableScope); const updateColumn = useSetAtom(updateColumnAtom, tableScope); const updateField = useSetAtom(updateFieldAtom, tableScope); @@ -460,6 +462,15 @@ export default function TableComponent() { }); (e.target as HTMLDivElement).focus(); }} + onContextMenu={(e) => { + e.preventDefault(); + setSelectedCell({ + path: row.original._rowy_ref.path, + columnKey: cell.column.id, + }); + (e.target as HTMLDivElement).focus(); + setContextMenuTarget(e.target as HTMLElement); + }} >
+ +
); } From 1f581af858180bddefc08f8c042a912677ea5f2a Mon Sep 17 00:00:00 2001 From: Sidney Alcantara Date: Mon, 31 Oct 2022 17:04:18 +1100 Subject: [PATCH 20/66] use existing ColumnHeader to enable sort, column menu --- .../Table/ColumnHeader/ColumnHeader.tsx | 117 +++++++++--------- .../Table/ColumnHeader/ColumnHeaderSort.tsx | 39 +++--- src/components/Table/Styled/StyledResizer.tsx | 5 +- src/components/Table/Styled/StyledRow.tsx | 2 + src/components/Table/Styled/StyledTable.tsx | 2 + src/components/Table/Table.tsx | 51 ++++---- 6 files changed, 108 insertions(+), 108 deletions(-) diff --git a/src/components/Table/ColumnHeader/ColumnHeader.tsx b/src/components/Table/ColumnHeader/ColumnHeader.tsx index a1652c04..7ad33e72 100644 --- a/src/components/Table/ColumnHeader/ColumnHeader.tsx +++ b/src/components/Table/ColumnHeader/ColumnHeader.tsx @@ -1,36 +1,33 @@ -import { useRef } from "react"; +import { forwardRef, useRef } from "react"; import { useAtom, useSetAtom } from "jotai"; -import { useDrag, useDrop } from "react-dnd"; import { styled, - alpha, Tooltip, TooltipProps, tooltipClasses, Fade, Grid, + GridProps, IconButton, Typography, } from "@mui/material"; import DropdownIcon from "@mui/icons-material/MoreHoriz"; import LockIcon from "@mui/icons-material/LockOutlined"; -import ColumnHeaderSort from "./ColumnHeaderSort"; +import ColumnHeaderSort, { SORT_STATES } from "./ColumnHeaderSort"; -import { - projectScope, - userRolesAtom, - altPressAtom, -} from "@src/atoms/projectScope"; +import { projectScope, altPressAtom } from "@src/atoms/projectScope"; import { tableScope, - updateColumnAtom, columnMenuAtom, + tableSortsAtom, } from "@src/atoms/tableScope"; import { getFieldProp } from "@src/components/fields"; import { COLUMN_HEADER_HEIGHT } from "@src/components/Table/Column"; import { ColumnConfig } from "@src/types/table"; +import { FieldType } from "@src/constants/fields"; +import { spreadSx } from "@src/utils/ui"; export { COLUMN_HEADER_HEIGHT }; @@ -47,36 +44,20 @@ const LightTooltip = styled(({ className, ...props }: TooltipProps) => ( }, })); -export interface IDraggableHeaderRendererProps { +export interface IColumnHeaderProps extends Partial { column: ColumnConfig; + width: number; + focusInsideCell: boolean; + children: React.ReactNode; } -export default function DraggableHeaderRenderer({ - column, -}: IDraggableHeaderRendererProps) { - const [userRoles] = useAtom(userRolesAtom, projectScope); - const updateColumn = useSetAtom(updateColumnAtom, tableScope); +export const ColumnHeader = forwardRef(function ColumnHeader( + { column, width, focusInsideCell, children, ...props }: IColumnHeaderProps, + ref: React.Ref +) { const openColumnMenu = useSetAtom(columnMenuAtom, tableScope); const [altPress] = useAtom(altPressAtom, projectScope); - - const [{ isDragging }, dragRef] = useDrag({ - type: "COLUMN_DRAG", - item: { key: column.key }, - collect: (monitor) => ({ - isDragging: monitor.isDragging(), - }), - }); - - const [{ isOver }, dropRef] = useDrop({ - accept: "COLUMN_DRAG", - drop: ({ key }: { key: string }) => { - updateColumn({ key, config: {}, index: column.index }); - }, - collect: (monitor) => ({ - isOver: monitor.isOver(), - canDrop: monitor.canDrop(), - }), - }); + const [tableSorts] = useAtom(tableSortsAtom, tableScope); const buttonRef = useRef(null); @@ -85,14 +66,26 @@ export default function DraggableHeaderRenderer({ openColumnMenu({ column, anchorEl: buttonRef.current }); }; + const _sortKey = getFieldProp("sortKey", (column as any).type); + const sortKey = _sortKey ? `${column.key}.${_sortKey}` : column.key; + const currentSort: typeof SORT_STATES[number] = + tableSorts[0]?.key !== sortKey + ? "none" + : tableSorts[0]?.direction || "none"; + return ( { - dragRef(ref); - dropRef(ref); - }} + ref={ref} + {...props} + aria-sort={ + currentSort === "none" + ? "none" + : currentSort === "asc" + ? "ascending" + : "descending" + } container alignItems="center" wrap="nowrap" @@ -100,7 +93,8 @@ export default function DraggableHeaderRenderer({ sx={[ { height: "100%", - "& svg, & button": { display: "block" }, + "& svg, & button": { display: "block", zIndex: 1 }, + border: (theme) => `1px solid ${theme.palette.divider}`, color: "text.secondary", transition: (theme) => @@ -109,29 +103,18 @@ export default function DraggableHeaderRenderer({ }), "&:hover": { color: "text.primary" }, - cursor: "move", + position: "relative", py: 0, pr: 0.5, pl: 1, width: "100%", }, - isDragging - ? { opacity: 0.5 } - : isOver - ? { - backgroundColor: (theme) => - alpha( - theme.palette.primary.main, - theme.palette.action.focusOpacity - ), - color: "primary.main", - } - : {}, + ...spreadSx(props.sx), ]} className="column-header" > - {(column.width as number) > 140 && ( + {width > 140 && ( @@ -149,6 +132,7 @@ export default function DraggableHeaderRenderer({ onClick={() => { navigator.clipboard.writeText(column.key); }} + style={{ position: "relative", zIndex: 2 }} > {column.editable === false ? ( @@ -190,6 +174,8 @@ export default function DraggableHeaderRenderer({ fontWeight: "fontWeightMedium", lineHeight: `${COLUMN_HEADER_HEIGHT}px`, textOverflow: "clip", + position: "relative", + zIndex: 1, }} component="div" color="inherit" @@ -205,15 +191,22 @@ export default function DraggableHeaderRenderer({ - - - + {column.type !== FieldType.id && ( + + + + )} + + {children} ); -} +}); + +export default ColumnHeader; diff --git a/src/components/Table/ColumnHeader/ColumnHeaderSort.tsx b/src/components/Table/ColumnHeader/ColumnHeaderSort.tsx index deab488b..c8c4878f 100644 --- a/src/components/Table/ColumnHeader/ColumnHeaderSort.tsx +++ b/src/components/Table/ColumnHeader/ColumnHeaderSort.tsx @@ -1,4 +1,4 @@ -import { useAtom } from "jotai"; +import { useSetAtom } from "jotai"; import { colord } from "colord"; import { Tooltip, IconButton } from "@mui/material"; @@ -8,27 +8,22 @@ import IconSlash, { } from "@src/components/IconSlash"; import { tableScope, tableSortsAtom } from "@src/atoms/tableScope"; -import { FieldType } from "@src/constants/fields"; -import { getFieldProp } from "@src/components/fields"; -import { ColumnConfig } from "@src/types/table"; - -const SORT_STATES = ["none", "desc", "asc"] as const; +export const SORT_STATES = ["none", "desc", "asc"] as const; export interface IColumnHeaderSortProps { - column: ColumnConfig; + sortKey: string; + currentSort: typeof SORT_STATES[number]; + tabIndex?: number; } -export default function ColumnHeaderSort({ column }: IColumnHeaderSortProps) { - const [tableSorts, setTableSorts] = useAtom(tableSortsAtom, tableScope); +export default function ColumnHeaderSort({ + sortKey, + currentSort, + tabIndex, +}: IColumnHeaderSortProps) { + const setTableSorts = useSetAtom(tableSortsAtom, tableScope); - const _sortKey = getFieldProp("sortKey", (column as any).type); - const sortKey = _sortKey ? `${column.key}.${_sortKey}` : column.key; - - const currentSort: typeof SORT_STATES[number] = - tableSorts[0]?.key !== sortKey - ? "none" - : tableSorts[0]?.direction || "none"; const nextSort = SORT_STATES[SORT_STATES.indexOf(currentSort) + 1] ?? SORT_STATES[0]; @@ -37,8 +32,6 @@ export default function ColumnHeaderSort({ column }: IColumnHeaderSortProps) { else setTableSorts([{ key: sortKey, direction: nextSort }]); }; - if (column.type === FieldType.id) return null; - return ( colord(theme.palette.background.default) .mix( @@ -74,7 +68,8 @@ export default function ColumnHeaderSort({ column }: IColumnHeaderSortProps) { position: "relative", opacity: currentSort !== "none" ? 1 : 0, - ".column-header:hover &": { opacity: 1 }, + "[role='columnheader']:hover &, [role='columnheader']:focus &, [role='columnheader']:focus-within &, &:focus": + { opacity: 1 }, transition: (theme) => theme.transitions.create(["background-color", "opacity"], { @@ -89,7 +84,7 @@ export default function ColumnHeaderSort({ column }: IColumnHeaderSortProps) { transform: currentSort === "asc" ? "rotate(180deg)" : "none", }, - "&:hover .arrow": { + "&:hover .arrow, &:focus .arrow": { transform: currentSort === "asc" || nextSort === "asc" ? "rotate(180deg)" @@ -100,7 +95,7 @@ export default function ColumnHeaderSort({ column }: IColumnHeaderSortProps) { strokeDashoffset: currentSort === "none" ? 0 : ICON_SLASH_STROKE_DASHOFFSET, }, - "&:hover .icon-slash": { + "&:hover .icon-slash, &:focus .icon-slash": { strokeDashoffset: nextSort === "none" ? 0 : ICON_SLASH_STROKE_DASHOFFSET, }, diff --git a/src/components/Table/Styled/StyledResizer.tsx b/src/components/Table/Styled/StyledResizer.tsx index 83e005e0..7f23f1da 100644 --- a/src/components/Table/Styled/StyledResizer.tsx +++ b/src/components/Table/Styled/StyledResizer.tsx @@ -9,6 +9,7 @@ export const StyledResizer = styled("div", { shouldForwardProp: (prop) => prop !== "isResizing", })(({ theme, isResizing }) => ({ position: "absolute", + zIndex: 5, right: 0, top: 0, height: "100%", @@ -39,7 +40,7 @@ export const StyledResizer = styled("div", { height: "50%", width: 4, borderRadius: 2, - marginRight: 3, + marginRight: 2, background: isResizing ? theme.palette.primary.main @@ -51,3 +52,5 @@ export const StyledResizer = styled("div", { }, })); StyledResizer.displayName = "StyledResizer"; + +export default StyledResizer; diff --git a/src/components/Table/Styled/StyledRow.tsx b/src/components/Table/Styled/StyledRow.tsx index da2e11a6..5033152b 100644 --- a/src/components/Table/Styled/StyledRow.tsx +++ b/src/components/Table/Styled/StyledRow.tsx @@ -32,3 +32,5 @@ export const StyledRow = styled("div")(({ theme }) => ({ }, })); StyledRow.displayName = "StyledRow"; + +export default StyledRow; diff --git a/src/components/Table/Styled/StyledTable.tsx b/src/components/Table/Styled/StyledTable.tsx index 0033ee68..23c648e5 100644 --- a/src/components/Table/Styled/StyledTable.tsx +++ b/src/components/Table/Styled/StyledTable.tsx @@ -31,3 +31,5 @@ export const StyledTable = styled("div")(({ theme }) => ({ }, })); StyledTable.displayName = "StyledTable"; + +export default StyledTable; diff --git a/src/components/Table/Table.tsx b/src/components/Table/Table.tsx index 4f2f51c3..fc9a2c8a 100644 --- a/src/components/Table/Table.tsx +++ b/src/components/Table/Table.tsx @@ -1,29 +1,27 @@ import { useMemo, useRef, useState, useEffect, useCallback } from "react"; import { useAtom, useSetAtom } from "jotai"; import { useThrottledCallback } from "use-debounce"; -import { - DragDropContext, - DropResult, - Droppable, - Draggable, -} from "react-beautiful-dnd"; - import { createColumnHelper, flexRender, getCoreRowModel, useReactTable, } from "@tanstack/react-table"; +import { + DragDropContext, + DropResult, + Droppable, + Draggable, +} from "react-beautiful-dnd"; +import { Portal } from "@mui/material"; -import { StyledTable } from "./Styled/StyledTable"; -import { StyledRow } from "./Styled/StyledRow"; -import { StyledResizer } from "./Styled/StyledResizer"; -import ColumnHeaderComponent from "./Column"; +import StyledTable from "./Styled/StyledTable"; +import StyledRow from "./Styled/StyledRow"; +import ColumnHeader from "./ColumnHeader"; +import StyledResizer from "./Styled/StyledResizer"; import OutOfOrderIndicator from "./OutOfOrderIndicator"; +import ContextMenu from "./ContextMenu"; -import { IconButton, Portal } from "@mui/material"; - -import ColumnHeader, { COLUMN_HEADER_HEIGHT } from "./ColumnHeader"; import FinalColumnHeader from "./FinalColumnHeader"; import FinalColumn from "./formatters/FinalColumn"; // import TableRow from "./TableRow"; @@ -31,8 +29,6 @@ import EmptyState from "@src/components/EmptyState"; // import BulkActions from "./BulkActions"; import AddRow from "@src/components/TableToolbar/AddRow"; import { AddRow as AddRowIcon } from "@src/assets/icons"; -import Loading from "@src/components/Loading"; -import ContextMenu from "./ContextMenu"; import { projectScope, @@ -291,6 +287,8 @@ export default function TableComponent() { ref={provided.innerRef} > {headerGroup.headers.map((header) => { + if (!header.column.columnDef.meta) return null; + const isSelectedCell = (!selectedCell && header.index === 0) || (selectedCell?.path === "_rowy_header" && @@ -305,7 +303,7 @@ export default function TableComponent() { disableInteractiveElementBlocking > {(provided, snapshot) => ( -
{header.column.getCanResize() && ( @@ -370,7 +371,7 @@ export default function TableComponent() { onTouchStart={header.getResizeHandler()} /> )} - + )} ); From 19aee53038fd754c4fda34dde61fcc51bb975609 Mon Sep 17 00:00:00 2001 From: Sidney Alcantara Date: Mon, 31 Oct 2022 17:56:54 +1100 Subject: [PATCH 21/66] add final column --- .../Table/ColumnHeader/ColumnHeader.tsx | 2 +- .../Table/ContextMenu/ContextMenuItem.tsx | 27 +- .../Table/ContextMenu/MenuContents.tsx | 310 +++++++++--------- .../FinalColumn.tsx | 62 +++- .../Table/FinalColumn/FinalColumnHeader.tsx | 58 ++++ src/components/Table/FinalColumnHeader.tsx | 29 -- src/components/Table/Styled/StyledRow.tsx | 16 +- src/components/Table/Table.tsx | 33 +- src/components/Table/TableRow.tsx | 32 -- 9 files changed, 315 insertions(+), 254 deletions(-) rename src/components/Table/{formatters => FinalColumn}/FinalColumn.tsx (67%) create mode 100644 src/components/Table/FinalColumn/FinalColumnHeader.tsx delete mode 100644 src/components/Table/FinalColumnHeader.tsx delete mode 100644 src/components/Table/TableRow.tsx diff --git a/src/components/Table/ColumnHeader/ColumnHeader.tsx b/src/components/Table/ColumnHeader/ColumnHeader.tsx index 7ad33e72..1a01c9dd 100644 --- a/src/components/Table/ColumnHeader/ColumnHeader.tsx +++ b/src/components/Table/ColumnHeader/ColumnHeader.tsx @@ -96,6 +96,7 @@ export const ColumnHeader = forwardRef(function ColumnHeader( "& svg, & button": { display: "block", zIndex: 1 }, border: (theme) => `1px solid ${theme.palette.divider}`, + backgroundColor: "background.default", color: "text.secondary", transition: (theme) => theme.transitions.create("color", { @@ -112,7 +113,6 @@ export const ColumnHeader = forwardRef(function ColumnHeader( }, ...spreadSx(props.sx), ]} - className="column-header" > {width > 140 && ( { disabled?: boolean; hotkeyLabel?: string; divider?: boolean; + subItems?: IContextMenuItem[]; } export interface IContextMenuItemProps extends IContextMenuItem { @@ -82,16 +83,20 @@ export default function ContextMenuItem({ ); } else { - return ( - - {icon} - {label} - {hotkeyLabel && ( - - {hotkeyLabel} - - )} - - ); + if (props.divider) { + return ; + } else { + return ( + + {icon} + {label} + {hotkeyLabel && ( + + {hotkeyLabel} + + )} + + ); + } } } diff --git a/src/components/Table/ContextMenu/MenuContents.tsx b/src/components/Table/ContextMenu/MenuContents.tsx index 60d58069..c098ebe2 100644 --- a/src/components/Table/ContextMenu/MenuContents.tsx +++ b/src/components/Table/ContextMenu/MenuContents.tsx @@ -62,182 +62,188 @@ export default function MenuContents({ onClose }: IMenuContentsProps) { if (!tableSchema.columns || !selectedCell) return null; const selectedColumn = tableSchema.columns[selectedCell.columnKey]; - const menuActions = getFieldProp("contextMenuActions", selectedColumn.type); + const row = find(tableRows, ["_rowy_ref.path", selectedCell.path]); + + if (!row) return null; const actionGroups: IContextMenuItem[][] = []; - const row = find(tableRows, ["_rowy_ref.path", selectedCell.path]); - // Field type actions - const fieldTypeActions = menuActions - ? menuActions(selectedCell, onClose) - : []; - if (fieldTypeActions.length > 0) actionGroups.push(fieldTypeActions); - if (selectedColumn.type === FieldType.derivative) { - const renderedFieldMenuActions = getFieldProp( - "contextMenuActions", - selectedColumn.config?.renderFieldType - ); - if (renderedFieldMenuActions) { - actionGroups.push(renderedFieldMenuActions(selectedCell, onClose)); - } - } - - // Cell actions - // TODO: Add copy and paste here - const cellValue = row?.[selectedCell.columnKey]; - const handleClearValue = () => - updateField({ - path: selectedCell.path, - fieldName: selectedColumn.fieldName, - value: null, - deleteField: true, + const handleDuplicate = () => { + addRow({ + row, + setId: addRowIdType === "custom" ? "decrement" : addRowIdType, }); - const columnFilters = getFieldProp("filter", selectedColumn.type); - const handleFilterValue = () => { - openTableFiltersPopover({ - defaultQuery: { - key: selectedColumn.fieldName, - operator: columnFilters!.operators[0]?.value || "==", - value: cellValue, - }, - }); - onClose(); }; - const cellActions = [ + const handleDelete = () => deleteRow(row._rowy_ref.path); + const rowActions: IContextMenuItem[] = [ { - label: altPress ? "Clear value" : "Clear value…", - color: "error", - icon: , + label: "Copy ID", + icon: , + onClick: () => { + navigator.clipboard.writeText(row._rowy_ref.id); + onClose(); + }, + }, + { + label: "Copy path", + icon: , + onClick: () => { + navigator.clipboard.writeText(row._rowy_ref.path); + onClose(); + }, + }, + { + label: "Open in Firebase Console", + icon: , + onClick: () => { + window.open( + `https://console.firebase.google.com/project/${projectId}/firestore/data/~2F${row._rowy_ref.path.replace( + /\//g, + "~2F" + )}` + ); + onClose(); + }, + }, + { label: "Divider", divider: true }, + { + label: "Duplicate", + icon: , disabled: - selectedColumn.editable === false || - !row || - cellValue || - getFieldProp("group", selectedColumn.type) === "Auditing", + tableSettings.tableType === "collectionGroup" || + (!userRoles.includes("ADMIN") && tableSettings.readOnly), onClick: altPress - ? handleClearValue + ? handleDuplicate : () => { confirm({ - title: "Clear cell value?", - body: "The cell’s value cannot be recovered after", - confirm: "Delete", - confirmColor: "error", - handleConfirm: handleClearValue, + title: "Duplicate row?", + body: ( + <> + Row path: +
+ + {row._rowy_ref.path} + + + ), + confirm: "Duplicate", + handleConfirm: handleDuplicate, }); onClose(); }, }, { - label: "Filter value", - icon: , - disabled: !columnFilters || cellValue === undefined, - onClick: handleFilterValue, + label: altPress ? "Delete" : "Delete…", + color: "error", + icon: , + disabled: !userRoles.includes("ADMIN") && tableSettings.readOnly, + onClick: altPress + ? handleDelete + : () => { + confirm({ + title: "Delete row?", + body: ( + <> + Row path: +
+ + {row._rowy_ref.path} + + + ), + confirm: "Delete", + confirmColor: "error", + handleConfirm: handleDelete, + }); + onClose(); + }, }, ]; - actionGroups.push(cellActions); - // Row actions - if (row) { - const handleDuplicate = () => { - addRow({ - row, - setId: addRowIdType === "custom" ? "decrement" : addRowIdType, + if (selectedColumn) { + const menuActions = getFieldProp( + "contextMenuActions", + selectedColumn?.type + ); + + // Field type actions + const fieldTypeActions = menuActions + ? menuActions(selectedCell, onClose) + : []; + if (fieldTypeActions.length > 0) actionGroups.push(fieldTypeActions); + + if (selectedColumn?.type === FieldType.derivative) { + const renderedFieldMenuActions = getFieldProp( + "contextMenuActions", + selectedColumn.config?.renderFieldType + ); + if (renderedFieldMenuActions) { + actionGroups.push(renderedFieldMenuActions(selectedCell, onClose)); + } + } + + // Cell actions + // TODO: Add copy and paste here + const cellValue = row?.[selectedCell.columnKey]; + const handleClearValue = () => + updateField({ + path: selectedCell.path, + fieldName: selectedColumn.fieldName, + value: null, + deleteField: true, }); + const columnFilters = getFieldProp("filter", selectedColumn?.type); + const handleFilterValue = () => { + openTableFiltersPopover({ + defaultQuery: { + key: selectedColumn.fieldName, + operator: columnFilters!.operators[0]?.value || "==", + value: cellValue, + }, + }); + onClose(); }; - const handleDelete = () => deleteRow(row._rowy_ref.path); - const rowActions = [ + const cellActions = [ + { + label: altPress ? "Clear value" : "Clear value…", + color: "error", + icon: , + disabled: + selectedColumn?.editable === false || + !row || + cellValue || + getFieldProp("group", selectedColumn?.type) === "Auditing", + onClick: altPress + ? handleClearValue + : () => { + confirm({ + title: "Clear cell value?", + body: "The cell’s value cannot be recovered after", + confirm: "Delete", + confirmColor: "error", + handleConfirm: handleClearValue, + }); + onClose(); + }, + }, + { + label: "Filter value", + icon: , + disabled: !columnFilters || cellValue === undefined, + onClick: handleFilterValue, + }, + ]; + actionGroups.push(cellActions); + + // Row actions as sub-menu + actionGroups.push([ { label: "Row", icon: , - subItems: [ - { - label: "Copy ID", - icon: , - onClick: () => { - navigator.clipboard.writeText(row._rowy_ref.id); - onClose(); - }, - }, - { - label: "Copy path", - icon: , - onClick: () => { - navigator.clipboard.writeText(row._rowy_ref.path); - onClose(); - }, - }, - { - label: "Open in Firebase Console", - icon: , - onClick: () => { - window.open( - `https://console.firebase.google.com/project/${projectId}/firestore/data/~2F${row._rowy_ref.path.replace( - /\//g, - "~2F" - )}` - ); - onClose(); - }, - }, - { label: "Divider", divider: true }, - { - label: "Duplicate", - icon: , - disabled: - tableSettings.tableType === "collectionGroup" || - (!userRoles.includes("ADMIN") && tableSettings.readOnly), - onClick: altPress - ? handleDuplicate - : () => { - confirm({ - title: "Duplicate row?", - body: ( - <> - Row path: -
- - {row._rowy_ref.path} - - - ), - confirm: "Duplicate", - handleConfirm: handleDuplicate, - }); - onClose(); - }, - }, - { - label: altPress ? "Delete" : "Delete…", - color: "error", - icon: , - disabled: !userRoles.includes("ADMIN") && tableSettings.readOnly, - onClick: altPress - ? handleDelete - : () => { - confirm({ - title: "Delete row?", - body: ( - <> - Row path: -
- - {row._rowy_ref.path} - - - ), - confirm: "Delete", - confirmColor: "error", - handleConfirm: handleDelete, - }); - onClose(); - }, - }, - ], + subItems: rowActions, }, - ]; + ]); + } else { actionGroups.push(rowActions); } diff --git a/src/components/Table/formatters/FinalColumn.tsx b/src/components/Table/FinalColumn/FinalColumn.tsx similarity index 67% rename from src/components/Table/formatters/FinalColumn.tsx rename to src/components/Table/FinalColumn/FinalColumn.tsx index 84dd4563..0d5212ff 100644 --- a/src/components/Table/formatters/FinalColumn.tsx +++ b/src/components/Table/FinalColumn/FinalColumn.tsx @@ -1,9 +1,10 @@ import { useAtom, useSetAtom } from "jotai"; -import type { FormatterProps } from "react-data-grid"; +import type { CellContext } from "@tanstack/table-core"; import { Stack, Tooltip, IconButton, alpha } from "@mui/material"; import { CopyCells as CopyCellsIcon } from "@src/assets/icons"; import DeleteIcon from "@mui/icons-material/DeleteOutlined"; +import MenuIcon from "@mui/icons-material/MoreHoriz"; import { projectScope, @@ -17,10 +18,14 @@ import { tableSettingsAtom, addRowAtom, deleteRowAtom, + contextMenuTargetAtom, } from "@src/atoms/tableScope"; import { TableRow } from "@src/types/table"; -export default function FinalColumn({ row }: FormatterProps) { +export default function FinalColumn({ + row, + focusInsideCell, +}: CellContext & { focusInsideCell: boolean }) { const [userRoles] = useAtom(userRolesAtom, projectScope); const [addRowIdType] = useAtom(tableAddRowIdTypeAtom, projectScope); const confirm = useSetAtom(confirmDialogAtom, projectScope); @@ -28,12 +33,13 @@ export default function FinalColumn({ row }: FormatterProps) { const [tableSettings] = useAtom(tableSettingsAtom, tableScope); const addRow = useSetAtom(addRowAtom, tableScope); const deleteRow = useSetAtom(deleteRowAtom, tableScope); + const setContextMenuTarget = useSetAtom(contextMenuTargetAtom, tableScope); const [altPress] = useAtom(altPressAtom, projectScope); - const handleDelete = () => deleteRow(row._rowy_ref.path); + const handleDelete = () => deleteRow(row.original._rowy_ref.path); const handleDuplicate = () => { addRow({ - row, + row: row.original, setId: addRowIdType === "custom" ? "decrement" : addRowIdType, }); }; @@ -42,7 +48,26 @@ export default function FinalColumn({ row }: FormatterProps) { return null; return ( - + + + { + setContextMenuTarget(e.target as HTMLElement); + }} + className="row-hover-iconButton" + tabIndex={focusInsideCell ? 0 : -1} + > + + + + ) { - {row._rowy_ref.path} + {row.original._rowy_ref.path} ), @@ -70,8 +95,8 @@ export default function FinalColumn({ row }: FormatterProps) { }); } } - aria-label="Duplicate row" className="row-hover-iconButton" + tabIndex={focusInsideCell ? 0 : -1} > @@ -94,7 +119,7 @@ export default function FinalColumn({ row }: FormatterProps) { - {row._rowy_ref.path} + {row.original._rowy_ref.path} ), @@ -104,19 +129,20 @@ export default function FinalColumn({ row }: FormatterProps) { }); } } - aria-label={`Delete row${altPress ? "" : "…"}`} className="row-hover-iconButton" + tabIndex={focusInsideCell ? 0 : -1} sx={{ - ".rdg-row:hover .row-hover-iconButton&&": { - color: "error.main", - backgroundColor: (theme) => - alpha( - theme.palette.error.main, - theme.palette.action.hoverOpacity * 2 - ), - }, + "[role='row']:hover .row-hover-iconButton&&, .row-hover-iconButton&&:focus": + { + color: "error.main", + backgroundColor: (theme) => + alpha( + theme.palette.error.main, + theme.palette.action.hoverOpacity * 2 + ), + }, }} - disabled={!row._rowy_ref.path} + disabled={!row.original._rowy_ref.path} > diff --git a/src/components/Table/FinalColumn/FinalColumnHeader.tsx b/src/components/Table/FinalColumn/FinalColumnHeader.tsx new file mode 100644 index 00000000..0a949885 --- /dev/null +++ b/src/components/Table/FinalColumn/FinalColumnHeader.tsx @@ -0,0 +1,58 @@ +import { useAtom, useSetAtom } from "jotai"; + +import { Box, BoxProps, Button } from "@mui/material"; +import { AddColumn as AddColumnIcon } from "@src/assets/icons"; + +import { projectScope, userRolesAtom } from "@src/atoms/projectScope"; +import { tableScope, columnModalAtom } from "@src/atoms/tableScope"; +import { spreadSx } from "@src/utils/ui"; + +export interface IFinalColumnHeaderProps extends Partial { + focusInsideCell: boolean; +} + +export default function FinalColumnHeader({ + focusInsideCell, + ...props +}: IFinalColumnHeaderProps) { + const [userRoles] = useAtom(userRolesAtom, projectScope); + const openColumnModal = useSetAtom(columnModalAtom, tableScope); + + if (!userRoles.includes("ADMIN")) return null; + + return ( + `1px solid ${theme.palette.divider}`, + borderLeft: "none", + borderTopRightRadius: (theme) => theme.shape.borderRadius, + borderBottomRightRadius: (theme) => theme.shape.borderRadius, + + display: "flex", + alignItems: "center", + + width: 32 * 3 + 4 * 2 + 10 * 2, + overflow: "visible", + px: 1, + }, + ...spreadSx(props.sx), + ]} + className="column-header" + > + + + ); +} diff --git a/src/components/Table/FinalColumnHeader.tsx b/src/components/Table/FinalColumnHeader.tsx deleted file mode 100644 index b425d1b1..00000000 --- a/src/components/Table/FinalColumnHeader.tsx +++ /dev/null @@ -1,29 +0,0 @@ -import { useAtom, useSetAtom } from "jotai"; -import { Column } from "react-data-grid"; - -import { Button } from "@mui/material"; -import { AddColumn as AddColumnIcon } from "@src/assets/icons"; - -import { projectScope, userRolesAtom } from "@src/atoms/projectScope"; -import { tableScope, columnModalAtom } from "@src/atoms/tableScope"; - -const FinalColumnHeader: Column["headerRenderer"] = ({ column }) => { - const [userRoles] = useAtom(userRolesAtom, projectScope); - const openColumnModal = useSetAtom(columnModalAtom, tableScope); - - if (!userRoles.includes("ADMIN")) return null; - - return ( - - ); -}; - -export default FinalColumnHeader; diff --git a/src/components/Table/Styled/StyledRow.tsx b/src/components/Table/Styled/StyledRow.tsx index 5033152b..cbaed8c2 100644 --- a/src/components/Table/Styled/StyledRow.tsx +++ b/src/components/Table/Styled/StyledRow.tsx @@ -1,4 +1,4 @@ -import { styled } from "@mui/material"; +import { styled, alpha } from "@mui/material"; import { DEFAULT_ROW_HEIGHT } from "@src/components/Table"; @@ -30,6 +30,20 @@ export const StyledRow = styled("div")(({ theme }) => ({ borderBottomRightRadius: theme.shape.borderRadius, }, }, + + "& .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 + ), + }, })); StyledRow.displayName = "StyledRow"; diff --git a/src/components/Table/Table.tsx b/src/components/Table/Table.tsx index fc9a2c8a..904d3d3b 100644 --- a/src/components/Table/Table.tsx +++ b/src/components/Table/Table.tsx @@ -19,12 +19,11 @@ import StyledTable from "./Styled/StyledTable"; import StyledRow from "./Styled/StyledRow"; import ColumnHeader from "./ColumnHeader"; import StyledResizer from "./Styled/StyledResizer"; +import FinalColumnHeader from "./FinalColumn/FinalColumnHeader"; +import FinalColumn from "./FinalColumn/FinalColumn"; import OutOfOrderIndicator from "./OutOfOrderIndicator"; import ContextMenu from "./ContextMenu"; -import FinalColumnHeader from "./FinalColumnHeader"; -import FinalColumn from "./formatters/FinalColumn"; -// import TableRow from "./TableRow"; import EmptyState from "@src/components/EmptyState"; // import BulkActions from "./BulkActions"; import AddRow from "@src/components/TableToolbar/AddRow"; @@ -133,7 +132,7 @@ export default function TableComponent() { _columns.push( columnHelper.display({ id: "_rowy_column_actions", - header: () => "Actions", + cell: FinalColumn as any, // cell: () => ( // <> // M @@ -287,13 +286,27 @@ export default function TableComponent() { ref={provided.innerRef} > {headerGroup.headers.map((header) => { - if (!header.column.columnDef.meta) return null; - const isSelectedCell = (!selectedCell && header.index === 0) || (selectedCell?.path === "_rowy_header" && selectedCell?.columnKey === header.id); + if (header.id === "_rowy_column_actions") + return ( + + ); + + if (!header.column.columnDef.meta) return null; + return ( - {flexRender( - cell.column.columnDef.cell, - cell.getContext() - )} + {flexRender(cell.column.columnDef.cell, { + ...cell.getContext(), + focusInsideCell: isSelectedCell && focusInsideCell, + })}
{/*
+
+
+ + + } + style={{ position: "absolute", inset: 0 }} + /> + } + /> From e436e2083a6767525fa82f7b839a3ddfb538f2ed Mon Sep 17 00:00:00 2001 From: Sidney Alcantara Date: Wed, 2 Nov 2022 16:36:10 +1100 Subject: [PATCH 27/66] re-enable table cells --- src/components/Table/CellValidation.tsx | 42 +++--------- .../Table/FinalColumn/FinalColumn.tsx | 8 +-- src/components/Table/Styled/StyledCell.tsx | 8 ++- src/components/Table/Table.tsx | 53 ++++++++------- .../fields/ConnectTable/InlineCell.tsx | 4 +- .../fields/_withTableCell/withBasicCell.tsx | 32 +++------ .../fields/_withTableCell/withHeavyCell.tsx | 61 +++++------------ .../fields/_withTableCell/withPopoverCell.tsx | 67 +++++-------------- src/components/fields/types.ts | 10 +-- src/types/table.d.ts | 16 +++-- 10 files changed, 107 insertions(+), 194 deletions(-) diff --git a/src/components/Table/CellValidation.tsx b/src/components/Table/CellValidation.tsx index ac6a9764..ad7560dc 100644 --- a/src/components/Table/CellValidation.tsx +++ b/src/components/Table/CellValidation.tsx @@ -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 ( - <> + } title="Invalid data" @@ -74,14 +54,13 @@ export default function CellValidation({ placement="right" render={({ openTooltip }) => } /> - - {children} - + {children} + ); if (isMissing) return ( - <> + } title="Required field" @@ -89,10 +68,9 @@ export default function CellValidation({ placement="right" render={({ openTooltip }) => } /> - - {children} - + {children} + ); - return {children}; + return {children}; } diff --git a/src/components/Table/FinalColumn/FinalColumn.tsx b/src/components/Table/FinalColumn/FinalColumn.tsx index 0d5212ff..c9d23dca 100644 --- a/src/components/Table/FinalColumn/FinalColumn.tsx +++ b/src/components/Table/FinalColumn/FinalColumn.tsx @@ -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 & { focusInsideCell: boolean }) { +export default function FinalColumn({ row, focusInsideCell }: TableCellProps) { const [userRoles] = useAtom(userRolesAtom, projectScope); const [addRowIdType] = useAtom(tableAddRowIdTypeAtom, projectScope); const confirm = useSetAtom(confirmDialogAtom, projectScope); diff --git a/src/components/Table/Styled/StyledCell.tsx b/src/components/Table/Styled/StyledCell.tsx index 58de99fd..af2e03b4 100644 --- a/src/components/Table/Styled/StyledCell.tsx +++ b/src/components/Table/Styled/StyledCell.tsx @@ -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; diff --git a/src/components/Table/Table.tsx b/src/components/Table/Table.tsx index 614067b7..6f219236 100644 --- a/src/components/Table/Table.tsx +++ b/src/components/Table/Table.tsx @@ -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 & { + focusInsideCell: boolean; +}; + declare module "@tanstack/table-core" { interface ColumnMeta 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 ( -
- {flexRender(cell.column.columnDef.cell, { - ...cell.getContext(), - focusInsideCell: isSelectedCell && focusInsideCell, - })} + + {flexRender(cell.column.columnDef.cell, { + ...cell.getContext(), + focusInsideCell: isSelectedCell && focusInsideCell, + })} +
- {/* */} -
+ ); })} diff --git a/src/components/fields/ConnectTable/InlineCell.tsx b/src/components/fields/ConnectTable/InlineCell.tsx index 72d886ad..3f8f3147 100644 --- a/src/components/fields/ConnectTable/InlineCell.tsx +++ b/src/components/fields/ConnectTable/InlineCell.tsx @@ -32,7 +32,7 @@ export const ConnectTable = forwardRef(function ConnectTable( value.map((item: any) => ( item.snapshot[key]) .join(" ")} /> @@ -41,7 +41,7 @@ export const ConnectTable = forwardRef(function ConnectTable( ) : value ? ( value.snapshot[key]) .join(" ")} /> diff --git a/src/components/fields/_withTableCell/withBasicCell.tsx b/src/components/fields/_withTableCell/withBasicCell.tsx index 09d0bcb9..a268bf2e 100644 --- a/src/components/fields/_withTableCell/withBasicCell.tsx +++ b/src/components/fields/_withTableCell/withBasicCell.tsx @@ -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 ) { - return function BasicCell(props: FormatterProps) { - const { name, key } = props.column; - const value = get(props.row, key); - - const { validationRegex, required } = (props.column as any).config; + return function BasicCell({ row, column, getValue }: TableCellProps) { + const columnConfig = column.columnDef.meta!; + const { name } = columnConfig; + const value = getValue(); return ( - - - - - + ); }; } diff --git a/src/components/fields/_withTableCell/withHeavyCell.tsx b/src/components/fields/_withTableCell/withHeavyCell.tsx index 29eaf370..394a497c 100644 --- a/src/components/fields/_withTableCell/withHeavyCell.tsx +++ b/src/components/fields/_withTableCell/withHeavyCell.tsx @@ -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, readOnly: boolean = false ) { - return function HeavyCell(props: FormatterProps) { + 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 = ; - if (displayedComponent === "basic") - return ( - - - {basicCell} - - - ); + 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 ( - - - - - - - + + + ); // Should not reach this line diff --git a/src/components/fields/_withTableCell/withPopoverCell.tsx b/src/components/fields/_withTableCell/withPopoverCell.tsx index 5da116ca..5cb12ac5 100644 --- a/src/components/fields/_withTableCell/withPopoverCell.tsx +++ b/src/components/fields/_withTableCell/withPopoverCell.tsx @@ -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, options?: IPopoverCellOptions ) { - return function PopoverCell(props: FormatterProps) { + 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(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 ( - - - - - - ); + return ; // This is where we update the documents const handleSubmit = (value: any) => { if (options?.readOnly) return; updateField({ - path: props.row._rowy_ref.path, - fieldName: props.column.key, + 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( ); - if (displayedComponent === "inline") - return ( - - - {inlineCell} - - - ); + if (displayedComponent === "inline") return inlineCell; const parentRef = inlineCellRef.current?.parentElement; if (displayedComponent === "popover") return ( - - - {inlineCell} - + <> + {inlineCell} - + ); // Should not reach this line diff --git a/src/components/fields/types.ts b/src/components/fields/types.ts index 66890472..c7309452 100644 --- a/src/components/fields/types.ts +++ b/src/components/fields/types.ts @@ -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>; + TableCell: React.ComponentType; TableEditor: React.ComponentType>; SideDrawerField: React.ComponentType; settings?: React.ComponentType; @@ -49,10 +50,9 @@ export interface IBasicCellProps { type: FieldType; name: string; } -export interface IHeavyCellProps - extends IBasicCellProps, - FormatterProps { - column: FormatterProps["column"] & { config?: Record }; +export interface IHeavyCellProps extends IBasicCellProps { + row: TableRow; + column: ColumnConfig; onSubmit: (value: any) => void; docRef: TableRowRef; disabled: boolean; diff --git a/src/types/table.d.ts b/src/types/table.d.ts index 376cd3ea..5b54b9be 100644 --- a/src/types/table.d.ts +++ b/src/types/table.d.ts @@ -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 = { From 796c980337084a29c98916276fde0bf2c123df7a Mon Sep 17 00:00:00 2001 From: Sidney Alcantara Date: Tue, 8 Nov 2022 16:18:33 +1100 Subject: [PATCH 28/66] unify withTableCell HOCs into single HOC with memo --- src/atoms/tableScope/ui.ts | 1 + src/components/SideDrawer/SideDrawer.tsx | 6 +- src/components/Table/CellValidation.tsx | 9 +- .../Table/FinalColumn/FinalColumn.tsx | 11 +- src/components/Table/Styled/StyledCell.tsx | 20 +- src/components/Table/Styled/StyledRow.tsx | 34 +-- src/components/Table/Styled/StyledTable.tsx | 13 ++ src/components/Table/Table.tsx | 75 ++++--- src/components/Table/TableContainer.tsx | 2 +- src/components/Table/editors/TextEditor.tsx | 2 +- .../Table/useKeyboardNavigation.tsx | 13 +- src/components/Table/useVirtualization.tsx | 4 +- src/components/Table/withTableCell.tsx | 198 ++++++++++++++++++ src/components/TableToolbar/RowHeight.tsx | 4 +- .../fields/Checkbox/DisplayCell.tsx | 30 +++ .../{TableCell.tsx => EditorCell.tsx} | 6 +- src/components/fields/Checkbox/index.tsx | 14 +- .../{InlineCell.tsx => DisplayCell.tsx} | 39 ++-- .../fields/MultiSelect/PopoverCell.tsx | 4 +- src/components/fields/MultiSelect/index.tsx | 10 +- .../fields/Reference/EditorCell.tsx | 2 +- .../fields/ShortText/EditorCell.tsx | 61 ++++++ src/components/fields/ShortText/index.tsx | 7 +- .../fields/_BasicCell/BasicCellNull.tsx | 4 +- .../fields/_withTableCell/withHeavyCell.tsx | 7 +- .../fields/_withTableCell/withPopoverCell.tsx | 12 +- src/components/fields/types.ts | 25 ++- src/hooks/useSaveOnUnmount.ts | 19 ++ src/index.tsx | 10 +- src/pages/Table/TablePage.tsx | 7 +- 30 files changed, 509 insertions(+), 140 deletions(-) create mode 100644 src/components/Table/withTableCell.tsx create mode 100644 src/components/fields/Checkbox/DisplayCell.tsx rename src/components/fields/Checkbox/{TableCell.tsx => EditorCell.tsx} (92%) rename src/components/fields/MultiSelect/{InlineCell.tsx => DisplayCell.tsx} (50%) create mode 100644 src/components/fields/ShortText/EditorCell.tsx create mode 100644 src/hooks/useSaveOnUnmount.ts diff --git a/src/atoms/tableScope/ui.ts b/src/atoms/tableScope/ui.ts index f0083eda..3d11a081 100644 --- a/src/atoms/tableScope/ui.ts +++ b/src/atoms/tableScope/ui.ts @@ -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(null); diff --git a/src/components/SideDrawer/SideDrawer.tsx b/src/components/SideDrawer/SideDrawer.tsx index 54b82de0..04f896bd 100644 --- a/src/components/SideDrawer/SideDrawer.tsx +++ b/src/components/SideDrawer/SideDrawer.tsx @@ -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( diff --git a/src/components/Table/CellValidation.tsx b/src/components/Table/CellValidation.tsx index ad7560dc..02284cb3 100644 --- a/src/components/Table/CellValidation.tsx +++ b/src/components/Table/CellValidation.tsx @@ -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 {children}; -} +}); + +export default CellValidation; diff --git a/src/components/Table/FinalColumn/FinalColumn.tsx b/src/components/Table/FinalColumn/FinalColumn.tsx index c9d23dca..4b7c8bd0 100644 --- a/src/components/Table/FinalColumn/FinalColumn.tsx +++ b/src/components/Table/FinalColumn/FinalColumn.tsx @@ -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) { @@ -145,4 +149,5 @@ export default function FinalColumn({ row, focusInsideCell }: TableCellProps) { ); -} +}); +export default FinalColumn; diff --git a/src/components/Table/Styled/StyledCell.tsx b/src/components/Table/Styled/StyledCell.tsx index af2e03b4..23ffb2ef 100644 --- a/src/components/Table/Styled/StyledCell.tsx +++ b/src/components/Table/Styled/StyledCell.tsx @@ -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'] &": { diff --git a/src/components/Table/Styled/StyledRow.tsx b/src/components/Table/Styled/StyledRow.tsx index cbaed8c2..49c3408f 100644 --- a/src/components/Table/Styled/StyledRow.tsx +++ b/src/components/Table/Styled/StyledRow.tsx @@ -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"; diff --git a/src/components/Table/Styled/StyledTable.tsx b/src/components/Table/Styled/StyledTable.tsx index 23c648e5..904520ba 100644 --- a/src/components/Table/Styled/StyledTable.tsx +++ b/src/components/Table/Styled/StyledTable.tsx @@ -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", diff --git a/src/components/Table/Table.tsx b/src/components/Table/Table.tsx index 6f219236..a3d60147 100644 --- a/src/components/Table/Table.tsx +++ b/src/components/Table/Table.tsx @@ -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 & { 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({ { 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 } > -
- - {flexRender(cell.column.columnDef.cell, { - ...cell.getContext(), - focusInsideCell: isSelectedCell && focusInsideCell, - })} - -
+ + {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, + })} + ); })} diff --git a/src/components/Table/TableContainer.tsx b/src/components/Table/TableContainer.tsx index eb372f3e..10061298 100644 --- a/src/components/Table/TableContainer.tsx +++ b/src/components/Table/TableContainer.tsx @@ -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) diff --git a/src/components/Table/editors/TextEditor.tsx b/src/components/Table/editors/TextEditor.tsx index 2652c330..58229457 100644 --- a/src/components/Table/editors/TextEditor.tsx +++ b/src/components/Table/editors/TextEditor.tsx @@ -81,7 +81,7 @@ export default function TextEditor({ row, column }: EditorProps) { sx={{ width: "100%", height: "100%", - backgroundColor: "var(--background-color)", + backgroundColor: "var(--cell-background-color)", "& .MuiInputBase-root": { height: "100%", diff --git a/src/components/Table/useKeyboardNavigation.tsx b/src/components/Table/useKeyboardNavigation.tsx index cd2332c6..57c8829c 100644 --- a/src/components/Table/useKeyboardNavigation.tsx +++ b/src/components/Table/useKeyboardNavigation.tsx @@ -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) => { // 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; diff --git a/src/components/Table/useVirtualization.tsx b/src/components/Table/useVirtualization.tsx index 66b226ea..b9b3ca55 100644 --- a/src/components/Table/useVirtualization.tsx +++ b/src/components/Table/useVirtualization.tsx @@ -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( diff --git a/src/components/Table/withTableCell.tsx b/src/components/Table/withTableCell.tsx new file mode 100644 index 00000000..3021fe40 --- /dev/null +++ b/src/components/Table/withTableCell.tsx @@ -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; +} + +/** + * 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, + EditorCellComponent: React.ComponentType, + 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(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 = ( +
+ +
+ ); + 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 = ( + + ); + + if (editorMode === "focus" && focusInsideCell) { + return editorCell; + } + + if (editorMode === "inline") { + return ( +
+ {editorCell} +
+ ); + } + + if (editorMode === "popover") + return ( + <> + {displayCell} + + + 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} + + + + ); + + // 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; + } + ); +} diff --git a/src/components/TableToolbar/RowHeight.tsx b/src/components/TableToolbar/RowHeight.tsx index 59e8523b..88f56944 100644 --- a/src/components/TableToolbar/RowHeight.tsx +++ b/src/components/TableToolbar/RowHeight.tsx @@ -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() { Row height {ROW_HEIGHTS.map((height) => ( - {height}px + {height - 1}px ))} diff --git a/src/components/fields/Checkbox/DisplayCell.tsx b/src/components/fields/Checkbox/DisplayCell.tsx new file mode 100644 index 00000000..67943586 --- /dev/null +++ b/src/components/fields/Checkbox/DisplayCell.tsx @@ -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 ( + + } + 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 }, + }} + /> + ); +} diff --git a/src/components/fields/Checkbox/TableCell.tsx b/src/components/fields/Checkbox/EditorCell.tsx similarity index 92% rename from src/components/fields/Checkbox/TableCell.tsx rename to src/components/fields/Checkbox/EditorCell.tsx index 5cef0a3b..4785700c 100644 --- a/src/components/fields/Checkbox/TableCell.tsx +++ b/src/components/fields/Checkbox/EditorCell.tsx @@ -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} diff --git a/src/components/fields/Checkbox/index.tsx b/src/components/fields/Checkbox/index.tsx index 848a52c0..291d074c 100644 --- a/src/components/fields/Checkbox/index.tsx +++ b/src/components/fields/Checkbox/index.tsx @@ -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: , 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; diff --git a/src/components/fields/MultiSelect/InlineCell.tsx b/src/components/fields/MultiSelect/DisplayCell.tsx similarity index 50% rename from src/components/fields/MultiSelect/InlineCell.tsx rename to src/components/fields/MultiSelect/DisplayCell.tsx index 8f27323c..94a07938 100644 --- a/src/components/fields/MultiSelect/InlineCell.tsx +++ b/src/components/fields/MultiSelect/DisplayCell.tsx @@ -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 -) { - if (typeof value === "string" && value !== "") - return ; +export default function MultiSelect({ + value, + showPopoverCell, + disabled, +}: IDisplayCellProps) { + // if (typeof value === "string" && value !== "") + // return ; return ( 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( )} - {!disabled && ( - - )} + {!disabled && } ); -}); - -export default MultiSelect; +} diff --git a/src/components/fields/MultiSelect/PopoverCell.tsx b/src/components/fields/MultiSelect/PopoverCell.tsx index 50c0ad05..0f35e52d 100644 --- a/src/components/fields/MultiSelect/PopoverCell.tsx +++ b/src/components/fields/MultiSelect/PopoverCell.tsx @@ -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 ( diff --git a/src/components/fields/MultiSelect/index.tsx b/src/components/fields/MultiSelect/index.tsx index b6f3a853..d5a18ed5 100644 --- a/src/components/fields/MultiSelect/index.tsx +++ b/src/components/fields/MultiSelect/index.tsx @@ -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: , 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, diff --git a/src/components/fields/Reference/EditorCell.tsx b/src/components/fields/Reference/EditorCell.tsx index 12c5e6b2..8b96b8b3 100644 --- a/src/components/fields/Reference/EditorCell.tsx +++ b/src/components/fields/Reference/EditorCell.tsx @@ -61,7 +61,7 @@ export default function TextEditor({ row, column }: EditorProps) { sx={{ width: "100%", height: "100%", - backgroundColor: "var(--background-color)", + backgroundColor: "var(--cell-background-color)", "& .MuiInputBase-root": { height: "100%", diff --git a/src/components/fields/ShortText/EditorCell.tsx b/src/components/fields/ShortText/EditorCell.tsx new file mode 100644 index 00000000..418ce4dd --- /dev/null +++ b/src/components/fields/ShortText/EditorCell.tsx @@ -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) { + const [localValue, setLocalValue] = useSaveOnUnmount(value, onSubmit); + const maxLength = column.config?.maxLength; + + return ( + 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()} + /> + ); +} diff --git a/src/components/fields/ShortText/index.tsx b/src/components/fields/ShortText/index.tsx index 88ef7a20..7bd36ddc 100644 --- a/src/components/fields/ShortText/index.tsx +++ b/src/components/fields/ShortText/index.tsx @@ -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: , description: "Text displayed on a single line.", contextMenuActions: BasicContextMenuActions, - TableCell: withBasicCell(BasicCell), - TableEditor: TextEditor, + TableCell: withTableCell(BasicCell, EditorCell), SideDrawerField, settings: Settings, filter: { diff --git a/src/components/fields/_BasicCell/BasicCellNull.tsx b/src/components/fields/_BasicCell/BasicCellNull.tsx index 43d91ccb..6b8471b3 100644 --- a/src/components/fields/_BasicCell/BasicCellNull.tsx +++ b/src/components/fields/_BasicCell/BasicCellNull.tsx @@ -1,3 +1,3 @@ -export default function BasicCellNull() { - return null; +export default function BasicCellNull(props: any) { + return
; } diff --git a/src/components/fields/_withTableCell/withHeavyCell.tsx b/src/components/fields/_withTableCell/withHeavyCell.tsx index 394a497c..64c612fb 100644 --- a/src/components/fields/_withTableCell/withHeavyCell.tsx +++ b/src/components/fields/_withTableCell/withHeavyCell.tsx @@ -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 = ; diff --git a/src/components/fields/_withTableCell/withPopoverCell.tsx b/src/components/fields/_withTableCell/withPopoverCell.tsx index 5cb12ac5..8a1f6b20 100644 --- a/src/components/fields/_withTableCell/withPopoverCell.tsx +++ b/src/components/fields/_withTableCell/withPopoverCell.tsx @@ -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); diff --git a/src/components/fields/types.ts b/src/components/fields/types.ts index c7309452..1837e1db 100644 --- a/src/components/fields/types.ts +++ b/src/components/fields/types.ts @@ -30,7 +30,8 @@ export interface IFieldConfig { reset: () => void ) => IContextMenuItem[]; TableCell: React.ComponentType; - TableEditor: React.ComponentType>; + /** @deprecated TODO: REMOVE */ + TableEditor?: React.ComponentType>; SideDrawerField: React.ComponentType; settings?: React.ComponentType; settingsValidator?: (config: Record) => Record; @@ -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>; } +/** @deprecated TODO: REMOVE */ export interface IPopoverCellProps extends IPopoverInlineCellProps { parentRef: PopoverProps["anchorEl"]; } +export interface IDisplayCellProps { + 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 extends IDisplayCellProps { + onSubmit: (value: T) => void; + tabIndex: number; + parentRef: PopoverProps["anchorEl"]; +} + /** Props to be passed to all SideDrawerFields */ export interface ISideDrawerFieldProps { /** The column config */ diff --git a/src/hooks/useSaveOnUnmount.ts b/src/hooks/useSaveOnUnmount.ts new file mode 100644 index 00000000..7807ce23 --- /dev/null +++ b/src/hooks/useSaveOnUnmount.ts @@ -0,0 +1,19 @@ +/* eslint-disable react-hooks/exhaustive-deps */ +import { useLayoutEffect } from "react"; +import useState from "react-usestateref"; + +export function useSaveOnUnmount( + initialValue: T, + onSave: (value: T) => void +) { + const [localValue, setLocalValue, localValueRef] = useState(initialValue); + + useLayoutEffect(() => { + return () => { + onSave(localValueRef.current); + }; + }, []); + + return [localValue, setLocalValue, localValueRef] as const; +} +export default useSaveOnUnmount; diff --git a/src/index.tsx b/src/index.tsx index f4d7b2c9..5d545f47 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -8,11 +8,11 @@ import reportWebVitals from "./reportWebVitals"; const container = document.getElementById("root")!; const root = createRoot(container); root.render( - // - - - - // + + + + + ); // If you want to start measuring performance in your app, pass a function diff --git a/src/pages/Table/TablePage.tsx b/src/pages/Table/TablePage.tsx index d324a69d..234be866 100644 --- a/src/pages/Table/TablePage.tsx +++ b/src/pages/Table/TablePage.tsx @@ -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); From 73af4d768bd9786625143f5250d8e358cf39343a Mon Sep 17 00:00:00 2001 From: Sidney Alcantara Date: Wed, 9 Nov 2022 15:15:58 +1100 Subject: [PATCH 29/66] separate TablePage into its own chunk --- src/pages/Table/ProvidedSubTablePage.tsx | 6 ++++-- src/pages/Table/ProvidedTablePage.tsx | 6 ++++-- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/src/pages/Table/ProvidedSubTablePage.tsx b/src/pages/Table/ProvidedSubTablePage.tsx index 2b3e090b..58380d27 100644 --- a/src/pages/Table/ProvidedSubTablePage.tsx +++ b/src/pages/Table/ProvidedSubTablePage.tsx @@ -1,4 +1,4 @@ -import { Suspense, useMemo } from "react"; +import { lazy, Suspense, useMemo } from "react"; import { useAtom, Provider } from "jotai"; import { selectAtom } from "jotai/utils"; import { DebugAtoms } from "@src/atoms/utils"; @@ -10,7 +10,6 @@ import Modal from "@src/components/Modal"; import BreadcrumbsSubTable from "@src/components/Table/BreadcrumbsSubTable"; import ErrorFallback from "@src/components/ErrorFallback"; import TableSourceFirestore from "@src/sources/TableSourceFirestore"; -import TablePage from "./TablePage"; import TableToolbarSkeleton from "@src/components/TableToolbar/TableToolbarSkeleton"; import TableSkeleton from "@src/components/Table/TableSkeleton"; @@ -25,6 +24,9 @@ import { ROUTES } from "@src/constants/routes"; import { TOP_BAR_HEIGHT } from "@src/layouts/Navigation/TopBar"; import { TABLE_TOOLBAR_HEIGHT } from "@src/components/TableToolbar"; +// prettier-ignore +const TablePage = lazy(() => import("./TablePage" /* webpackChunkName: "TablePage" */)); + /** * Wraps `TablePage` with the data for a top-level table. */ diff --git a/src/pages/Table/ProvidedTablePage.tsx b/src/pages/Table/ProvidedTablePage.tsx index d2e5cb17..ab2b7614 100644 --- a/src/pages/Table/ProvidedTablePage.tsx +++ b/src/pages/Table/ProvidedTablePage.tsx @@ -1,4 +1,4 @@ -import { Suspense } from "react"; +import { lazy, Suspense } from "react"; import { useAtom, Provider } from "jotai"; import { DebugAtoms } from "@src/atoms/utils"; import { useParams, useOutlet } from "react-router-dom"; @@ -9,7 +9,6 @@ import ErrorFallback, { ERROR_TABLE_NOT_FOUND, } from "@src/components/ErrorFallback"; import TableSourceFirestore from "@src/sources/TableSourceFirestore"; -import TablePage from "./TablePage"; import TableToolbarSkeleton from "@src/components/TableToolbar/TableToolbarSkeleton"; import TableSkeleton from "@src/components/Table/TableSkeleton"; @@ -28,6 +27,9 @@ import { import { SyncAtomValue } from "@src/atoms/utils"; import useDocumentTitle from "@src/hooks/useDocumentTitle"; +// prettier-ignore +const TablePage = lazy(() => import("./TablePage" /* webpackChunkName: "TablePage" */)); + /** * Wraps `TablePage` with the data for a top-level table. * `SubTablePage` is inserted in the outlet, alongside `TablePage`. From 2dc2bdb6bc8ea4ee93a9aa9e8ee33791ed8f1d57 Mon Sep 17 00:00:00 2001 From: Sidney Alcantara Date: Wed, 9 Nov 2022 15:59:27 +1100 Subject: [PATCH 30/66] Multiselect field: add back convert to array --- src/components/Table/withTableCell.tsx | 1 + .../MultiSelect/ConvertStringToArray.tsx | 29 ------------------- .../fields/MultiSelect/DisplayCell.tsx | 29 ++++++++++++------- .../fields/MultiSelect/PopoverCell.tsx | 20 +++++++++++++ .../fields/MultiSelect/SideDrawerField.tsx | 25 ++++++++++++++-- 5 files changed, 62 insertions(+), 42 deletions(-) delete mode 100644 src/components/fields/MultiSelect/ConvertStringToArray.tsx diff --git a/src/components/Table/withTableCell.tsx b/src/components/Table/withTableCell.tsx index 3021fe40..88aa7e68 100644 --- a/src/components/Table/withTableCell.tsx +++ b/src/components/Table/withTableCell.tsx @@ -164,6 +164,7 @@ export default function withTableCell( onClick={(e) => e.stopPropagation()} onDoubleClick={(e) => e.stopPropagation()} onKeyDown={(e) => e.stopPropagation()} + onContextMenu={(e) => e.stopPropagation()} > {editorCell} diff --git a/src/components/fields/MultiSelect/ConvertStringToArray.tsx b/src/components/fields/MultiSelect/ConvertStringToArray.tsx deleted file mode 100644 index 9d3f3ce0..00000000 --- a/src/components/fields/MultiSelect/ConvertStringToArray.tsx +++ /dev/null @@ -1,29 +0,0 @@ -import { IPopoverInlineCellProps } from "@src/components/fields/types"; -import { Grid, Tooltip, Button } from "@mui/material"; - -export const ConvertStringToArray = ({ - value, - onSubmit, -}: Pick) => ( - - - {value} - - - - - - - -); diff --git a/src/components/fields/MultiSelect/DisplayCell.tsx b/src/components/fields/MultiSelect/DisplayCell.tsx index 94a07938..9cb58369 100644 --- a/src/components/fields/MultiSelect/DisplayCell.tsx +++ b/src/components/fields/MultiSelect/DisplayCell.tsx @@ -1,6 +1,7 @@ import { IDisplayCellProps } from "@src/components/fields/types"; import { ButtonBase, Grid } from "@mui/material"; +import WarningIcon from "@mui/icons-material/WarningAmber"; import { ChevronDown } from "@src/assets/icons"; import { sanitiseValue } from "./utils"; @@ -29,16 +30,24 @@ export default function MultiSelect({ justifyContent: "flex-start", }} > - - {sanitiseValue(value).map( - (item) => - typeof item === "string" && ( - - - - ) - )} - + {typeof value === "string" && value !== "" ? ( +
+ +   + {value} +
+ ) : ( + + {sanitiseValue(value).map( + (item) => + typeof item === "string" && ( + + + + ) + )} + + )} {!disabled && } diff --git a/src/components/fields/MultiSelect/PopoverCell.tsx b/src/components/fields/MultiSelect/PopoverCell.tsx index 0f35e52d..8ef894cf 100644 --- a/src/components/fields/MultiSelect/PopoverCell.tsx +++ b/src/components/fields/MultiSelect/PopoverCell.tsx @@ -1,6 +1,9 @@ import { IEditorCellProps } from "@src/components/fields/types"; +import { Typography, Button } from "@mui/material"; +import WarningIcon from "@mui/icons-material/WarningAmber"; import MultiSelectComponent from "@rowy/multiselect"; +import EmptyState from "@src/components/EmptyState"; import { sanitiseValue } from "./utils"; @@ -14,6 +17,23 @@ export default function MultiSelect({ }: IEditorCellProps) { const config = column.config ?? {}; + if (typeof value === "string" && value !== "") + return ( + + This cell’s value is a string + + + } + sx={{ my: 3, width: column.width }} + /> + ); + return ( ; + return ( + + + +  {value} + + + + + + ); return ( <> From 387c2db2bcf5e7fe9685a0bf35cfea5e2489d053 Mon Sep 17 00:00:00 2001 From: Sidney Alcantara Date: Wed, 9 Nov 2022 15:59:46 +1100 Subject: [PATCH 31/66] fix final/actions column for read-only users --- .../Table/FinalColumn/FinalColumnHeader.tsx | 32 +++++++++++++++++-- src/components/Table/Styled/StyledRow.tsx | 3 +- src/components/Table/Table.tsx | 1 + 3 files changed, 33 insertions(+), 3 deletions(-) diff --git a/src/components/Table/FinalColumn/FinalColumnHeader.tsx b/src/components/Table/FinalColumn/FinalColumnHeader.tsx index 0a949885..416cab5c 100644 --- a/src/components/Table/FinalColumn/FinalColumnHeader.tsx +++ b/src/components/Table/FinalColumn/FinalColumnHeader.tsx @@ -9,16 +9,44 @@ import { spreadSx } from "@src/utils/ui"; export interface IFinalColumnHeaderProps extends Partial { focusInsideCell: boolean; + canAddColumn: boolean; } export default function FinalColumnHeader({ focusInsideCell, + canAddColumn, ...props }: IFinalColumnHeaderProps) { const [userRoles] = useAtom(userRolesAtom, projectScope); const openColumnModal = useSetAtom(columnModalAtom, tableScope); - if (!userRoles.includes("ADMIN")) return null; + if (!userRoles.includes("ADMIN")) + return ( + `1px solid ${theme.palette.divider}`, + borderLeft: "none", + borderTopRightRadius: (theme) => theme.shape.borderRadius, + borderBottomRightRadius: (theme) => theme.shape.borderRadius, + + display: "flex", + alignItems: "center", + + width: 32 * 3 + 4 * 2 + 10 * 2, + px: 1.5, + color: "text.secondary", + }, + ...spreadSx(props.sx), + ]} + className="column-header" + > + Actions + + ); return ( ({ flexShrink: 0, borderRadius: theme.shape.borderRadius, padding: (32 - 20) / 2, - boxSizing: "content-box", + width: 32, + height: 32, "&.end": { marginRight: theme.spacing(0.5) }, }, diff --git a/src/components/Table/Table.tsx b/src/components/Table/Table.tsx index a3d60147..f7d72c76 100644 --- a/src/components/Table/Table.tsx +++ b/src/components/Table/Table.tsx @@ -283,6 +283,7 @@ export default function Table({ aria-colindex={header.index + 1} aria-readonly={!canEditColumn} aria-selected={isSelectedCell} + canAddColumn={canAddColumn} /> ); From 5678f96c2b8cc86d728eed4469d61ac906924aea Mon Sep 17 00:00:00 2001 From: Sidney Alcantara Date: Wed, 9 Nov 2022 17:07:22 +1100 Subject: [PATCH 32/66] migrate field types based on TextEditor --- src/components/Table/EditorCellTextField.tsx | 77 +++++++++++ src/components/Table/Styled/StyledCell.tsx | 3 + src/components/Table/Table.tsx | 11 +- src/components/Table/editors/TextEditor.tsx | 122 ------------------ src/components/Table/withTableCell.tsx | 7 +- src/components/fields/Email/EditorCell.tsx | 6 + src/components/fields/Email/index.tsx | 9 +- .../Id/{TableCell.tsx => DisplayCell.tsx} | 4 +- src/components/fields/Id/index.tsx | 17 +-- .../{BasicCell.tsx => DisplayCell.tsx} | 7 +- src/components/fields/LongText/EditorCell.tsx | 6 + src/components/fields/LongText/index.tsx | 17 +-- src/components/fields/Number/BasicCell.tsx | 5 - src/components/fields/Number/DisplayCell.tsx | 5 + src/components/fields/Number/EditorCell.tsx | 12 ++ src/components/fields/Number/index.tsx | 9 +- .../fields/Percentage/BasicCell.tsx | 23 ---- .../{TableCell.tsx => DisplayCell.tsx} | 4 +- .../fields/Percentage/EditorCell.tsx | 13 ++ src/components/fields/Percentage/index.tsx | 16 +-- src/components/fields/Phone/EditorCell.tsx | 6 + src/components/fields/Phone/index.tsx | 9 +- .../{BasicCell.tsx => DisplayCell.tsx} | 7 +- .../fields/Reference/EditorCell.tsx | 111 ++++------------ src/components/fields/Reference/index.tsx | 18 +-- .../fields/ShortText/EditorCell.tsx | 61 +-------- .../Url/{TableCell.tsx => DisplayCell.tsx} | 7 +- src/components/fields/Url/EditorCell.tsx | 6 + src/components/fields/Url/index.tsx | 11 +- 29 files changed, 223 insertions(+), 386 deletions(-) create mode 100644 src/components/Table/EditorCellTextField.tsx delete mode 100644 src/components/Table/editors/TextEditor.tsx create mode 100644 src/components/fields/Email/EditorCell.tsx rename src/components/fields/Id/{TableCell.tsx => DisplayCell.tsx} (69%) rename src/components/fields/LongText/{BasicCell.tsx => DisplayCell.tsx} (58%) create mode 100644 src/components/fields/LongText/EditorCell.tsx delete mode 100644 src/components/fields/Number/BasicCell.tsx create mode 100644 src/components/fields/Number/DisplayCell.tsx create mode 100644 src/components/fields/Number/EditorCell.tsx delete mode 100644 src/components/fields/Percentage/BasicCell.tsx rename src/components/fields/Percentage/{TableCell.tsx => DisplayCell.tsx} (85%) create mode 100644 src/components/fields/Percentage/EditorCell.tsx create mode 100644 src/components/fields/Phone/EditorCell.tsx rename src/components/fields/Reference/{BasicCell.tsx => DisplayCell.tsx} (81%) rename src/components/fields/Url/{TableCell.tsx => DisplayCell.tsx} (77%) create mode 100644 src/components/fields/Url/EditorCell.tsx diff --git a/src/components/Table/EditorCellTextField.tsx b/src/components/Table/EditorCellTextField.tsx new file mode 100644 index 00000000..ee0fd9f3 --- /dev/null +++ b/src/components/Table/EditorCellTextField.tsx @@ -0,0 +1,77 @@ +import type { IEditorCellProps } from "@src/components/fields/types"; +import { useSaveOnUnmount } from "@src/hooks/useSaveOnUnmount"; + +import { InputBase, InputBaseProps } from "@mui/material"; + +import { spreadSx } from "@src/utils/ui"; + +export interface IEditorCellTextFieldProps extends IEditorCellProps { + InputProps?: Partial; +} + +export default function EditorCellTextField({ + column, + value, + onSubmit, + setFocusInsideCell, + InputProps = {}, +}: IEditorCellTextFieldProps) { + const [localValue, setLocalValue] = useSaveOnUnmount(value, onSubmit); + const maxLength = column.config?.maxLength; + + return ( + setLocalValue(e.target.value)} + fullWidth + autoFocus + onKeyDown={(e) => { + if ( + e.key === "ArrowLeft" || + e.key === "ArrowRight" || + e.key === "ArrowUp" || + e.key === "ArrowDown" + ) { + e.stopPropagation(); + } + if (e.key === "Escape") { + // Escape removes focus inside cell, this runs before save on unmount + setLocalValue(value); + } + if (e.key === "Enter" && !e.shiftKey) { + // Removes focus from inside cell, triggering save on unmount + setFocusInsideCell(false); + } + }} + onClick={(e) => e.stopPropagation()} + onDoubleClick={(e) => e.stopPropagation()} + {...InputProps} + inputProps={{ maxLength, ...InputProps.inputProps }} + 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, + }, + }, + ...spreadSx(InputProps.sx), + ]} + /> + ); +} diff --git a/src/components/Table/Styled/StyledCell.tsx b/src/components/Table/Styled/StyledCell.tsx index 23ffb2ef..c11fee62 100644 --- a/src/components/Table/Styled/StyledCell.tsx +++ b/src/components/Table/Styled/StyledCell.tsx @@ -14,6 +14,9 @@ export const StyledCell = styled("div")(({ theme }) => ({ height: "100%", contain: "strict", overflow: "hidden", + + display: "flex", + alignItems: "center", }, backgroundColor: "var(--cell-background-color)", diff --git a/src/components/Table/Table.tsx b/src/components/Table/Table.tsx index f7d72c76..f8ca9486 100644 --- a/src/components/Table/Table.tsx +++ b/src/components/Table/Table.tsx @@ -446,7 +446,11 @@ export default function Table({ cell.column.columnDef.meta?.config?.required )} aria-selected={isSelectedCell} - aria-describedby="rowy-table-cell-description" + aria-describedby={ + canEditCell + ? "rowy-table-editable-cell-description" + : undefined + } style={{ width: cell.column.getSize(), height: tableSchema.rowHeight, @@ -537,7 +541,10 @@ export default function Table({ -
+
Press Enter to edit.
diff --git a/src/components/Table/editors/TextEditor.tsx b/src/components/Table/editors/TextEditor.tsx deleted file mode 100644 index 58229457..00000000 --- a/src/components/Table/editors/TextEditor.tsx +++ /dev/null @@ -1,122 +0,0 @@ -import { useRef, useLayoutEffect } from "react"; -import { EditorProps } from "react-data-grid"; -import { useSetAtom } from "jotai"; -import { get } from "lodash-es"; - -import { TextField } from "@mui/material"; - -import { tableScope, updateFieldAtom } from "@src/atoms/tableScope"; -import { FieldType } from "@src/constants/fields"; -import { getFieldType } from "@src/components/fields"; - -/** WARNING: THIS DOES NOT WORK IN REACT 18 STRICT MODE */ -export default function TextEditor({ row, column }: EditorProps) { - const updateField = useSetAtom(updateFieldAtom, tableScope); - - const type = getFieldType(column as any); - - const cellValue = get(row, column.key); - const defaultValue = - type === FieldType.percentage && typeof cellValue === "number" - ? cellValue * 100 - : cellValue; - - const inputRef = useRef(null); - - // WARNING: THIS DOES NOT WORK IN REACT 18 STRICT MODE - useLayoutEffect(() => { - const inputElement = inputRef.current; - return () => { - const newValue = inputElement?.value; - let formattedValue: any = newValue; - if (newValue !== undefined) { - if (type === FieldType.number) { - formattedValue = Number(newValue); - } else if (type === FieldType.percentage) { - formattedValue = Number(newValue) / 100; - } - - updateField({ - path: row._rowy_ref.path, - fieldName: column.key, - value: formattedValue, - }); - } - }; - }, [column.key, row._rowy_ref.path, type, updateField]); - - let inputType = "text"; - switch (type) { - case FieldType.email: - inputType = "email"; - break; - case FieldType.phone: - inputType = "tel"; - break; - case FieldType.url: - inputType = "url"; - break; - case FieldType.number: - case FieldType.percentage: - inputType = "number"; - break; - - default: - break; - } - - const { maxLength } = (column as any).config; - - return ( - theme.typography.body2.lineHeight, - maxHeight: "100%", - boxSizing: "border-box", - py: 3 / 8, - }, - }} - InputProps={{ - endAdornment: - (column as any).type === FieldType.percentage ? "%" : undefined, - }} - autoFocus - onKeyDown={(e) => { - if (e.key === "ArrowLeft" || e.key === "ArrowRight") { - e.stopPropagation(); - } - - if (e.key === "Escape") { - (e.target as any).value = defaultValue; - } - }} - /> - ); -} diff --git a/src/components/Table/withTableCell.tsx b/src/components/Table/withTableCell.tsx index 88aa7e68..5e0b5478 100644 --- a/src/components/Table/withTableCell.tsx +++ b/src/components/Table/withTableCell.tsx @@ -36,7 +36,7 @@ export interface ICellOptions { */ export default function withTableCell( DisplayCellComponent: React.ComponentType, - EditorCellComponent: React.ComponentType, + EditorCellComponent: React.ComponentType | null, editorMode: "focus" | "inline" | "popover" = "focus", options: ICellOptions = {} ) { @@ -115,13 +115,16 @@ export default function withTableCell( }); }; - const editorCell = ( + // Show displayCell as a fallback if intentionally null + const editorCell = EditorCellComponent ? ( + ) : ( + displayCell ); if (editorMode === "focus" && focusInsideCell) { diff --git a/src/components/fields/Email/EditorCell.tsx b/src/components/fields/Email/EditorCell.tsx new file mode 100644 index 00000000..c14a114a --- /dev/null +++ b/src/components/fields/Email/EditorCell.tsx @@ -0,0 +1,6 @@ +import type { IEditorCellProps } from "@src/components/fields/types"; +import EditorCellTextField from "@src/components/Table/EditorCellTextField"; + +export default function Email(props: IEditorCellProps) { + return ; +} diff --git a/src/components/fields/Email/index.tsx b/src/components/fields/Email/index.tsx index 87b3a07a..d8a461ac 100644 --- a/src/components/fields/Email/index.tsx +++ b/src/components/fields/Email/index.tsx @@ -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 EmailIcon from "@mui/icons-material/MailOutlined"; -import BasicCell from "@src/components/fields/_BasicCell/BasicCellValue"; -import TextEditor from "@src/components/Table/editors/TextEditor"; +import DisplayCell from "@src/components/fields/_BasicCell/BasicCellValue"; +import EditorCell from "./EditorCell"; import { filterOperators } from "@src/components/fields/ShortText/Filter"; import BasicContextMenuActions from "@src/components/fields/_BasicCell/BasicCellContextMenuActions"; @@ -23,8 +23,7 @@ export const config: IFieldConfig = { icon: , description: "Email address. Not validated.", contextMenuActions: BasicContextMenuActions, - TableCell: withBasicCell(BasicCell), - TableEditor: TextEditor, + TableCell: withTableCell(DisplayCell, EditorCell), SideDrawerField, filter: { operators: filterOperators, diff --git a/src/components/fields/Id/TableCell.tsx b/src/components/fields/Id/DisplayCell.tsx similarity index 69% rename from src/components/fields/Id/TableCell.tsx rename to src/components/fields/Id/DisplayCell.tsx index 2ff1feff..fe86e927 100644 --- a/src/components/fields/Id/TableCell.tsx +++ b/src/components/fields/Id/DisplayCell.tsx @@ -1,8 +1,8 @@ -import { IHeavyCellProps } from "@src/components/fields/types"; +import { IDisplayCellProps } from "@src/components/fields/types"; import { useTheme } from "@mui/material"; -export default function Id({ docRef }: IHeavyCellProps) { +export default function Id({ docRef }: IDisplayCellProps) { const theme = useTheme(); return ( diff --git a/src/components/fields/Id/index.tsx b/src/components/fields/Id/index.tsx index c6222990..757ed231 100644 --- a/src/components/fields/Id/index.tsx +++ b/src/components/fields/Id/index.tsx @@ -1,17 +1,9 @@ -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 DisplayCell from "./DisplayCell"; +import SideDrawerField from "./SideDrawerField"; import { Id as IdIcon } from "@src/assets/icons"; -import BasicCell from "@src/components/fields/_BasicCell/BasicCellValue"; -import withSideDrawerEditor from "@src/components/Table/editors/withSideDrawerEditor"; - -const TableCell = lazy( - () => import("./TableCell" /* webpackChunkName: "TableCell-Id" */) -); -const SideDrawerField = lazy( - () => import("./SideDrawerField" /* webpackChunkName: "SideDrawerField-Id" */) -); export const config: IFieldConfig = { type: FieldType.id, @@ -21,8 +13,7 @@ export const config: IFieldConfig = { initialValue: "", icon: , description: "Displays the row’s ID. Read-only. Cannot be sorted.", - TableCell: withHeavyCell(BasicCell, TableCell), - TableEditor: withSideDrawerEditor(TableCell), + TableCell: withTableCell(DisplayCell, null), SideDrawerField, }; export default config; diff --git a/src/components/fields/LongText/BasicCell.tsx b/src/components/fields/LongText/DisplayCell.tsx similarity index 58% rename from src/components/fields/LongText/BasicCell.tsx rename to src/components/fields/LongText/DisplayCell.tsx index 14c6e23e..1e8e0fc2 100644 --- a/src/components/fields/LongText/BasicCell.tsx +++ b/src/components/fields/LongText/DisplayCell.tsx @@ -1,16 +1,15 @@ -import { IBasicCellProps } from "@src/components/fields/types"; +import { IDisplayCellProps } from "@src/components/fields/types"; import { useTheme } from "@mui/material"; -export default function LongText({ value }: IBasicCellProps) { +export default function LongText({ value }: IDisplayCellProps) { const theme = useTheme(); return (
) { + return ; +} diff --git a/src/components/fields/LongText/index.tsx b/src/components/fields/LongText/index.tsx index c4cec831..ef0ce27c 100644 --- a/src/components/fields/LongText/index.tsx +++ b/src/components/fields/LongText/index.tsx @@ -1,21 +1,15 @@ 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 LongTextIcon from "@mui/icons-material/Notes"; -import BasicCell from "./BasicCell"; -import TextEditor from "@src/components/Table/editors/TextEditor"; +import DisplayCell from "./DisplayCell"; +import EditorCell from "./EditorCell"; +import SideDrawerField from "./SideDrawerField"; import { filterOperators } from "./Filter"; import BasicContextMenuActions from "@src/components/fields/_BasicCell/BasicCellContextMenuActions"; -const SideDrawerField = lazy( - () => - import( - "./SideDrawerField" /* webpackChunkName: "SideDrawerField-LongText" */ - ) -); - const Settings = lazy( () => import("./Settings" /* webpackChunkName: "Settings-LongText" */) ); @@ -30,8 +24,7 @@ export const config: IFieldConfig = { icon: , description: "Text displayed on multiple lines.", contextMenuActions: BasicContextMenuActions, - TableCell: withBasicCell(BasicCell), - TableEditor: TextEditor, + TableCell: withTableCell(DisplayCell, EditorCell), SideDrawerField, settings: Settings, filter: { diff --git a/src/components/fields/Number/BasicCell.tsx b/src/components/fields/Number/BasicCell.tsx deleted file mode 100644 index 71d6be89..00000000 --- a/src/components/fields/Number/BasicCell.tsx +++ /dev/null @@ -1,5 +0,0 @@ -import { IBasicCellProps } from "@src/components/fields/types"; - -export default function Number_({ value }: IBasicCellProps) { - return <>{`${value ?? ""}`}; -} diff --git a/src/components/fields/Number/DisplayCell.tsx b/src/components/fields/Number/DisplayCell.tsx new file mode 100644 index 00000000..5256a741 --- /dev/null +++ b/src/components/fields/Number/DisplayCell.tsx @@ -0,0 +1,5 @@ +import { IDisplayCellProps } from "@src/components/fields/types"; + +export default function Number_({ value }: IDisplayCellProps) { + return <>{`${value ?? ""}`}; +} diff --git a/src/components/fields/Number/EditorCell.tsx b/src/components/fields/Number/EditorCell.tsx new file mode 100644 index 00000000..93c5c31c --- /dev/null +++ b/src/components/fields/Number/EditorCell.tsx @@ -0,0 +1,12 @@ +import type { IEditorCellProps } from "@src/components/fields/types"; +import EditorCellTextField from "@src/components/Table/EditorCellTextField"; + +export default function Number_(props: IEditorCellProps) { + return ( + props.onSubmit(Number(v))} + /> + ); +} diff --git a/src/components/fields/Number/index.tsx b/src/components/fields/Number/index.tsx index a7fdc94c..7bed8e43 100644 --- a/src/components/fields/Number/index.tsx +++ b/src/components/fields/Number/index.tsx @@ -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 { Number as NumberIcon } from "@src/assets/icons"; -import BasicCell from "./BasicCell"; -import TextEditor from "@src/components/Table/editors/TextEditor"; +import DisplayCell from "./DisplayCell"; +import EditorCell from "./EditorCell"; import { filterOperators } from "./Filter"; import BasicContextMenuActions from "@src/components/fields/_BasicCell/BasicCellContextMenuActions"; const SideDrawerField = lazy( @@ -22,8 +22,7 @@ export const config: IFieldConfig = { icon: , description: "Numeric value.", contextMenuActions: BasicContextMenuActions, - TableCell: withBasicCell(BasicCell), - TableEditor: TextEditor, + TableCell: withTableCell(DisplayCell, EditorCell), SideDrawerField, filter: { operators: filterOperators, diff --git a/src/components/fields/Percentage/BasicCell.tsx b/src/components/fields/Percentage/BasicCell.tsx deleted file mode 100644 index 5f6228aa..00000000 --- a/src/components/fields/Percentage/BasicCell.tsx +++ /dev/null @@ -1,23 +0,0 @@ -import { IBasicCellProps } from "@src/components/fields/types"; - -import { useTheme } from "@mui/material"; - -export default function Percentage({ value }: IBasicCellProps) { - const theme = useTheme(); - - if (value === null || value === undefined) return null; - - const percentage = typeof value === "number" ? value : 0; - return ( -
- {Math.round(percentage * 100)}% -
- ); -} diff --git a/src/components/fields/Percentage/TableCell.tsx b/src/components/fields/Percentage/DisplayCell.tsx similarity index 85% rename from src/components/fields/Percentage/TableCell.tsx rename to src/components/fields/Percentage/DisplayCell.tsx index 29174a39..532103e4 100644 --- a/src/components/fields/Percentage/TableCell.tsx +++ b/src/components/fields/Percentage/DisplayCell.tsx @@ -1,9 +1,9 @@ -import { IHeavyCellProps } from "@src/components/fields/types"; +import { IDisplayCellProps } from "@src/components/fields/types"; import { useTheme } from "@mui/material"; import { resultColorsScale } from "@src/utils/color"; -export default function Percentage({ column, value }: IHeavyCellProps) { +export default function Percentage({ column, value }: IDisplayCellProps) { const theme = useTheme(); const { colors } = (column as any).config; diff --git a/src/components/fields/Percentage/EditorCell.tsx b/src/components/fields/Percentage/EditorCell.tsx new file mode 100644 index 00000000..652cdcd0 --- /dev/null +++ b/src/components/fields/Percentage/EditorCell.tsx @@ -0,0 +1,13 @@ +import type { IEditorCellProps } from "@src/components/fields/types"; +import EditorCellTextField from "@src/components/Table/EditorCellTextField"; + +export default function Percentage(props: IEditorCellProps) { + return ( + props.onSubmit(Number(v) / 100)} + /> + ); +} diff --git a/src/components/fields/Percentage/index.tsx b/src/components/fields/Percentage/index.tsx index f353774f..99239f7c 100644 --- a/src/components/fields/Percentage/index.tsx +++ b/src/components/fields/Percentage/index.tsx @@ -1,20 +1,13 @@ 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 { Percentage as PercentageIcon } from "@src/assets/icons"; -import TextEditor from "@src/components/Table/editors/TextEditor"; +import DisplayCell from "./DisplayCell"; +import EditorCell from "./EditorCell"; import { filterOperators } from "@src/components/fields/Number/Filter"; import BasicContextMenuActions from "@src/components/fields/_BasicCell/BasicCellContextMenuActions"; -const BasicCell = lazy( - () => import("./BasicCell" /* webpackChunkName: "BasicCell-Percentage" */) -); - -const TableCell = lazy( - () => import("./TableCell" /* webpackChunkName: "TableCell-Percentage" */) -); - const SideDrawerField = lazy( () => import( @@ -37,8 +30,7 @@ export const config: IFieldConfig = { requireConfiguration: true, description: "Percentage stored as a number between 0 and 1.", contextMenuActions: BasicContextMenuActions, - TableCell: withHeavyCell(BasicCell, TableCell), - TableEditor: TextEditor, + TableCell: withTableCell(DisplayCell, EditorCell), SideDrawerField, settings: Settings, filter: { diff --git a/src/components/fields/Phone/EditorCell.tsx b/src/components/fields/Phone/EditorCell.tsx new file mode 100644 index 00000000..825fd00f --- /dev/null +++ b/src/components/fields/Phone/EditorCell.tsx @@ -0,0 +1,6 @@ +import type { IEditorCellProps } from "@src/components/fields/types"; +import EditorCellTextField from "@src/components/Table/EditorCellTextField"; + +export default function Phone(props: IEditorCellProps) { + return ; +} diff --git a/src/components/fields/Phone/index.tsx b/src/components/fields/Phone/index.tsx index 96115973..9e5c1ebe 100644 --- a/src/components/fields/Phone/index.tsx +++ b/src/components/fields/Phone/index.tsx @@ -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 PhoneIcon from "@mui/icons-material/PhoneOutlined"; -import BasicCell from "@src/components/fields/_BasicCell/BasicCellValue"; -import TextEditor from "@src/components/Table/editors/TextEditor"; +import DisplayCell from "@src/components/fields/_BasicCell/BasicCellValue"; +import EditorCell from "./EditorCell"; import { filterOperators } from "@src/components/fields/ShortText/Filter"; import BasicContextMenuActions from "@src/components/fields/_BasicCell/BasicCellContextMenuActions"; @@ -23,8 +23,7 @@ export const config: IFieldConfig = { icon: , description: "Phone number stored as text. Not validated.", contextMenuActions: BasicContextMenuActions, - TableCell: withBasicCell(BasicCell), - TableEditor: TextEditor, + TableCell: withTableCell(DisplayCell, EditorCell), SideDrawerField, filter: { operators: filterOperators, diff --git a/src/components/fields/Reference/BasicCell.tsx b/src/components/fields/Reference/DisplayCell.tsx similarity index 81% rename from src/components/fields/Reference/BasicCell.tsx rename to src/components/fields/Reference/DisplayCell.tsx index b75b62d2..a15d9db4 100644 --- a/src/components/fields/Reference/BasicCell.tsx +++ b/src/components/fields/Reference/DisplayCell.tsx @@ -1,12 +1,12 @@ import { useAtom } from "jotai"; -import { IBasicCellProps } from "@src/components/fields/types"; +import { IDisplayCellProps } from "@src/components/fields/types"; import { Stack, IconButton } from "@mui/material"; import LaunchIcon from "@mui/icons-material/Launch"; import { projectScope, projectIdAtom } from "@src/atoms/projectScope"; -export default function Reference({ value }: IBasicCellProps) { +export default function Reference({ value }: IDisplayCellProps) { const [projectId] = useAtom(projectIdAtom, projectScope); const path = value?.path ?? ""; @@ -17,8 +17,7 @@ export default function Reference({ value }: IBasicCellProps) { direction="row" alignItems="center" justifyContent="space-between" - className="cell-collapse-padding" - sx={{ p: "var(--cell-padding)", pr: 0.5 }} + sx={{ p: "var(--cell-padding)", pr: 0.5, width: "100%" }} >
{path}
diff --git a/src/components/fields/Reference/EditorCell.tsx b/src/components/fields/Reference/EditorCell.tsx index 8b96b8b3..770f352c 100644 --- a/src/components/fields/Reference/EditorCell.tsx +++ b/src/components/fields/Reference/EditorCell.tsx @@ -1,100 +1,33 @@ -import { useRef, useLayoutEffect } from "react"; -import { useAtom, useSetAtom } from "jotai"; -import { EditorProps } from "react-data-grid"; -import { get } from "lodash-es"; -import { useSnackbar } from "notistack"; +import type { IEditorCellProps } from "@src/components/fields/types"; +import EditorCellTextField from "@src/components/Table/EditorCellTextField"; -import { TextField } from "@mui/material"; +import { useAtom } from "jotai"; +import { doc, deleteField } from "firebase/firestore"; +import { useSnackbar } from "notistack"; import { projectScope } from "@src/atoms/projectScope"; import { firebaseDbAtom } from "@src/sources/ProjectSourceFirebase"; -import { tableScope, updateFieldAtom } from "@src/atoms/tableScope"; -import { doc, deleteField } from "firebase/firestore"; -/** WARNING: THIS DOES NOT WORK IN REACT 18 STRICT MODE */ -export default function TextEditor({ row, column }: EditorProps) { - const [firebaseDb] = useAtom(firebaseDbAtom, projectScope); - const updateField = useSetAtom(updateFieldAtom, tableScope); +export default function Reference( + props: IEditorCellProps> +) { const { enqueueSnackbar } = useSnackbar(); - - const inputRef = useRef(null); - - // WARNING: THIS DOES NOT WORK IN REACT 18 STRICT MODE - useLayoutEffect(() => { - const inputElement = inputRef.current; - return () => { - const newValue = inputElement?.value; - if (newValue !== undefined && newValue !== "") { - try { - const refValue = doc(firebaseDb, newValue); - - updateField({ - path: row._rowy_ref.path, - fieldName: column.key, - value: refValue, - }); - } catch (e: any) { - enqueueSnackbar(`Invalid path: ${e.message}`, { variant: "error" }); - } - } else { - updateField({ - path: row._rowy_ref.path, - fieldName: column.key, - value: deleteField(), - }); - } - }; - }, [column.key, row._rowy_ref.path, updateField]); - - const defaultValue = get(row, column.key)?.path ?? ""; - const { maxLength } = (column as any).config; + const [firebaseDb] = useAtom(firebaseDbAtom, projectScope); return ( - theme.typography.body2.lineHeight, - maxHeight: "100%", - boxSizing: "border-box", - py: 3 / 8, - }, - }} - // InputProps={{ - // endAdornment: - // (column as any).type === FieldType.percentage ? "%" : undefined, - // }} - autoFocus - onKeyDown={(e) => { - if (e.key === "ArrowLeft" || e.key === "ArrowRight") { - e.stopPropagation(); - } - - if (e.key === "Escape") { - (e.target as any).value = defaultValue; + { + if (newValue !== undefined && newValue !== "") { + try { + const refValue = doc(firebaseDb, newValue); + props.onSubmit(refValue); + } catch (e: any) { + enqueueSnackbar(`Invalid path: ${e.message}`, { variant: "error" }); + } + } else { + props.onSubmit(deleteField() as any); } }} /> diff --git a/src/components/fields/Reference/index.tsx b/src/components/fields/Reference/index.tsx index 9d5b0f43..fcf0345d 100644 --- a/src/components/fields/Reference/index.tsx +++ b/src/components/fields/Reference/index.tsx @@ -1,24 +1,18 @@ import { lazy } from "react"; import { IFieldConfig, FieldType } from "@src/components/fields/types"; +import withTableCell from "@src/components/Table/withTableCell"; import { Reference } from "@src/assets/icons"; -//import InlineCell from "./InlineCell"; -import BasicCell from "./BasicCell"; +import DisplayCell from "./DisplayCell"; +import EditorCell from "./EditorCell"; import { filterOperators } from "@src/components/fields/ShortText/Filter"; -import withBasicCell from "@src/components/fields/_withTableCell/withBasicCell"; -const EditorCell = lazy( - () => import("./EditorCell" /* webpackChunkName: "EditorCell-Reference" */) -); const SideDrawerField = lazy( () => import( "./SideDrawerField" /* webpackChunkName: "SideDrawerField-Reference" */ ) ); -// const Settings = lazy( -// () => import("./Settings" /* webpackChunkName: "Settings-Reference" */) -// ); export const config: IFieldConfig = { type: FieldType.reference, @@ -29,10 +23,10 @@ export const config: IFieldConfig = { initializable: true, icon: , description: "Firestore document reference", - TableCell: withBasicCell(BasicCell), - TableEditor: EditorCell, + TableCell: withTableCell(DisplayCell, EditorCell, "focus", { + disablePadding: true, + }), SideDrawerField, - //settings: Settings, filter: { operators: filterOperators }, }; export default config; diff --git a/src/components/fields/ShortText/EditorCell.tsx b/src/components/fields/ShortText/EditorCell.tsx index 418ce4dd..0e34e5a8 100644 --- a/src/components/fields/ShortText/EditorCell.tsx +++ b/src/components/fields/ShortText/EditorCell.tsx @@ -1,61 +1,6 @@ import type { IEditorCellProps } from "@src/components/fields/types"; -import { useSaveOnUnmount } from "@src/hooks/useSaveOnUnmount"; +import EditorCellTextField from "@src/components/Table/EditorCellTextField"; -import { InputBase } from "@mui/material"; - -export default function ShortText({ - column, - value, - onSubmit, - setFocusInsideCell, -}: IEditorCellProps) { - const [localValue, setLocalValue] = useSaveOnUnmount(value, onSubmit); - const maxLength = column.config?.maxLength; - - return ( - 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()} - /> - ); +export default function ShortText(props: IEditorCellProps) { + return ; } diff --git a/src/components/fields/Url/TableCell.tsx b/src/components/fields/Url/DisplayCell.tsx similarity index 77% rename from src/components/fields/Url/TableCell.tsx rename to src/components/fields/Url/DisplayCell.tsx index 6c8affc3..b3145716 100644 --- a/src/components/fields/Url/TableCell.tsx +++ b/src/components/fields/Url/DisplayCell.tsx @@ -1,9 +1,9 @@ -import { IBasicCellProps } from "@src/components/fields/types"; +import { IDisplayCellProps } from "@src/components/fields/types"; import { Stack, IconButton } from "@mui/material"; import LaunchIcon from "@mui/icons-material/Launch"; -export default function Url({ value }: IBasicCellProps) { +export default function Url({ value }: IDisplayCellProps) { if (!value || typeof value !== "string") return null; const href = value.includes("http") ? value : `https://${value}`; @@ -13,8 +13,7 @@ export default function Url({ value }: IBasicCellProps) { direction="row" alignItems="center" justifyContent="space-between" - className="cell-collapse-padding" - sx={{ p: "var(--cell-padding)", pr: 0.5 }} + sx={{ p: "var(--cell-padding)", pr: 0.5, width: "100%" }} >
{value}
diff --git a/src/components/fields/Url/EditorCell.tsx b/src/components/fields/Url/EditorCell.tsx new file mode 100644 index 00000000..6f39d2d9 --- /dev/null +++ b/src/components/fields/Url/EditorCell.tsx @@ -0,0 +1,6 @@ +import type { IEditorCellProps } from "@src/components/fields/types"; +import EditorCellTextField from "@src/components/Table/EditorCellTextField"; + +export default function Url(props: IEditorCellProps) { + return ; +} diff --git a/src/components/fields/Url/index.tsx b/src/components/fields/Url/index.tsx index 95376b06..c6fb3fd7 100644 --- a/src/components/fields/Url/index.tsx +++ b/src/components/fields/Url/index.tsx @@ -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 UrlIcon from "@mui/icons-material/Link"; -import TableCell from "./TableCell"; -import TextEditor from "@src/components/Table/editors/TextEditor"; +import DisplayCell from "./DisplayCell"; +import EditorCell from "./EditorCell"; import { filterOperators } from "@src/components/fields/ShortText/Filter"; import BasicContextMenuActions from "@src/components/fields/_BasicCell/BasicCellContextMenuActions"; @@ -23,8 +23,9 @@ export const config: IFieldConfig = { icon: , description: "Web address. Not validated.", contextMenuActions: BasicContextMenuActions, - TableCell: withBasicCell(TableCell), - TableEditor: TextEditor, + TableCell: withTableCell(DisplayCell, EditorCell, "focus", { + disablePadding: true, + }), SideDrawerField, filter: { operators: filterOperators, From 314824fd2b559b463a19da8d78fda89125b0aa4e Mon Sep 17 00:00:00 2001 From: Sidney Alcantara Date: Wed, 9 Nov 2022 17:38:19 +1100 Subject: [PATCH 33/66] add tabIndex to DisplayCells --- src/components/SideDrawer/MemoizedField.tsx | 4 --- src/components/Table/withTableCell.tsx | 26 ++++++++++++++----- src/components/fields/Action/BasicCell.tsx | 5 ---- src/components/fields/Action/DisplayCell.tsx | 5 ++++ .../Action/{TableCell.tsx => EditorCell.tsx} | 9 ++++--- src/components/fields/Action/index.tsx | 14 +++++----- .../fields/MultiSelect/DisplayCell.tsx | 2 ++ .../fields/Reference/DisplayCell.tsx | 3 ++- src/components/fields/Url/DisplayCell.tsx | 3 ++- src/components/fields/types.ts | 12 +++------ 10 files changed, 47 insertions(+), 36 deletions(-) delete mode 100644 src/components/fields/Action/BasicCell.tsx create mode 100644 src/components/fields/Action/DisplayCell.tsx rename src/components/fields/Action/{TableCell.tsx => EditorCell.tsx} (81%) diff --git a/src/components/SideDrawer/MemoizedField.tsx b/src/components/SideDrawer/MemoizedField.tsx index 8333631e..835ed74e 100644 --- a/src/components/SideDrawer/MemoizedField.tsx +++ b/src/components/SideDrawer/MemoizedField.tsx @@ -78,10 +78,6 @@ export const MemoizedField = memo( }, onSubmit: handleSubmit, disabled, - // TODO: Remove - control: {} as any, - useFormMethods: {} as any, - docRef: _rowy_ref, })} ); diff --git a/src/components/Table/withTableCell.tsx b/src/components/Table/withTableCell.tsx index 5e0b5478..93d5641c 100644 --- a/src/components/Table/withTableCell.tsx +++ b/src/components/Table/withTableCell.tsx @@ -9,7 +9,11 @@ import { import { Popover, PopoverProps } from "@mui/material"; -import { tableScope, updateFieldAtom } from "@src/atoms/tableScope"; +import { + tableScope, + updateFieldAtom, + sideDrawerOpenAtom, +} from "@src/atoms/tableScope"; export interface ICellOptions { /** If the rest of the row’s data is used, set this to true for memoization */ @@ -32,12 +36,13 @@ export interface ICellOptions { * - "focus" (default) - when the cell is focused (Enter or double-click) * - "inline" - inline with deferred render * - "popover" - as a popover + * - "sideDrawer" - open the side drawer * @param options - {@link ICellOptions} */ export default function withTableCell( DisplayCellComponent: React.ComponentType, EditorCellComponent: React.ComponentType | null, - editorMode: "focus" | "inline" | "popover" = "focus", + editorMode: "focus" | "inline" | "popover" | "sideDrawer" = "focus", options: ICellOptions = {} ) { return memo( @@ -51,6 +56,7 @@ export default function withTableCell( }: TableCellProps) { const value = getValue(); const updateField = useSetAtom(updateFieldAtom, tableScope); + const setSideDrawerOpen = useSetAtom(sideDrawerOpenAtom, tableScope); // Store ref to rendered DisplayCell to get positioning for PopoverCell const displayCellRef = useRef(null); @@ -59,8 +65,11 @@ export default function withTableCell( // Store Popover open state here so we can add delay for close transition const [popoverOpen, setPopoverOpen] = useState(false); useEffect(() => { - if (focusInsideCell) setPopoverOpen(true); - }, [focusInsideCell]); + if (focusInsideCell) { + setPopoverOpen(true); + if (editorMode === "sideDrawer") setSideDrawerOpen(true); + } + }, [focusInsideCell, setSideDrawerOpen]); const showPopoverCell = (popover: boolean) => { if (popover) { setPopoverOpen(true); @@ -86,7 +95,9 @@ export default function withTableCell( row: row.original, column: column.columnDef.meta!, docRef: row.original._rowy_ref, + _rowy_ref: row.original._rowy_ref, disabled: column.columnDef.meta!.editable === false, + tabIndex: focusInsideCell ? 0 : -1, showPopoverCell, setFocusInsideCell, }; @@ -101,7 +112,11 @@ export default function withTableCell(
); - if (disabled || (editorMode !== "inline" && !focusInsideCell)) + if ( + disabled || + (editorMode !== "inline" && !focusInsideCell) || + editorMode === "sideDrawer" + ) return displayCell; // This is where we update the documents @@ -119,7 +134,6 @@ export default function withTableCell( const editorCell = EditorCellComponent ? ( diff --git a/src/components/fields/Action/BasicCell.tsx b/src/components/fields/Action/BasicCell.tsx deleted file mode 100644 index 76630d8f..00000000 --- a/src/components/fields/Action/BasicCell.tsx +++ /dev/null @@ -1,5 +0,0 @@ -import { IBasicCellProps } from "@src/components/fields/types"; - -export default function Action({ name, value }: IBasicCellProps) { - return <>{value ? value.status : name}; -} diff --git a/src/components/fields/Action/DisplayCell.tsx b/src/components/fields/Action/DisplayCell.tsx new file mode 100644 index 00000000..f852ce6b --- /dev/null +++ b/src/components/fields/Action/DisplayCell.tsx @@ -0,0 +1,5 @@ +import { IDisplayCellProps } from "@src/components/fields/types"; + +export default function Action({ name, value }: IDisplayCellProps) { + return <>{value ? value.status : name}; +} diff --git a/src/components/fields/Action/TableCell.tsx b/src/components/fields/Action/EditorCell.tsx similarity index 81% rename from src/components/fields/Action/TableCell.tsx rename to src/components/fields/Action/EditorCell.tsx index d762d913..e05afbc8 100644 --- a/src/components/fields/Action/TableCell.tsx +++ b/src/components/fields/Action/EditorCell.tsx @@ -1,4 +1,4 @@ -import { IHeavyCellProps } from "@src/components/fields/types"; +import { IEditorCellProps } from "@src/components/fields/types"; import { Stack } from "@mui/material"; @@ -11,15 +11,15 @@ export default function Action({ value, onSubmit, disabled, -}: IHeavyCellProps) { + tabIndex, +}: IEditorCellProps) { const hasRan = value && ![null, undefined].includes(value.status); return (
{hasRan && isUrl(value.status) ? ( @@ -39,6 +39,7 @@ export default function Action({ onSubmit={onSubmit} value={value} disabled={disabled} + tabIndex={tabIndex} /> ); diff --git a/src/components/fields/Action/index.tsx b/src/components/fields/Action/index.tsx index 6a2b6de6..0677e7e4 100644 --- a/src/components/fields/Action/index.tsx +++ b/src/components/fields/Action/index.tsx @@ -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 ActionIcon from "@mui/icons-material/TouchAppOutlined"; -import BasicCell from "./BasicCell"; -import NullEditor from "@src/components/Table/editors/NullEditor"; +import DisplayCell from "./DisplayCell"; -const TableCell = lazy( - () => import("./TableCell" /* webpackChunkName: "TableCell-Action" */) +const EditorCell = lazy( + () => import("./EditorCell" /* webpackChunkName: "EditorCell-Action" */) ); const SideDrawerField = lazy( () => @@ -25,8 +24,9 @@ export const config: IFieldConfig = { icon: , description: "Button with pre-defined action script or triggers a Cloud Function. Optionally supports Undo and Redo.", - TableCell: withHeavyCell(BasicCell, TableCell), - TableEditor: NullEditor as any, + TableCell: withTableCell(DisplayCell, EditorCell, "inline", { + disablePadding: true, + }), SideDrawerField, settings: Settings, requireConfiguration: true, diff --git a/src/components/fields/MultiSelect/DisplayCell.tsx b/src/components/fields/MultiSelect/DisplayCell.tsx index 9cb58369..91bcee84 100644 --- a/src/components/fields/MultiSelect/DisplayCell.tsx +++ b/src/components/fields/MultiSelect/DisplayCell.tsx @@ -12,6 +12,7 @@ export default function MultiSelect({ value, showPopoverCell, disabled, + tabIndex, }: IDisplayCellProps) { // if (typeof value === "string" && value !== "") // return ; @@ -29,6 +30,7 @@ export default function MultiSelect({ textAlign: "inherit", justifyContent: "flex-start", }} + tabIndex={tabIndex} > {typeof value === "string" && value !== "" ? (
diff --git a/src/components/fields/Reference/DisplayCell.tsx b/src/components/fields/Reference/DisplayCell.tsx index a15d9db4..98705d43 100644 --- a/src/components/fields/Reference/DisplayCell.tsx +++ b/src/components/fields/Reference/DisplayCell.tsx @@ -6,7 +6,7 @@ import LaunchIcon from "@mui/icons-material/Launch"; import { projectScope, projectIdAtom } from "@src/atoms/projectScope"; -export default function Reference({ value }: IDisplayCellProps) { +export default function Reference({ value, tabIndex }: IDisplayCellProps) { const [projectId] = useAtom(projectIdAtom, projectScope); const path = value?.path ?? ""; @@ -32,6 +32,7 @@ export default function Reference({ value }: IDisplayCellProps) { aria-label="Open in Firebase Console" className="row-hover-iconButton" style={{ flexShrink: 0 }} + tabIndex={tabIndex} > diff --git a/src/components/fields/Url/DisplayCell.tsx b/src/components/fields/Url/DisplayCell.tsx index b3145716..20bd48dd 100644 --- a/src/components/fields/Url/DisplayCell.tsx +++ b/src/components/fields/Url/DisplayCell.tsx @@ -3,7 +3,7 @@ import { IDisplayCellProps } from "@src/components/fields/types"; import { Stack, IconButton } from "@mui/material"; import LaunchIcon from "@mui/icons-material/Launch"; -export default function Url({ value }: IDisplayCellProps) { +export default function Url({ value, tabIndex }: IDisplayCellProps) { if (!value || typeof value !== "string") return null; const href = value.includes("http") ? value : `https://${value}`; @@ -25,6 +25,7 @@ export default function Url({ value }: IDisplayCellProps) { size="small" style={{ flexShrink: 0 }} aria-label="Open in new tab" + tabIndex={tabIndex} > diff --git a/src/components/fields/types.ts b/src/components/fields/types.ts index 1837e1db..a875a456 100644 --- a/src/components/fields/types.ts +++ b/src/components/fields/types.ts @@ -75,14 +75,17 @@ export interface IDisplayCellProps { name: string; row: TableRow; column: ColumnConfig; + /** @deprecated */ docRef: TableRowRef; + /** The row’s _rowy_ref object */ + _rowy_ref: TableRowRef; disabled: boolean; + tabIndex: number; showPopoverCell: (value: boolean) => void; setFocusInsideCell: (focusInside: boolean) => void; } export interface IEditorCellProps extends IDisplayCellProps { onSubmit: (value: T) => void; - tabIndex: number; parentRef: PopoverProps["anchorEl"]; } @@ -104,13 +107,6 @@ export interface ISideDrawerFieldProps { /** Field locked. Do NOT check `column.locked` */ disabled: boolean; - - /** @deprecated */ - docRef: TableRowRef; - /** @deprecated */ - control: Control; - /** @deprecated */ - useFormMethods: UseFormReturn; } export interface ISettingsProps { From fd717075f8237d29e31f563ade58a5ab3770be50 Mon Sep 17 00:00:00 2001 From: Sidney Alcantara Date: Wed, 9 Nov 2022 18:39:14 +1100 Subject: [PATCH 34/66] use side drawer fields as EditorCells --- src/atoms/tableScope/columnActions.ts | 2 - src/components/Table/EditorCellTextField.tsx | 21 ++-- src/components/Table/withTableCell.tsx | 116 ++++++++++++------ src/components/fields/Action/ActionFab.tsx | 2 - src/components/fields/Action/EditorCell.tsx | 2 - .../fields/Action/SideDrawerField.tsx | 6 - src/components/fields/Checkbox/EditorCell.tsx | 9 +- .../Code/{BasicCell.tsx => DisplayCell.tsx} | 7 +- src/components/fields/Code/index.tsx | 8 +- .../fields/MultiSelect/PopoverCell.tsx | 16 ++- src/components/fields/Number/EditorCell.tsx | 2 +- .../fields/Percentage/EditorCell.tsx | 2 +- .../fields/Reference/EditorCell.tsx | 46 +++++-- src/components/fields/types.ts | 18 ++- 14 files changed, 166 insertions(+), 91 deletions(-) rename src/components/fields/Code/{BasicCell.tsx => DisplayCell.tsx} (67%) diff --git a/src/atoms/tableScope/columnActions.ts b/src/atoms/tableScope/columnActions.ts index 58e44405..96a37d2a 100644 --- a/src/atoms/tableScope/columnActions.ts +++ b/src/atoms/tableScope/columnActions.ts @@ -93,8 +93,6 @@ export const updateColumnAtom = atom( }); } - console.log(tableColumnsOrdered); - // Reduce array into single object with updated indexes const updatedColumns = tableColumnsOrdered.reduce(tableColumnsReducer, {}); await updateTableSchema({ columns: updatedColumns }); diff --git a/src/components/Table/EditorCellTextField.tsx b/src/components/Table/EditorCellTextField.tsx index ee0fd9f3..3d89d8be 100644 --- a/src/components/Table/EditorCellTextField.tsx +++ b/src/components/Table/EditorCellTextField.tsx @@ -1,8 +1,5 @@ import type { IEditorCellProps } from "@src/components/fields/types"; -import { useSaveOnUnmount } from "@src/hooks/useSaveOnUnmount"; - import { InputBase, InputBaseProps } from "@mui/material"; - import { spreadSx } from "@src/utils/ui"; export interface IEditorCellTextFieldProps extends IEditorCellProps { @@ -12,17 +9,18 @@ export interface IEditorCellTextFieldProps extends IEditorCellProps { export default function EditorCellTextField({ column, value, - onSubmit, + onDirty, + onChange, setFocusInsideCell, InputProps = {}, }: IEditorCellTextFieldProps) { - const [localValue, setLocalValue] = useSaveOnUnmount(value, onSubmit); const maxLength = column.config?.maxLength; return ( setLocalValue(e.target.value)} + value={value} + onBlur={() => onDirty()} + onChange={(e) => onChange(e.target.value)} fullWidth autoFocus onKeyDown={(e) => { @@ -34,9 +32,14 @@ export default function EditorCellTextField({ ) { e.stopPropagation(); } + // Escape prevents saving the new value if (e.key === "Escape") { - // Escape removes focus inside cell, this runs before save on unmount - setLocalValue(value); + // Setting isDirty to false prevents saving + onDirty(false); + // Stop propagation to prevent the table from closing the editor + e.stopPropagation(); + // Close the editor after isDirty is set to false again + setTimeout(() => setFocusInsideCell(false)); } if (e.key === "Enter" && !e.shiftKey) { // Removes focus from inside cell, triggering save on unmount diff --git a/src/components/Table/withTableCell.tsx b/src/components/Table/withTableCell.tsx index 93d5641c..65993041 100644 --- a/src/components/Table/withTableCell.tsx +++ b/src/components/Table/withTableCell.tsx @@ -1,4 +1,12 @@ -import { memo, Suspense, useState, useEffect, useRef } from "react"; +import { + memo, + Suspense, + useState, + useEffect, + useRef, + useLayoutEffect, +} from "react"; +import useStateRef from "react-usestateref"; import { useSetAtom } from "jotai"; import { get, isEqual } from "lodash-es"; import type { TableCellProps } from "@src/components/Table"; @@ -14,6 +22,7 @@ import { updateFieldAtom, sideDrawerOpenAtom, } from "@src/atoms/tableScope"; +import { spreadSx } from "@src/utils/ui"; export interface ICellOptions { /** If the rest of the row’s data is used, set this to true for memoization */ @@ -36,13 +45,12 @@ export interface ICellOptions { * - "focus" (default) - when the cell is focused (Enter or double-click) * - "inline" - inline with deferred render * - "popover" - as a popover - * - "sideDrawer" - open the side drawer * @param options - {@link ICellOptions} */ export default function withTableCell( DisplayCellComponent: React.ComponentType, EditorCellComponent: React.ComponentType | null, - editorMode: "focus" | "inline" | "popover" | "sideDrawer" = "focus", + editorMode: "focus" | "inline" | "popover" = "focus", options: ICellOptions = {} ) { return memo( @@ -55,8 +63,6 @@ export default function withTableCell( disabled, }: TableCellProps) { const value = getValue(); - const updateField = useSetAtom(updateFieldAtom, tableScope); - const setSideDrawerOpen = useSetAtom(sideDrawerOpenAtom, tableScope); // Store ref to rendered DisplayCell to get positioning for PopoverCell const displayCellRef = useRef(null); @@ -65,11 +71,8 @@ export default function withTableCell( // Store Popover open state here so we can add delay for close transition const [popoverOpen, setPopoverOpen] = useState(false); useEffect(() => { - if (focusInsideCell) { - setPopoverOpen(true); - if (editorMode === "sideDrawer") setSideDrawerOpen(true); - } - }, [focusInsideCell, setSideDrawerOpen]); + if (focusInsideCell) setPopoverOpen(true); + }, [focusInsideCell]); const showPopoverCell = (popover: boolean) => { if (popover) { setPopoverOpen(true); @@ -112,30 +115,16 @@ export default function withTableCell(
); - if ( - disabled || - (editorMode !== "inline" && !focusInsideCell) || - editorMode === "sideDrawer" - ) + 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, - }); - }; - // Show displayCell as a fallback if intentionally null const editorCell = EditorCellComponent ? ( - ) : ( displayCell @@ -169,15 +158,17 @@ export default function withTableCell( onClose={() => showPopoverCell(false)} anchorOrigin={{ horizontal: "left", vertical: "bottom" }} {...options.popoverProps} - sx={ - options.transparent - ? { - "& .MuiPopover-paper": { - backgroundColor: "transparent", - }, - } - : {} - } + sx={[ + { + "& .MuiPopover-paper": { + backgroundColor: options.transparent + ? "transparent" + : undefined, + minWidth: column.getSize(), + }, + }, + ...spreadSx(options.popoverProps?.sx), + ]} onClick={(e) => e.stopPropagation()} onDoubleClick={(e) => e.stopPropagation()} onKeyDown={(e) => e.stopPropagation()} @@ -214,3 +205,54 @@ export default function withTableCell( } ); } + +interface IEditorCellManagerProps extends IDisplayCellProps { + EditorCellComponent: React.ComponentType; + parentRef: IEditorCellProps["parentRef"]; + saveOnUnmount: boolean; +} + +function EditorCellManager({ + EditorCellComponent, + saveOnUnmount, + ...props +}: IEditorCellManagerProps) { + const [localValue, setLocalValue, localValueRef] = useStateRef(props.value); + const [, setIsDirty, isDirtyRef] = useStateRef(false); + const updateField = useSetAtom(updateFieldAtom, tableScope); + + // 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/fields/Action/ActionFab.tsx b/src/components/fields/Action/ActionFab.tsx index 4f8c31cc..6f87fb15 100644 --- a/src/components/fields/Action/ActionFab.tsx +++ b/src/components/fields/Action/ActionFab.tsx @@ -51,7 +51,6 @@ const getStateIcon = (actionState: "undo" | "redo" | string, config: any) => { export interface IActionFabProps extends Partial { row: any; column: any; - onSubmit: (value: any) => void; value: any; disabled: boolean; } @@ -59,7 +58,6 @@ export interface IActionFabProps extends Partial { export default function ActionFab({ row, column, - onSubmit, value, disabled, ...props diff --git a/src/components/fields/Action/EditorCell.tsx b/src/components/fields/Action/EditorCell.tsx index e05afbc8..1706c4e2 100644 --- a/src/components/fields/Action/EditorCell.tsx +++ b/src/components/fields/Action/EditorCell.tsx @@ -9,7 +9,6 @@ export default function Action({ column, row, value, - onSubmit, disabled, tabIndex, }: IEditorCellProps) { @@ -36,7 +35,6 @@ export default function Action({ { - onChange(value); - onSubmit(); - }} value={value} disabled={disabled} id={getFieldId(column.key)} diff --git a/src/components/fields/Checkbox/EditorCell.tsx b/src/components/fields/Checkbox/EditorCell.tsx index 4785700c..620a8e62 100644 --- a/src/components/fields/Checkbox/EditorCell.tsx +++ b/src/components/fields/Checkbox/EditorCell.tsx @@ -15,6 +15,7 @@ export default function Checkbox({ row, column, value, + onChange, onSubmit, disabled, tabIndex, @@ -29,10 +30,14 @@ export default function Checkbox({ /\{\{(.*?)\}\}/g, replacer(row) ), - handleConfirm: () => onSubmit(!value), + handleConfirm: () => { + onChange(!value); + onSubmit(); + }, }); } else { - onSubmit(!value); + onChange(!value); + onSubmit(); } }; diff --git a/src/components/fields/Code/BasicCell.tsx b/src/components/fields/Code/DisplayCell.tsx similarity index 67% rename from src/components/fields/Code/BasicCell.tsx rename to src/components/fields/Code/DisplayCell.tsx index 8b776896..2d8aa566 100644 --- a/src/components/fields/Code/BasicCell.tsx +++ b/src/components/fields/Code/DisplayCell.tsx @@ -1,20 +1,21 @@ -import { IBasicCellProps } from "@src/components/fields/types"; +import { IDisplayCellProps } from "@src/components/fields/types"; import { useTheme } from "@mui/material"; -export default function Code({ value }: IBasicCellProps) { +export default function Code({ value }: IDisplayCellProps) { const theme = useTheme(); return (
{value} diff --git a/src/components/fields/Code/index.tsx b/src/components/fields/Code/index.tsx index 8db3ae97..18a7da7d 100644 --- a/src/components/fields/Code/index.tsx +++ b/src/components/fields/Code/index.tsx @@ -1,10 +1,9 @@ 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 CodeIcon from "@mui/icons-material/Code"; -import BasicCell from "./BasicCell"; -import withSideDrawerEditor from "@src/components/Table/editors/withSideDrawerEditor"; +import DisplayCell from "./DisplayCell"; const Settings = lazy( () => import("./Settings" /* webpackChunkName: "Settings-Code" */) @@ -24,8 +23,7 @@ export const config: IFieldConfig = { initializable: true, icon: , description: "Raw code edited with the Monaco Editor.", - TableCell: withBasicCell(BasicCell), - TableEditor: withSideDrawerEditor(BasicCell), + TableCell: withTableCell(DisplayCell, SideDrawerField, "popover"), SideDrawerField, settings: Settings, }; diff --git a/src/components/fields/MultiSelect/PopoverCell.tsx b/src/components/fields/MultiSelect/PopoverCell.tsx index 8ef894cf..c96f5746 100644 --- a/src/components/fields/MultiSelect/PopoverCell.tsx +++ b/src/components/fields/MultiSelect/PopoverCell.tsx @@ -9,6 +9,7 @@ import { sanitiseValue } from "./utils"; export default function MultiSelect({ value, + onChange, onSubmit, column, parentRef, @@ -25,7 +26,13 @@ export default function MultiSelect({ description={ <> This cell’s value is a string - @@ -37,7 +44,7 @@ export default function MultiSelect({ return ( showPopoverCell(false)} + onClose={() => { + showPopoverCell(false); + onSubmit(); + }} /> ); } diff --git a/src/components/fields/Number/EditorCell.tsx b/src/components/fields/Number/EditorCell.tsx index 93c5c31c..5ae634a2 100644 --- a/src/components/fields/Number/EditorCell.tsx +++ b/src/components/fields/Number/EditorCell.tsx @@ -6,7 +6,7 @@ export default function Number_(props: IEditorCellProps) { props.onSubmit(Number(v))} + onChange={(v) => props.onChange(Number(v))} /> ); } diff --git a/src/components/fields/Percentage/EditorCell.tsx b/src/components/fields/Percentage/EditorCell.tsx index 652cdcd0..16dd9a88 100644 --- a/src/components/fields/Percentage/EditorCell.tsx +++ b/src/components/fields/Percentage/EditorCell.tsx @@ -7,7 +7,7 @@ export default function Percentage(props: IEditorCellProps) { {...(props as any)} InputProps={{ type: "number", endAdornment: "%" }} value={typeof props.value === "number" ? props.value * 100 : props.value} - onSubmit={(v) => props.onSubmit(Number(v) / 100)} + onChange={(v) => props.onChange(Number(v) / 100)} /> ); } diff --git a/src/components/fields/Reference/EditorCell.tsx b/src/components/fields/Reference/EditorCell.tsx index 770f352c..27232cff 100644 --- a/src/components/fields/Reference/EditorCell.tsx +++ b/src/components/fields/Reference/EditorCell.tsx @@ -1,34 +1,56 @@ +import { useState } from "react"; +import { useAtom } from "jotai"; +import { doc, deleteField } from "firebase/firestore"; + import type { IEditorCellProps } from "@src/components/fields/types"; import EditorCellTextField from "@src/components/Table/EditorCellTextField"; -import { useAtom } from "jotai"; -import { doc, deleteField } from "firebase/firestore"; -import { useSnackbar } from "notistack"; +import { InputAdornment, Tooltip } from "@mui/material"; +import ErrorIcon from "@mui/icons-material/ErrorOutline"; import { projectScope } from "@src/atoms/projectScope"; import { firebaseDbAtom } from "@src/sources/ProjectSourceFirebase"; -export default function Reference( - props: IEditorCellProps> -) { - const { enqueueSnackbar } = useSnackbar(); +export default function Reference({ + value, + ...props +}: IEditorCellProps>) { const [firebaseDb] = useAtom(firebaseDbAtom, projectScope); + const [localValue, setLocalValue] = useState( + Boolean(value) && "path" in value && typeof value.path === "string" + ? value.path + : "" + ); + const [error, setError] = useState(""); + return ( { + value={localValue} + onChange={(newValue) => { if (newValue !== undefined && newValue !== "") { try { const refValue = doc(firebaseDb, newValue); - props.onSubmit(refValue); + props.onChange(refValue); + setError(""); } catch (e: any) { - enqueueSnackbar(`Invalid path: ${e.message}`, { variant: "error" }); + setError(e.message); } } else { - props.onSubmit(deleteField() as any); + props.onChange(deleteField() as any); } + + setLocalValue(newValue); + }} + InputProps={{ + endAdornment: error && ( + + + + + + ), }} /> ); diff --git a/src/components/fields/types.ts b/src/components/fields/types.ts index a875a456..95e57c68 100644 --- a/src/components/fields/types.ts +++ b/src/components/fields/types.ts @@ -85,23 +85,29 @@ export interface IDisplayCellProps { setFocusInsideCell: (focusInside: boolean) => void; } export interface IEditorCellProps extends IDisplayCellProps { - onSubmit: (value: T) => void; + /** Call when the user has input but changes have not been saved */ + onDirty: (dirty?: boolean) => void; + /** Update the local value. Also calls onDirty */ + onChange: (value: T) => void; + /** Call when user input is ready to be saved (e.g. onBlur) */ + onSubmit: () => void; + /** Get parent element for popover positioning */ parentRef: PopoverProps["anchorEl"]; } /** Props to be passed to all SideDrawerFields */ -export interface ISideDrawerFieldProps { +export interface ISideDrawerFieldProps { /** The column config */ - column: FormatterProps["column"] & ColumnConfig; + column: ColumnConfig; /** The row’s _rowy_ref object */ _rowy_ref: TableRowRef; /** The field’s local value – synced with db when field is not dirty */ - value: any; + value: T; /** Call when the user has input but changes have not been saved */ - onDirty: () => void; + onDirty: (dirty?: boolean) => void; /** Update the local value. Also calls onDirty */ - onChange: (value: any) => void; + onChange: (T: any) => void; /** Call when user input is ready to be saved (e.g. onBlur) */ onSubmit: () => void; From fe665fef2a8a525b9a7712db91d2ed2dcf7d0fc9 Mon Sep 17 00:00:00 2001 From: Sidney Alcantara Date: Thu, 10 Nov 2022 18:21:32 +1100 Subject: [PATCH 35/66] migrate all side drawer cells --- src/components/ColumnMenu/ColumnMenu.tsx | 2 +- src/components/SideDrawer/MemoizedField.tsx | 2 +- .../Table/ContextMenu/MenuContents.tsx | 2 +- src/components/Table/withTableCell.tsx | 72 +++++------ src/components/fields/Code/DisplayCell.tsx | 5 +- src/components/fields/Code/index.tsx | 7 +- src/components/fields/Color/DisplayCell.tsx | 60 +++++++++ src/components/fields/Color/EditorCell.tsx | 19 +++ src/components/fields/Color/InlineCell.tsx | 41 ------ src/components/fields/Color/PopoverCell.tsx | 30 ----- .../fields/Color/SideDrawerField.tsx | 4 +- src/components/fields/Color/index.tsx | 15 +-- .../fields/ConnectService/DisplayCell.tsx | 50 +++++++ .../{PopoverCell.tsx => EditorCell.tsx} | 12 +- .../fields/ConnectService/InlineCell.tsx | 56 -------- .../fields/ConnectService/index.tsx | 17 +-- .../fields/ConnectTable/DisplayCell.tsx | 61 +++++++++ .../{PopoverCell.tsx => EditorCell.tsx} | 12 +- .../fields/ConnectTable/InlineCell.tsx | 68 ---------- src/components/fields/ConnectTable/index.tsx | 18 +-- .../fields/Connector/DisplayCell.tsx | 48 +++++++ .../fields/Connector/EditorCell.tsx | 25 ++++ .../fields/Connector/InlineCell.tsx | 57 -------- .../fields/Connector/PopoverCell.tsx | 35 ----- .../fields/Connector/Select/PopupContents.tsx | 122 ++++++++---------- .../fields/Connector/Select/index.tsx | 8 +- .../fields/Connector/SideDrawerField.tsx | 4 +- src/components/fields/Connector/index.tsx | 19 ++- .../{TableCell.tsx => DisplayCell.tsx} | 4 +- src/components/fields/CreatedAt/index.tsx | 11 +- .../{TableCell.tsx => DisplayCell.tsx} | 4 +- src/components/fields/CreatedBy/index.tsx | 11 +- .../Date/{BasicCell.tsx => DisplayCell.tsx} | 18 ++- .../Date/{TableCell.tsx => EditorCell.tsx} | 49 +++---- src/components/fields/Date/index.tsx | 14 +- .../{BasicCell.tsx => DisplayCell.tsx} | 18 ++- .../{TableCell.tsx => EditorCell.tsx} | 50 +++---- src/components/fields/DateTime/index.tsx | 14 +- src/components/fields/Derivative/index.tsx | 9 +- .../{TableCell.tsx => DisplayCell.tsx} | 4 +- src/components/fields/Duration/index.tsx | 13 +- .../{TableCell.tsx => DisplayCell.tsx} | 4 +- src/components/fields/GeoPoint/index.tsx | 14 +- src/components/fields/Id/index.tsx | 6 +- .../Json/{BasicCell.tsx => DisplayCell.tsx} | 8 +- .../fields/Json/SideDrawerField.tsx | 8 +- src/components/fields/Json/index.tsx | 10 +- src/components/fields/LongText/index.tsx | 7 +- .../{BasicCell.tsx => DisplayCell.tsx} | 4 +- .../fields/Markdown/SideDrawerField.tsx | 2 +- src/components/fields/Markdown/index.tsx | 8 +- .../fields/MultiSelect/DisplayCell.tsx | 50 +++---- .../{PopoverCell.tsx => EditorCell.tsx} | 7 +- .../fields/MultiSelect/SideDrawerField.tsx | 7 +- src/components/fields/MultiSelect/index.tsx | 11 +- .../fields/RichText/DisplayCell.tsx | 101 +++++++++++++++ src/components/fields/RichText/TableCell.tsx | 98 -------------- src/components/fields/RichText/index.tsx | 11 +- .../Slider/{TableCell.tsx => DisplayCell.tsx} | 4 +- src/components/fields/Slider/index.tsx | 13 +- .../{TableCell.tsx => DisplayCell.tsx} | 4 +- src/components/fields/UpdatedAt/index.tsx | 11 +- .../{TableCell.tsx => DisplayCell.tsx} | 4 +- src/components/fields/UpdatedBy/index.tsx | 11 +- .../User/{TableCell.tsx => DisplayCell.tsx} | 4 +- src/components/fields/User/index.tsx | 11 +- ...json-stable-stringify-without-jsonify.d.ts | 2 +- 67 files changed, 722 insertions(+), 788 deletions(-) create mode 100644 src/components/fields/Color/DisplayCell.tsx create mode 100644 src/components/fields/Color/EditorCell.tsx delete mode 100644 src/components/fields/Color/InlineCell.tsx delete mode 100644 src/components/fields/Color/PopoverCell.tsx create mode 100644 src/components/fields/ConnectService/DisplayCell.tsx rename src/components/fields/ConnectService/{PopoverCell.tsx => EditorCell.tsx} (81%) delete mode 100644 src/components/fields/ConnectService/InlineCell.tsx create mode 100644 src/components/fields/ConnectTable/DisplayCell.tsx rename src/components/fields/ConnectTable/{PopoverCell.tsx => EditorCell.tsx} (79%) delete mode 100644 src/components/fields/ConnectTable/InlineCell.tsx create mode 100644 src/components/fields/Connector/DisplayCell.tsx create mode 100644 src/components/fields/Connector/EditorCell.tsx delete mode 100644 src/components/fields/Connector/InlineCell.tsx delete mode 100644 src/components/fields/Connector/PopoverCell.tsx rename src/components/fields/CreatedAt/{TableCell.tsx => DisplayCell.tsx} (70%) rename src/components/fields/CreatedBy/{TableCell.tsx => DisplayCell.tsx} (85%) rename src/components/fields/Date/{BasicCell.tsx => DisplayCell.tsx} (51%) rename src/components/fields/Date/{TableCell.tsx => EditorCell.tsx} (69%) rename src/components/fields/DateTime/{BasicCell.tsx => DisplayCell.tsx} (51%) rename src/components/fields/DateTime/{TableCell.tsx => EditorCell.tsx} (70%) rename src/components/fields/Duration/{TableCell.tsx => DisplayCell.tsx} (67%) rename src/components/fields/GeoPoint/{TableCell.tsx => DisplayCell.tsx} (90%) rename src/components/fields/Json/{BasicCell.tsx => DisplayCell.tsx} (66%) rename src/components/fields/Markdown/{BasicCell.tsx => DisplayCell.tsx} (75%) rename src/components/fields/MultiSelect/{PopoverCell.tsx => EditorCell.tsx} (86%) create mode 100644 src/components/fields/RichText/DisplayCell.tsx delete mode 100644 src/components/fields/RichText/TableCell.tsx rename src/components/fields/Slider/{TableCell.tsx => DisplayCell.tsx} (89%) rename src/components/fields/UpdatedAt/{TableCell.tsx => DisplayCell.tsx} (70%) rename src/components/fields/UpdatedBy/{TableCell.tsx => DisplayCell.tsx} (87%) rename src/components/fields/User/{TableCell.tsx => DisplayCell.tsx} (83%) diff --git a/src/components/ColumnMenu/ColumnMenu.tsx b/src/components/ColumnMenu/ColumnMenu.tsx index 4884a75a..9d193179 100644 --- a/src/components/ColumnMenu/ColumnMenu.tsx +++ b/src/components/ColumnMenu/ColumnMenu.tsx @@ -205,7 +205,7 @@ export default function ColumnMenu() { }); handleClose(); }, - active: !column.editable, + active: column.editable === false, }, { label: "Disable resize", diff --git a/src/components/SideDrawer/MemoizedField.tsx b/src/components/SideDrawer/MemoizedField.tsx index 835ed74e..0fb62f62 100644 --- a/src/components/SideDrawer/MemoizedField.tsx +++ b/src/components/SideDrawer/MemoizedField.tsx @@ -53,7 +53,7 @@ export const MemoizedField = memo( // Should not reach this state if (isEmpty(fieldComponent)) { - // console.error('Could not find SideDrawerField component', field); + console.error("Could not find SideDrawerField component", field); return null; } diff --git a/src/components/Table/ContextMenu/MenuContents.tsx b/src/components/Table/ContextMenu/MenuContents.tsx index c098ebe2..5a10aaef 100644 --- a/src/components/Table/ContextMenu/MenuContents.tsx +++ b/src/components/Table/ContextMenu/MenuContents.tsx @@ -211,7 +211,7 @@ export default function MenuContents({ onClose }: IMenuContentsProps) { disabled: selectedColumn?.editable === false || !row || - cellValue || + cellValue === undefined || getFieldProp("group", selectedColumn?.type) === "Auditing", onClick: altPress ? handleClearValue diff --git a/src/components/Table/withTableCell.tsx b/src/components/Table/withTableCell.tsx index 65993041..28a7d3bd 100644 --- a/src/components/Table/withTableCell.tsx +++ b/src/components/Table/withTableCell.tsx @@ -17,11 +17,7 @@ import { import { Popover, PopoverProps } from "@mui/material"; -import { - tableScope, - updateFieldAtom, - sideDrawerOpenAtom, -} from "@src/atoms/tableScope"; +import { tableScope, updateFieldAtom } from "@src/atoms/tableScope"; import { spreadSx } from "@src/utils/ui"; export interface ICellOptions { @@ -30,7 +26,7 @@ export interface ICellOptions { /** Handle padding inside the cell component */ disablePadding?: boolean; /** Set popover background to be transparent */ - transparent?: boolean; + transparentPopover?: boolean; /** Props to pass to MUI Popover component */ popoverProps?: Partial; } @@ -120,12 +116,14 @@ export default function withTableCell( // Show displayCell as a fallback if intentionally null const editorCell = EditorCellComponent ? ( - + + + ) : ( displayCell ); @@ -151,32 +149,32 @@ export default function withTableCell( <> {displayCell} - - showPopoverCell(false)} - anchorOrigin={{ horizontal: "left", vertical: "bottom" }} - {...options.popoverProps} - sx={[ - { - "& .MuiPopover-paper": { - backgroundColor: options.transparent - ? "transparent" - : undefined, - minWidth: column.getSize(), - }, + showPopoverCell(false)} + anchorOrigin={{ horizontal: "center", vertical: "bottom" }} + transformOrigin={{ horizontal: "center", vertical: "top" }} + {...options.popoverProps} + sx={[ + { + "& .MuiPopover-paper": { + backgroundColor: options.transparentPopover + ? "transparent" + : undefined, + boxShadow: options.transparentPopover ? "none" : undefined, + minWidth: column.getSize(), }, - ...spreadSx(options.popoverProps?.sx), - ]} - onClick={(e) => e.stopPropagation()} - onDoubleClick={(e) => e.stopPropagation()} - onKeyDown={(e) => e.stopPropagation()} - onContextMenu={(e) => e.stopPropagation()} - > - {editorCell} - - + }, + ...spreadSx(options.popoverProps?.sx), + ]} + onClick={(e) => e.stopPropagation()} + onDoubleClick={(e) => e.stopPropagation()} + onKeyDown={(e) => e.stopPropagation()} + onContextMenu={(e) => e.stopPropagation()} + > + {editorCell} + ); diff --git a/src/components/fields/Code/DisplayCell.tsx b/src/components/fields/Code/DisplayCell.tsx index 2d8aa566..215266f2 100644 --- a/src/components/fields/Code/DisplayCell.tsx +++ b/src/components/fields/Code/DisplayCell.tsx @@ -4,6 +4,9 @@ import { useTheme } from "@mui/material"; export default function Code({ value }: IDisplayCellProps) { const theme = useTheme(); + + if (typeof value !== "string") return null; + return (
- {value} + {value.substring(0, 1000)}
); } diff --git a/src/components/fields/Code/index.tsx b/src/components/fields/Code/index.tsx index 18a7da7d..72738586 100644 --- a/src/components/fields/Code/index.tsx +++ b/src/components/fields/Code/index.tsx @@ -23,7 +23,12 @@ export const config: IFieldConfig = { initializable: true, icon: , description: "Raw code edited with the Monaco Editor.", - TableCell: withTableCell(DisplayCell, SideDrawerField, "popover"), + TableCell: withTableCell(DisplayCell, SideDrawerField, "popover", { + popoverProps: { + anchorOrigin: { vertical: "top", horizontal: "center" }, + PaperProps: { sx: { borderRadius: 1 } }, + }, + }), SideDrawerField, settings: Settings, }; diff --git a/src/components/fields/Color/DisplayCell.tsx b/src/components/fields/Color/DisplayCell.tsx new file mode 100644 index 00000000..acdf325a --- /dev/null +++ b/src/components/fields/Color/DisplayCell.tsx @@ -0,0 +1,60 @@ +import { IDisplayCellProps } from "@src/components/fields/types"; + +import { ButtonBase, Box } from "@mui/material"; +import { ChevronDown } from "@src/assets/icons"; + +export default function Color({ + value, + showPopoverCell, + disabled, + tabIndex, +}: IDisplayCellProps) { + const rendered = ( +
+ {value?.hex && ( + `0 0 0 1px ${theme.palette.divider} inset`, + borderRadius: 0.5, + }} + /> + )} + + {value?.hex} +
+ ); + + if (disabled) return rendered; + + return ( + showPopoverCell(true)} + sx={{ + width: "100%", + height: "100%", + font: "inherit", + color: "inherit", + letterSpacing: "inherit", + justifyContent: "flex-start", + }} + tabIndex={tabIndex} + > + {rendered} + + + ); +} diff --git a/src/components/fields/Color/EditorCell.tsx b/src/components/fields/Color/EditorCell.tsx new file mode 100644 index 00000000..a0ce903b --- /dev/null +++ b/src/components/fields/Color/EditorCell.tsx @@ -0,0 +1,19 @@ +import { IEditorCellProps } from "@src/components/fields/types"; +import { ColorPicker, toColor } from "react-color-palette"; +import "react-color-palette/lib/css/styles.css"; + +import { Box } from "@mui/material"; + +export default function Color({ value, onChange }: IEditorCellProps) { + return ( + + + + ); +} diff --git a/src/components/fields/Color/InlineCell.tsx b/src/components/fields/Color/InlineCell.tsx deleted file mode 100644 index af36b90f..00000000 --- a/src/components/fields/Color/InlineCell.tsx +++ /dev/null @@ -1,41 +0,0 @@ -import { forwardRef } from "react"; -import { IPopoverInlineCellProps } from "@src/components/fields/types"; - -import { ButtonBase, Box } from "@mui/material"; - -export const Color = forwardRef(function Color( - { value, showPopoverCell, disabled }: IPopoverInlineCellProps, - ref: React.Ref -) { - return ( - showPopoverCell(true)} - ref={ref} - disabled={disabled} - className="cell-collapse-padding" - sx={{ - font: "inherit", - letterSpacing: "inherit", - p: "var(--cell-padding)", - justifyContent: "flex-start", - height: "100%", - }} - > - `0 0 0 1px ${theme.palette.divider} inset`, - borderRadius: 0.5, - }} - /> - - {value?.hex} - - ); -}); - -export default Color; diff --git a/src/components/fields/Color/PopoverCell.tsx b/src/components/fields/Color/PopoverCell.tsx deleted file mode 100644 index cfdbb219..00000000 --- a/src/components/fields/Color/PopoverCell.tsx +++ /dev/null @@ -1,30 +0,0 @@ -import { useEffect, useState } from "react"; -import { IPopoverCellProps } from "@src/components/fields/types"; -import { useDebouncedCallback } from "use-debounce"; -import { ColorPicker, toColor } from "react-color-palette"; -import "react-color-palette/lib/css/styles.css"; - -import { Box } from "@mui/material"; - -export default function Color({ value, onSubmit }: IPopoverCellProps) { - const [localValue, setLocalValue] = useState(value); - const handleChangeComplete = useDebouncedCallback((color) => { - onSubmit(color); - }, 400); - - useEffect(() => { - handleChangeComplete(localValue); - }, [localValue]); - - return ( - - - - ); -} diff --git a/src/components/fields/Color/SideDrawerField.tsx b/src/components/fields/Color/SideDrawerField.tsx index 2090b68c..8f9499e3 100644 --- a/src/components/fields/Color/SideDrawerField.tsx +++ b/src/components/fields/Color/SideDrawerField.tsx @@ -87,7 +87,9 @@ export default function Color({ import("./PopoverCell" /* webpackChunkName: "PopoverCell-Color" */) +const EditorCell = lazy( + () => import("./EditorCell" /* webpackChunkName: "EditorCell-Color" */) ); const SideDrawerField = lazy( () => @@ -26,10 +24,9 @@ export const config: IFieldConfig = { icon: , description: "Color stored as Hex, RGB, and HSV. Edited with a visual picker.", - TableCell: withPopoverCell(BasicCell, InlineCell, PopoverCell, { - anchorOrigin: { horizontal: "left", vertical: "bottom" }, + TableCell: withTableCell(DisplayCell, EditorCell, "popover", { + disablePadding: true, }), - TableEditor: NullEditor as any, SideDrawerField, csvImportParser: (value: string) => { try { diff --git a/src/components/fields/ConnectService/DisplayCell.tsx b/src/components/fields/ConnectService/DisplayCell.tsx new file mode 100644 index 00000000..e4d08055 --- /dev/null +++ b/src/components/fields/ConnectService/DisplayCell.tsx @@ -0,0 +1,50 @@ +import { IDisplayCellProps } from "@src/components/fields/types"; + +import { ButtonBase, Grid, Chip } from "@mui/material"; +import { ChevronDown } from "@src/assets/icons"; + +import ChipList from "@src/components/Table/formatters/ChipList"; +import { get } from "lodash-es"; + +export default function ConnectService({ + value, + showPopoverCell, + disabled, + column, + tabIndex, +}: IDisplayCellProps) { + const config = column.config ?? {}; + const displayKey = config.titleKey ?? config.primaryKey; + + const rendered = ( + + {Array.isArray(value) && + value.map((snapshot) => ( + + + + ))} + + ); + + if (disabled) return rendered; + + return ( + showPopoverCell(true)} + style={{ + width: "100%", + height: "100%", + font: "inherit", + color: "inherit !important", + letterSpacing: "inherit", + textAlign: "inherit", + justifyContent: "flex-start", + }} + tabIndex={tabIndex} + > + {rendered} + + + ); +} diff --git a/src/components/fields/ConnectService/PopoverCell.tsx b/src/components/fields/ConnectService/EditorCell.tsx similarity index 81% rename from src/components/fields/ConnectService/PopoverCell.tsx rename to src/components/fields/ConnectService/EditorCell.tsx index cc79c729..6d506f9c 100644 --- a/src/components/fields/ConnectService/PopoverCell.tsx +++ b/src/components/fields/ConnectService/EditorCell.tsx @@ -1,26 +1,26 @@ -import { IPopoverCellProps } from "@src/components/fields/types"; +import { IEditorCellProps } from "@src/components/fields/types"; import ConnectServiceSelect from "./ConnectServiceSelect"; export default function ConnectService({ value, - onSubmit, + onChange, column, parentRef, showPopoverCell, disabled, - docRef, -}: IPopoverCellProps) { + _rowy_ref, +}: IEditorCellProps) { const config = column.config ?? {}; if (!config) return null; return ( -) { - const config = column.config ?? {}; - const displayKey = config.titleKey ?? config.primaryKey; - return ( - showPopoverCell(true)} - ref={ref} - disabled={disabled} - className="cell-collapse-padding" - sx={{ - height: "100%", - font: "inherit", - color: "inherit !important", - letterSpacing: "inherit", - textAlign: "inherit", - justifyContent: "flex-start", - }} - > - - {Array.isArray(value) && - value.map((snapshot) => ( - - - - ))} - - - {!disabled && ( - - )} - - ); -}); - -export default ConnectService; diff --git a/src/components/fields/ConnectService/index.tsx b/src/components/fields/ConnectService/index.tsx index fd34cda9..2495b867 100644 --- a/src/components/fields/ConnectService/index.tsx +++ b/src/components/fields/ConnectService/index.tsx @@ -1,15 +1,13 @@ 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 ConnectServiceIcon from "@mui/icons-material/Http"; -import BasicCell from "@src/components/fields/_BasicCell/BasicCellNull"; -import InlineCell from "./InlineCell"; -import NullEditor from "@src/components/Table/editors/NullEditor"; +import DisplayCell from "./DisplayCell"; -const PopoverCell = lazy( +const EditorCell = lazy( () => - import("./PopoverCell" /* webpackChunkName: "PopoverCell-ConnectService" */) + import("./EditorCell" /* webpackChunkName: "EditorCell-ConnectService" */) ); const SideDrawerField = lazy( () => @@ -30,11 +28,10 @@ export const config: IFieldConfig = { icon: , description: "Connects to an external web service to fetch a list of results.", - TableCell: withPopoverCell(BasicCell, InlineCell, PopoverCell, { - anchorOrigin: { horizontal: "left", vertical: "bottom" }, - transparent: true, + TableCell: withTableCell(DisplayCell, EditorCell, "popover", { + disablePadding: true, + transparentPopover: true, }), - TableEditor: NullEditor as any, SideDrawerField, requireConfiguration: true, settings: Settings, diff --git a/src/components/fields/ConnectTable/DisplayCell.tsx b/src/components/fields/ConnectTable/DisplayCell.tsx new file mode 100644 index 00000000..75c0da2b --- /dev/null +++ b/src/components/fields/ConnectTable/DisplayCell.tsx @@ -0,0 +1,61 @@ +import { IDisplayCellProps } from "@src/components/fields/types"; + +import { ButtonBase, Grid, Chip } from "@mui/material"; +import { ChevronDown } from "@src/assets/icons"; + +import ChipList from "@src/components/Table/formatters/ChipList"; + +export default function ConnectTable({ + value, + showPopoverCell, + disabled, + column, + tabIndex, +}: IDisplayCellProps) { + const config = column.config ?? {}; + + const rendered = ( + + {Array.isArray(value) ? ( + value.map((item: any) => ( + + item.snapshot[key]) + .join(" ")} + /> + + )) + ) : value ? ( + + value.snapshot[key]) + .join(" ")} + /> + + ) : null} + + ); + + if (disabled) return rendered; + + return ( + showPopoverCell(true)} + style={{ + width: "100%", + height: "100%", + font: "inherit", + color: "inherit !important", + letterSpacing: "inherit", + textAlign: "inherit", + justifyContent: "flex-start", + }} + tabIndex={tabIndex} + > + {rendered} + + + ); +} diff --git a/src/components/fields/ConnectTable/PopoverCell.tsx b/src/components/fields/ConnectTable/EditorCell.tsx similarity index 79% rename from src/components/fields/ConnectTable/PopoverCell.tsx rename to src/components/fields/ConnectTable/EditorCell.tsx index 41192ef0..e4a7af9f 100644 --- a/src/components/fields/ConnectTable/PopoverCell.tsx +++ b/src/components/fields/ConnectTable/EditorCell.tsx @@ -1,16 +1,17 @@ -import { IPopoverCellProps } from "@src/components/fields/types"; +import { IEditorCellProps } from "@src/components/fields/types"; import ConnectTableSelect from "./ConnectTableSelect"; export default function ConnectTable({ value, + onChange, onSubmit, column, parentRef, showPopoverCell, row, disabled, -}: IPopoverCellProps) { +}: IEditorCellProps) { const config = column.config ?? {}; if (!config || !config.primaryKeys) return null; @@ -19,7 +20,7 @@ export default function ConnectTable({ row={row} column={column} value={value} - onChange={onSubmit} + onChange={onChange} config={(config as any) ?? {}} disabled={disabled} TextFieldProps={{ @@ -33,7 +34,10 @@ export default function ConnectTable({ }, }, }} - onClose={() => showPopoverCell(false)} + onClose={() => { + showPopoverCell(false); + onSubmit(); + }} loadBeforeOpen /> ); diff --git a/src/components/fields/ConnectTable/InlineCell.tsx b/src/components/fields/ConnectTable/InlineCell.tsx deleted file mode 100644 index 3f8f3147..00000000 --- a/src/components/fields/ConnectTable/InlineCell.tsx +++ /dev/null @@ -1,68 +0,0 @@ -import { forwardRef } from "react"; -import { IPopoverInlineCellProps } from "@src/components/fields/types"; - -import { ButtonBase, Grid, Chip } from "@mui/material"; -import { ChevronDown } from "@src/assets/icons"; - -import ChipList from "@src/components/Table/formatters/ChipList"; - -export const ConnectTable = forwardRef(function ConnectTable( - { value, showPopoverCell, disabled, column }: IPopoverInlineCellProps, - ref: React.Ref -) { - const config = column.config ?? {}; - - return ( - showPopoverCell(true)} - ref={ref} - disabled={disabled} - className="cell-collapse-padding" - sx={{ - height: "100%", - font: "inherit", - color: "inherit !important", - letterSpacing: "inherit", - textAlign: "inherit", - justifyContent: "flex-start", - }} - > - - {Array.isArray(value) ? ( - value.map((item: any) => ( - - item.snapshot[key]) - .join(" ")} - /> - - )) - ) : value ? ( - - value.snapshot[key]) - .join(" ")} - /> - - ) : null} - - - {!disabled && ( - - )} - - ); -}); - -export default ConnectTable; diff --git a/src/components/fields/ConnectTable/index.tsx b/src/components/fields/ConnectTable/index.tsx index 30fae9c4..e5cd002b 100644 --- a/src/components/fields/ConnectTable/index.tsx +++ b/src/components/fields/ConnectTable/index.tsx @@ -1,15 +1,12 @@ 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 { ConnectTable as ConnectTableIcon } from "@src/assets/icons"; -import BasicCell from "@src/components/fields/_BasicCell/BasicCellNull"; -import InlineCell from "./InlineCell"; -import NullEditor from "@src/components/Table/editors/NullEditor"; +import DisplayCell from "./DisplayCell"; -const PopoverCell = lazy( - () => - import("./PopoverCell" /* webpackChunkName: "PopoverCell-ConnectTable" */) +const EditorCell = lazy( + () => import("./EditorCell" /* webpackChunkName: "EditorCell-ConnectTable" */) ); const SideDrawerField = lazy( () => @@ -31,11 +28,10 @@ export const config: IFieldConfig = { icon: , description: "Connects to an existing table to fetch a snapshot of values from a row. Requires Rowy Run and Algolia setup.", - TableCell: withPopoverCell(BasicCell, InlineCell, PopoverCell, { - anchorOrigin: { horizontal: "left", vertical: "bottom" }, - transparent: true, + TableCell: withTableCell(DisplayCell, EditorCell, "popover", { + disablePadding: true, + transparentPopover: true, }), - TableEditor: NullEditor as any, SideDrawerField, settings: Settings, requireConfiguration: true, diff --git a/src/components/fields/Connector/DisplayCell.tsx b/src/components/fields/Connector/DisplayCell.tsx new file mode 100644 index 00000000..8c6316bc --- /dev/null +++ b/src/components/fields/Connector/DisplayCell.tsx @@ -0,0 +1,48 @@ +import { IDisplayCellProps } from "@src/components/fields/types"; + +import { ButtonBase, Grid, Chip } from "@mui/material"; +import { ChevronDown } from "@src/assets/icons"; + +import ChipList from "@src/components/Table/formatters/ChipList"; +import { get } from "lodash-es"; +import { getLabel } from "./utils"; + +export default function Connector({ + value, + showPopoverCell, + disabled, + column, + tabIndex, +}: IDisplayCellProps) { + const rendered = ( + + {Array.isArray(value) && + value.map((item) => ( + + + + ))} + + ); + + if (disabled) return rendered; + + return ( + showPopoverCell(true)} + sx={{ + width: "100%", + height: "100%", + font: "inherit", + color: "inherit !important", + letterSpacing: "inherit", + textAlign: "inherit", + justifyContent: "flex-start", + }} + tabIndex={tabIndex} + > + {rendered} + + + ); +} diff --git a/src/components/fields/Connector/EditorCell.tsx b/src/components/fields/Connector/EditorCell.tsx new file mode 100644 index 00000000..e665af15 --- /dev/null +++ b/src/components/fields/Connector/EditorCell.tsx @@ -0,0 +1,25 @@ +import { Suspense } from "react"; +import { IEditorCellProps } from "@src/components/fields/types"; + +import PopupContents from "./Select/PopupContents"; +import Loading from "@src/components/Loading"; + +export default function Connector({ + value, + onChange, + column, + disabled, + _rowy_ref, +}: IEditorCellProps) { + return ( + }> + + + ); +} diff --git a/src/components/fields/Connector/InlineCell.tsx b/src/components/fields/Connector/InlineCell.tsx deleted file mode 100644 index bd99142e..00000000 --- a/src/components/fields/Connector/InlineCell.tsx +++ /dev/null @@ -1,57 +0,0 @@ -import { forwardRef } from "react"; -import { IPopoverInlineCellProps } from "@src/components/fields/types"; - -import { ButtonBase, Grid, Chip } from "@mui/material"; -import { ChevronDown } from "@src/assets/icons"; - -import ChipList from "@src/components/Table/formatters/ChipList"; -import { get } from "lodash-es"; -import { getLabel } from "./utils"; - -export const Connector = forwardRef(function Connector( - { value, showPopoverCell, disabled, column }: IPopoverInlineCellProps, - ref: React.Ref -) { - const config = column.config ?? {}; - const displayKey = config.titleKey ?? config.primaryKey; - return ( - showPopoverCell(true)} - ref={ref} - disabled={disabled} - className="cell-collapse-padding" - sx={{ - height: "100%", - font: "inherit", - color: "inherit !important", - letterSpacing: "inherit", - textAlign: "inherit", - justifyContent: "flex-start", - }} - > - - {Array.isArray(value) && - value.map((item) => ( - - - - ))} - - - {!disabled && ( - - )} - - ); -}); - -export default Connector; diff --git a/src/components/fields/Connector/PopoverCell.tsx b/src/components/fields/Connector/PopoverCell.tsx deleted file mode 100644 index e52e94ea..00000000 --- a/src/components/fields/Connector/PopoverCell.tsx +++ /dev/null @@ -1,35 +0,0 @@ -import { IPopoverCellProps } from "@src/components/fields/types"; - -import Selector from "./Select"; - -export default function ConnectService({ - value, - onSubmit, - column, - parentRef, - showPopoverCell, - disabled, - docRef, -}: IPopoverCellProps) { - return ( - showPopoverCell(false), - }, - }} - /> - ); -} diff --git a/src/components/fields/Connector/Select/PopupContents.tsx b/src/components/fields/Connector/Select/PopupContents.tsx index 29453471..941418c7 100644 --- a/src/components/fields/Connector/Select/PopupContents.tsx +++ b/src/components/fields/Connector/Select/PopupContents.tsx @@ -1,5 +1,4 @@ -import React, { useEffect, useState } from "react"; -import clsx from "clsx"; +import { useEffect, useState } from "react"; import { useDebouncedCallback } from "use-debounce"; import { get } from "lodash-es"; import { useAtom } from "jotai"; @@ -7,7 +6,6 @@ import { useAtom } from "jotai"; import { Button, Checkbox, - Divider, Grid, InputAdornment, List, @@ -21,7 +19,6 @@ import { import SearchIcon from "@mui/icons-material/Search"; import { IConnectorSelectProps } from "."; -import useStyles from "./styles"; import Loading from "@src/components/Loading"; import { getLabel } from "@src/components/fields/Connector/utils"; import { useSnackbar } from "notistack"; @@ -37,7 +34,7 @@ export default function PopupContents({ value = [], onChange, column, - docRef, + _rowy_ref, }: IPopupContentsProps) { const [rowyRun] = useAtom(rowyRunAtom, projectScope); const [tableSettings] = useAtom(tableSettingsAtom, tableScope); @@ -48,8 +45,6 @@ export default function PopupContents({ const elementId = config.elementId; const multiple = Boolean(config.multiple); - const { classes } = useStyles(); - // Webservice search query const [query, setQuery] = useState(""); // Webservice response @@ -75,7 +70,7 @@ export default function PopupContents({ columnKey: column.key, query: query, schemaDocPath: getTableSchemaPath(tableSettings), - rowDocPath: docRef.path, + rowDocPath: _rowy_ref.path, }, }); setResponse(resp); @@ -105,93 +100,88 @@ export default function PopupContents({ const clearSelection = () => onChange([]); return ( - - + + setQuery(e.target.value)} fullWidth variant="filled" - margin="dense" label="Search items" - className={classes.noMargins} + hiddenLabel + placeholder="Search items" InputProps={{ - endAdornment: ( - + startAdornment: ( + ), }} + InputLabelProps={{ className: "visually-hidden" }} onClick={(e) => e.stopPropagation()} onKeyDown={(e) => e.stopPropagation()} /> - - + + {hits.map((hit) => { const isSelected = selectedValues.some((v) => v === hit[elementId]); return ( - - = config.max - } - > - - {multiple ? ( - - ) : ( - - )} - - - - - + = config.max} + disableGutters + style={{ margin: 0, width: "100%" }} + > + + {multiple ? ( + + ) : ( + + )} + + + ); })} {multiple && ( - + - + {value?.length} of {hits?.length} @@ -199,9 +189,9 @@ export default function PopupContents({ disabled={!value || value.length === 0} onClick={clearSelection} color="primary" - className={classes.selectAllButton} + variant="text" > - Clear selection + Clear diff --git a/src/components/fields/Connector/Select/index.tsx b/src/components/fields/Connector/Select/index.tsx index 9e09ba40..2025ec37 100644 --- a/src/components/fields/Connector/Select/index.tsx +++ b/src/components/fields/Connector/Select/index.tsx @@ -24,7 +24,7 @@ export interface IConnectorSelectProps { className?: string; /** Override any props of the root MUI `TextField` component */ TextFieldProps?: Partial; - docRef: TableRowRef; + _rowy_ref: TableRowRef; disabled?: boolean; } @@ -56,7 +56,11 @@ export default function ConnectorSelect({ // prop for this component to a comma-separated string MenuProps: { classes: { paper: classes.paper, list: classes.menuChild }, - MenuListProps: { disablePadding: true }, + MenuListProps: { + disablePadding: true, + style: { padding: 0 }, + component: "div", + } as any, anchorOrigin: { vertical: "bottom", horizontal: "center" }, transformOrigin: { vertical: "top", horizontal: "center" }, ...TextFieldProps.SelectProps?.MenuProps, diff --git a/src/components/fields/Connector/SideDrawerField.tsx b/src/components/fields/Connector/SideDrawerField.tsx index feccb0aa..b97f1491 100644 --- a/src/components/fields/Connector/SideDrawerField.tsx +++ b/src/components/fields/Connector/SideDrawerField.tsx @@ -11,7 +11,6 @@ export default function Connector({ column, _rowy_ref, value, - onDirty, onChange, onSubmit, disabled, @@ -32,7 +31,7 @@ export default function Connector({ column={column} value={value} onChange={onChange} - docRef={_rowy_ref as any} + _rowy_ref={_rowy_ref} TextFieldProps={{ label: "", hiddenLabel: true, @@ -50,7 +49,6 @@ export default function Connector({ {value.map((item) => { const key = get(item, config.elementId); - console.log(key, item); return ( - import("./PopoverCell" /* webpackChunkName: "PopoverCell-ConnectService" */) + import("./EditorCell" /* webpackChunkName: "EditorCell-ConnectService" */) ); const SideDrawerField = lazy( () => @@ -30,11 +29,9 @@ export const config: IFieldConfig = { icon: , description: "Connects to any table or API to fetch a list of results based on a text query or row data.", - TableCell: withPopoverCell(BasicCell, InlineCell, PopoverCell, { - anchorOrigin: { horizontal: "left", vertical: "bottom" }, - transparent: true, + TableCell: withTableCell(DisplayCell, EditorCell, "popover", { + disablePadding: true, }), - TableEditor: NullEditor as any, SideDrawerField, requireConfiguration: true, settings: Settings, diff --git a/src/components/fields/CreatedAt/TableCell.tsx b/src/components/fields/CreatedAt/DisplayCell.tsx similarity index 70% rename from src/components/fields/CreatedAt/TableCell.tsx rename to src/components/fields/CreatedAt/DisplayCell.tsx index b659618d..04b56517 100644 --- a/src/components/fields/CreatedAt/TableCell.tsx +++ b/src/components/fields/CreatedAt/DisplayCell.tsx @@ -1,9 +1,9 @@ -import { IHeavyCellProps } from "@src/components/fields/types"; +import { IDisplayCellProps } from "@src/components/fields/types"; import { format } from "date-fns"; import { DATE_TIME_FORMAT } from "@src/constants/dates"; -export default function CreatedAt({ column, value }: IHeavyCellProps) { +export default function CreatedAt({ column, value }: IDisplayCellProps) { if (!value) return null; const dateLabel = format( value.toDate ? value.toDate() : value, diff --git a/src/components/fields/CreatedAt/index.tsx b/src/components/fields/CreatedAt/index.tsx index 7f306509..151466fe 100644 --- a/src/components/fields/CreatedAt/index.tsx +++ b/src/components/fields/CreatedAt/index.tsx @@ -1,14 +1,10 @@ import { lazy } from "react"; import { IFieldConfig, FieldType } from "@src/components/fields/types"; -import withHeavyCell from "@src/components/fields/_withTableCell/withHeavyCell"; +import withTableCell from "@src/components/Table/withTableCell"; import { CreatedAt as CreatedAtIcon } from "@src/assets/icons"; -import BasicCell from "@src/components/fields/_BasicCell/BasicCellNull"; -import withSideDrawerEditor from "@src/components/Table/editors/withSideDrawerEditor"; +import DisplayCell from "./DisplayCell"; -const TableCell = lazy( - () => import("./TableCell" /* webpackChunkName: "TableCell-CreatedAt" */) -); const SideDrawerField = lazy( () => import( @@ -28,8 +24,7 @@ export const config: IFieldConfig = { initialValue: null, icon: , description: "Displays the timestamp of when the row was created. Read-only.", - TableCell: withHeavyCell(BasicCell, TableCell), - TableEditor: withSideDrawerEditor(TableCell), + TableCell: withTableCell(DisplayCell, null), SideDrawerField, settings: Settings, }; diff --git a/src/components/fields/CreatedBy/TableCell.tsx b/src/components/fields/CreatedBy/DisplayCell.tsx similarity index 85% rename from src/components/fields/CreatedBy/TableCell.tsx rename to src/components/fields/CreatedBy/DisplayCell.tsx index 69cc68e9..17ca0396 100644 --- a/src/components/fields/CreatedBy/TableCell.tsx +++ b/src/components/fields/CreatedBy/DisplayCell.tsx @@ -1,11 +1,11 @@ -import { IHeavyCellProps } from "@src/components/fields/types"; +import { IDisplayCellProps } from "@src/components/fields/types"; import { Tooltip, Stack, Avatar } from "@mui/material"; import { format } from "date-fns"; import { DATE_TIME_FORMAT } from "@src/constants/dates"; -export default function CreatedBy({ column, value }: IHeavyCellProps) { +export default function CreatedBy({ column, value }: IDisplayCellProps) { if (!value || !value.displayName || !value.timestamp) return null; const dateLabel = format( value.timestamp.toDate ? value.timestamp.toDate() : value.timestamp, diff --git a/src/components/fields/CreatedBy/index.tsx b/src/components/fields/CreatedBy/index.tsx index c844ec51..8bb0329c 100644 --- a/src/components/fields/CreatedBy/index.tsx +++ b/src/components/fields/CreatedBy/index.tsx @@ -1,14 +1,10 @@ import { lazy } from "react"; import { IFieldConfig, FieldType } from "@src/components/fields/types"; -import withHeavyCell from "@src/components/fields/_withTableCell/withHeavyCell"; +import withTableCell from "@src/components/Table/withTableCell"; import { CreatedBy as CreatedByIcon } from "@src/assets/icons"; -import BasicCell from "@src/components/fields/_BasicCell/BasicCellNull"; -import withSideDrawerEditor from "@src/components/Table/editors/withSideDrawerEditor"; +import DisplayCell from "./DisplayCell"; -const TableCell = lazy( - () => import("./TableCell" /* webpackChunkName: "TableCell-CreatedBy" */) -); const SideDrawerField = lazy( () => import( @@ -29,8 +25,7 @@ export const config: IFieldConfig = { icon: , description: "Displays the user that created the row and timestamp. Read-only.", - TableCell: withHeavyCell(BasicCell, TableCell), - TableEditor: withSideDrawerEditor(TableCell), + TableCell: withTableCell(DisplayCell, null), SideDrawerField, settings: Settings, }; diff --git a/src/components/fields/Date/BasicCell.tsx b/src/components/fields/Date/DisplayCell.tsx similarity index 51% rename from src/components/fields/Date/BasicCell.tsx rename to src/components/fields/Date/DisplayCell.tsx index 11181f9a..719f2801 100644 --- a/src/components/fields/Date/BasicCell.tsx +++ b/src/components/fields/Date/DisplayCell.tsx @@ -1,20 +1,24 @@ -import { IBasicCellProps } from "@src/components/fields/types"; +import { IDisplayCellProps } from "@src/components/fields/types"; import { isFunction, isDate } from "lodash-es"; import { format } from "date-fns"; import { DATE_FORMAT } from "@src/constants/dates"; -export default function Date_({ - value, - format: formatProp, -}: IBasicCellProps & { format?: string }) { +export default function Date_({ value, column }: IDisplayCellProps) { if ((!!value && isFunction(value.toDate)) || isDate(value)) { try { const formatted = format( isDate(value) ? value : value.toDate(), - formatProp || DATE_FORMAT + column.config?.format || DATE_FORMAT ); return ( - {formatted} +
+ {formatted} +
); } catch (e) { return null; diff --git a/src/components/fields/Date/TableCell.tsx b/src/components/fields/Date/EditorCell.tsx similarity index 69% rename from src/components/fields/Date/TableCell.tsx rename to src/components/fields/Date/EditorCell.tsx index 08295168..2b9523ed 100644 --- a/src/components/fields/Date/TableCell.tsx +++ b/src/components/fields/Date/EditorCell.tsx @@ -1,5 +1,5 @@ import { useDebouncedCallback } from "use-debounce"; -import { IHeavyCellProps } from "@src/components/fields/types"; +import { IEditorCellProps } from "@src/components/fields/types"; import DatePicker from "@mui/lab/DatePicker"; import { TextField } from "@mui/material"; @@ -7,33 +7,25 @@ import { ChevronDown } from "@src/assets/icons"; import { transformValue, sanitizeValue } from "./utils"; import { DATE_FORMAT } from "@src/constants/dates"; -import BasicCell from "./BasicCell"; export default function Date_({ column, value, disabled, + onChange, onSubmit, -}: IHeavyCellProps) { + tabIndex, +}: IEditorCellProps) { const format = column.config?.format ?? DATE_FORMAT; const transformedValue = transformValue(value); const handleDateChange = useDebouncedCallback((date: Date | null) => { const sanitized = sanitizeValue(date); if (sanitized === undefined) return; - onSubmit(sanitized); + onChange(sanitized); + onSubmit(); }, 500); - if (disabled) - return ( - - ); - return ( ( @@ -43,24 +35,21 @@ export default function Date_({ label="" hiddenLabel aria-label={column.name as string} - className="cell-collapse-padding" sx={{ width: "100%", height: "100%", - "& .MuiInputBase-root": { + "&& .MuiInputBase-root": { height: "100%", font: "inherit", // Prevent text jumping letterSpacing: "inherit", // Prevent text jumping - ".rdg-cell &": { - background: "none !important", - boxShadow: "none", - borderRadius: 0, - padding: 0, + background: "none !important", + boxShadow: "none", + borderRadius: 0, + padding: 0, - "&::after": { width: "100%", left: 0 }, - }, + "&::after": { width: "100%", left: 0 }, }, "& .MuiInputBase-input": { height: "100%", @@ -68,11 +57,9 @@ export default function Date_({ letterSpacing: "inherit", // Prevent text jumping fontVariantNumeric: "tabular-nums", - ".rdg-cell &": { - padding: "var(--cell-padding)", - pr: 0, - pb: 1 / 8, - }, + padding: "0 var(--cell-padding)", + pr: 0, + pb: 1 / 8, }, "& .MuiInputAdornment-root": { m: 0 }, }} @@ -81,6 +68,7 @@ export default function Date_({ onKeyDown={(e) => e.stopPropagation()} // Touch mode: make the whole field clickable onClick={props.inputProps?.onClick as any} + inputProps={{ ...props.inputProps, tabIndex }} /> )} label={column.name} @@ -91,12 +79,13 @@ export default function Date_({ clearable OpenPickerButtonProps={{ size: "small", - className: "row-hover-iconButton", + className: "row-hover-iconButton end", edge: false, - sx: { mr: 3 / 8, width: 32, height: 32 }, + tabIndex, }} components={{ OpenPickerIcon: ChevronDown }} disableOpenPicker={false} + disabled={disabled} /> ); } diff --git a/src/components/fields/Date/index.tsx b/src/components/fields/Date/index.tsx index a4a6e353..fdc634d0 100644 --- a/src/components/fields/Date/index.tsx +++ b/src/components/fields/Date/index.tsx @@ -1,16 +1,15 @@ 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 { parse, format } from "date-fns"; import { DATE_FORMAT } from "@src/constants/dates"; import DateIcon from "@mui/icons-material/TodayOutlined"; -import BasicCell from "./BasicCell"; -import NullEditor from "@src/components/Table/editors/NullEditor"; +import DisplayCell from "./DisplayCell"; import { filterOperators, valueFormatter } from "./filters"; -const TableCell = lazy( - () => import("./TableCell" /* webpackChunkName: "TableCell-Date" */) +const EditorCell = lazy( + () => import("./EditorCell" /* webpackChunkName: "EditorCell-Date" */) ); const SideDrawerField = lazy( () => @@ -29,8 +28,9 @@ export const config: IFieldConfig = { initializable: true, icon: , description: `Formatted date. Format is configurable, default: ${DATE_FORMAT}. Edited with a visual picker.`, - TableCell: withHeavyCell(BasicCell, TableCell), - TableEditor: NullEditor as any, + TableCell: withTableCell(DisplayCell, EditorCell, "inline", { + disablePadding: true, + }), SideDrawerField, filter: { operators: filterOperators, valueFormatter }, settings: Settings, diff --git a/src/components/fields/DateTime/BasicCell.tsx b/src/components/fields/DateTime/DisplayCell.tsx similarity index 51% rename from src/components/fields/DateTime/BasicCell.tsx rename to src/components/fields/DateTime/DisplayCell.tsx index 6c4bac2f..f104d979 100644 --- a/src/components/fields/DateTime/BasicCell.tsx +++ b/src/components/fields/DateTime/DisplayCell.tsx @@ -1,20 +1,24 @@ -import { IBasicCellProps } from "@src/components/fields/types"; +import { IDisplayCellProps } from "@src/components/fields/types"; import { isFunction, isDate } from "lodash-es"; import { format } from "date-fns"; import { DATE_TIME_FORMAT } from "@src/constants/dates"; -export default function DateTime({ - value, - format: formatProp, -}: IBasicCellProps & { format?: string }) { +export default function DateTime({ value, column }: IDisplayCellProps) { if ((!!value && isFunction(value.toDate)) || isDate(value)) { try { const formatted = format( isDate(value) ? value : value.toDate(), - formatProp || DATE_TIME_FORMAT + column.config?.format || DATE_TIME_FORMAT ); return ( - {formatted} +
+ {formatted} +
); } catch (e) { return null; diff --git a/src/components/fields/DateTime/TableCell.tsx b/src/components/fields/DateTime/EditorCell.tsx similarity index 70% rename from src/components/fields/DateTime/TableCell.tsx rename to src/components/fields/DateTime/EditorCell.tsx index 53439ff0..acf8ca69 100644 --- a/src/components/fields/DateTime/TableCell.tsx +++ b/src/components/fields/DateTime/EditorCell.tsx @@ -1,5 +1,5 @@ import { useDebouncedCallback } from "use-debounce"; -import { IHeavyCellProps } from "@src/components/fields/types"; +import { IEditorCellProps } from "@src/components/fields/types"; import { setSeconds } from "date-fns"; import DateTimePicker from "@mui/lab/DateTimePicker"; @@ -11,34 +11,26 @@ import { sanitizeValue, } from "@src/components/fields/Date/utils"; import { DATE_TIME_FORMAT } from "@src/constants/dates"; -import BasicCell from "./BasicCell"; export default function DateTime({ column, value, disabled, + onChange, onSubmit, -}: IHeavyCellProps) { + tabIndex, +}: IEditorCellProps) { const transformedValue = transformValue(value); const handleDateChange = useDebouncedCallback((date: Date | null) => { const sanitized = sanitizeValue(date); if (!sanitized) return; // Temp disable setting it to null - onSubmit(sanitized); + onChange(sanitized); + onSubmit(); }, 500); const format = column.config?.format ?? DATE_TIME_FORMAT; - if (disabled) - return ( - - ); - return ( ( @@ -48,24 +40,21 @@ export default function DateTime({ label="" hiddenLabel aria-label={column.name as string} - className="cell-collapse-padding" sx={{ width: "100%", height: "100%", - "& .MuiInputBase-root": { + "&& .MuiInputBase-root": { height: "100%", font: "inherit", // Prevent text jumping letterSpacing: "inherit", // Prevent text jumping - ".rdg-cell &": { - background: "none !important", - boxShadow: "none", - borderRadius: 0, - padding: 0, + background: "none !important", + boxShadow: "none", + borderRadius: 0, + padding: 0, - "&::after": { width: "100%", left: 0 }, - }, + "&::after": { width: "100%", left: 0 }, }, "& .MuiInputBase-input": { height: "100%", @@ -73,11 +62,9 @@ export default function DateTime({ letterSpacing: "inherit", // Prevent text jumping fontVariantNumeric: "tabular-nums", - ".rdg-cell &": { - padding: "var(--cell-padding)", - pr: 0, - pb: 1 / 8, - }, + padding: "0 var(--cell-padding)", + pr: 0, + pb: 1 / 8, }, "& .MuiInputAdornment-root": { m: 0 }, }} @@ -86,22 +73,23 @@ export default function DateTime({ onKeyDown={(e) => e.stopPropagation()} // Touch mode: make the whole field clickable onClick={props.inputProps?.onClick as any} + inputProps={{ ...props.inputProps, tabIndex }} /> )} label={column.name} value={transformedValue} onChange={(date) => handleDateChange(date ? setSeconds(date, 0) : null)} inputFormat={format} - mask={format.replace(/[A-Za-z]/g, "_")} clearable OpenPickerButtonProps={{ size: "small", - className: "row-hover-iconButton", + className: "row-hover-iconButton end", edge: false, - sx: { mr: 3 / 8, width: 32, height: 32 }, + tabIndex, }} components={{ OpenPickerIcon: ChevronDown }} disableOpenPicker={false} + disabled={disabled} /> ); } diff --git a/src/components/fields/DateTime/index.tsx b/src/components/fields/DateTime/index.tsx index 880557a0..dade54b4 100644 --- a/src/components/fields/DateTime/index.tsx +++ b/src/components/fields/DateTime/index.tsx @@ -1,16 +1,15 @@ 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 { parseJSON, format } from "date-fns"; import { DATE_TIME_FORMAT } from "@src/constants/dates"; import DateTimeIcon from "@mui/icons-material/AccessTime"; -import BasicCell from "./BasicCell"; -import NullEditor from "@src/components/Table/editors/NullEditor"; +import DisplayCell from "./DisplayCell"; import { filterOperators, valueFormatter } from "./filters"; -const TableCell = lazy( - () => import("./TableCell" /* webpackChunkName: "TableCell-DateTime" */) +const EditorCell = lazy( + () => import("./EditorCell" /* webpackChunkName: "EditorCell-DateTime" */) ); const SideDrawerField = lazy( () => @@ -37,8 +36,9 @@ export const config: IFieldConfig = { initializable: true, icon: , description: `Formatted date & time. Format is configurable, default: ${DATE_TIME_FORMAT}. Edited with a visual picker.`, - TableCell: withHeavyCell(BasicCell, TableCell), - TableEditor: NullEditor as any, + TableCell: withTableCell(DisplayCell, EditorCell, "inline", { + disablePadding: true, + }), SideDrawerField, filter: { operators: filterOperators, diff --git a/src/components/fields/Derivative/index.tsx b/src/components/fields/Derivative/index.tsx index a42f2f8c..f37897ed 100644 --- a/src/components/fields/Derivative/index.tsx +++ b/src/components/fields/Derivative/index.tsx @@ -1,9 +1,7 @@ import { IFieldConfig, FieldType } from "@src/components/fields/types"; -import withBasicCell from "@src/components/fields/_withTableCell/withBasicCell"; +import withTableCell from "@src/components/Table/withTableCell"; import { Derivative as DerivativeIcon } from "@src/assets/icons"; -import BasicCell from "@src/components/fields/_BasicCell/BasicCellNull"; -import NullEditor from "@src/components/Table/editors/NullEditor"; import Settings, { settingsValidator } from "./Settings"; import ContextMenuActions from "./ContextMenuActions"; @@ -17,9 +15,8 @@ export const config: IFieldConfig = { icon: , description: "Value derived from the rest of the row’s values. Displayed using any other field type. Requires Rowy Run set up.", - TableCell: withBasicCell(BasicCell), - TableEditor: NullEditor as any, - SideDrawerField: BasicCell as any, + TableCell: withTableCell(() => null, null), + SideDrawerField: () => null as any, contextMenuActions: ContextMenuActions, settings: Settings, settingsValidator, diff --git a/src/components/fields/Duration/TableCell.tsx b/src/components/fields/Duration/DisplayCell.tsx similarity index 67% rename from src/components/fields/Duration/TableCell.tsx rename to src/components/fields/Duration/DisplayCell.tsx index 78be60c0..a5bade41 100644 --- a/src/components/fields/Duration/TableCell.tsx +++ b/src/components/fields/Duration/DisplayCell.tsx @@ -1,8 +1,8 @@ -import { IBasicCellProps } from "@src/components/fields/types"; +import { IDisplayCellProps } from "@src/components/fields/types"; import { getDurationString } from "./utils"; -export default function Duration({ value }: IBasicCellProps) { +export default function Duration({ value }: IDisplayCellProps) { if ( !value || !value.start || diff --git a/src/components/fields/Duration/index.tsx b/src/components/fields/Duration/index.tsx index 92d42ef9..56f05132 100644 --- a/src/components/fields/Duration/index.tsx +++ b/src/components/fields/Duration/index.tsx @@ -1,14 +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 DurationIcon from "@mui/icons-material/TimerOutlined"; -import BasicCell from "@src/components/fields/_BasicCell/BasicCellNull"; -import withSideDrawerEditor from "@src/components/Table/editors/withSideDrawerEditor"; +import DisplayCell from "./DisplayCell"; -const TableCell = lazy( - () => import("./TableCell" /* webpackChunkName: "TableCell-Duration" */) -); const SideDrawerField = lazy( () => import( @@ -24,8 +20,9 @@ export const config: IFieldConfig = { initialValue: {}, icon: , description: "Duration calculated from two timestamps.", - TableCell: withBasicCell(TableCell), - TableEditor: withSideDrawerEditor(TableCell), + TableCell: withTableCell(DisplayCell, SideDrawerField, "popover", { + popoverProps: { PaperProps: { sx: { p: 1 } } }, + }), SideDrawerField, }; export default config; diff --git a/src/components/fields/GeoPoint/TableCell.tsx b/src/components/fields/GeoPoint/DisplayCell.tsx similarity index 90% rename from src/components/fields/GeoPoint/TableCell.tsx rename to src/components/fields/GeoPoint/DisplayCell.tsx index 2e57a835..f452c3fc 100644 --- a/src/components/fields/GeoPoint/TableCell.tsx +++ b/src/components/fields/GeoPoint/DisplayCell.tsx @@ -1,7 +1,7 @@ -import { IBasicCellProps } from "@src/components/fields/types"; +import { IDisplayCellProps } from "@src/components/fields/types"; import { Typography } from "@mui/material"; -export default function GeoPoint({ value }: IBasicCellProps) { +export default function GeoPoint({ value }: IDisplayCellProps) { if (!value) return null; const { latitude, longitude } = value; diff --git a/src/components/fields/GeoPoint/index.tsx b/src/components/fields/GeoPoint/index.tsx index 21b898b1..481198f6 100644 --- a/src/components/fields/GeoPoint/index.tsx +++ b/src/components/fields/GeoPoint/index.tsx @@ -1,14 +1,11 @@ import { lazy } from "react"; import { GeoPoint } from "firebase/firestore"; import { IFieldConfig, FieldType } from "@src/components/fields/types"; -import withBasicCell from "@src/components/fields/_withTableCell/withBasicCell"; +import withTableCell from "@src/components/Table/withTableCell"; import GeoPointIcon from "@mui/icons-material/PinDropOutlined"; -import withSideDrawerEditor from "@src/components/Table/editors/withSideDrawerEditor"; +import DisplayCell from "./DisplayCell"; -const TableCell = lazy( - () => import("./TableCell" /* webpackChunkName: "TableCell-GeoPoint" */) -); const SideDrawerField = lazy( () => import( @@ -24,8 +21,9 @@ export const config: IFieldConfig = { initialValue: {}, icon: , description: "Geo point is represented as latitude/longitude pair.", - TableCell: withBasicCell(TableCell), - TableEditor: withSideDrawerEditor(TableCell), + TableCell: withTableCell(DisplayCell, SideDrawerField, "popover", { + popoverProps: { PaperProps: { sx: { p: 1, pt: 0 } } }, + }), SideDrawerField, csvImportParser: (value: string) => { try { @@ -35,7 +33,7 @@ export const config: IFieldConfig = { } throw new Error(); } catch (e) { - console.error("Invalid Geopoint value"); + console.error("Invalid GeoPoint value"); return null; } }, diff --git a/src/components/fields/Id/index.tsx b/src/components/fields/Id/index.tsx index 757ed231..0b2a5b85 100644 --- a/src/components/fields/Id/index.tsx +++ b/src/components/fields/Id/index.tsx @@ -1,10 +1,14 @@ +import { lazy } from "react"; import { IFieldConfig, FieldType } from "@src/components/fields/types"; import withTableCell from "@src/components/Table/withTableCell"; import DisplayCell from "./DisplayCell"; -import SideDrawerField from "./SideDrawerField"; import { Id as IdIcon } from "@src/assets/icons"; +const SideDrawerField = lazy( + () => import("./SideDrawerField" /* webpackChunkName: "SideDrawerField-Id" */) +); + export const config: IFieldConfig = { type: FieldType.id, name: "ID", diff --git a/src/components/fields/Json/BasicCell.tsx b/src/components/fields/Json/DisplayCell.tsx similarity index 66% rename from src/components/fields/Json/BasicCell.tsx rename to src/components/fields/Json/DisplayCell.tsx index ababa833..82a1eea1 100644 --- a/src/components/fields/Json/BasicCell.tsx +++ b/src/components/fields/Json/DisplayCell.tsx @@ -1,21 +1,21 @@ import stringify from "json-stable-stringify-without-jsonify"; -import { IBasicCellProps } from "@src/components/fields/types"; +import { IDisplayCellProps } from "@src/components/fields/types"; import { useTheme } from "@mui/material"; -export default function Json({ value }: IBasicCellProps) { +export default function Json({ value }: IDisplayCellProps) { const theme = useTheme(); if (!value) return null; - const formattedJson = stringify(value, { space: 2 }); + const formattedJson = stringify(value, { space: 2 }).substring(0, 1000); return (
diff --git a/src/components/fields/Json/index.tsx b/src/components/fields/Json/index.tsx index 8981d877..a847f009 100644 --- a/src/components/fields/Json/index.tsx +++ b/src/components/fields/Json/index.tsx @@ -1,10 +1,9 @@ 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 { Json as JsonIcon } from "@src/assets/icons"; -import BasicCell from "./BasicCell"; -import withSideDrawerEditor from "@src/components/Table/editors/withSideDrawerEditor"; +import DisplayCell from "./DisplayCell"; import ContextMenuActions from "./ContextMenuActions"; const SideDrawerField = lazy( @@ -25,8 +24,9 @@ export const config: IFieldConfig = { initializable: true, icon: , description: "Object edited with a visual JSON editor.", - TableCell: withBasicCell(BasicCell), - TableEditor: withSideDrawerEditor(BasicCell), + TableCell: withTableCell(DisplayCell, SideDrawerField, "popover", { + popoverProps: { PaperProps: { sx: { p: 1 } } }, + }), csvImportParser: (value) => { try { return JSON.parse(value); diff --git a/src/components/fields/LongText/index.tsx b/src/components/fields/LongText/index.tsx index ef0ce27c..6685d26c 100644 --- a/src/components/fields/LongText/index.tsx +++ b/src/components/fields/LongText/index.tsx @@ -5,11 +5,16 @@ import withTableCell from "@src/components/Table/withTableCell"; import LongTextIcon from "@mui/icons-material/Notes"; import DisplayCell from "./DisplayCell"; import EditorCell from "./EditorCell"; -import SideDrawerField from "./SideDrawerField"; import { filterOperators } from "./Filter"; import BasicContextMenuActions from "@src/components/fields/_BasicCell/BasicCellContextMenuActions"; +const SideDrawerField = lazy( + () => + import( + "./SideDrawerField" /* webpackChunkName: "SideDrawerField-LongText" */ + ) +); const Settings = lazy( () => import("./Settings" /* webpackChunkName: "Settings-LongText" */) ); diff --git a/src/components/fields/Markdown/BasicCell.tsx b/src/components/fields/Markdown/DisplayCell.tsx similarity index 75% rename from src/components/fields/Markdown/BasicCell.tsx rename to src/components/fields/Markdown/DisplayCell.tsx index 37e825c1..8fe5d93f 100644 --- a/src/components/fields/Markdown/BasicCell.tsx +++ b/src/components/fields/Markdown/DisplayCell.tsx @@ -1,10 +1,10 @@ -import { IBasicCellProps } from "@src/components/fields/types"; +import { IDisplayCellProps } from "@src/components/fields/types"; import { useTheme } from "@mui/material"; import MDEditor from "@uiw/react-md-editor"; -export default function Markdown({ value }: IBasicCellProps) { +export default function Markdown({ value }: IDisplayCellProps) { const theme = useTheme(); if (!value || typeof value !== "string") return null; diff --git a/src/components/fields/Markdown/SideDrawerField.tsx b/src/components/fields/Markdown/SideDrawerField.tsx index 815c539d..2b51d9b1 100644 --- a/src/components/fields/Markdown/SideDrawerField.tsx +++ b/src/components/fields/Markdown/SideDrawerField.tsx @@ -34,7 +34,7 @@ export default function Markdown({ { display: "block", padding: 0, - "& .wmde-markdown-var": { boxShadow: "none" }, + "& .wmde-markdown-var": { boxShadow: (fieldSx as any)?.boxShadow }, }, ]} data-color-mode={theme.palette.mode} diff --git a/src/components/fields/Markdown/index.tsx b/src/components/fields/Markdown/index.tsx index 6053f093..23776019 100644 --- a/src/components/fields/Markdown/index.tsx +++ b/src/components/fields/Markdown/index.tsx @@ -1,10 +1,9 @@ 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 { Markdown as MarkdownIcon } from "@src/assets/icons"; -import BasicCell from "./BasicCell"; -import withSideDrawerEditor from "@src/components/Table/editors/withSideDrawerEditor"; +import DisplayCell from "./DisplayCell"; const SideDrawerField = lazy( () => @@ -22,8 +21,7 @@ export const config: IFieldConfig = { initializable: true, icon: , description: "Markdown editor with preview", - TableCell: withBasicCell(BasicCell), - TableEditor: withSideDrawerEditor(BasicCell), + TableCell: withTableCell(DisplayCell, SideDrawerField, "popover"), SideDrawerField, }; export default config; diff --git a/src/components/fields/MultiSelect/DisplayCell.tsx b/src/components/fields/MultiSelect/DisplayCell.tsx index 91bcee84..eb5e8267 100644 --- a/src/components/fields/MultiSelect/DisplayCell.tsx +++ b/src/components/fields/MultiSelect/DisplayCell.tsx @@ -1,6 +1,6 @@ import { IDisplayCellProps } from "@src/components/fields/types"; -import { ButtonBase, Grid } from "@mui/material"; +import { ButtonBase, Grid, Tooltip } from "@mui/material"; import WarningIcon from "@mui/icons-material/WarningAmber"; import { ChevronDown } from "@src/assets/icons"; @@ -14,13 +14,33 @@ export default function MultiSelect({ disabled, tabIndex, }: IDisplayCellProps) { - // if (typeof value === "string" && value !== "") - // return ; + const rendered = + typeof value === "string" && value !== "" ? ( +
+ + + +   + {value} +
+ ) : ( + + {sanitiseValue(value).map( + (item) => + typeof item === "string" && ( + + + + ) + )} + + ); + + if (disabled) return rendered; return ( showPopoverCell(true)} - disabled={disabled} style={{ width: "100%", height: "100%", @@ -32,26 +52,8 @@ export default function MultiSelect({ }} tabIndex={tabIndex} > - {typeof value === "string" && value !== "" ? ( -
- -   - {value} -
- ) : ( - - {sanitiseValue(value).map( - (item) => - typeof item === "string" && ( - - - - ) - )} - - )} - - {!disabled && } + {rendered} +
); } diff --git a/src/components/fields/MultiSelect/PopoverCell.tsx b/src/components/fields/MultiSelect/EditorCell.tsx similarity index 86% rename from src/components/fields/MultiSelect/PopoverCell.tsx rename to src/components/fields/MultiSelect/EditorCell.tsx index c96f5746..46fa6837 100644 --- a/src/components/fields/MultiSelect/PopoverCell.tsx +++ b/src/components/fields/MultiSelect/EditorCell.tsx @@ -57,8 +57,11 @@ export default function MultiSelect({ open: true, MenuProps: { anchorEl: parentRef, - anchorOrigin: { vertical: "bottom", horizontal: "left" }, - transformOrigin: { vertical: "top", horizontal: "left" }, + anchorOrigin: { vertical: "bottom", horizontal: "center" }, + transformOrigin: { vertical: "top", horizontal: "center" }, + sx: { + "& .MuiPaper-root": { minWidth: `${column.width}px !important` }, + }, }, }, }} diff --git a/src/components/fields/MultiSelect/SideDrawerField.tsx b/src/components/fields/MultiSelect/SideDrawerField.tsx index 2716e34c..99c260be 100644 --- a/src/components/fields/MultiSelect/SideDrawerField.tsx +++ b/src/components/fields/MultiSelect/SideDrawerField.tsx @@ -1,6 +1,6 @@ import { ISideDrawerFieldProps } from "@src/components/fields/types"; -import { Grid, Button } from "@mui/material"; +import { Grid, Button, Tooltip } from "@mui/material"; import WarningIcon from "@mui/icons-material/WarningAmber"; import MultiSelectComponent from "@rowy/multiselect"; import FormattedChip from "@src/components/FormattedChip"; @@ -28,7 +28,9 @@ export default function MultiSelect({ return ( - + + +  {value} @@ -38,6 +40,7 @@ export default function MultiSelect({ onChange([value]); onSubmit(); }} + disabled={disabled} > Convert to array diff --git a/src/components/fields/MultiSelect/index.tsx b/src/components/fields/MultiSelect/index.tsx index d5a18ed5..cdda43c0 100644 --- a/src/components/fields/MultiSelect/index.tsx +++ b/src/components/fields/MultiSelect/index.tsx @@ -4,11 +4,10 @@ import withTableCell from "@src/components/Table/withTableCell"; import { MultiSelect as MultiSelectIcon } from "@src/assets/icons"; import DisplayCell from "./DisplayCell"; -import NullEditor from "@src/components/Table/editors/NullEditor"; import { filterOperators } from "./Filter"; -const PopoverCell = lazy( - () => - import("./PopoverCell" /* webpackChunkName: "PopoverCell-MultiSelect" */) + +const EditorCell = lazy( + () => import("./EditorCell" /* webpackChunkName: "EditorCell-MultiSelect" */) ); const SideDrawerField = lazy( () => @@ -33,10 +32,10 @@ export const config: IFieldConfig = { icon: , description: "Multiple values from predefined options. Options are searchable and users can optionally input custom values.", - TableCell: withTableCell(DisplayCell, PopoverCell, "popover", { + TableCell: withTableCell(DisplayCell, EditorCell, "popover", { disablePadding: true, + transparentPopover: true, }), - TableEditor: NullEditor as any, SideDrawerField, settings: Settings, csvImportParser: (v) => { diff --git a/src/components/fields/RichText/DisplayCell.tsx b/src/components/fields/RichText/DisplayCell.tsx new file mode 100644 index 00000000..0513e198 --- /dev/null +++ b/src/components/fields/RichText/DisplayCell.tsx @@ -0,0 +1,101 @@ +import { IDisplayCellProps } from "@src/components/fields/types"; +// import { useAtom } from "jotai"; + +import { + // styled, + useTheme, + // Tooltip, + // TooltipProps, + // tooltipClasses, + // Fade, +} from "@mui/material"; +import RenderedHtml from "@src/components/RenderedHtml"; + +// import { tableScope, tableSchemaAtom } from "@src/atoms/tableScope"; +// import { DEFAULT_ROW_HEIGHT } from "@src/components/Table"; + +// type StylesProps = { width: number; rowHeight: number }; + +// const StyledTooltip = styled( +// ({ className, width, rowHeight, ...props }: TooltipProps & StylesProps) => ( +// +// ) +// )(({ theme, width, rowHeight }) => ({ +// [`& .${tooltipClasses.tooltip}`]: { +// margin: 0, +// marginTop: `-${rowHeight - 1}px !important`, +// padding: theme.spacing(3 / 8, 1.25), + +// width: width - 1, +// maxWidth: "none", +// minHeight: rowHeight - 1, +// overflowX: "hidden", + +// background: theme.palette.background.paper, +// borderRadius: 0, +// boxShadow: `0 0 0 1px ${theme.palette.divider}, ${theme.shadows[4]}`, +// color: theme.palette.text.primary, + +// display: "flex", +// alignItems: "center", +// }, +// })); + +export default function RichText({ column, value }: IDisplayCellProps) { + // const [tableSchema] = useAtom(tableSchemaAtom, tableScope); + + const theme = useTheme(); + + if (!value) return null; + + const content = ( + + ); + + // temp disable tooltip, which causes performance issues + return content; + + // return ( + // + //
+ // {content} + //
+ //
+ // ); +} diff --git a/src/components/fields/RichText/TableCell.tsx b/src/components/fields/RichText/TableCell.tsx deleted file mode 100644 index 354a9a76..00000000 --- a/src/components/fields/RichText/TableCell.tsx +++ /dev/null @@ -1,98 +0,0 @@ -import { IHeavyCellProps } from "@src/components/fields/types"; -import { useAtom } from "jotai"; - -import { - styled, - useTheme, - Tooltip, - TooltipProps, - tooltipClasses, - Fade, -} from "@mui/material"; -import RenderedHtml from "@src/components/RenderedHtml"; - -import { tableScope, tableSchemaAtom } from "@src/atoms/tableScope"; -import { DEFAULT_ROW_HEIGHT } from "@src/components/Table"; - -type StylesProps = { width: number; rowHeight: number }; - -const StyledTooltip = styled( - ({ className, width, rowHeight, ...props }: TooltipProps & StylesProps) => ( - - ) -)(({ theme, width, rowHeight }) => ({ - [`& .${tooltipClasses.tooltip}`]: { - margin: 0, - marginTop: `-${rowHeight - 1}px !important`, - padding: theme.spacing(3 / 8, 1.25), - - width: width - 1, - maxWidth: "none", - minHeight: rowHeight - 1, - overflowX: "hidden", - - background: theme.palette.background.paper, - borderRadius: 0, - boxShadow: `0 0 0 1px ${theme.palette.divider}, ${theme.shadows[4]}`, - color: theme.palette.text.primary, - - display: "flex", - alignItems: "center", - }, -})); - -export default function RichText({ column, value }: IHeavyCellProps) { - const [tableSchema] = useAtom(tableSchemaAtom, tableScope); - - const theme = useTheme(); - - if (!value) return null; - - const content = ( - - ); - - return ( - -
- {content} -
-
- ); -} diff --git a/src/components/fields/RichText/index.tsx b/src/components/fields/RichText/index.tsx index 890f1d68..80202a98 100644 --- a/src/components/fields/RichText/index.tsx +++ b/src/components/fields/RichText/index.tsx @@ -1,15 +1,11 @@ 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 RichTextIcon from "@mui/icons-material/TextFormat"; -import BasicCell from "@src/components/fields/_BasicCell/BasicCellNull"; -import withSideDrawerEditor from "@src/components/Table/editors/withSideDrawerEditor"; +import DisplayCell from "./DisplayCell"; import BasicContextMenuActions from "@src/components/fields/_BasicCell/BasicCellContextMenuActions"; -const TableCell = lazy( - () => import("./TableCell" /* webpackChunkName: "TableCell-RichText" */) -); const SideDrawerField = lazy( () => import( @@ -27,8 +23,7 @@ export const config: IFieldConfig = { icon: , description: "HTML edited with a rich text editor.", contextMenuActions: BasicContextMenuActions, - TableCell: withHeavyCell(BasicCell, TableCell), - TableEditor: withSideDrawerEditor(TableCell), + TableCell: withTableCell(DisplayCell, SideDrawerField, "popover"), SideDrawerField, }; export default config; diff --git a/src/components/fields/Slider/TableCell.tsx b/src/components/fields/Slider/DisplayCell.tsx similarity index 89% rename from src/components/fields/Slider/TableCell.tsx rename to src/components/fields/Slider/DisplayCell.tsx index 80699449..ae8cc736 100644 --- a/src/components/fields/Slider/TableCell.tsx +++ b/src/components/fields/Slider/DisplayCell.tsx @@ -1,10 +1,10 @@ -import { IHeavyCellProps } from "@src/components/fields/types"; +import { IDisplayCellProps } from "@src/components/fields/types"; import { Grid, Box } from "@mui/material"; import { resultColorsScale } from "@src/utils/color"; -export default function Slider({ column, value }: IHeavyCellProps) { +export default function Slider({ column, value }: IDisplayCellProps) { const { max, min, diff --git a/src/components/fields/Slider/index.tsx b/src/components/fields/Slider/index.tsx index 80816fdf..6d21dc9d 100644 --- a/src/components/fields/Slider/index.tsx +++ b/src/components/fields/Slider/index.tsx @@ -1,15 +1,11 @@ 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 { Slider as SliderIcon } from "@src/assets/icons"; -import BasicCell from "@src/components/fields/_BasicCell/BasicCellNull"; -import withSideDrawerEditor from "@src/components/Table/editors/withSideDrawerEditor"; +import DisplayCell from "./DisplayCell"; import { filterOperators } from "@src/components/fields/Number/Filter"; -const TableCell = lazy( - () => import("./TableCell" /* webpackChunkName: "TableCell-Slider" */) -); const SideDrawerField = lazy( () => import("./SideDrawerField" /* webpackChunkName: "SideDrawerField-Slider" */) @@ -28,8 +24,9 @@ export const config: IFieldConfig = { icon: , requireConfiguration: true, description: "Numeric value edited with a Slider. Range is configurable.", - TableCell: withHeavyCell(BasicCell, TableCell), - TableEditor: withSideDrawerEditor(TableCell), + TableCell: withTableCell(DisplayCell, SideDrawerField, "popover", { + popoverProps: { PaperProps: { sx: { p: 1, pt: 5 } } }, + }), settings: Settings, filter: { operators: filterOperators, diff --git a/src/components/fields/UpdatedAt/TableCell.tsx b/src/components/fields/UpdatedAt/DisplayCell.tsx similarity index 70% rename from src/components/fields/UpdatedAt/TableCell.tsx rename to src/components/fields/UpdatedAt/DisplayCell.tsx index 52417bce..c6b29d9a 100644 --- a/src/components/fields/UpdatedAt/TableCell.tsx +++ b/src/components/fields/UpdatedAt/DisplayCell.tsx @@ -1,9 +1,9 @@ -import { IHeavyCellProps } from "@src/components/fields/types"; +import { IDisplayCellProps } from "@src/components/fields/types"; import { format } from "date-fns"; import { DATE_TIME_FORMAT } from "@src/constants/dates"; -export default function UpdatedAt({ column, value }: IHeavyCellProps) { +export default function UpdatedAt({ column, value }: IDisplayCellProps) { if (!value) return null; const dateLabel = format( value.toDate ? value.toDate() : value, diff --git a/src/components/fields/UpdatedAt/index.tsx b/src/components/fields/UpdatedAt/index.tsx index 3c2c3f28..9b7476eb 100644 --- a/src/components/fields/UpdatedAt/index.tsx +++ b/src/components/fields/UpdatedAt/index.tsx @@ -1,14 +1,10 @@ import { lazy } from "react"; import { IFieldConfig, FieldType } from "@src/components/fields/types"; -import withHeavyCell from "@src/components/fields/_withTableCell/withHeavyCell"; +import withTableCell from "@src/components/Table/withTableCell"; import { UpdatedAt as UpdatedAtIcon } from "@src/assets/icons"; -import BasicCell from "@src/components/fields/_BasicCell/BasicCellNull"; -import withSideDrawerEditor from "@src/components/Table/editors/withSideDrawerEditor"; +import DisplayCell from "./DisplayCell"; -const TableCell = lazy( - () => import("./TableCell" /* webpackChunkName: "TableCell-UpdatedAt" */) -); const SideDrawerField = lazy( () => import( @@ -29,8 +25,7 @@ export const config: IFieldConfig = { icon: , description: "Displays the timestamp of the last update to the row. Read-only.", - TableCell: withHeavyCell(BasicCell, TableCell), - TableEditor: withSideDrawerEditor(TableCell), + TableCell: withTableCell(DisplayCell, null), SideDrawerField, settings: Settings, }; diff --git a/src/components/fields/UpdatedBy/TableCell.tsx b/src/components/fields/UpdatedBy/DisplayCell.tsx similarity index 87% rename from src/components/fields/UpdatedBy/TableCell.tsx rename to src/components/fields/UpdatedBy/DisplayCell.tsx index 5484626f..1bf71be3 100644 --- a/src/components/fields/UpdatedBy/TableCell.tsx +++ b/src/components/fields/UpdatedBy/DisplayCell.tsx @@ -1,11 +1,11 @@ -import { IHeavyCellProps } from "@src/components/fields/types"; +import { IDisplayCellProps } from "@src/components/fields/types"; import { Tooltip, Stack, Avatar } from "@mui/material"; import { format } from "date-fns"; import { DATE_TIME_FORMAT } from "@src/constants/dates"; -export default function UpdatedBy({ column, value }: IHeavyCellProps) { +export default function UpdatedBy({ column, value }: IDisplayCellProps) { if (!value || !value.displayName || !value.timestamp) return null; const dateLabel = format( value.timestamp.toDate ? value.timestamp.toDate() : value.timestamp, diff --git a/src/components/fields/UpdatedBy/index.tsx b/src/components/fields/UpdatedBy/index.tsx index 09ca1e2f..5ddfb877 100644 --- a/src/components/fields/UpdatedBy/index.tsx +++ b/src/components/fields/UpdatedBy/index.tsx @@ -1,14 +1,10 @@ import { lazy } from "react"; import { IFieldConfig, FieldType } from "@src/components/fields/types"; -import withHeavyCell from "@src/components/fields/_withTableCell/withHeavyCell"; +import withTableCell from "@src/components/Table/withTableCell"; import { UpdatedBy as UpdatedByIcon } from "@src/assets/icons"; -import BasicCell from "@src/components/fields/_BasicCell/BasicCellNull"; -import withSideDrawerEditor from "@src/components/Table/editors/withSideDrawerEditor"; +import DisplayCell from "./DisplayCell"; -const TableCell = lazy( - () => import("./TableCell" /* webpackChunkName: "TableCell-UpdatedBy" */) -); const SideDrawerField = lazy( () => import( @@ -30,8 +26,7 @@ export const config: IFieldConfig = { icon: , description: "Displays the user that last updated the row, timestamp, and updated field key. Read-only.", - TableCell: withHeavyCell(BasicCell, TableCell), - TableEditor: withSideDrawerEditor(TableCell), + TableCell: withTableCell(DisplayCell, null), SideDrawerField, settings: Settings, }; diff --git a/src/components/fields/User/TableCell.tsx b/src/components/fields/User/DisplayCell.tsx similarity index 83% rename from src/components/fields/User/TableCell.tsx rename to src/components/fields/User/DisplayCell.tsx index e1761826..d299b6c5 100644 --- a/src/components/fields/User/TableCell.tsx +++ b/src/components/fields/User/DisplayCell.tsx @@ -1,11 +1,11 @@ -import { IHeavyCellProps } from "@src/components/fields/types"; +import { IDisplayCellProps } from "@src/components/fields/types"; import { Tooltip, Stack, Avatar } from "@mui/material"; import { format } from "date-fns"; import { DATE_TIME_FORMAT } from "@src/constants/dates"; -export default function User({ value, column }: IHeavyCellProps) { +export default function User({ value, column }: IDisplayCellProps) { if (!value || !value.displayName) return null; const chip = ( diff --git a/src/components/fields/User/index.tsx b/src/components/fields/User/index.tsx index 70ec0a83..29f996d8 100644 --- a/src/components/fields/User/index.tsx +++ b/src/components/fields/User/index.tsx @@ -1,14 +1,10 @@ import { lazy } from "react"; import { IFieldConfig, FieldType } from "@src/components/fields/types"; -import withHeavyCell from "@src/components/fields/_withTableCell/withHeavyCell"; +import withTableCell from "@src/components/Table/withTableCell"; import UserIcon from "@mui/icons-material/PersonOutlined"; -import BasicCell from "@src/components/fields/_BasicCell/BasicCellNull"; -import withSideDrawerEditor from "@src/components/Table/editors/withSideDrawerEditor"; +import DisplayCell from "./DisplayCell"; -const TableCell = lazy( - () => import("./TableCell" /* webpackChunkName: "TableCell-User" */) -); const SideDrawerField = lazy( () => import("./SideDrawerField" /* webpackChunkName: "SideDrawerField-User" */) @@ -27,8 +23,7 @@ export const config: IFieldConfig = { initialValue: null, icon: , description: "User information and optionally, timestamp. Read-only.", - TableCell: withHeavyCell(BasicCell, TableCell), - TableEditor: withSideDrawerEditor(TableCell), + TableCell: withTableCell(DisplayCell, null), SideDrawerField, settings: Settings, }; diff --git a/src/types/json-stable-stringify-without-jsonify.d.ts b/src/types/json-stable-stringify-without-jsonify.d.ts index 3e1639a6..a0f5dc4b 100644 --- a/src/types/json-stable-stringify-without-jsonify.d.ts +++ b/src/types/json-stable-stringify-without-jsonify.d.ts @@ -1,4 +1,4 @@ declare module "json-stable-stringify-without-jsonify" { - const stringify: any; + const stringify: (...args: any) => string; export default stringify; } From 26ea9abaf4312ee516bed8f9fff99c5dfa5f2d0d Mon Sep 17 00:00:00 2001 From: Sidney Alcantara Date: Fri, 11 Nov 2022 14:20:53 +1100 Subject: [PATCH 36/66] fix rendered rich text & markdown being tab-able --- src/components/fields/Markdown/DisplayCell.tsx | 4 +++- src/components/fields/RichText/DisplayCell.tsx | 4 +++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/src/components/fields/Markdown/DisplayCell.tsx b/src/components/fields/Markdown/DisplayCell.tsx index 8fe5d93f..9bf4c028 100644 --- a/src/components/fields/Markdown/DisplayCell.tsx +++ b/src/components/fields/Markdown/DisplayCell.tsx @@ -4,7 +4,7 @@ import { useTheme } from "@mui/material"; import MDEditor from "@uiw/react-md-editor"; -export default function Markdown({ value }: IDisplayCellProps) { +export default function Markdown({ value, tabIndex }: IDisplayCellProps) { const theme = useTheme(); if (!value || typeof value !== "string") return null; @@ -13,6 +13,8 @@ export default function Markdown({ value }: IDisplayCellProps) {
diff --git a/src/components/fields/RichText/DisplayCell.tsx b/src/components/fields/RichText/DisplayCell.tsx index 0513e198..aceb4d5d 100644 --- a/src/components/fields/RichText/DisplayCell.tsx +++ b/src/components/fields/RichText/DisplayCell.tsx @@ -41,7 +41,7 @@ import RenderedHtml from "@src/components/RenderedHtml"; // }, // })); -export default function RichText({ column, value }: IDisplayCellProps) { +export default function RichText({ value, tabIndex }: IDisplayCellProps) { // const [tableSchema] = useAtom(tableSchemaAtom, tableScope); const theme = useTheme(); @@ -59,6 +59,8 @@ export default function RichText({ column, value }: IDisplayCellProps) { fontSize: "0.75rem", lineHeight: theme.typography.body2.lineHeight, }} + // Prevent user tabbing into any rendered links + {...({ inert: tabIndex === -1 ? "inert" : undefined } as any)} /> ); From 742a992098e6554cbc2644bc644712bfe394a23a Mon Sep 17 00:00:00 2001 From: Sidney Alcantara Date: Fri, 11 Nov 2022 14:59:07 +1100 Subject: [PATCH 37/66] fix date fields render loop --- src/components/fields/Date/EditorCell.tsx | 7 ++----- src/components/fields/Date/index.tsx | 2 +- src/components/fields/DateTime/EditorCell.tsx | 6 ++---- src/components/fields/DateTime/index.tsx | 2 +- 4 files changed, 6 insertions(+), 11 deletions(-) diff --git a/src/components/fields/Date/EditorCell.tsx b/src/components/fields/Date/EditorCell.tsx index 2b9523ed..56de95c1 100644 --- a/src/components/fields/Date/EditorCell.tsx +++ b/src/components/fields/Date/EditorCell.tsx @@ -63,11 +63,7 @@ export default function Date_({ }, "& .MuiInputAdornment-root": { m: 0 }, }} - // Prevent react-data-grid showing NullEditor, which unmounts this field - onDoubleClick={(e) => e.stopPropagation()} - onKeyDown={(e) => e.stopPropagation()} - // Touch mode: make the whole field clickable - onClick={props.inputProps?.onClick as any} + onClick={(e) => e.stopPropagation()} inputProps={{ ...props.inputProps, tabIndex }} /> )} @@ -86,6 +82,7 @@ export default function Date_({ components={{ OpenPickerIcon: ChevronDown }} disableOpenPicker={false} disabled={disabled} + PopperProps={{ onClick: (e) => e.stopPropagation() }} /> ); } diff --git a/src/components/fields/Date/index.tsx b/src/components/fields/Date/index.tsx index fdc634d0..1ff9b01a 100644 --- a/src/components/fields/Date/index.tsx +++ b/src/components/fields/Date/index.tsx @@ -28,7 +28,7 @@ export const config: IFieldConfig = { initializable: true, icon: , description: `Formatted date. Format is configurable, default: ${DATE_FORMAT}. Edited with a visual picker.`, - TableCell: withTableCell(DisplayCell, EditorCell, "inline", { + TableCell: withTableCell(DisplayCell, EditorCell, "focus", { disablePadding: true, }), SideDrawerField, diff --git a/src/components/fields/DateTime/EditorCell.tsx b/src/components/fields/DateTime/EditorCell.tsx index acf8ca69..5a8cf2f1 100644 --- a/src/components/fields/DateTime/EditorCell.tsx +++ b/src/components/fields/DateTime/EditorCell.tsx @@ -68,11 +68,8 @@ export default function DateTime({ }, "& .MuiInputAdornment-root": { m: 0 }, }} - // Prevent react-data-grid showing NullEditor, which unmounts this field - onDoubleClick={(e) => e.stopPropagation()} onKeyDown={(e) => e.stopPropagation()} - // Touch mode: make the whole field clickable - onClick={props.inputProps?.onClick as any} + onClick={(e) => e.stopPropagation()} inputProps={{ ...props.inputProps, tabIndex }} /> )} @@ -90,6 +87,7 @@ export default function DateTime({ components={{ OpenPickerIcon: ChevronDown }} disableOpenPicker={false} disabled={disabled} + PopperProps={{ onClick: (e) => e.stopPropagation() }} /> ); } diff --git a/src/components/fields/DateTime/index.tsx b/src/components/fields/DateTime/index.tsx index dade54b4..cc247b97 100644 --- a/src/components/fields/DateTime/index.tsx +++ b/src/components/fields/DateTime/index.tsx @@ -36,7 +36,7 @@ export const config: IFieldConfig = { initializable: true, icon: , description: `Formatted date & time. Format is configurable, default: ${DATE_TIME_FORMAT}. Edited with a visual picker.`, - TableCell: withTableCell(DisplayCell, EditorCell, "inline", { + TableCell: withTableCell(DisplayCell, EditorCell, "focus", { disablePadding: true, }), SideDrawerField, From f9656e2c4ed63954e9d129dcefa649f2324d7a6d Mon Sep 17 00:00:00 2001 From: Sidney Alcantara Date: Fri, 11 Nov 2022 15:49:47 +1100 Subject: [PATCH 38/66] migrate all remaining fields --- src/components/Table/Table.tsx | 25 +- src/components/Table/TableContainer.tsx | 252 ------------------ src/components/fields/File/DisplayCell.tsx | 36 +++ .../File/{TableCell.tsx => EditorCell.tsx} | 28 +- src/components/fields/File/index.tsx | 14 +- src/components/fields/Image/DisplayCell.tsx | 113 ++++++++ .../Image/{TableCell.tsx => EditorCell.tsx} | 184 ++++--------- src/components/fields/Image/index.tsx | 14 +- src/components/fields/Rating/DisplayCell.tsx | 68 +++++ src/components/fields/Rating/EditorCell.tsx | 27 ++ .../fields/Rating/SideDrawerField.tsx | 3 +- src/components/fields/Rating/TableCell.tsx | 55 ---- src/components/fields/Rating/index.tsx | 12 +- .../fields/SingleSelect/DisplayCell.tsx | 46 ++++ .../{PopoverCell.tsx => EditorCell.tsx} | 23 +- .../fields/SingleSelect/InlineCell.tsx | 51 ---- src/components/fields/SingleSelect/index.tsx | 18 +- src/components/fields/Status/DisplayCell.tsx | 71 +++++ .../{PopoverCell.tsx => EditorCell.tsx} | 23 +- src/components/fields/Status/InlineCell.tsx | 69 ----- src/components/fields/Status/index.tsx | 19 +- .../{TableCell.tsx => DisplayCell.tsx} | 24 +- .../fields/SubTable/SideDrawerField.tsx | 4 +- src/components/fields/SubTable/index.tsx | 14 +- src/hooks/useFirebaseStorageUploader.tsx | 5 +- 25 files changed, 527 insertions(+), 671 deletions(-) delete mode 100644 src/components/Table/TableContainer.tsx create mode 100644 src/components/fields/File/DisplayCell.tsx rename src/components/fields/File/{TableCell.tsx => EditorCell.tsx} (89%) create mode 100644 src/components/fields/Image/DisplayCell.tsx rename src/components/fields/Image/{TableCell.tsx => EditorCell.tsx} (51%) create mode 100644 src/components/fields/Rating/DisplayCell.tsx create mode 100644 src/components/fields/Rating/EditorCell.tsx delete mode 100644 src/components/fields/Rating/TableCell.tsx create mode 100644 src/components/fields/SingleSelect/DisplayCell.tsx rename src/components/fields/SingleSelect/{PopoverCell.tsx => EditorCell.tsx} (54%) delete mode 100644 src/components/fields/SingleSelect/InlineCell.tsx create mode 100644 src/components/fields/Status/DisplayCell.tsx rename src/components/fields/Status/{PopoverCell.tsx => EditorCell.tsx} (67%) delete mode 100644 src/components/fields/Status/InlineCell.tsx rename src/components/fields/SubTable/{TableCell.tsx => DisplayCell.tsx} (60%) diff --git a/src/components/Table/Table.tsx b/src/components/Table/Table.tsx index f8ca9486..ad26cb42 100644 --- a/src/components/Table/Table.tsx +++ b/src/components/Table/Table.tsx @@ -15,7 +15,6 @@ import { Draggable, } from "react-beautiful-dnd"; import { get } from "lodash-es"; -import { Portal } from "@mui/material"; import { ErrorBoundary } from "react-error-boundary"; import StyledTable from "./Styled/StyledTable"; @@ -426,6 +425,14 @@ export default function Table({ selectedCell?.path === row.original._rowy_ref.path && selectedCell?.columnKey === cell.column.id; + const fieldTypeGroup = getFieldProp( + "group", + cell.column.columnDef.meta?.type + ); + const isReadOnlyCell = + fieldTypeGroup === "Auditing" || + fieldTypeGroup === "Metadata"; + return ( - -
- Press Enter to edit. -
-
+
+ Press Enter to edit. +
diff --git a/src/components/Table/TableContainer.tsx b/src/components/Table/TableContainer.tsx deleted file mode 100644 index 10061298..00000000 --- a/src/components/Table/TableContainer.tsx +++ /dev/null @@ -1,252 +0,0 @@ -import { colord } from "colord"; -import { styled, alpha, darken, lighten } from "@mui/material"; -import { TOP_BAR_HEIGHT } from "@src/layouts/Navigation/TopBar"; -import { TABLE_TOOLBAR_HEIGHT } from "@src/components/TableToolbar"; -import { - DRAWER_COLLAPSED_WIDTH, - DRAWER_WIDTH, -} from "@src/components/SideDrawer"; - -export const OUT_OF_ORDER_MARGIN = 8; - -export const TableContainer = styled("div", { - shouldForwardProp: (prop) => prop !== "rowHeight", -})<{ rowHeight: number }>(({ theme, rowHeight }) => ({ - display: "flex", - position: "relative", - flexDirection: "column", - height: `calc(100vh - ${TOP_BAR_HEIGHT}px - ${TABLE_TOOLBAR_HEIGHT}px)`, - - "& .left-scroll-divider": { - position: "absolute", - top: 0, - bottom: 0, - left: 0, - width: 1, - zIndex: 1, - - backgroundColor: colord(theme.palette.background.paper) - .mix(theme.palette.divider, 0.12) - .alpha(1) - .toHslString(), - }, - - "& > .rdg": { - width: `calc(100% - ${DRAWER_COLLAPSED_WIDTH}px)`, - flex: 1, - paddingBottom: `max(env(safe-area-inset-bottom), ${theme.spacing(2)})`, - }, - - [theme.breakpoints.down("sm")]: { width: "100%" }, - - "& .rdg": { - "--color": theme.palette.text.primary, - "--border-color": theme.palette.divider, - // "--summary-border-color": "#aaa", - "--cell-background-color": - theme.palette.mode === "light" - ? theme.palette.background.paper - : colord(theme.palette.background.paper) - .mix("#fff", 0.04) - .alpha(1) - .toHslString(), - "--header-background-color": theme.palette.background.default, - "--row-hover-background-color": colord(theme.palette.background.paper) - .mix(theme.palette.action.hover, theme.palette.action.hoverOpacity) - .alpha(1) - .toHslString(), - "--row-selected-background-color": - theme.palette.mode === "light" - ? lighten(theme.palette.primary.main, 0.9) - : darken(theme.palette.primary.main, 0.8), - "--row-selected-hover-background-color": - theme.palette.mode === "light" - ? lighten(theme.palette.primary.main, 0.8) - : darken(theme.palette.primary.main, 0.7), - "--checkbox-color": theme.palette.primary.main, - "--checkbox-focus-color": theme.palette.primary.main, - "--checkbox-disabled-border-color": "#ccc", - "--checkbox-disabled-background-color": "#ddd", - "--selection-color": theme.palette.primary.main, - "--font-size": "0.75rem", - "--cell-padding": theme.spacing(0, 1.25), - - border: "none", - backgroundColor: "transparent", - - ...(theme.typography.caption as any), - // fontSize: "0.8125rem", - lineHeight: "inherit !important", - - "& .rdg-cell": { - display: "flex", - alignItems: "center", - padding: 0, - - overflow: "visible", - contain: "none", - position: "relative", - - lineHeight: "calc(var(--row-height) - 1px)", - }, - - "& .rdg-cell-frozen": { - position: "sticky", - }, - "& .rdg-cell-frozen-last": { - boxShadow: theme.shadows[2] - .replace(/, 0 (\d+px)/g, ", $1 0") - .split("),") - .slice(1) - .join("),"), - - "&[aria-selected=true]": { - boxShadow: - theme.shadows[2] - .replace(/, 0 (\d+px)/g, ", $1 0") - .split("),") - .slice(1) - .join("),") + ", inset 0 0 0 2px var(--selection-color)", - }, - }, - - "& .rdg-cell-copied": { - backgroundColor: - theme.palette.mode === "light" - ? lighten(theme.palette.primary.main, 0.7) - : darken(theme.palette.primary.main, 0.6), - }, - - "& .final-column-cell": { - backgroundColor: "var(--header-background-color)", - borderColor: "var(--header-background-color)", - color: theme.palette.text.disabled, - padding: "var(--cell-padding)", - }, - }, - - ".rdg-row, .rdg-header-row": { - marginLeft: `max(env(safe-area-inset-left), ${theme.spacing(2)})`, - marginRight: `max(env(safe-area-inset-right), ${DRAWER_WIDTH}px)`, - display: "inline-grid", // Fix Safari not showing margin-right - }, - - ".rdg-header-row .rdg-cell:first-of-type": { - borderTopLeftRadius: theme.shape.borderRadius, - }, - ".rdg-header-row .rdg-cell:last-of-type": { - borderTopRightRadius: theme.shape.borderRadius, - }, - - ".rdg-header-row .rdg-cell.final-column-header": { - border: "none", - padding: theme.spacing(0, 0.75), - borderBottomRightRadius: theme.shape.borderRadius, - - display: "flex", - alignItems: "center", - justifyContent: "flex-start", - - position: "relative", - "&::before": { - content: "''", - display: "block", - width: 88, - height: "100%", - - position: "absolute", - top: 0, - left: 0, - - border: "1px solid var(--border-color)", - borderLeftWidth: 0, - borderTopRightRadius: theme.shape.borderRadius, - borderBottomRightRadius: theme.shape.borderRadius, - }, - }, - - ".rdg-row .rdg-cell:first-of-type, .rdg-header-row .rdg-cell:first-of-type": { - borderLeft: "1px solid var(--border-color)", - }, - - ".rdg-row:last-of-type": { - borderBottomLeftRadius: theme.shape.borderRadius, - borderBottomRightRadius: theme.shape.borderRadius, - - "& .rdg-cell:first-of-type": { - borderBottomLeftRadius: theme.shape.borderRadius, - }, - "& .rdg-cell:nth-last-of-type(2)": { - borderBottomRightRadius: theme.shape.borderRadius, - }, - }, - - ".rdg-header-row .rdg-cell": { - borderTop: "1px solid var(--border-color)", - }, - - ".rdg-row:hover": { color: theme.palette.text.primary }, - - ".row-hover-iconButton": { - color: theme.palette.text.disabled, - transitionDuration: "0s", - }, - ".rdg-row:hover .row-hover-iconButton": { - color: theme.palette.text.primary, - backgroundColor: alpha( - theme.palette.action.hover, - theme.palette.action.hoverOpacity * 1.5 - ), - }, - - ".cell-collapse-padding": { - margin: theme.spacing(0, -1.25), - width: `calc(100% + ${theme.spacing(1.25 * 2)})`, - }, - - ".rdg-row.out-of-order": { - "--row-height": rowHeight + 1 + "px !important", - marginTop: -1, - marginBottom: OUT_OF_ORDER_MARGIN, - borderBottomLeftRadius: theme.shape.borderRadius, - - "& .rdg-cell:not(:last-of-type)": { - borderTop: `1px solid var(--border-color)`, - }, - "& .rdg-cell:first-of-type": { - borderBottomLeftRadius: theme.shape.borderRadius, - }, - "& .rdg-cell:nth-last-of-type(2)": { - borderBottomRightRadius: theme.shape.borderRadius, - }, - "&:not(:nth-of-type(4))": { - borderTopLeftRadius: theme.shape.borderRadius, - - "& .rdg-cell:first-of-type": { - borderTopLeftRadius: theme.shape.borderRadius, - }, - "& .rdg-cell:nth-last-of-type(2)": { - borderTopRightRadius: theme.shape.borderRadius, - }, - }, - - "& + .rdg-row:not(.out-of-order)": { - "--row-height": rowHeight + 1 + "px !important", - marginTop: -1, - borderTopLeftRadius: theme.shape.borderRadius, - - "& .rdg-cell:not(:last-of-type)": { - borderTop: `1px solid var(--border-color)`, - }, - "& .rdg-cell:first-of-type": { - borderTopLeftRadius: theme.shape.borderRadius, - }, - "& .rdg-cell:nth-last-of-type(2)": { - borderTopRightRadius: theme.shape.borderRadius, - }, - }, - }, -})); -TableContainer.displayName = "TableContainer"; - -export default TableContainer; diff --git a/src/components/fields/File/DisplayCell.tsx b/src/components/fields/File/DisplayCell.tsx new file mode 100644 index 00000000..73303ddf --- /dev/null +++ b/src/components/fields/File/DisplayCell.tsx @@ -0,0 +1,36 @@ +import { IDisplayCellProps } from "@src/components/fields/types"; + +import { Grid, Chip } from "@mui/material"; +import ChipList from "@src/components/Table/formatters/ChipList"; + +import { FileIcon } from "."; +import { FileValue } from "@src/types/table"; + +export default function File_({ value, tabIndex }: IDisplayCellProps) { + return ( + + {Array.isArray(value) && + value.map((file: FileValue) => ( + 1 ? { maxWidth: `calc(100% - 12px)` } : {} + } + > + } + label={file.name} + onClick={(e) => { + window.open(file.downloadURL); + e.stopPropagation(); + }} + style={{ width: "100%" }} + tabIndex={tabIndex} + /> + + ))} + + ); +} diff --git a/src/components/fields/File/TableCell.tsx b/src/components/fields/File/EditorCell.tsx similarity index 89% rename from src/components/fields/File/TableCell.tsx rename to src/components/fields/File/EditorCell.tsx index 685cf62e..deb5b5fa 100644 --- a/src/components/fields/File/TableCell.tsx +++ b/src/components/fields/File/EditorCell.tsx @@ -1,5 +1,5 @@ import { useCallback } from "react"; -import { IHeavyCellProps } from "@src/components/fields/types"; +import { IEditorCellProps } from "@src/components/fields/types"; import { useSetAtom } from "jotai"; import { findIndex } from "lodash-es"; @@ -20,12 +20,13 @@ import { FileValue } from "@src/types/table"; export default function File_({ column, - row, value, + onChange, onSubmit, disabled, - docRef, -}: IHeavyCellProps) { + _rowy_ref, + tabIndex, +}: IEditorCellProps) { const confirm = useSetAtom(confirmDialogAtom, projectScope); const updateField = useSetAtom(updateFieldAtom, tableScope); @@ -38,13 +39,13 @@ export default function File_({ if (file) { upload({ - docRef: docRef! as any, + docRef: _rowy_ref, fieldName: column.key, files: [file], previousValue: value, onComplete: (newValue) => { updateField({ - path: docRef.path, + path: _rowy_ref.path, fieldName: column.key, value: newValue, }); @@ -60,7 +61,8 @@ export default function File_({ const index = findIndex(newValue, ["ref", ref]); const toBeDeleted = newValue.splice(index, 1); toBeDeleted.length && deleteUpload(toBeDeleted[0]); - onSubmit(newValue); + onChange(newValue); + onSubmit(); }; const { getRootProps, getInputProps, isDragActive } = useDropzone({ @@ -73,11 +75,10 @@ export default function File_({ return ( @@ -130,6 +132,7 @@ export default function File_({ confirmColor: "error", }) } + tabIndex={tabIndex} style={{ width: "100%" }} /> @@ -146,8 +149,9 @@ export default function File_({ e.stopPropagation(); }} style={{ display: "flex" }} - className={docRef && "row-hover-iconButton"} - disabled={!docRef} + className={_rowy_ref && "row-hover-iconButton end"} + disabled={!_rowy_ref} + tabIndex={tabIndex} > @@ -163,7 +167,7 @@ export default function File_({
)} - + ); } diff --git a/src/components/fields/File/index.tsx b/src/components/fields/File/index.tsx index bdd0a323..2f883954 100644 --- a/src/components/fields/File/index.tsx +++ b/src/components/fields/File/index.tsx @@ -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 FileIcon from "@mui/icons-material/AttachFile"; -import BasicCell from "@src/components/fields/_BasicCell/BasicCellNull"; -import NullEditor from "@src/components/Table/editors/NullEditor"; +import DisplayCell from "./DisplayCell"; -const TableCell = lazy( - () => import("./TableCell" /* webpackChunkName: "TableCell-File" */) +const EditorCell = lazy( + () => import("./EditorCell" /* webpackChunkName: "EditorCell-File" */) ); const SideDrawerField = lazy( () => @@ -23,8 +22,9 @@ export const config: IFieldConfig = { initialValue: [], icon: , description: "File uploaded to Firebase Storage. Supports any file type.", - TableCell: withHeavyCell(BasicCell, TableCell), - TableEditor: NullEditor as any, + TableCell: withTableCell(DisplayCell, EditorCell, "inline", { + disablePadding: true, + }), SideDrawerField, }; export default config; diff --git a/src/components/fields/Image/DisplayCell.tsx b/src/components/fields/Image/DisplayCell.tsx new file mode 100644 index 00000000..0a4f955f --- /dev/null +++ b/src/components/fields/Image/DisplayCell.tsx @@ -0,0 +1,113 @@ +import { IDisplayCellProps } from "@src/components/fields/types"; +import { useAtom } from "jotai"; + +import { alpha, Theme, Stack, Grid, ButtonBase } from "@mui/material"; +import OpenIcon from "@mui/icons-material/OpenInNewOutlined"; + +import Thumbnail from "@src/components/Thumbnail"; + +import { tableSchemaAtom, tableScope } from "@src/atoms/tableScope"; +import { DEFAULT_ROW_HEIGHT } from "@src/components/Table"; +import { FileValue } from "@src/types/table"; + +// MULTIPLE +export const imgSx = (rowHeight: number) => ({ + position: "relative", + display: "flex", + + width: (theme: Theme) => `calc(${rowHeight}px - ${theme.spacing(1)} - 1px)`, + height: (theme: Theme) => `calc(${rowHeight}px - ${theme.spacing(1)} - 1px)`, + + backgroundSize: "contain", + backgroundPosition: "center center", + backgroundRepeat: "no-repeat", + + borderRadius: 1, +}); +export const thumbnailSx = { + position: "absolute", + top: 0, + left: 0, + width: "100%", + height: "100%", +}; +export const deleteImgHoverSx = { + position: "absolute", + top: 0, + left: 0, + bottom: 0, + right: 0, + + color: "text.secondary", + boxShadow: (theme: Theme) => `0 0 0 1px ${theme.palette.divider} inset`, + borderRadius: 1, + + transition: (theme: Theme) => + theme.transitions.create("background-color", { + duration: theme.transitions.duration.shortest, + }), + + "& *": { + opacity: 0, + transition: (theme: Theme) => + theme.transitions.create("opacity", { + duration: theme.transitions.duration.shortest, + }), + }, + + ".img:hover &, .img:focus &": { + backgroundColor: (theme: Theme) => + alpha(theme.palette.background.paper, 0.8), + "& *": { opacity: 1 }, + }, +}; + +export default function Image_({ value, tabIndex }: IDisplayCellProps) { + const [tableSchema] = useAtom(tableSchemaAtom, tableScope); + + const rowHeight = tableSchema.rowHeight ?? DEFAULT_ROW_HEIGHT; + let thumbnailSize = "100x100"; + if (rowHeight > 50) thumbnailSize = "200x200"; + if (rowHeight > 100) thumbnailSize = "400x400"; + + return ( + + + {Array.isArray(value) && + value.map((file: FileValue, i) => ( + + { + window.open(file.downloadURL, "_blank")} + tabIndex={tabIndex} + > + + + + + + } + + ))} + + + ); +} diff --git a/src/components/fields/Image/TableCell.tsx b/src/components/fields/Image/EditorCell.tsx similarity index 51% rename from src/components/fields/Image/TableCell.tsx rename to src/components/fields/Image/EditorCell.tsx index 9455a059..6d7cdbe2 100644 --- a/src/components/fields/Image/TableCell.tsx +++ b/src/components/fields/Image/EditorCell.tsx @@ -1,23 +1,12 @@ import { useCallback, useState } from "react"; -import { IHeavyCellProps } from "@src/components/fields/types"; +import { IEditorCellProps } from "@src/components/fields/types"; import { useAtom, useSetAtom } from "jotai"; -import { findIndex } from "lodash-es"; import { useDropzone } from "react-dropzone"; -import { - alpha, - Theme, - Box, - Stack, - Grid, - IconButton, - ButtonBase, - Tooltip, -} from "@mui/material"; +import { alpha, Box, Stack, Grid, IconButton, ButtonBase } from "@mui/material"; import AddIcon from "@mui/icons-material/AddAPhotoOutlined"; import DeleteIcon from "@mui/icons-material/DeleteOutlined"; -import OpenIcon from "@mui/icons-material/OpenInNewOutlined"; import Thumbnail from "@src/components/Thumbnail"; import CircularProgressOptical from "@src/components/CircularProgressOptical"; @@ -32,66 +21,17 @@ import useUploader from "@src/hooks/useFirebaseStorageUploader"; import { IMAGE_MIME_TYPES } from "./index"; import { DEFAULT_ROW_HEIGHT } from "@src/components/Table"; import { FileValue } from "@src/types/table"; - -// MULTIPLE -const imgSx = (rowHeight: number) => ({ - position: "relative", - display: "flex", - - width: (theme: Theme) => `calc(${rowHeight}px - ${theme.spacing(1)} - 1px)`, - height: (theme: Theme) => `calc(${rowHeight}px - ${theme.spacing(1)} - 1px)`, - - backgroundSize: "contain", - backgroundPosition: "center center", - backgroundRepeat: "no-repeat", - - borderRadius: 1, -}); -const thumbnailSx = { - position: "absolute", - top: 0, - left: 0, - width: "100%", - height: "100%", -}; -const deleteImgHoverSx = { - position: "absolute", - top: 0, - left: 0, - bottom: 0, - right: 0, - - color: "text.secondary", - boxShadow: (theme: Theme) => `0 0 0 1px ${theme.palette.divider} inset`, - borderRadius: 1, - - transition: (theme: Theme) => - theme.transitions.create("background-color", { - duration: theme.transitions.duration.shortest, - }), - - "& *": { - opacity: 0, - transition: (theme: Theme) => - theme.transitions.create("opacity", { - duration: theme.transitions.duration.shortest, - }), - }, - - ".img:hover &": { - backgroundColor: (theme: Theme) => - alpha(theme.palette.background.paper, 0.8), - "& *": { opacity: 1 }, - }, -}; +import { imgSx, thumbnailSx, deleteImgHoverSx } from "./DisplayCell"; export default function Image_({ column, value, + onChange, onSubmit, disabled, - docRef, -}: IHeavyCellProps) { + _rowy_ref, + tabIndex, +}: IEditorCellProps) { const confirm = useSetAtom(confirmDialogAtom, projectScope); const updateField = useSetAtom(updateFieldAtom, tableScope); const [tableSchema] = useAtom(tableSchemaAtom, tableScope); @@ -107,13 +47,13 @@ export default function Image_({ if (imageFile) { upload({ - docRef: docRef! as any, + docRef: _rowy_ref, fieldName: column.key, files: [imageFile], previousValue: value, onComplete: (newValue) => { updateField({ - path: docRef.path, + path: _rowy_ref.path, fieldName: column.key, value: newValue, }); @@ -130,7 +70,8 @@ export default function Image_({ const newValue = [...value]; const toBeDeleted = newValue.splice(index, 1); toBeDeleted.length && deleteUpload(toBeDeleted[0]); - onSubmit(newValue); + onChange(newValue); + onSubmit(); }; const { getRootProps, getInputProps, isDragActive } = useDropzone({ @@ -154,9 +95,8 @@ export default function Image_({ { py: 0, pl: 1, - pr: 0.5, - outline: "none", height: "100%", + width: "100%", }, isDragActive ? { @@ -172,6 +112,7 @@ export default function Image_({ ]} alignItems="center" {...dropzoneProps} + tabIndex={tabIndex} onClick={undefined} >
( - {disabled ? ( - - window.open(file.downloadURL, "_blank")} - > - - - {disabled ? ( - - ) : ( - - )} - - - - ) : ( - -
- { - confirm({ - title: "Delete image?", - body: "This image cannot be recovered after", - confirm: "Delete", - confirmColor: "error", - handleConfirm: handleDelete(i), - }); - }} - > - - - - - -
-
- )} + { + confirm({ + title: "Delete image?", + body: "This image cannot be recovered after", + confirm: "Delete", + confirmColor: "error", + handleConfirm: handleDelete(i), + }); + }} + disabled={disabled} + tabIndex={tabIndex} + > + + + + +
))} @@ -275,8 +186,9 @@ export default function Image_({ e.stopPropagation(); }} style={{ display: "flex" }} - className={docRef && "row-hover-iconButton"} - disabled={!docRef} + className={_rowy_ref && "row-hover-iconButton end"} + disabled={!_rowy_ref} + tabIndex={tabIndex} > @@ -292,7 +204,7 @@ export default function Image_({
)} - + ); } diff --git a/src/components/fields/Image/index.tsx b/src/components/fields/Image/index.tsx index b29a7dab..1dbcde26 100644 --- a/src/components/fields/Image/index.tsx +++ b/src/components/fields/Image/index.tsx @@ -1,14 +1,13 @@ 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 { Image as ImageIcon } from "@src/assets/icons"; -import BasicCell from "@src/components/fields/_BasicCell/BasicCellNull"; -import NullEditor from "@src/components/Table/editors/NullEditor"; +import DisplayCell from "./DisplayCell"; import ContextMenuActions from "./ContextMenuActions"; -const TableCell = lazy( - () => import("./TableCell" /* webpackChunkName: "TableCell-Image" */) +const EditorCell = lazy( + () => import("./EditorCell" /* webpackChunkName: "EditorCell-Image" */) ); const SideDrawerField = lazy( () => @@ -24,8 +23,9 @@ export const config: IFieldConfig = { icon: , description: "Image file uploaded to Firebase Storage. Supports JPEG, PNG, SVG, GIF, WebP, AVIF, JPEG XL.", - TableCell: withHeavyCell(BasicCell, TableCell), - TableEditor: NullEditor as any, + TableCell: withTableCell(DisplayCell, EditorCell, "inline", { + disablePadding: true, + }), SideDrawerField, contextMenuActions: ContextMenuActions, }; diff --git a/src/components/fields/Rating/DisplayCell.tsx b/src/components/fields/Rating/DisplayCell.tsx new file mode 100644 index 00000000..1e539ad4 --- /dev/null +++ b/src/components/fields/Rating/DisplayCell.tsx @@ -0,0 +1,68 @@ +import React, { forwardRef } from "react"; +import { IDisplayCellProps } from "@src/components/fields/types"; + +import MuiRating, { RatingProps as MuiRatingProps } from "@mui/material/Rating"; +import RatingIcon from "@mui/icons-material/Star"; +import RatingOutlineIcon from "@mui/icons-material/StarBorder"; +import { get } from "lodash-es"; + +export const getStateIcon = (config: any) => { + // only use the config to get the custom rating icon if enabled via toggle + if (!get(config, "customIcons.enabled")) { + return ; + } + return get(config, "customIcons.rating") || ; +}; + +export const getStateOutline = (config: any) => { + if (!get(config, "customIcons.enabled")) { + return ; + } + return get(config, "customIcons.rating") || ; +}; + +export const Rating = forwardRef(function Rating( + { + _rowy_ref, + column, + value, + disabled, + onChange, + }: IDisplayCellProps & Pick, + ref: React.Ref +) { + // Set max and precision from config + const { + max, + precision, + }: { + max: number; + precision: number; + } = { + max: 5, + precision: 1, + ...column.config, + }; + + return ( + e.stopPropagation()} + onKeyDown={(e) => { + if (["ArrowLeft", "ArrowRight", "ArrowUp", "ArrowDown"].includes(e.key)) + e.stopPropagation(); + }} + icon={getStateIcon(column.config)} + size="small" + readOnly={disabled} + emptyIcon={getStateOutline(column.config)} + max={max} + precision={precision} + sx={{ mx: -0.25 }} + /> + ); +}); +export default Rating; diff --git a/src/components/fields/Rating/EditorCell.tsx b/src/components/fields/Rating/EditorCell.tsx new file mode 100644 index 00000000..4d3c0c23 --- /dev/null +++ b/src/components/fields/Rating/EditorCell.tsx @@ -0,0 +1,27 @@ +import { useRef, useEffect } from "react"; +import { IEditorCellProps } from "@src/components/fields/types"; +import DisplayCell from "./DisplayCell"; + +export default function Rating({ + onChange, + tabIndex, + ...props +}: IEditorCellProps) { + const ref = useRef(null); + useEffect(() => { + const el = ref.current; + if (!el) return; + const inputs = el.querySelectorAll("input"); + for (const input of inputs) + input.setAttribute("tabindex", tabIndex.toString()); + }, [tabIndex]); + + return ( + onChange(newValue)} + ref={ref} + /> + ); +} diff --git a/src/components/fields/Rating/SideDrawerField.tsx b/src/components/fields/Rating/SideDrawerField.tsx index eb117dd4..389d3211 100644 --- a/src/components/fields/Rating/SideDrawerField.tsx +++ b/src/components/fields/Rating/SideDrawerField.tsx @@ -3,7 +3,7 @@ import { ISideDrawerFieldProps } from "@src/components/fields/types"; import { Grid } from "@mui/material"; import { Rating as MuiRating } from "@mui/material"; import "@mui/lab"; -import { getStateIcon, getStateOutline } from "./TableCell"; +import { getStateIcon, getStateOutline } from "./DisplayCell"; import { fieldSx } from "@src/components/SideDrawer/utils"; export default function Rating({ @@ -24,7 +24,6 @@ export default function Rating({ value={typeof value === "number" ? value : 0} disabled={disabled} onChange={(_, newValue) => { - console.log("onChange", newValue); onChange(newValue); onSubmit(); }} diff --git a/src/components/fields/Rating/TableCell.tsx b/src/components/fields/Rating/TableCell.tsx deleted file mode 100644 index 2e7aee52..00000000 --- a/src/components/fields/Rating/TableCell.tsx +++ /dev/null @@ -1,55 +0,0 @@ -import { IHeavyCellProps } from "@src/components/fields/types"; - -import MuiRating from "@mui/material/Rating"; -import RatingIcon from "@mui/icons-material/Star"; -import RatingOutlineIcon from "@mui/icons-material/StarBorder" -import { get } from "lodash-es"; - - -export const getStateIcon = (config: any) => { - // only use the config to get the custom rating icon if enabled via toggle - if (!get(config, "customIcons.enabled")) { return } - return get(config, "customIcons.rating") || ; -}; - -export const getStateOutline = (config: any) => { - if (!get(config, "customIcons.enabled")) { return } - return get(config, "customIcons.rating") || ; -} - -export default function Rating({ - row, - column, - value, - onSubmit, - disabled, -}: IHeavyCellProps) { - // Set max and precision from config - const { - max, - precision, - }: { - max: number; - precision: number; - } = { - max: 5, - precision: 1, - ...column.config, - }; - - return ( - e.stopPropagation()} - icon={getStateIcon(column.config)} - size="small" - disabled={disabled} - onChange={(_, newValue) => onSubmit(newValue)} - emptyIcon={getStateOutline(column.config)} - max={max} - precision={precision} - sx={{ mx: -0.25 }} - /> - ); -} diff --git a/src/components/fields/Rating/index.tsx b/src/components/fields/Rating/index.tsx index 3cbc5108..eac10ed3 100644 --- a/src/components/fields/Rating/index.tsx +++ b/src/components/fields/Rating/index.tsx @@ -1,15 +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 RatingIcon from "@mui/icons-material/StarBorder"; -import BasicCell from "@src/components/fields/_BasicCell/BasicCellNull"; -import NullEditor from "@src/components/Table/editors/NullEditor"; +import DisplayCell from "./DisplayCell"; +import EditorCell from "./EditorCell"; import { filterOperators } from "@src/components/fields/Number/Filter"; -const TableCell = lazy( - () => import("./TableCell" /* webpackChunkName: "TableCell-Rating" */) -); const SideDrawerField = lazy( () => import("./SideDrawerField" /* webpackChunkName: "SideDrawerField-Rating" */) @@ -29,8 +26,7 @@ export const config: IFieldConfig = { requireConfiguration: true, description: "Rating displayed as stars. Max stars is configurable, default: 5 stars.", - TableCell: withHeavyCell(BasicCell, TableCell), - TableEditor: NullEditor as any, + TableCell: withTableCell(DisplayCell, EditorCell, "inline"), settings: Settings, SideDrawerField, filter: { diff --git a/src/components/fields/SingleSelect/DisplayCell.tsx b/src/components/fields/SingleSelect/DisplayCell.tsx new file mode 100644 index 00000000..ece750d1 --- /dev/null +++ b/src/components/fields/SingleSelect/DisplayCell.tsx @@ -0,0 +1,46 @@ +import { IDisplayCellProps } from "@src/components/fields/types"; + +import { ButtonBase } from "@mui/material"; +import { ChevronDown } from "@src/assets/icons"; + +import { sanitiseValue } from "./utils"; + +export default function SingleSelect({ + value, + showPopoverCell, + disabled, + tabIndex, +}: IDisplayCellProps) { + const rendered = ( +
+ {sanitiseValue(value)} +
+ ); + + if (disabled) return rendered; + + return ( + showPopoverCell(true)} + style={{ + width: "100%", + height: "100%", + font: "inherit", + color: "inherit !important", + letterSpacing: "inherit", + textAlign: "inherit", + justifyContent: "flex-start", + }} + tabIndex={tabIndex} + > + {rendered} + + + ); +} diff --git a/src/components/fields/SingleSelect/PopoverCell.tsx b/src/components/fields/SingleSelect/EditorCell.tsx similarity index 54% rename from src/components/fields/SingleSelect/PopoverCell.tsx rename to src/components/fields/SingleSelect/EditorCell.tsx index 4aafda6b..0fd1c317 100644 --- a/src/components/fields/SingleSelect/PopoverCell.tsx +++ b/src/components/fields/SingleSelect/EditorCell.tsx @@ -1,23 +1,24 @@ -import { IPopoverCellProps } from "@src/components/fields/types"; +import { IEditorCellProps } from "@src/components/fields/types"; -import MultiSelect_ from "@rowy/multiselect"; +import MultiSelectComponent from "@rowy/multiselect"; import { sanitiseValue } from "./utils"; export default function SingleSelect({ value, + onChange, onSubmit, column, parentRef, showPopoverCell, disabled, -}: IPopoverCellProps) { +}: IEditorCellProps) { const config = column.config ?? {}; return ( - showPopoverCell(false)} + onClose={() => { + showPopoverCell(false); + onSubmit(); + }} /> ); } diff --git a/src/components/fields/SingleSelect/InlineCell.tsx b/src/components/fields/SingleSelect/InlineCell.tsx deleted file mode 100644 index d133b1cc..00000000 --- a/src/components/fields/SingleSelect/InlineCell.tsx +++ /dev/null @@ -1,51 +0,0 @@ -import { forwardRef } from "react"; -import { IPopoverInlineCellProps } from "@src/components/fields/types"; - -import { ButtonBase } from "@mui/material"; -import { ChevronDown } from "@src/assets/icons"; - -import { sanitiseValue } from "./utils"; - -export const SingleSelect = forwardRef(function SingleSelect( - { value, showPopoverCell, disabled }: IPopoverInlineCellProps, - ref: React.Ref -) { - return ( - showPopoverCell(true)} - ref={ref} - disabled={disabled} - className="cell-collapse-padding" - style={{ - padding: "var(--cell-padding)", - paddingRight: 0, - height: "100%", - - font: "inherit", - color: "inherit !important", - letterSpacing: "inherit", - textAlign: "inherit", - justifyContent: "flex-start", - }} - > -
- {sanitiseValue(value)} -
- - {!disabled && ( - - )} -
- ); -}); - -export default SingleSelect; diff --git a/src/components/fields/SingleSelect/index.tsx b/src/components/fields/SingleSelect/index.tsx index 9d396280..187be997 100644 --- a/src/components/fields/SingleSelect/index.tsx +++ b/src/components/fields/SingleSelect/index.tsx @@ -1,17 +1,12 @@ 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 { SingleSelect as SingleSelectIcon } from "@src/assets/icons"; -import BasicCell from "@src/components/fields/_BasicCell/BasicCellNull"; -import InlineCell from "./InlineCell"; -import NullEditor from "@src/components/Table/editors/NullEditor"; +import DisplayCell from "./DisplayCell"; +import EditorCell from "./EditorCell"; import { filterOperators } from "@src/components/fields/ShortText/Filter"; -const PopoverCell = lazy( - () => - import("./PopoverCell" /* webpackChunkName: "PopoverCell-SingleSelect" */) -); const SideDrawerField = lazy( () => import( @@ -32,11 +27,10 @@ export const config: IFieldConfig = { icon: , description: "Single value 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, EditorCell, "popover", { + disablePadding: true, + transparentPopover: true, }), - TableEditor: NullEditor as any, SideDrawerField, settings: Settings, filter: { operators: filterOperators }, diff --git a/src/components/fields/Status/DisplayCell.tsx b/src/components/fields/Status/DisplayCell.tsx new file mode 100644 index 00000000..9717a19c --- /dev/null +++ b/src/components/fields/Status/DisplayCell.tsx @@ -0,0 +1,71 @@ +import { forwardRef, useMemo } from "react"; +import { IDisplayCellProps } from "@src/components/fields/types"; + +import { ButtonBase } from "@mui/material"; +import { ChevronDown } from "@src/assets/icons"; +import getLabel from "./utils/getLabelHelper"; + +export const StatusSingleSelect = forwardRef(function StatusSingleSelect({ + column, + value, + showPopoverCell, + disabled, + tabIndex, +}: IDisplayCellProps) { + const conditions = column.config?.conditions; + + const rendered = useMemo(() => { + const lowPriorityOperator = ["<", "<=", ">=", ">"]; + const otherOperator = (conditions ?? []).filter( + (c: any) => !lowPriorityOperator.includes(c.operator) + ); + + /**Revisit this */ + const sortLowPriorityList = (conditions ?? []) + .filter((c: any) => { + return lowPriorityOperator.includes(c.operator); + }) + .sort((a: any, b: any) => { + const aDistFromValue = Math.abs(value - a.value); + const bDistFromValue = Math.abs(value - b.value); + //return the smallest distance + return aDistFromValue - bDistFromValue; + }); + const sortedConditions = [...otherOperator, ...sortLowPriorityList]; + + return ( +
+ {getLabel(value, sortedConditions)} +
+ ); + }, [value, conditions]); + + if (disabled) return rendered; + + return ( + showPopoverCell(true)} + style={{ + width: "100%", + height: "100%", + font: "inherit", + color: "inherit !important", + letterSpacing: "inherit", + textAlign: "inherit", + justifyContent: "flex-start", + }} + tabIndex={tabIndex} + > + {rendered} + + + ); +}); + +export default StatusSingleSelect; diff --git a/src/components/fields/Status/PopoverCell.tsx b/src/components/fields/Status/EditorCell.tsx similarity index 67% rename from src/components/fields/Status/PopoverCell.tsx rename to src/components/fields/Status/EditorCell.tsx index 8ca1a63e..538aa4fc 100644 --- a/src/components/fields/Status/PopoverCell.tsx +++ b/src/components/fields/Status/EditorCell.tsx @@ -1,14 +1,15 @@ -import { IPopoverCellProps } from "@src/components/fields/types"; -import MultiSelect_ from "@rowy/multiselect"; +import { IEditorCellProps } from "@src/components/fields/types"; +import MultiSelectComponent from "@rowy/multiselect"; export default function StatusSingleSelect({ value, + onChange, onSubmit, column, parentRef, showPopoverCell, disabled, -}: IPopoverCellProps) { +}: IEditorCellProps) { const config = column.config ?? {}; const conditions = config.conditions ?? []; /**Revisit eventually, can we abstract or use a helper function to clean this? */ @@ -22,9 +23,9 @@ export default function StatusSingleSelect({ }); return ( // eslint-disable-next-line react/jsx-pascal-case - onSubmit(v)} + onChange={(v) => onChange(v)} options={conditions.length >= 1 ? reMappedConditions : []} // this handles when conditions are deleted multiple={false} freeText={config.freeText} @@ -37,12 +38,18 @@ export default function StatusSingleSelect({ open: true, MenuProps: { anchorEl: parentRef, - anchorOrigin: { vertical: "bottom", horizontal: "left" }, - transformOrigin: { vertical: "top", horizontal: "left" }, + anchorOrigin: { vertical: "bottom", horizontal: "center" }, + transformOrigin: { vertical: "top", horizontal: "center" }, + sx: { + "& .MuiPaper-root": { minWidth: `${column.width}px !important` }, + }, }, }, }} - onClose={() => showPopoverCell(false)} + onClose={() => { + showPopoverCell(false); + onSubmit(); + }} /> ); } diff --git a/src/components/fields/Status/InlineCell.tsx b/src/components/fields/Status/InlineCell.tsx deleted file mode 100644 index 35176e3d..00000000 --- a/src/components/fields/Status/InlineCell.tsx +++ /dev/null @@ -1,69 +0,0 @@ -import { forwardRef, useMemo } from "react"; -import { IPopoverInlineCellProps } from "@src/components/fields/types"; - -import { ButtonBase } from "@mui/material"; -import { ChevronDown } from "@src/assets/icons"; -import getLabel from "./utils/getLabelHelper"; - -export const StatusSingleSelect = forwardRef(function StatusSingleSelect( - { column, value, showPopoverCell, disabled }: IPopoverInlineCellProps, - ref: React.Ref -) { - const conditions = column.config?.conditions ?? []; - const lowPriorityOperator = ["<", "<=", ">=", ">"]; - const otherOperator = conditions.filter( - (c: any) => !lowPriorityOperator.includes(c.operator) - ); - - /**Revisit this */ - const sortLowPriorityList = conditions - .filter((c: any) => { - return lowPriorityOperator.includes(c.operator); - }) - .sort((a: any, b: any) => { - const aDistFromValue = Math.abs(value - a.value); - const bDistFromValue = Math.abs(value - b.value); - //return the smallest distance - return aDistFromValue - bDistFromValue; - }); - const sortedConditions = [...otherOperator, ...sortLowPriorityList]; - const label = useMemo( - () => getLabel(value, sortedConditions), - [value, sortedConditions] - ); - return ( - showPopoverCell(true)} - ref={ref} - disabled={disabled} - className="cell-collapse-padding" - style={{ - padding: "var(--cell-padding)", - paddingRight: 0, - height: "100%", - font: "inherit", - color: "inherit !important", - letterSpacing: "inherit", - textAlign: "inherit", - justifyContent: "flex-start", - }} - > -
{label}
- - {!disabled && ( - - )} -
- ); -}); - -export default StatusSingleSelect; diff --git a/src/components/fields/Status/index.tsx b/src/components/fields/Status/index.tsx index 16b1ecf8..d4d0e978 100644 --- a/src/components/fields/Status/index.tsx +++ b/src/components/fields/Status/index.tsx @@ -1,13 +1,11 @@ import { lazy } from "react"; import { IFieldConfig, FieldType } from "@src/components/fields/types"; -import { Status as StatusIcon } from "@src/assets/icons"; -import NullEditor from "@src/components/Table/editors/NullEditor"; +import withTableCell from "@src/components/Table/withTableCell"; +import { Status as StatusIcon } from "@src/assets/icons"; +import DisplayCell from "./DisplayCell"; +import EditorCell from "./EditorCell"; import { filterOperators } from "./Filter"; -import BasicCell from "@src/components/fields/_BasicCell/BasicCellNull"; -import PopoverCell from "./PopoverCell"; -import InlineCell from "./InlineCell"; -import withPopoverCell from "@src/components/fields/_withTableCell/withPopoverCell"; const SideDrawerField = lazy( () => @@ -25,12 +23,11 @@ export const config: IFieldConfig = { initialValue: undefined, initializable: true, icon: , - description: "Displays field value as custom status text. Read-only. ", - TableCell: withPopoverCell(BasicCell, InlineCell, PopoverCell, { - anchorOrigin: { horizontal: "left", vertical: "bottom" }, - transparent: true, + description: "Displays field value as custom status text.", + TableCell: withTableCell(DisplayCell, EditorCell, "popover", { + disablePadding: true, + transparentPopover: true, }), - TableEditor: NullEditor as any, settings: Settings, SideDrawerField, requireConfiguration: true, diff --git a/src/components/fields/SubTable/TableCell.tsx b/src/components/fields/SubTable/DisplayCell.tsx similarity index 60% rename from src/components/fields/SubTable/TableCell.tsx rename to src/components/fields/SubTable/DisplayCell.tsx index 72c2df42..f9c9c3e7 100644 --- a/src/components/fields/SubTable/TableCell.tsx +++ b/src/components/fields/SubTable/DisplayCell.tsx @@ -1,27 +1,31 @@ -import { IHeavyCellProps } from "@src/components/fields/types"; +import { IDisplayCellProps } from "@src/components/fields/types"; import { Link } from "react-router-dom"; import { Stack, IconButton } from "@mui/material"; -import LaunchIcon from "@mui/icons-material/Launch"; +import OpenIcon from "@mui/icons-material/OpenInBrowser"; import { useSubTableData } from "./utils"; -export default function SubTable({ column, row }: IHeavyCellProps) { +export default function SubTable({ + column, + row, + _rowy_ref, + tabIndex, +}: IDisplayCellProps) { const { documentCount, label, subTablePath } = useSubTableData( column as any, row, - row._rowy_ref + _rowy_ref ); - if (!row._rowy_ref) return null; + if (!_rowy_ref) return null; return (
{documentCount} {column.name as string}: {label} @@ -30,12 +34,12 @@ export default function SubTable({ column, row }: IHeavyCellProps) { - + ); diff --git a/src/components/fields/SubTable/SideDrawerField.tsx b/src/components/fields/SubTable/SideDrawerField.tsx index 32f2b1f8..abf8e1e0 100644 --- a/src/components/fields/SubTable/SideDrawerField.tsx +++ b/src/components/fields/SubTable/SideDrawerField.tsx @@ -6,7 +6,7 @@ import { ISideDrawerFieldProps } from "@src/components/fields/types"; import { Link } from "react-router-dom"; import { Box, Stack, IconButton } from "@mui/material"; -import LaunchIcon from "@mui/icons-material/Launch"; +import OpenIcon from "@mui/icons-material/OpenInBrowser"; import { tableScope, tableRowsAtom } from "@src/atoms/tableScope"; import { fieldSx, getFieldId } from "@src/components/SideDrawer/utils"; @@ -46,7 +46,7 @@ export default function SubTable({ column, _rowy_ref }: ISideDrawerFieldProps) { sx={{ ml: 1 }} disabled={!subTablePath} > - + ); diff --git a/src/components/fields/SubTable/index.tsx b/src/components/fields/SubTable/index.tsx index b7ce171e..84df815d 100644 --- a/src/components/fields/SubTable/index.tsx +++ b/src/components/fields/SubTable/index.tsx @@ -1,14 +1,10 @@ import { lazy } from "react"; import { IFieldConfig, FieldType } from "@src/components/fields/types"; -import withHeavyCell from "@src/components/fields/_withTableCell/withHeavyCell"; +import withTableCell from "@src/components/Table/withTableCell"; import { SubTable as SubTableIcon } from "@src/assets/icons"; -import BasicCell from "@src/components/fields/_BasicCell/BasicCellName"; -import NullEditor from "@src/components/Table/editors/NullEditor"; +import DisplayCell from "./DisplayCell"; -const TableCell = lazy( - () => import("./TableCell" /* webpackChunkName: "TableCell-SubTable" */) -); const SideDrawerField = lazy( () => import( @@ -28,8 +24,10 @@ export const config: IFieldConfig = { settings: Settings, description: "Connects to a sub-table in the current row. Also displays number of rows inside the sub-table. Max sub-table depth: 100.", - TableCell: withHeavyCell(BasicCell, TableCell), - TableEditor: NullEditor as any, + TableCell: withTableCell(DisplayCell, null, "focus", { + usesRowData: true, + disablePadding: true, + }), SideDrawerField, initializable: false, requireConfiguration: true, diff --git a/src/hooks/useFirebaseStorageUploader.tsx b/src/hooks/useFirebaseStorageUploader.tsx index 16fbd521..ae0681fd 100644 --- a/src/hooks/useFirebaseStorageUploader.tsx +++ b/src/hooks/useFirebaseStorageUploader.tsx @@ -1,7 +1,6 @@ import { useReducer } from "react"; import { useAtom } from "jotai"; import { useSnackbar } from "notistack"; -import type { DocumentReference } from "firebase/firestore"; import { ref, uploadBytesResumable, @@ -15,7 +14,7 @@ import InlineOpenInNewIcon from "@src/components/InlineOpenInNewIcon"; import { projectScope } from "@src/atoms/projectScope"; import { firebaseStorageAtom } from "@src/sources/ProjectSourceFirebase"; import { WIKI_LINKS } from "@src/constants/externalLinks"; -import { FileValue } from "@src/types/table"; +import { FileValue, TableRowRef } from "@src/types/table"; export type UploaderState = { progress: number; @@ -30,7 +29,7 @@ const uploadReducer = ( ) => ({ ...prevState, ...newProps }); export type UploadProps = { - docRef: DocumentReference; + docRef: TableRowRef; fieldName: string; files: File[]; previousValue?: FileValue[]; From 06049830ecc85d1ca90c91cc61c0f67c029b5548 Mon Sep 17 00:00:00 2001 From: Sidney Alcantara Date: Fri, 11 Nov 2022 15:49:55 +1100 Subject: [PATCH 39/66] remove unused code --- .../BasicCellContextMenuActions.tsx | 0 src/components/Table/editors/NullEditor.tsx | 25 --- src/components/Table/editors/styles.ts | 9 -- .../Table/editors/withNullEditor.tsx | 44 ----- .../Table/editors/withSideDrawerEditor.tsx | 53 ------ src/components/fields/Email/index.tsx | 4 +- src/components/fields/LongText/index.tsx | 2 +- src/components/fields/Number/index.tsx | 2 +- src/components/fields/Percentage/index.tsx | 2 +- src/components/fields/Phone/index.tsx | 4 +- src/components/fields/RichText/index.tsx | 2 +- .../fields/ShortText/DisplayCell.tsx | 6 + src/components/fields/ShortText/index.tsx | 6 +- src/components/fields/Url/index.tsx | 2 +- .../fields/_BasicCell/BasicCellName.tsx | 5 - .../fields/_BasicCell/BasicCellNull.tsx | 3 - .../fields/_BasicCell/BasicCellValue.tsx | 6 - .../fields/_withTableCell/withBasicCell.tsx | 29 ---- .../fields/_withTableCell/withHeavyCell.tsx | 83 ---------- .../fields/_withTableCell/withPopoverCell.tsx | 151 ------------------ src/components/fields/types.ts | 25 --- 21 files changed, 18 insertions(+), 445 deletions(-) rename src/components/{fields/_BasicCell => Table/ContextMenu}/BasicCellContextMenuActions.tsx (100%) delete mode 100644 src/components/Table/editors/NullEditor.tsx delete mode 100644 src/components/Table/editors/styles.ts delete mode 100644 src/components/Table/editors/withNullEditor.tsx delete mode 100644 src/components/Table/editors/withSideDrawerEditor.tsx create mode 100644 src/components/fields/ShortText/DisplayCell.tsx delete mode 100644 src/components/fields/_BasicCell/BasicCellName.tsx delete mode 100644 src/components/fields/_BasicCell/BasicCellNull.tsx delete mode 100644 src/components/fields/_BasicCell/BasicCellValue.tsx delete mode 100644 src/components/fields/_withTableCell/withBasicCell.tsx delete mode 100644 src/components/fields/_withTableCell/withHeavyCell.tsx delete mode 100644 src/components/fields/_withTableCell/withPopoverCell.tsx diff --git a/src/components/fields/_BasicCell/BasicCellContextMenuActions.tsx b/src/components/Table/ContextMenu/BasicCellContextMenuActions.tsx similarity index 100% rename from src/components/fields/_BasicCell/BasicCellContextMenuActions.tsx rename to src/components/Table/ContextMenu/BasicCellContextMenuActions.tsx diff --git a/src/components/Table/editors/NullEditor.tsx b/src/components/Table/editors/NullEditor.tsx deleted file mode 100644 index 58a91fd1..00000000 --- a/src/components/Table/editors/NullEditor.tsx +++ /dev/null @@ -1,25 +0,0 @@ -import React from "react"; -import { EditorProps } from "react-data-grid"; -import { GlobalStyles } from "tss-react"; - -/** - * Allow the cell to be editable, but disable react-data-grid’s default - * text editor to show. - * - * Hides the editor container so the cell below remains editable inline. - * - * Use for cells that have inline editing and don’t need to be double-clicked. - * - * TODO: fix NullEditor overwriting the formatter component - */ -export default class NullEditor extends React.Component> { - getInputNode = () => null; - getValue = () => null; - render = () => ( - - ); -} diff --git a/src/components/Table/editors/styles.ts b/src/components/Table/editors/styles.ts deleted file mode 100644 index 0eb31315..00000000 --- a/src/components/Table/editors/styles.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { createStyles } from "@mui/material"; - -export const styles = createStyles({ - "@global": { - ".rdg-editor-container": { display: "none" }, - }, -}); - -export default styles; diff --git a/src/components/Table/editors/withNullEditor.tsx b/src/components/Table/editors/withNullEditor.tsx deleted file mode 100644 index 7d8ed000..00000000 --- a/src/components/Table/editors/withNullEditor.tsx +++ /dev/null @@ -1,44 +0,0 @@ -import { get } from "lodash-es"; -import { EditorProps } from "react-data-grid"; -import { IHeavyCellProps } from "@src/components/fields/types"; - -/** - * Allow the cell to be editable, but disable react-data-grid’s default - * text editor to show. - * - * Hides the editor container so the cell below remains editable inline. - * - * Use for cells that have inline editing and don’t need to be double-clicked. - */ -export default function withNullEditor( - HeavyCell?: React.ComponentType -) { - return function NullEditor(props: EditorProps) { - const { row, column } = props; - - return HeavyCell ? ( -
- {}} - disabled={props.column.editable === false} - /> -
- ) : null; - }; -} diff --git a/src/components/Table/editors/withSideDrawerEditor.tsx b/src/components/Table/editors/withSideDrawerEditor.tsx deleted file mode 100644 index e7522f7e..00000000 --- a/src/components/Table/editors/withSideDrawerEditor.tsx +++ /dev/null @@ -1,53 +0,0 @@ -import { useEffect } from "react"; -import { useSetAtom } from "jotai"; -import { EditorProps } from "react-data-grid"; -import { get } from "lodash-es"; - -import { tableScope, sideDrawerOpenAtom } from "@src/atoms/tableScope"; -import { IHeavyCellProps } from "@src/components/fields/types"; - -/** - * Allow the cell to be editable, but disable react-data-grid’s default - * text editor to show. Opens the side drawer in the appropriate position. - * - * Displays the current HeavyCell or HeavyCell since it overwrites cell contents. - * - * Use for cells that do not support any type of in-cell editing. - */ -export default function withSideDrawerEditor( - HeavyCell?: React.ComponentType -) { - return function SideDrawerEditor(props: EditorProps) { - const { row, column } = props; - - const setSideDrawerOpen = useSetAtom(sideDrawerOpenAtom, tableScope); - useEffect(() => { - setSideDrawerOpen(true); - }, [setSideDrawerOpen]); - - return HeavyCell ? ( -
- {}} - disabled={props.column.editable === false} - /> -
- ) : null; - }; -} diff --git a/src/components/fields/Email/index.tsx b/src/components/fields/Email/index.tsx index d8a461ac..1450f513 100644 --- a/src/components/fields/Email/index.tsx +++ b/src/components/fields/Email/index.tsx @@ -3,10 +3,10 @@ import { IFieldConfig, FieldType } from "@src/components/fields/types"; import withTableCell from "@src/components/Table/withTableCell"; import EmailIcon from "@mui/icons-material/MailOutlined"; -import DisplayCell from "@src/components/fields/_BasicCell/BasicCellValue"; +import DisplayCell from "@src/components/fields/ShortText/DisplayCell"; import EditorCell from "./EditorCell"; import { filterOperators } from "@src/components/fields/ShortText/Filter"; -import BasicContextMenuActions from "@src/components/fields/_BasicCell/BasicCellContextMenuActions"; +import BasicContextMenuActions from "@src/components/Table/ContextMenu/BasicCellContextMenuActions"; const SideDrawerField = lazy( () => diff --git a/src/components/fields/LongText/index.tsx b/src/components/fields/LongText/index.tsx index 6685d26c..00c79f31 100644 --- a/src/components/fields/LongText/index.tsx +++ b/src/components/fields/LongText/index.tsx @@ -7,7 +7,7 @@ import DisplayCell from "./DisplayCell"; import EditorCell from "./EditorCell"; import { filterOperators } from "./Filter"; -import BasicContextMenuActions from "@src/components/fields/_BasicCell/BasicCellContextMenuActions"; +import BasicContextMenuActions from "@src/components/Table/ContextMenu/BasicCellContextMenuActions"; const SideDrawerField = lazy( () => diff --git a/src/components/fields/Number/index.tsx b/src/components/fields/Number/index.tsx index 7bed8e43..0a25fca8 100644 --- a/src/components/fields/Number/index.tsx +++ b/src/components/fields/Number/index.tsx @@ -6,7 +6,7 @@ import { Number as NumberIcon } from "@src/assets/icons"; import DisplayCell from "./DisplayCell"; import EditorCell from "./EditorCell"; import { filterOperators } from "./Filter"; -import BasicContextMenuActions from "@src/components/fields/_BasicCell/BasicCellContextMenuActions"; +import BasicContextMenuActions from "@src/components/Table/ContextMenu/BasicCellContextMenuActions"; const SideDrawerField = lazy( () => import("./SideDrawerField" /* webpackChunkName: "SideDrawerField-Number" */) diff --git a/src/components/fields/Percentage/index.tsx b/src/components/fields/Percentage/index.tsx index 99239f7c..86ff6fe6 100644 --- a/src/components/fields/Percentage/index.tsx +++ b/src/components/fields/Percentage/index.tsx @@ -6,7 +6,7 @@ import { Percentage as PercentageIcon } from "@src/assets/icons"; import DisplayCell from "./DisplayCell"; import EditorCell from "./EditorCell"; import { filterOperators } from "@src/components/fields/Number/Filter"; -import BasicContextMenuActions from "@src/components/fields/_BasicCell/BasicCellContextMenuActions"; +import BasicContextMenuActions from "@src/components/Table/ContextMenu/BasicCellContextMenuActions"; const SideDrawerField = lazy( () => diff --git a/src/components/fields/Phone/index.tsx b/src/components/fields/Phone/index.tsx index 9e5c1ebe..ec8792a6 100644 --- a/src/components/fields/Phone/index.tsx +++ b/src/components/fields/Phone/index.tsx @@ -3,10 +3,10 @@ import { IFieldConfig, FieldType } from "@src/components/fields/types"; import withTableCell from "@src/components/Table/withTableCell"; import PhoneIcon from "@mui/icons-material/PhoneOutlined"; -import DisplayCell from "@src/components/fields/_BasicCell/BasicCellValue"; +import DisplayCell from "@src/components/fields/ShortText/DisplayCell"; import EditorCell from "./EditorCell"; import { filterOperators } from "@src/components/fields/ShortText/Filter"; -import BasicContextMenuActions from "@src/components/fields/_BasicCell/BasicCellContextMenuActions"; +import BasicContextMenuActions from "@src/components/Table/ContextMenu/BasicCellContextMenuActions"; const SideDrawerField = lazy( () => diff --git a/src/components/fields/RichText/index.tsx b/src/components/fields/RichText/index.tsx index 80202a98..bd53d610 100644 --- a/src/components/fields/RichText/index.tsx +++ b/src/components/fields/RichText/index.tsx @@ -4,7 +4,7 @@ import withTableCell from "@src/components/Table/withTableCell"; import RichTextIcon from "@mui/icons-material/TextFormat"; import DisplayCell from "./DisplayCell"; -import BasicContextMenuActions from "@src/components/fields/_BasicCell/BasicCellContextMenuActions"; +import BasicContextMenuActions from "@src/components/Table/ContextMenu/BasicCellContextMenuActions"; const SideDrawerField = lazy( () => diff --git a/src/components/fields/ShortText/DisplayCell.tsx b/src/components/fields/ShortText/DisplayCell.tsx new file mode 100644 index 00000000..f29adca6 --- /dev/null +++ b/src/components/fields/ShortText/DisplayCell.tsx @@ -0,0 +1,6 @@ +import { IDisplayCellProps } from "@src/components/fields/types"; + +export default function DisplayCellValue({ value }: IDisplayCellProps) { + if (typeof value !== "string") return null; + return <>{value}; +} diff --git a/src/components/fields/ShortText/index.tsx b/src/components/fields/ShortText/index.tsx index 7bd36ddc..0160a64a 100644 --- a/src/components/fields/ShortText/index.tsx +++ b/src/components/fields/ShortText/index.tsx @@ -3,11 +3,11 @@ import { IFieldConfig, FieldType } from "@src/components/fields/types"; import withTableCell from "@src/components/Table/withTableCell"; import ShortTextIcon from "@mui/icons-material/ShortText"; -import BasicCell from "@src/components/fields/_BasicCell/BasicCellValue"; +import DisplayCell from "@src/components/fields/ShortText/DisplayCell"; import EditorCell from "./EditorCell"; import { filterOperators } from "./Filter"; -import BasicContextMenuActions from "@src/components/fields/_BasicCell/BasicCellContextMenuActions"; +import BasicContextMenuActions from "@src/components/Table/ContextMenu/BasicCellContextMenuActions"; const SideDrawerField = lazy( () => @@ -30,7 +30,7 @@ export const config: IFieldConfig = { icon: , description: "Text displayed on a single line.", contextMenuActions: BasicContextMenuActions, - TableCell: withTableCell(BasicCell, EditorCell), + TableCell: withTableCell(DisplayCell, EditorCell), SideDrawerField, settings: Settings, filter: { diff --git a/src/components/fields/Url/index.tsx b/src/components/fields/Url/index.tsx index c6fb3fd7..612423d0 100644 --- a/src/components/fields/Url/index.tsx +++ b/src/components/fields/Url/index.tsx @@ -6,7 +6,7 @@ import UrlIcon from "@mui/icons-material/Link"; import DisplayCell from "./DisplayCell"; import EditorCell from "./EditorCell"; import { filterOperators } from "@src/components/fields/ShortText/Filter"; -import BasicContextMenuActions from "@src/components/fields/_BasicCell/BasicCellContextMenuActions"; +import BasicContextMenuActions from "@src/components/Table/ContextMenu/BasicCellContextMenuActions"; const SideDrawerField = lazy( () => diff --git a/src/components/fields/_BasicCell/BasicCellName.tsx b/src/components/fields/_BasicCell/BasicCellName.tsx deleted file mode 100644 index 5d93cd7b..00000000 --- a/src/components/fields/_BasicCell/BasicCellName.tsx +++ /dev/null @@ -1,5 +0,0 @@ -import { IBasicCellProps } from "@src/components/fields/types"; - -export default function BasicCellName({ name }: IBasicCellProps) { - return <>{name}; -} diff --git a/src/components/fields/_BasicCell/BasicCellNull.tsx b/src/components/fields/_BasicCell/BasicCellNull.tsx deleted file mode 100644 index 6b8471b3..00000000 --- a/src/components/fields/_BasicCell/BasicCellNull.tsx +++ /dev/null @@ -1,3 +0,0 @@ -export default function BasicCellNull(props: any) { - return
; -} diff --git a/src/components/fields/_BasicCell/BasicCellValue.tsx b/src/components/fields/_BasicCell/BasicCellValue.tsx deleted file mode 100644 index 8d8b0445..00000000 --- a/src/components/fields/_BasicCell/BasicCellValue.tsx +++ /dev/null @@ -1,6 +0,0 @@ -import { IBasicCellProps } from "@src/components/fields/types"; - -export default function BasicCellValue({ value }: IBasicCellProps) { - if (typeof value !== "string") return null; - return <>{value}; -} diff --git a/src/components/fields/_withTableCell/withBasicCell.tsx b/src/components/fields/_withTableCell/withBasicCell.tsx deleted file mode 100644 index a268bf2e..00000000 --- a/src/components/fields/_withTableCell/withBasicCell.tsx +++ /dev/null @@ -1,29 +0,0 @@ -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"; - -/** - * HOC to wrap around table cell components. - * Renders read-only BasicCell only. - * @param BasicCellComponent - The light cell component to display at all times - */ -export default function withBasicCell( - BasicCellComponent: React.ComponentType -) { - return function BasicCell({ row, column, getValue }: TableCellProps) { - const columnConfig = column.columnDef.meta!; - const { name } = columnConfig; - const value = getValue(); - - return ( - - ); - }; -} diff --git a/src/components/fields/_withTableCell/withHeavyCell.tsx b/src/components/fields/_withTableCell/withHeavyCell.tsx deleted file mode 100644 index 64c612fb..00000000 --- a/src/components/fields/_withTableCell/withHeavyCell.tsx +++ /dev/null @@ -1,83 +0,0 @@ -import { Suspense, useState, useEffect, startTransition } from "react"; -import { useSetAtom } from "jotai"; -import { get } from "lodash-es"; -import type { TableCellProps } from "@src/components/Table"; -import { IBasicCellProps, IHeavyCellProps } from "@src/components/fields/types"; - -import { tableScope, updateFieldAtom } from "@src/atoms/tableScope"; -import { FieldType } from "@src/constants/fields"; - -/** - * HOC to wrap table cell components. - * Renders read-only BasicCell while scrolling for better scroll performance. - * @param BasicCellComponent - The lighter cell component to display while scrolling - * @param HeavyCellComponent - The read/write cell component to display - * @param readOnly - Prevent the component from updating the cell value - */ -export default function withHeavyCell( - BasicCellComponent: React.ComponentType, - HeavyCellComponent: React.ComponentType, - readOnly: boolean = false -) { - return function HeavyCell({ 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(() => { - startTransition(() => { - setDisplayedComponent("heavy"); - }); - }, []); - - // TODO: Investigate if this still needs to be a state - const value = getValue(); - const [localValue, setLocalValue] = useState(value); - useEffect(() => { - setLocalValue(value); - }, [value]); - - // Declare basicCell here so props can be reused by HeavyCellComponent - const basicCellProps = { - value: localValue, - name: column.columnDef.meta!.name, - type: column.columnDef.meta!.type, - onMouseOver: () => setDisplayedComponent("heavy"), - onMouseLeave: () => setDisplayedComponent("basic"), - }; - const basicCell = ; - - if (displayedComponent === "basic") return basicCell; - - const handleSubmit = (value: any) => { - if (readOnly) return; - updateField({ - path: row.original._rowy_ref.path, - fieldName: column.id, - value, - }); - setLocalValue(value); - }; - - if (displayedComponent === "heavy") - return ( - - - - ); - - // Should not reach this line - return null; - }; -} diff --git a/src/components/fields/_withTableCell/withPopoverCell.tsx b/src/components/fields/_withTableCell/withPopoverCell.tsx deleted file mode 100644 index 8a1f6b20..00000000 --- a/src/components/fields/_withTableCell/withPopoverCell.tsx +++ /dev/null @@ -1,151 +0,0 @@ -import { Suspense, useState, useEffect, useRef } from "react"; -import { useSetAtom } from "jotai"; -import { get } from "lodash-es"; -import type { TableCellProps } from "@src/components/Table"; -import { - IBasicCellProps, - IPopoverInlineCellProps, - IPopoverCellProps, -} from "@src/components/fields/types"; - -import { Popover, PopoverProps } from "@mui/material"; - -import { tableScope, updateFieldAtom } from "@src/atoms/tableScope"; -import { FieldType } from "@src/constants/fields"; - -export interface IPopoverCellOptions extends Partial { - transparent?: boolean; - readOnly?: boolean; -} - -/** - * HOC to wrap around table cell formatters. - * Renders read-only BasicCell while scrolling for better scroll performance. - * When the user clicks the heavier inline cell, it displays PopoverCell. - * @param BasicCellComponent - The lighter cell component to display while scrolling - * @param InlineCellComponent - The heavier cell component to display inline - * @param PopoverCellComponent - The heavy read/write cell component to display in Popover - * @param options - {@link IPopoverCellOptions} - */ -export default function withPopoverCell( - BasicCellComponent: React.ComponentType, - InlineCellComponent: React.ForwardRefExoticComponent< - IPopoverInlineCellProps & React.RefAttributes - >, - PopoverCellComponent: React.ComponentType, - options?: IPopoverCellOptions -) { - return function PopoverCell({ row, column, getValue }: TableCellProps) { - const { transparent, ...popoverProps } = options ?? {}; - - const updateField = useSetAtom(updateFieldAtom, tableScope); - - // Initially display BasicCell to improve scroll performance - const [displayedComponent, setDisplayedComponent] = useState< - "basic" | "inline" | "popover" - >("inline"); - // Then switch to heavier InlineCell once completed - // useEffect(() => { - // setTimeout(() => { - // setDisplayedComponent("inline"); - // }); - // }, []); - - // Store Popover open state here so we can add delay for close transition - const [popoverOpen, setPopoverOpen] = useState(false); - - // Store ref to rendered InlineCell here to get positioning for PopoverCell - const inlineCellRef = useRef(null); - - // TODO: Investigate if this still needs to be a state - const value = getValue(); - const [localValue, setLocalValue] = useState(value); - useEffect(() => { - setLocalValue(value); - }, [value]); - - // Declare basicCell here so props can be reused by HeavyCellComponent - const basicCellProps = { - value: localValue, - name: column.columnDef.meta!.name, - type: column.columnDef.meta!.type, - }; - - if (displayedComponent === "basic") - return ; - - // This is where we update the documents - const handleSubmit = (value: any) => { - if (options?.readOnly) return; - updateField({ - path: row.original._rowy_ref.path, - fieldName: column.id, - value, - deleteField: value === undefined, - }); - setLocalValue(value); - }; - const showPopoverCell: any = (popover: boolean) => { - if (popover) { - setPopoverOpen(true); - setDisplayedComponent("popover"); - } else { - setPopoverOpen(false); - setTimeout(() => setDisplayedComponent("inline"), 300); - } - }; - - // Declare inlineCell and props here so it can be reused later - const commonCellProps = { - ...basicCellProps, - row: row.original, - column: column.columnDef.meta!, - onSubmit: handleSubmit, - disabled: column.columnDef.meta!.editable === false, - docRef: row.original._rowy_ref, - showPopoverCell, - ref: inlineCellRef, - }; - const inlineCell = ( - - ); - - if (displayedComponent === "inline") return inlineCell; - - const parentRef = inlineCellRef.current?.parentElement; - - if (displayedComponent === "popover") - return ( - <> - {inlineCell} - - - showPopoverCell(false)} - {...popoverProps} - sx={ - transparent - ? { - "& .MuiPopover-paper": { backgroundColor: "transparent" }, - } - : {} - } - onClick={(e) => e.stopPropagation()} - onDoubleClick={(e) => e.stopPropagation()} - onKeyDown={(e) => e.stopPropagation()} - > - - - - - ); - - // Should not reach this line - return null; - }; -} diff --git a/src/components/fields/types.ts b/src/components/fields/types.ts index 95e57c68..8897e003 100644 --- a/src/components/fields/types.ts +++ b/src/components/fields/types.ts @@ -30,8 +30,6 @@ export interface IFieldConfig { reset: () => void ) => IContextMenuItem[]; TableCell: React.ComponentType; - /** @deprecated TODO: REMOVE */ - TableEditor?: React.ComponentType>; SideDrawerField: React.ComponentType; settings?: React.ComponentType; settingsValidator?: (config: Record) => Record; @@ -46,29 +44,6 @@ 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; - onSubmit: (value: any) => void; - docRef: TableRowRef; - disabled: boolean; -} -/** @deprecated TODO: REMOVE */ -export interface IPopoverInlineCellProps extends IHeavyCellProps { - showPopoverCell: React.Dispatch>; -} -/** @deprecated TODO: REMOVE */ -export interface IPopoverCellProps extends IPopoverInlineCellProps { - parentRef: PopoverProps["anchorEl"]; -} - export interface IDisplayCellProps { value: T; type: FieldType; From 69226d69108d6055ee4e565a855580fb1c55fe10 Mon Sep 17 00:00:00 2001 From: Sidney Alcantara Date: Fri, 11 Nov 2022 15:50:10 +1100 Subject: [PATCH 40/66] ButtonBase: show ripple on focus by default --- src/theme/components.tsx | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/theme/components.tsx b/src/theme/components.tsx index 4b4f9a0d..2bc4b2e5 100644 --- a/src/theme/components.tsx +++ b/src/theme/components.tsx @@ -680,6 +680,12 @@ export const components = (theme: Theme): ThemeOptions => { }, }, + MuiButtonBase: { + defaultProps: { + focusRipple: true, + }, + }, + MuiButton: { defaultProps: { variant: "outlined", From e9bc4a5a9b84b42ec5fa88ba5fa98a43d817fdc0 Mon Sep 17 00:00:00 2001 From: Sidney Alcantara Date: Mon, 14 Nov 2022 16:09:01 +1100 Subject: [PATCH 41/66] Merge branch 'develop' into feature/rowy-706-table-upgrade --- .github/workflows/deploy-preview.yml | 4 +- src/atoms/tableScope/columnActions.ts | 43 ++- src/components/ColumnMenu/ColumnMenu.tsx | 80 ++++- src/components/ErrorFallback.tsx | 58 ++-- .../TableInformationDrawer/Details.tsx | 294 ++++++++++++------ .../ExportModal/ModalContentsDownload.tsx | 6 +- .../ExtensionsModal/ExtensionsModal.tsx | 80 +++-- .../ExtensionsModal/RuntimeOptions.tsx | 117 +++++++ .../TableModals/ExtensionsModal/utils.ts | 6 + .../TableSettingsDialog/TableDetails.tsx | 23 +- .../TableToolbar/TableToolbarButton.tsx | 41 ++- src/components/fields/Action/EditorCell.tsx | 11 +- src/components/fields/Action/Settings.tsx | 50 ++- .../fields/Action/SideDrawerField.tsx | 3 +- src/components/fields/Color/filters.ts | 21 ++ src/components/fields/Color/index.tsx | 5 + .../fields/Json/SideDrawerField.tsx | 5 +- src/components/fields/Rating/DisplayCell.tsx | 24 +- src/components/fields/Rating/Icon.tsx | 31 ++ src/components/fields/Rating/Settings.tsx | 33 +- .../fields/Rating/SideDrawerField.tsx | 6 +- src/constants/externalLinks.ts | 4 +- src/hooks/useFirestoreCollectionWithAtom.ts | 12 +- src/index.tsx | 10 +- src/pages/Table/ProvidedTablePage.tsx | 50 ++- src/theme/components.tsx | 3 +- src/types/table.d.ts | 16 +- 27 files changed, 789 insertions(+), 247 deletions(-) create mode 100644 src/components/TableModals/ExtensionsModal/RuntimeOptions.tsx create mode 100644 src/components/fields/Color/filters.ts create mode 100644 src/components/fields/Rating/Icon.tsx diff --git a/.github/workflows/deploy-preview.yml b/.github/workflows/deploy-preview.yml index ed7b8fd7..9ca97e44 100644 --- a/.github/workflows/deploy-preview.yml +++ b/.github/workflows/deploy-preview.yml @@ -8,7 +8,7 @@ on: env: REACT_APP_FIREBASE_PROJECT_ID: rowyio REACT_APP_FIREBASE_PROJECT_WEB_API_KEY: - "${{ secrets.FIREBASE_WEB_API_KEY_ROWYIO }}" + "${{ secrets.FIREBASE_WEB_API_KEY_TRYROWY }}" CI: "" jobs: build_and_preview: @@ -27,6 +27,6 @@ jobs: with: repoToken: "${{ secrets.GITHUB_TOKEN }}" firebaseServiceAccount: - "${{ secrets.FIREBASE_SERVICE_ACCOUNT_ROWYIO }}" + "${{ secrets.FIREBASE_SERVICE_ACCOUNT_TRYROWY }}" expires: 14d projectId: rowyio diff --git a/src/atoms/tableScope/columnActions.ts b/src/atoms/tableScope/columnActions.ts index 96a37d2a..2f3926e8 100644 --- a/src/atoms/tableScope/columnActions.ts +++ b/src/atoms/tableScope/columnActions.ts @@ -1,10 +1,12 @@ import { atom } from "jotai"; import { findIndex } from "lodash-es"; +import { FieldType } from "@src/constants/fields"; import { tableColumnsOrderedAtom, tableColumnsReducer, updateTableSchemaAtom, + tableSchemaAtom, } from "./table"; import { ColumnConfig } from "@src/types/table"; @@ -14,6 +16,7 @@ export interface IAddColumnOptions { /** Index to add column at. If undefined, adds to end */ index?: number; } + /** * Set function adds a column to tableSchema, to the end or by index. * Also fixes any issues with column indexes, so they go from 0 to length - 1 @@ -52,6 +55,7 @@ export interface IUpdateColumnOptions { /** If passed, reorders the column to the index */ index?: number; } + /** * Set function updates a column in tableSchema * @throws Error if column not found @@ -110,13 +114,50 @@ export const updateColumnAtom = atom( * ``` */ export const deleteColumnAtom = atom(null, async (get, _set, key: string) => { + const tableSchema = get(tableSchemaAtom); const tableColumnsOrdered = [...get(tableColumnsOrderedAtom)]; const updateTableSchema = get(updateTableSchemaAtom); if (!updateTableSchema) throw new Error("Cannot update table schema"); const updatedColumns = tableColumnsOrdered .filter((c) => c.key !== key) + .map((c) => { + // remove column from derivatives listener fields + if (c.type === FieldType.derivative) { + return { + ...c, + config: { + ...c.config, + listenerFields: + c.config?.listenerFields?.filter((f) => f !== key) ?? [], + }, + }; + } else if (c.type === FieldType.action) { + return { + ...c, + config: { + ...c.config, + requiredFields: + c.config?.requiredFields?.filter((f) => f !== key) ?? [], + }, + }; + } else { + return c; + } + }) .reduce(tableColumnsReducer, {}); - await updateTableSchema({ columns: updatedColumns }, [`columns.${key}`]); + const updatedExtensionObjects = tableSchema?.extensionObjects?.map( + (extension) => { + return { + ...extension, + requiredFields: extension.requiredFields.filter((f) => f !== key), + }; + } + ); + + await updateTableSchema( + { columns: updatedColumns, extensionObjects: updatedExtensionObjects }, + [`columns.${key}`] + ); }); diff --git a/src/components/ColumnMenu/ColumnMenu.tsx b/src/components/ColumnMenu/ColumnMenu.tsx index 9d193179..54f6e0c3 100644 --- a/src/components/ColumnMenu/ColumnMenu.tsx +++ b/src/components/ColumnMenu/ColumnMenu.tsx @@ -7,6 +7,7 @@ import { ListItemIcon, ListItemText, Typography, + Divider, } from "@mui/material"; import FilterIcon from "@mui/icons-material/FilterList"; import LockOpenIcon from "@mui/icons-material/LockOpen"; @@ -50,12 +51,18 @@ import { columnModalAtom, tableFiltersPopoverAtom, tableNextPageAtom, + tableSchemaAtom, } from "@src/atoms/tableScope"; import { FieldType } from "@src/constants/fields"; import { getFieldProp } from "@src/components/fields"; import { analytics, logEvent } from "@src/analytics"; -import { formatSubTableName, getTableSchemaPath } from "@src/utils/table"; +import { + formatSubTableName, + getTableBuildFunctionPathname, + getTableSchemaPath, +} from "@src/utils/table"; import { runRoutes } from "@src/constants/runRoutes"; +import { useSnackLogContext } from "@src/contexts/SnackLogContext"; export interface IMenuModalProps { name: string; @@ -91,6 +98,8 @@ export default function ColumnMenu() { tableScope ); const [tableNextPage] = useAtom(tableNextPageAtom, tableScope); + const [tableSchema] = useAtom(tableSchemaAtom, tableScope); + const snackLogContext = useSnackLogContext(); const [altPress] = useAtom(altPressAtom, projectScope); const { enqueueSnackbar, closeSnackbar } = useSnackbar(); @@ -117,8 +126,42 @@ export default function ColumnMenu() { const userDocHiddenFields = userSettings.tables?.[formatSubTableName(tableId)]?.hiddenFields ?? []; + let referencedColumns: string[] = []; + let referencedExtensions: string[] = []; + Object.entries(tableSchema?.columns ?? {}).forEach(([key, c], index) => { + if ( + c.config?.listenerFields?.includes(column.key) || + c.config?.requiredFields?.includes(column.key) + ) { + referencedColumns.push(c.name); + } + }); + tableSchema?.extensionObjects?.forEach((extension) => { + if (extension.requiredFields.includes(column.key)) { + referencedExtensions.push(extension.name); + } + }); + const requireRebuild = + referencedColumns.length || referencedExtensions.length; + const handleDeleteColumn = () => { deleteColumn(column.key); + if (requireRebuild) { + snackLogContext.requestSnackLog(); + rowyRun({ + route: runRoutes.buildFunction, + body: { + tablePath: tableSettings.collection, + // pathname must match old URL format + pathname: getTableBuildFunctionPathname( + tableSettings.id, + tableSettings.tableType + ), + tableConfigPath: getTableSchemaPath(tableSettings), + }, + }); + logEvent(analytics, "deployed_extensions"); + } logEvent(analytics, "delete_column", { type: column.type }); handleClose(); }; @@ -360,8 +403,8 @@ export default function ColumnMenu() { icon: , onClick: altPress ? handleDeleteColumn - : () => - confirm({ + : () => { + return confirm({ title: "Delete column?", body: ( <> @@ -373,12 +416,39 @@ export default function ColumnMenu() { Key: {column.key} + {requireRebuild ? ( + <> + + {referencedColumns.length ? ( + + This column will be removed as a dependency of the + following columns:{" "} + + {referencedColumns.join(", ")} + + + ) : null} + {referencedExtensions.length ? ( + + This column will be removed as a dependency from the + following Extensions:{" "} + + {referencedExtensions.join(", ")} + + + ) : null} + + You need to re-deploy this table’s cloud function. + + + ) : null} ), - confirm: "Delete", + confirm: requireRebuild ? "Delete & re-deploy" : "Delete", confirmColor: "error", handleConfirm: handleDeleteColumn, - }), + }); + }, color: "error" as "error", }, ]; diff --git a/src/components/ErrorFallback.tsx b/src/components/ErrorFallback.tsx index 83f8fb7a..3d7f130a 100644 --- a/src/components/ErrorFallback.tsx +++ b/src/components/ErrorFallback.tsx @@ -1,21 +1,17 @@ import { useState, useEffect } from "react"; import { FallbackProps } from "react-error-boundary"; -import { useLocation, Link } from "react-router-dom"; +import { useLocation } from "react-router-dom"; import useOffline from "@src/hooks/useOffline"; import { Typography, Button } from "@mui/material"; import ReloadIcon from "@mui/icons-material/Refresh"; import InlineOpenInNewIcon from "@src/components/InlineOpenInNewIcon"; import OfflineIcon from "@mui/icons-material/CloudOff"; -import { Tables as TablesIcon } from "@src/assets/icons"; import EmptyState, { IEmptyStateProps } from "@src/components/EmptyState"; import AccessDenied from "@src/components/AccessDenied"; -import { ROUTES } from "@src/constants/routes"; -import meta from "@root/package.json"; - -export const ERROR_TABLE_NOT_FOUND = "Table not found"; +import { EXTERNAL_LINKS } from "@src/constants/externalLinks"; export interface IErrorFallbackProps extends FallbackProps, IEmptyStateProps {} @@ -43,9 +39,22 @@ export function ErrorFallbackContents({ - - ), - }; - } - } - if (error.message.startsWith("Loading chunk")) { if (isOffline) { renderProps = { Icon: OfflineIcon, message: "You’re offline" }; diff --git a/src/components/TableInformationDrawer/Details.tsx b/src/components/TableInformationDrawer/Details.tsx index 3f93721b..78dd0236 100644 --- a/src/components/TableInformationDrawer/Details.tsx +++ b/src/components/TableInformationDrawer/Details.tsx @@ -1,134 +1,244 @@ -import { useMemo } from "react"; +import { useMemo, useState } from "react"; import { format } from "date-fns"; -import { find } from "lodash-es"; +import { find, isEqual } from "lodash-es"; import MDEditor from "@uiw/react-md-editor"; -import { Box, IconButton, Stack, Typography } from "@mui/material"; +import { + Box, + IconButton, + Stack, + TextField, + Typography, + useTheme, +} from "@mui/material"; import EditIcon from "@mui/icons-material/EditOutlined"; +import EditOffIcon from "@mui/icons-material/EditOffOutlined"; import { tableScope, tableSettingsAtom } from "@src/atoms/tableScope"; -import { useAtom, useSetAtom } from "jotai"; +import { useAtom } from "jotai"; import { projectScope, tablesAtom, - tableSettingsDialogAtom, + updateTableAtom, userRolesAtom, } from "@src/atoms/projectScope"; import { DATE_TIME_FORMAT } from "@src/constants/dates"; +import SaveState from "@src/components/SideDrawer/SaveState"; export default function Details() { const [userRoles] = useAtom(userRolesAtom, projectScope); const [tableSettings] = useAtom(tableSettingsAtom, tableScope); const [tables] = useAtom(tablesAtom, projectScope); - const openTableSettingsDialog = useSetAtom( - tableSettingsDialogAtom, - projectScope - ); + const [updateTable] = useAtom(updateTableAtom, projectScope); + const theme = useTheme(); const settings = useMemo( () => find(tables, ["id", tableSettings.id]), [tables, tableSettings.id] ); + const { description, details, _createdBy } = settings ?? {}; + + const [editDescription, setEditDescription] = useState(false); + const [localDescription, setLocalDescription] = useState(description ?? ""); + const [localDetails, setLocalDetails] = useState(details ?? ""); + const [editDetails, setEditDetails] = useState(false); + const [mdFullScreen, setMdFullScreen] = useState(false); + + const [saveState, setSaveState] = useState< + "" | "unsaved" | "saving" | "saved" + >(""); + if (!settings) { return null; } - const editButton = userRoles.includes("ADMIN") && ( - - openTableSettingsDialog({ - mode: "update", - data: settings, - }) - } - disabled={!openTableSettingsDialog || settings.id.includes("/")} - > - - - ); + const handleSave = async () => { + setSaveState("saving"); + await updateTable!({ + ...settings, + description: localDescription, + details: localDetails, + }); + setSaveState("saved"); + }; - const { description, details, _createdBy } = settings; + const isAdmin = userRoles.includes("ADMIN"); return ( - .MuiGrid-root": { - position: "relative", - }, - }} - > - {/* Description */} - - - - Description - - {editButton} + <> + + + + + {/* Description */} + + + + Description + + {isAdmin && ( + { + setEditDescription(!editDescription); + }} + sx={{ top: 4 }} + > + {editDescription ? : } + + )} + + {editDescription ? ( + { + setLocalDescription(e.target.value); + saveState !== "unsaved" && setSaveState("unsaved"); + }} + onBlur={() => + isEqual(description, localDescription) + ? setSaveState("") + : handleSave() + } + rows={2} + minRows={2} + /> + ) : ( + + {localDescription ? localDescription : "No description"} + + )} - - {description ? description : "No description"} - - - - {/* Details */} - - - - Details - - {editButton} - - {!details ? ( - - No details - - ) : ( + {/* Details */} + + + + Details + + {isAdmin && ( + { + setEditDetails(!editDetails); + }} + sx={{ top: 4 }} + > + {editDetails ? : } + + )} + ul": { + display: "flex", + alignItems: "center", + }, + "& .w-md-editor-toolbar > ul:first-of-type": { + overflowX: "auto", + marginRight: theme.spacing(1), + }, + "& :is(h1, h2, h3, h4, h5, h6)": { + marginY: `${theme.spacing(1.5)} !important`, + borderBottom: "none !important", + }, + "& details summary": { + marginBottom: theme.spacing(1), + }, }} > - + {editDetails ? ( + { + if (command.name === "fullscreen") { + command.execute = () => setMdFullScreen(!mdFullScreen); + } + return command; + }} + textareaProps={{ + autoFocus: true, + onChange: (e) => { + setLocalDetails(e.target.value ?? ""); + saveState !== "unsaved" && setSaveState("unsaved"); + }, + onBlur: () => + isEqual(details, localDetails) + ? setSaveState("") + : handleSave(), + }} + /> + ) : !localDetails ? ( + No details + ) : ( + + )} + + {/* Table Audits */} + {_createdBy && ( + + + Created by{" "} + + {_createdBy.displayName} + {" "} + on{" "} + + {format(_createdBy.timestamp.toDate(), DATE_TIME_FORMAT)} + + + )} - - {/* Table Audits */} - {_createdBy && ( - - - Created by{" "} - - {_createdBy.displayName} - {" "} - on{" "} - - {format(_createdBy.timestamp.toDate(), DATE_TIME_FORMAT)} - - - - )} - + ); } diff --git a/src/components/TableModals/ExportModal/ModalContentsDownload.tsx b/src/components/TableModals/ExportModal/ModalContentsDownload.tsx index 468d52cc..8b6690dd 100644 --- a/src/components/TableModals/ExportModal/ModalContentsDownload.tsx +++ b/src/components/TableModals/ExportModal/ModalContentsDownload.tsx @@ -137,7 +137,11 @@ export default function Export({ x.key)} onChange={handleChange(setColumns)} - filterColumns={(column) => DOWNLOADABLE_COLUMNS.includes(column.type)} + filterColumns={(column) => + column.type === FieldType.derivative + ? DOWNLOADABLE_COLUMNS.includes(column.config?.renderFieldType) + : DOWNLOADABLE_COLUMNS.includes(column.type) + } label="Columns to export" labelPlural="columns" TextFieldProps={{ diff --git a/src/components/TableModals/ExtensionsModal/ExtensionsModal.tsx b/src/components/TableModals/ExtensionsModal/ExtensionsModal.tsx index 57f6fbee..71146804 100644 --- a/src/components/TableModals/ExtensionsModal/ExtensionsModal.tsx +++ b/src/components/TableModals/ExtensionsModal/ExtensionsModal.tsx @@ -1,6 +1,6 @@ import { useEffect, useState } from "react"; import { useAtom, useSetAtom } from "jotai"; -import { isEqual } from "lodash-es"; +import { isEqual, isUndefined } from "lodash-es"; import { ITableModalProps } from "@src/components/TableModals"; import Modal from "@src/components/Modal"; @@ -23,7 +23,6 @@ import { } from "@src/atoms/tableScope"; import { useSnackLogContext } from "@src/contexts/SnackLogContext"; -import { emptyExtensionObject, IExtension, ExtensionType } from "./utils"; import { runRoutes } from "@src/constants/runRoutes"; import { analytics, logEvent } from "@src/analytics"; import { @@ -31,6 +30,14 @@ import { getTableBuildFunctionPathname, } from "@src/utils/table"; +import { + emptyExtensionObject, + IExtension, + ExtensionType, + IRuntimeOptions, +} from "./utils"; +import RuntimeOptions from "./RuntimeOptions"; + export default function ExtensionsModal({ onClose }: ITableModalProps) { const [currentUser] = useAtom(currentUserAtom, projectScope); const [rowyRun] = useAtom(rowyRunAtom, projectScope); @@ -39,12 +46,25 @@ export default function ExtensionsModal({ onClose }: ITableModalProps) { const [tableSchema] = useAtom(tableSchemaAtom, tableScope); const [updateTableSchema] = useAtom(updateTableSchemaAtom, tableScope); - const currentExtensionObjects = (tableSchema.extensionObjects ?? - []) as IExtension[]; const [localExtensionsObjects, setLocalExtensionsObjects] = useState( - currentExtensionObjects + tableSchema.extensionObjects ?? [] ); + const [localRuntimeOptions, setLocalRuntimeOptions] = useState( + tableSchema.runtimeOptions ?? {} + ); + + const errors = { + runtimeOptions: { + timeoutSeconds: + !isUndefined(localRuntimeOptions.timeoutSeconds) && + !( + localRuntimeOptions.timeoutSeconds! > 0 && + localRuntimeOptions.timeoutSeconds! <= 540 + ), + }, + }; + const [openMigrationGuide, setOpenMigrationGuide] = useState(false); useEffect(() => { if (tableSchema.sparks) setOpenMigrationGuide(true); @@ -57,7 +77,9 @@ export default function ExtensionsModal({ onClose }: ITableModalProps) { } | null>(null); const snackLogContext = useSnackLogContext(); - const edited = !isEqual(currentExtensionObjects, localExtensionsObjects); + const edited = + !isEqual(tableSchema.extensionObjects ?? [], localExtensionsObjects) || + !isEqual(tableSchema.runtimeOptions ?? {}, localRuntimeOptions); const handleClose = ( _setOpen: React.Dispatch> @@ -70,7 +92,8 @@ export default function ExtensionsModal({ onClose }: ITableModalProps) { cancel: "Keep", handleConfirm: () => { _setOpen(false); - setLocalExtensionsObjects(currentExtensionObjects); + setLocalExtensionsObjects(tableSchema.extensionObjects ?? []); + setLocalRuntimeOptions(tableSchema.runtimeOptions ?? {}); onClose(); }, }); @@ -79,15 +102,18 @@ export default function ExtensionsModal({ onClose }: ITableModalProps) { } }; - const handleSaveExtensions = async (callback?: Function) => { + const handleSave = async (callback?: Function) => { if (updateTableSchema) - await updateTableSchema({ extensionObjects: localExtensionsObjects }); + await updateTableSchema({ + extensionObjects: localExtensionsObjects, + runtimeOptions: localRuntimeOptions, + }); if (callback) callback(); onClose(); }; const handleSaveDeploy = async () => { - handleSaveExtensions(() => { + handleSave(() => { try { snackLogContext.requestSnackLog(); rowyRun({ @@ -132,6 +158,13 @@ export default function ExtensionsModal({ onClose }: ITableModalProps) { setExtensionModal(null); }; + const handleUpdateRuntimeOptions = (update: IRuntimeOptions) => { + setLocalRuntimeOptions((runtimeOptions) => ({ + ...runtimeOptions, + ...update, + })); + }; + const handleUpdateActive = (index: number, active: boolean) => { setLocalExtensionsObjects( localExtensionsObjects.map((extensionObject, i) => { @@ -217,24 +250,31 @@ export default function ExtensionsModal({ onClose }: ITableModalProps) { /> } children={ - + <> + + + } actions={{ primary: { children: "Save & Deploy", onClick: handleSaveDeploy, - disabled: !edited, + disabled: !edited || errors.runtimeOptions.timeoutSeconds, }, secondary: { children: "Save", - onClick: () => handleSaveExtensions(), - disabled: !edited, + onClick: () => handleSave(), + disabled: !edited || errors.runtimeOptions.timeoutSeconds, }, }} /> diff --git a/src/components/TableModals/ExtensionsModal/RuntimeOptions.tsx b/src/components/TableModals/ExtensionsModal/RuntimeOptions.tsx new file mode 100644 index 00000000..90ca584c --- /dev/null +++ b/src/components/TableModals/ExtensionsModal/RuntimeOptions.tsx @@ -0,0 +1,117 @@ +import { useState } from "react"; +import { useAtom, useSetAtom } from "jotai"; + +import { + Accordion, + AccordionDetails, + AccordionSummary, + Button, + Grid, + InputAdornment, + TextField, + Typography, +} from "@mui/material"; +import { ChevronDown } from "@src/assets/icons"; +import MultiSelect from "@rowy/multiselect"; + +import { + compatibleRowyRunVersionAtom, + projectScope, + rowyRunModalAtom, +} from "@src/atoms/projectScope"; + +import { IRuntimeOptions } from "./utils"; + +export default function RuntimeOptions({ + runtimeOptions, + handleUpdate, + errors, +}: { + runtimeOptions: IRuntimeOptions; + handleUpdate: (runtimeOptions: IRuntimeOptions) => void; + errors: { timeoutSeconds: boolean }; +}) { + const [compatibleRowyRunVersion] = useAtom( + compatibleRowyRunVersionAtom, + projectScope + ); + const openRowyRunModal = useSetAtom(rowyRunModalAtom, projectScope); + + const [expanded, setExpanded] = useState(false); + + const isCompatibleRowyRun = compatibleRowyRunVersion({ minVersion: "1.6.4" }); + + return ( + + + ) : ( + + ) + } + onClick={() => + isCompatibleRowyRun + ? setExpanded(!expanded) + : openRowyRunModal({ + version: "1.6.4", + feature: "Runtime options", + }) + } + > + Runtime options + + + + + handleUpdate({ memory: value ?? "256MB" })} + multiple={false} + options={["128MB", "256MB", "512MB", "1GB", "2GB", "4GB", "8GB"]} + /> + + + + seconds + ), + }} + onChange={(e) => + !isNaN(Number(e.target.value)) && + handleUpdate({ + timeoutSeconds: Number(e.target.value), + }) + } + inputProps={{ + inputMode: "numeric", + }} + error={errors.timeoutSeconds} + helperText={ + errors.timeoutSeconds + ? "Timeout must be an integer between 1 and 540" + : "The maximum timeout that can be specified is 9 mins (540 seconds)" + } + /> + + + + + ); +} diff --git a/src/components/TableModals/ExtensionsModal/utils.ts b/src/components/TableModals/ExtensionsModal/utils.ts index 117ef381..28d24fda 100644 --- a/src/components/TableModals/ExtensionsModal/utils.ts +++ b/src/components/TableModals/ExtensionsModal/utils.ts @@ -52,6 +52,12 @@ export interface IExtension { trackedFields?: string[]; } +// https://firebase.google.com/docs/functions/manage-functions#set_runtime_options +export interface IRuntimeOptions { + memory?: "128MB" | "256MB" | "512MB" | "1GB" | "2GB" | "4GB" | "8GB"; + timeoutSeconds?: number; +} + export const triggerTypes: ExtensionTrigger[] = ["create", "update", "delete"]; const extensionBodyTemplate = { diff --git a/src/components/TableSettingsDialog/TableDetails.tsx b/src/components/TableSettingsDialog/TableDetails.tsx index 705233b8..c4cdbe05 100644 --- a/src/components/TableSettingsDialog/TableDetails.tsx +++ b/src/components/TableSettingsDialog/TableDetails.tsx @@ -14,19 +14,40 @@ export default function TableDetails({ ...props }) { {props.label ?? ""} ul": { + display: "flex", + alignItems: "center", + }, + "& .w-md-editor-toolbar > ul:first-of-type": { + overflowX: "auto", + marginRight: theme.spacing(1), + }, + "& :is(h1, h2, h3, h4, h5, h6)": { + marginY: `${theme.spacing(1.5)} !important`, + borderBottom: "none !important", + }, + "& details summary": { + marginBottom: theme.spacing(1), + }, }} > { title: string; icon: React.ReactNode; + tooltip?: string; } export const TableToolbarButton = forwardRef(function TableToolbarButton_( - { title, icon, ...props }: ITableToolbarButtonProps, + { title, icon, tooltip, ...props }: ITableToolbarButtonProps, ref: React.Ref ) { + // https://mui.com/material-ui/react-tooltip/#accessibility + const tooltipIsDescription = Boolean(tooltip); + + const button = ( + + ); + return ( - - - - + + {props.disabled ? {button} : button} ); }); diff --git a/src/components/fields/Action/EditorCell.tsx b/src/components/fields/Action/EditorCell.tsx index 1706c4e2..2f458e86 100644 --- a/src/components/fields/Action/EditorCell.tsx +++ b/src/components/fields/Action/EditorCell.tsx @@ -4,6 +4,15 @@ import { Stack } from "@mui/material"; import ActionFab from "./ActionFab"; import { sanitiseCallableName, isUrl } from "./utils"; +import { get } from "lodash-es"; + +export const getActionName = (column: any) => { + const config = get(column, "config"); + if (!get(config, "customName.enabled")) { + return get(column, "name"); + } + return get(config, "customName.actionName") || get(column, "name"); +}; export default function Action({ column, @@ -28,7 +37,7 @@ export default function Action({ ) : hasRan ? ( value.status ) : ( - sanitiseCallableName(column.key) + sanitiseCallableName(getActionName(column)) )}
diff --git a/src/components/fields/Action/Settings.tsx b/src/components/fields/Action/Settings.tsx index 0c0655b1..a40a3c54 100644 --- a/src/components/fields/Action/Settings.tsx +++ b/src/components/fields/Action/Settings.tsx @@ -559,20 +559,46 @@ const Settings = ({ config, onChange, fieldName }: ISettingsProps) => { title: "Customization", content: ( <> - + + onChange("customName.enabled")(e.target.checked) + } + name="customName.enabled" + /> + } + label="Customize label for action" + style={{ marginLeft: -11 }} + /> + {config.customName?.enabled && ( + - onChange("customIcons.enabled")(e.target.checked) + onChange("customName.actionName")(e.target.value) } - name="customIcons.enabled" - /> - } - label="Customize button icons with emoji" - style={{ marginLeft: -11 }} - /> - + label="Action name:" + className="labelHorizontal" + inputProps={{ style: { width: "10ch" } }} + > + )} + + onChange("customIcons.enabled")(e.target.checked) + } + name="customIcons.enabled" + /> + } + label="Customize button icons with emoji" + style={{ marginLeft: -11 }} + /> + {config.customIcons?.enabled && ( diff --git a/src/components/fields/Action/SideDrawerField.tsx b/src/components/fields/Action/SideDrawerField.tsx index f17dcb7e..1f13e6d4 100644 --- a/src/components/fields/Action/SideDrawerField.tsx +++ b/src/components/fields/Action/SideDrawerField.tsx @@ -10,6 +10,7 @@ import ActionFab from "./ActionFab"; import { tableScope, tableRowsAtom } from "@src/atoms/tableScope"; import { fieldSx, getFieldId } from "@src/components/SideDrawer/utils"; import { sanitiseCallableName, isUrl } from "./utils"; +import { getActionName } from "./TableCell"; export default function Action({ column, @@ -58,7 +59,7 @@ export default function Action({ ) : hasRan ? ( value.status ) : ( - sanitiseCallableName(column.key) + sanitiseCallableName(getActionName(column)) )} diff --git a/src/components/fields/Color/filters.ts b/src/components/fields/Color/filters.ts new file mode 100644 index 00000000..0a2732be --- /dev/null +++ b/src/components/fields/Color/filters.ts @@ -0,0 +1,21 @@ +import { IFilterOperator } from "@src/components/fields/types"; + +export const filterOperators: IFilterOperator[] = [ + { + label: "is", + secondaryLabel: "==", + value: "color-equal", + }, + { + label: "is not", + secondaryLabel: "!=", + value: "color-not-equal", + }, +]; + +export const valueFormatter = (value: any) => { + if (value && value.hex) { + return value.hex.toString(); + } + return ""; +}; diff --git a/src/components/fields/Color/index.tsx b/src/components/fields/Color/index.tsx index cb266301..140593a3 100644 --- a/src/components/fields/Color/index.tsx +++ b/src/components/fields/Color/index.tsx @@ -5,6 +5,7 @@ import { toColor } from "react-color-palette"; import ColorIcon from "@mui/icons-material/Colorize"; import DisplayCell from "./DisplayCell"; +import { filterOperators, valueFormatter } from "./filters"; const EditorCell = lazy( () => import("./EditorCell" /* webpackChunkName: "EditorCell-Color" */) @@ -28,6 +29,10 @@ export const config: IFieldConfig = { disablePadding: true, }), SideDrawerField, + filter: { + operators: filterOperators, + valueFormatter, + }, csvImportParser: (value: string) => { try { const obj = JSON.parse(value); diff --git a/src/components/fields/Json/SideDrawerField.tsx b/src/components/fields/Json/SideDrawerField.tsx index 89ddbc1e..4c0fbc96 100644 --- a/src/components/fields/Json/SideDrawerField.tsx +++ b/src/components/fields/Json/SideDrawerField.tsx @@ -40,13 +40,14 @@ export default function Json({ const [editor, setEditor] = useAtom(jsonEditorAtom, projectScope); const [codeValid, setCodeValid] = useState(true); - const sanitizedValue = + const baseValue = value !== undefined && isValidJson(value) ? value : column.config?.isArray ? [] : {}; - const formattedJson = stringify(sanitizedValue, { space: 2 }); + const formattedJson = stringify(baseValue, { space: 2 }); + const sanitizedValue = JSON.parse(formattedJson); if (disabled) return ( diff --git a/src/components/fields/Rating/DisplayCell.tsx b/src/components/fields/Rating/DisplayCell.tsx index 1e539ad4..b5d28471 100644 --- a/src/components/fields/Rating/DisplayCell.tsx +++ b/src/components/fields/Rating/DisplayCell.tsx @@ -2,24 +2,7 @@ import React, { forwardRef } from "react"; import { IDisplayCellProps } from "@src/components/fields/types"; import MuiRating, { RatingProps as MuiRatingProps } from "@mui/material/Rating"; -import RatingIcon from "@mui/icons-material/Star"; -import RatingOutlineIcon from "@mui/icons-material/StarBorder"; -import { get } from "lodash-es"; - -export const getStateIcon = (config: any) => { - // only use the config to get the custom rating icon if enabled via toggle - if (!get(config, "customIcons.enabled")) { - return ; - } - return get(config, "customIcons.rating") || ; -}; - -export const getStateOutline = (config: any) => { - if (!get(config, "customIcons.enabled")) { - return ; - } - return get(config, "customIcons.rating") || ; -}; +import Icon from "./Icon"; export const Rating = forwardRef(function Rating( { @@ -55,14 +38,15 @@ export const Rating = forwardRef(function Rating( if (["ArrowLeft", "ArrowRight", "ArrowUp", "ArrowDown"].includes(e.key)) e.stopPropagation(); }} - icon={getStateIcon(column.config)} + icon={} + emptyIcon={} size="small" readOnly={disabled} - emptyIcon={getStateOutline(column.config)} max={max} precision={precision} sx={{ mx: -0.25 }} /> ); }); + export default Rating; diff --git a/src/components/fields/Rating/Icon.tsx b/src/components/fields/Rating/Icon.tsx new file mode 100644 index 00000000..e65b8f3d --- /dev/null +++ b/src/components/fields/Rating/Icon.tsx @@ -0,0 +1,31 @@ +import RatingIcon from "@mui/icons-material/Star"; +import RatingOutlineIcon from "@mui/icons-material/StarBorder"; +import { get } from "lodash-es"; + +export interface IIconProps { + config: any; + isEmpty: boolean; +} + +export default function Icon({ config, isEmpty }: IIconProps) { + if (isEmpty) { + return getStateOutline(config); + } else { + return getStateIcon(config); + } +} + +const getStateIcon = (config: any) => { + // only use the config to get the custom rating icon if enabled via toggle + if (!get(config, "customIcons.enabled")) { + return ; + } + console.log(get(config, "customIcons.rating")); + return get(config, "customIcons.rating") || ; +}; +const getStateOutline = (config: any) => { + if (!get(config, "customIcons.enabled")) { + return ; + } + return get(config, "customIcons.rating") || ; +}; diff --git a/src/components/fields/Rating/Settings.tsx b/src/components/fields/Rating/Settings.tsx index 6134b530..7bf99d5b 100644 --- a/src/components/fields/Rating/Settings.tsx +++ b/src/components/fields/Rating/Settings.tsx @@ -1,11 +1,17 @@ import { ISettingsProps } from "@src/components/fields/types"; -import RatingIcon from "@mui/icons-material/Star"; -import RatingOutlineIcon from "@mui/icons-material/StarBorder" -import { InputLabel, TextField, Grid, FormControlLabel, Checkbox, Stack } from "@mui/material"; +import { + InputLabel, + TextField, + Grid, + FormControlLabel, + Checkbox, + Stack, +} from "@mui/material"; import ToggleButton from "@mui/material/ToggleButton"; import ToggleButtonGroup from "@mui/material/ToggleButtonGroup"; import MuiRating from "@mui/material/Rating"; import { get } from "lodash-es"; +import Icon from "./Icon"; export default function Settings({ onChange, config }: ISettingsProps) { return ( @@ -18,10 +24,13 @@ export default function Settings({ onChange, config }: ISettingsProps) { fullWidth error={false} onChange={(e) => { - let input = parseInt(e.target.value) || 0 - if (input > 20) { input = 20 } + let input = parseInt(e.target.value) || 0; + if (input > 20) { + input = 20; + } onChange("max")(input); }} + inputProps={{ min: 1, max: 20 }} /> @@ -68,28 +77,26 @@ export default function Settings({ onChange, config }: ISettingsProps) { - onChange("customIcons.rating")(e.target.value) - } + onChange={(e) => onChange("customIcons.rating")(e.target.value)} label="Custom icon preview:" className="labelHorizontal" inputProps={{ style: { width: "2ch" } }} /> - e.stopPropagation()} - icon={get(config, "customIcons.rating") || } + icon={} size="small" - emptyIcon={get(config, "customIcons.rating") || } + emptyIcon={} max={get(config, "max")} precision={get(config, "precision")} sx={{ pt: 0.5 }} /> - )} ); -} \ No newline at end of file +} diff --git a/src/components/fields/Rating/SideDrawerField.tsx b/src/components/fields/Rating/SideDrawerField.tsx index 389d3211..ea69325e 100644 --- a/src/components/fields/Rating/SideDrawerField.tsx +++ b/src/components/fields/Rating/SideDrawerField.tsx @@ -3,8 +3,8 @@ import { ISideDrawerFieldProps } from "@src/components/fields/types"; import { Grid } from "@mui/material"; import { Rating as MuiRating } from "@mui/material"; import "@mui/lab"; -import { getStateIcon, getStateOutline } from "./DisplayCell"; import { fieldSx } from "@src/components/SideDrawer/utils"; +import Icon from "./Icon"; export default function Rating({ column, @@ -27,8 +27,8 @@ export default function Rating({ onChange(newValue); onSubmit(); }} - icon={getStateIcon(column.config)} - emptyIcon={getStateOutline(column.config)} + icon={} + emptyIcon={} size="small" max={max} precision={precision} diff --git a/src/constants/externalLinks.ts b/src/constants/externalLinks.ts index bf2f41ab..4625d6ba 100644 --- a/src/constants/externalLinks.ts +++ b/src/constants/externalLinks.ts @@ -15,8 +15,8 @@ export const EXTERNAL_LINKS = { twitter: "https://twitter.com/rowyio", productHunt: "https://www.producthunt.com/products/rowy-2", - rowyRun: meta.repository.url.replace(".git", "Run"), - rowyRunGitHub: meta.repository.url.replace(".git", "Run"), + rowyRun: meta.repository.url.replace("rowy.git", "backend"), + rowyRunGitHub: meta.repository.url.replace("rowy.git", "backend"), // prettier-ignore rowyRunDeploy: `https://deploy.cloud.run/?git_repo=${meta.repository.url.replace(".git", "Run")}.git`, diff --git a/src/hooks/useFirestoreCollectionWithAtom.ts b/src/hooks/useFirestoreCollectionWithAtom.ts index 43f7165a..cf5d648c 100644 --- a/src/hooks/useFirestoreCollectionWithAtom.ts +++ b/src/hooks/useFirestoreCollectionWithAtom.ts @@ -376,12 +376,20 @@ export const tableFiltersToFirestoreFilters = (filters: TableFilter[]) => { } else if (filter.operator === "id-equal") { firestoreFilters.push(where(documentId(), "==", filter.value)); continue; + } else if (filter.operator === "color-equal") { + firestoreFilters.push( + where(filter.key.concat(".hex"), "==", filter.value.hex.toString()) + ); + continue; + } else if (filter.operator === "color-not-equal") { + firestoreFilters.push( + where(filter.key.concat(".hex"), "!=", filter.value.hex.toString()) + ); + continue; } - firestoreFilters.push( where(filter.key, filter.operator as WhereFilterOp, filter.value) ); } - return firestoreFilters; }; diff --git a/src/index.tsx b/src/index.tsx index 5d545f47..f4d7b2c9 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -8,11 +8,11 @@ import reportWebVitals from "./reportWebVitals"; const container = document.getElementById("root")!; const root = createRoot(container); root.render( - - - - - + // + + + + // ); // If you want to start measuring performance in your app, pass a function diff --git a/src/pages/Table/ProvidedTablePage.tsx b/src/pages/Table/ProvidedTablePage.tsx index ab2b7614..8f86be90 100644 --- a/src/pages/Table/ProvidedTablePage.tsx +++ b/src/pages/Table/ProvidedTablePage.tsx @@ -1,16 +1,20 @@ import { lazy, Suspense } from "react"; import { useAtom, Provider } from "jotai"; import { DebugAtoms } from "@src/atoms/utils"; -import { useParams, useOutlet } from "react-router-dom"; +import { useParams, useOutlet, Link } from "react-router-dom"; import { ErrorBoundary } from "react-error-boundary"; import { find, isEmpty } from "lodash-es"; +import useOffline from "@src/hooks/useOffline"; -import ErrorFallback, { - ERROR_TABLE_NOT_FOUND, -} from "@src/components/ErrorFallback"; +import { Typography, Button } from "@mui/material"; + +import ErrorFallback from "@src/components/ErrorFallback"; import TableSourceFirestore from "@src/sources/TableSourceFirestore"; import TableToolbarSkeleton from "@src/components/TableToolbar/TableToolbarSkeleton"; import TableSkeleton from "@src/components/Table/TableSkeleton"; +import EmptyState from "@src/components/EmptyState"; +import OfflineIcon from "@mui/icons-material/CloudOff"; +import { Tables as TablesIcon } from "@src/assets/icons"; import { projectScope, @@ -25,6 +29,7 @@ import { tableSettingsAtom, } from "@src/atoms/tableScope"; import { SyncAtomValue } from "@src/atoms/utils"; +import { ROUTES } from "@src/constants/routes"; import useDocumentTitle from "@src/hooks/useDocumentTitle"; // prettier-ignore @@ -41,6 +46,7 @@ export default function ProvidedTablePage() { const [currentUser] = useAtom(currentUserAtom, projectScope); const [projectSettings] = useAtom(projectSettingsAtom, projectScope); const [tables] = useAtom(tablesAtom, projectScope); + const isOffline = useOffline(); const tableSettings = find(tables, ["id", id]); useDocumentTitle(projectId, tableSettings ? tableSettings.name : "Not found"); @@ -54,7 +60,41 @@ export default function ProvidedTablePage() { ); } else { - throw new Error(ERROR_TABLE_NOT_FOUND + ": " + id); + if (isOffline) { + return ( + + ); + } else { + return ( + + + Make sure you have the right ID + + {id} + + + } + /> + ); + } } } diff --git a/src/theme/components.tsx b/src/theme/components.tsx index 2bc4b2e5..374dc25b 100644 --- a/src/theme/components.tsx +++ b/src/theme/components.tsx @@ -1381,11 +1381,12 @@ export const components = (theme: Theme): ThemeOptions => { MuiRating: { styleOverrides: { - iconFilled: { color: theme.palette.text.secondary }, icon: { // https://github.com/mui/material-ui/issues/32557 "& .MuiSvgIcon-root": { pointerEvents: "auto" }, + color: theme.palette.text.secondary, }, + iconEmpty: { opacity: 0.38 }, }, }, diff --git a/src/types/table.d.ts b/src/types/table.d.ts index 5b54b9be..b9753c41 100644 --- a/src/types/table.d.ts +++ b/src/types/table.d.ts @@ -4,7 +4,10 @@ import type { DocumentData, DocumentReference, } from "firebase/firestore"; -import { IExtension } from "@src/components/TableModals/ExtensionsModal/utils"; +import { + IExtension, + IRuntimeOptions, +} from "@src/components/TableModals/ExtensionsModal/utils"; import { IWebhook } from "@src/components/TableModals/WebhooksModal/utils"; /** @@ -104,6 +107,7 @@ export type TableSchema = { extensionObjects?: IExtension[]; compiledExtension?: string; webhooks?: IWebhook[]; + runtimeOptions?: IRuntimeOptions; /** @deprecated Migrate to Extensions */ sparks?: string; @@ -146,7 +150,11 @@ export type ColumnConfig = { /** Regex used in CellValidation */ validationRegex: string; /** FieldType to render for Derivative fields */ - renderFieldType: FieldType; + renderFieldType?: FieldType; + /** Used in Derivative fields */ + listenerFields?: string[]; + /** Used in Derivative and Action fields */ + requiredFields?: string[]; /** For sub-table fields */ parentLabel: string[]; @@ -167,7 +175,9 @@ export type TableFilter = { | "date-before-equal" | "date-after-equal" | "time-minute-equal" - | "id-equal"; + | "id-equal" + | "color-equal" + | "color-not-equal"; value: any; }; From 282268989703516a63137b88ac86e53c8b2526ea Mon Sep 17 00:00:00 2001 From: Sidney Alcantara Date: Mon, 14 Nov 2022 16:11:34 +1100 Subject: [PATCH 42/66] move Action getActionName util fn to DisplayCell.tsx --- src/components/fields/Action/DisplayCell.tsx | 13 +++++++++++-- src/components/fields/Action/EditorCell.tsx | 10 +--------- src/components/fields/Action/SideDrawerField.tsx | 2 +- 3 files changed, 13 insertions(+), 12 deletions(-) diff --git a/src/components/fields/Action/DisplayCell.tsx b/src/components/fields/Action/DisplayCell.tsx index f852ce6b..78871401 100644 --- a/src/components/fields/Action/DisplayCell.tsx +++ b/src/components/fields/Action/DisplayCell.tsx @@ -1,5 +1,14 @@ import { IDisplayCellProps } from "@src/components/fields/types"; +import { get } from "lodash-es"; -export default function Action({ name, value }: IDisplayCellProps) { - return <>{value ? value.status : name}; +export const getActionName = (column: IDisplayCellProps["column"]) => { + const config = get(column, "config"); + if (!get(config, "customName.enabled")) { + return get(column, "name"); + } + return get(config, "customName.actionName") || get(column, "name"); +}; + +export default function Action({ value, column }: IDisplayCellProps) { + return <>{value ? value.status : getActionName(column)}; } diff --git a/src/components/fields/Action/EditorCell.tsx b/src/components/fields/Action/EditorCell.tsx index 2f458e86..35a04cb6 100644 --- a/src/components/fields/Action/EditorCell.tsx +++ b/src/components/fields/Action/EditorCell.tsx @@ -4,15 +4,7 @@ import { Stack } from "@mui/material"; import ActionFab from "./ActionFab"; import { sanitiseCallableName, isUrl } from "./utils"; -import { get } from "lodash-es"; - -export const getActionName = (column: any) => { - const config = get(column, "config"); - if (!get(config, "customName.enabled")) { - return get(column, "name"); - } - return get(config, "customName.actionName") || get(column, "name"); -}; +import { getActionName } from "./DisplayCell"; export default function Action({ column, diff --git a/src/components/fields/Action/SideDrawerField.tsx b/src/components/fields/Action/SideDrawerField.tsx index 1f13e6d4..200fc05f 100644 --- a/src/components/fields/Action/SideDrawerField.tsx +++ b/src/components/fields/Action/SideDrawerField.tsx @@ -10,7 +10,7 @@ import ActionFab from "./ActionFab"; import { tableScope, tableRowsAtom } from "@src/atoms/tableScope"; import { fieldSx, getFieldId } from "@src/components/SideDrawer/utils"; import { sanitiseCallableName, isUrl } from "./utils"; -import { getActionName } from "./TableCell"; +import { getActionName } from "./DisplayCell"; export default function Action({ column, From f5754af73f99a2b7e1d582382879011e77849d35 Mon Sep 17 00:00:00 2001 From: Sidney Alcantara Date: Mon, 14 Nov 2022 16:11:46 +1100 Subject: [PATCH 43/66] Rating: remove expensive @mui/lab import --- src/components/fields/Rating/SideDrawerField.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/src/components/fields/Rating/SideDrawerField.tsx b/src/components/fields/Rating/SideDrawerField.tsx index ea69325e..24f1a8d6 100644 --- a/src/components/fields/Rating/SideDrawerField.tsx +++ b/src/components/fields/Rating/SideDrawerField.tsx @@ -2,7 +2,6 @@ import { ISideDrawerFieldProps } from "@src/components/fields/types"; import { Grid } from "@mui/material"; import { Rating as MuiRating } from "@mui/material"; -import "@mui/lab"; import { fieldSx } from "@src/components/SideDrawer/utils"; import Icon from "./Icon"; From 315f3c7d29456127bb63355ed254418bf4439ee8 Mon Sep 17 00:00:00 2001 From: Sidney Alcantara Date: Mon, 14 Nov 2022 17:04:14 +1100 Subject: [PATCH 44/66] improve visual affordance for column drag reorder --- src/assets/icons/index.ts | 3 ++ .../Table/ColumnHeader/ColumnHeader.tsx | 5 ++- src/components/Table/Table.tsx | 40 ++++++++++++++++--- 3 files changed, 40 insertions(+), 8 deletions(-) diff --git a/src/assets/icons/index.ts b/src/assets/icons/index.ts index 3503e99b..372af8ff 100644 --- a/src/assets/icons/index.ts +++ b/src/assets/icons/index.ts @@ -103,6 +103,9 @@ export { FileTableBoxOutline as Project }; import { TableColumn } from "mdi-material-ui"; export { TableColumn }; +import { DragVertical } from "mdi-material-ui"; +export { DragVertical }; + export * from "./AddRow"; export * from "./AddRowTop"; export * from "./ChevronDown"; diff --git a/src/components/Table/ColumnHeader/ColumnHeader.tsx b/src/components/Table/ColumnHeader/ColumnHeader.tsx index 1a01c9dd..345e856b 100644 --- a/src/components/Table/ColumnHeader/ColumnHeader.tsx +++ b/src/components/Table/ColumnHeader/ColumnHeader.tsx @@ -166,13 +166,13 @@ export const ColumnHeader = forwardRef(function ColumnHeader( placement="bottom-start" disableInteractive TransitionComponent={Fade} + sx={{ "& .MuiTooltip-tooltip": { marginTop: "-28px !important" } }} > diff --git a/src/components/Table/Table.tsx b/src/components/Table/Table.tsx index ad26cb42..5271e90d 100644 --- a/src/components/Table/Table.tsx +++ b/src/components/Table/Table.tsx @@ -16,6 +16,7 @@ import { } from "react-beautiful-dnd"; import { get } from "lodash-es"; import { ErrorBoundary } from "react-error-boundary"; +import { DragVertical } from "@src/assets/icons"; import StyledTable from "./Styled/StyledTable"; import StyledRow from "./Styled/StyledRow"; @@ -44,12 +45,9 @@ import { selectedCellAtom, contextMenuTargetAtom, } from "@src/atoms/tableScope"; -import { COLLECTION_PAGE_SIZE } from "@src/config/db"; import { getFieldType, getFieldProp } from "@src/components/fields"; -import { FieldType } from "@src/constants/fields"; import { TableRow, ColumnConfig } from "@src/types/table"; -import { StyledCell } from "./Styled/StyledCell"; import { useKeyboardNavigation } from "./useKeyboardNavigation"; import { useSaveColumnSizing } from "./useSaveColumnSizing"; import useVirtualization from "./useVirtualization"; @@ -89,7 +87,6 @@ export default function Table({ hiddenColumns, emptyState, }: ITableProps) { - const [tableSettings] = useAtom(tableSettingsAtom, tableScope); const [tableSchema] = useAtom(tableSchemaAtom, tableScope); const [tableColumnsOrdered] = useAtom(tableColumnsOrderedAtom, tableScope); const [tableRows] = useAtom(tableRowsAtom, tableScope); @@ -100,7 +97,6 @@ export default function Table({ const focusInsideCell = selectedCell?.focusInside ?? false; const updateColumn = useSetAtom(updateColumnAtom, tableScope); - const updateField = useSetAtom(updateFieldAtom, tableScope); const containerRef = useRef(null); const gridRef = useRef(null); @@ -365,8 +361,40 @@ export default function Table({ position: "absolute", inset: 0, zIndex: 0, + display: "flex", + alignItems: "center", + outline: "none", }} - /> + className="column-drag-handle" + > + + theme.transitions.create(["opacity"]), + "[role='columnheader']:hover &, [role='columnheader']:focus-within &": + { + opacity: 0.5, + }, + ".column-drag-handle:hover &": { + opacity: 1, + }, + ".column-drag-handle:active &": { + opacity: 1, + color: "primary.main", + }, + ".column-drag-handle:focus &": { + opacity: 1, + color: "primary.main", + outline: "2px solid", + outlineColor: "primary.main", + }, + }} + style={{ width: 8 }} + preserveAspectRatio="xMidYMid slice" + /> +
{header.column.getCanResize() && ( Date: Tue, 15 Nov 2022 16:53:20 +1100 Subject: [PATCH 45/66] memoize more components --- src/components/Table/CellValidation.tsx | 186 ++++++++--- .../Table/FinalColumn/FinalColumn.tsx | 4 +- .../Table/FinalColumn/FinalColumnHeader.tsx | 4 +- src/components/Table/Table.tsx | 298 ++---------------- src/components/Table/TableHeaderGroup.tsx | 193 ++++++++++++ src/components/Table/formatters/ChipList.tsx | 13 +- .../Table/useKeyboardNavigation.tsx | 205 ++++++------ src/components/Table/useSaveColumnSizing.tsx | 6 +- src/components/Table/withTableCell.tsx | 23 +- .../fields/ConnectService/DisplayCell.tsx | 3 +- .../fields/ConnectTable/DisplayCell.tsx | 3 +- .../fields/Connector/DisplayCell.tsx | 3 +- src/components/fields/File/DisplayCell.tsx | 8 +- src/components/fields/File/EditorCell.tsx | 3 +- src/components/fields/Id/DisplayCell.tsx | 4 +- .../fields/MultiSelect/DisplayCell.tsx | 3 +- src/components/fields/SubTable/utils.ts | 4 +- src/components/fields/types.ts | 17 +- src/pages/Table/TablePage.tsx | 12 +- 19 files changed, 535 insertions(+), 457 deletions(-) create mode 100644 src/components/Table/TableHeaderGroup.tsx diff --git a/src/components/Table/CellValidation.tsx b/src/components/Table/CellValidation.tsx index 02284cb3..e19dde1b 100644 --- a/src/components/Table/CellValidation.tsx +++ b/src/components/Table/CellValidation.tsx @@ -1,11 +1,25 @@ import { memo } from "react"; +import { useAtom, useSetAtom } from "jotai"; +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 "./Styled/StyledCell"; +import { InlineErrorFallback } from "@src/components/ErrorFallback"; import RichTooltip from "@src/components/RichTooltip"; +import { + tableScope, + selectedCellAtom, + contextMenuTargetAtom, +} from "@src/atoms/tableScope"; +import { TABLE_PADDING } from "./Table"; +import type { TableRow } from "@src/types/table"; + const Dot = styled("div")(({ theme }) => ({ position: "absolute", right: -5, @@ -25,55 +39,147 @@ const Dot = styled("div")(({ theme }) => ({ }, })); -export interface ICellValidationProps - extends React.DetailedHTMLProps< - React.HTMLAttributes, - HTMLDivElement - > { - value: any; - required?: boolean; - validationRegex?: string; +export interface ICellValidationProps { + row: Row; + cell: Cell; + index: number; + isSelectedCell: boolean; + isReadOnlyCell: boolean; + canEditCells: boolean; + rowHeight: number; + lastFrozen?: string; + left?: number; } export const CellValidation = memo(function MemoizedCellValidation({ - value, - required, - validationRegex, - children, - ...props + row, + cell, + index, + isSelectedCell, + isReadOnlyCell, + canEditCells, + rowHeight, + lastFrozen, + left, }: ICellValidationProps) { + const [selectedCell, setSelectedCell] = useAtom(selectedCellAtom, tableScope); + const focusInsideCell = selectedCell?.focusInside ?? false; + const setContextMenuTarget = useSetAtom(contextMenuTargetAtom, tableScope); + + const value = cell.getValue(); + const required = cell.column.columnDef.meta?.config?.required; + const validationRegex = cell.column.columnDef.meta?.config?.validationRegex; + const isInvalid = validationRegex && !new RegExp(validationRegex).test(value); const isMissing = required && value === undefined; - if (isInvalid) - return ( - - } - title="Invalid data" - message="This row will not be saved until all the required fields contain valid data" - placement="right" - render={({ openTooltip }) => } - /> - {children} - - ); + const renderedCell = ( + + {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: + !canEditCells || cell.column.columnDef.meta?.editable === false, + rowHeight, + })} + + ); - if (isMissing) - return ( - - } - title="Required field" - message="This row will not be saved until all the required fields contain valid data" - placement="right" - render={({ openTooltip }) => } - /> - {children} - - ); + // if (isInvalid) + // return ( + // + // } + // title="Invalid data" + // message="This row will not be saved until all the required fields contain valid data" + // placement="right" + // render={({ openTooltip }) => } + // /> + // {children} + // + // ); - return {children}; + // if (isMissing) + // return ( + // + // } + // title="Required field" + // message="This row will not be saved until all the required fields contain valid data" + // placement="right" + // render={({ openTooltip }) => } + // /> + // {children} + // + // ); + + return ( + { + 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(); + }} + onContextMenu={(e) => { + e.preventDefault(); + setSelectedCell({ + path: row.original._rowy_ref.path, + columnKey: cell.column.id, + focusInside: false, + }); + (e.target as HTMLDivElement).focus(); + setContextMenuTarget(e.target as HTMLElement); + }} + > + {renderedCell} + + ); }); export default CellValidation; diff --git a/src/components/Table/FinalColumn/FinalColumn.tsx b/src/components/Table/FinalColumn/FinalColumn.tsx index 4b7c8bd0..6f058eda 100644 --- a/src/components/Table/FinalColumn/FinalColumn.tsx +++ b/src/components/Table/FinalColumn/FinalColumn.tsx @@ -1,6 +1,6 @@ import { memo } from "react"; import { useAtom, useSetAtom } from "jotai"; -import type { TableCellProps } from "@src/components/Table"; +import type { ITableCellProps } from "@src/components/Table/withTableCell"; import { Stack, Tooltip, IconButton, alpha } from "@mui/material"; import { CopyCells as CopyCellsIcon } from "@src/assets/icons"; @@ -25,7 +25,7 @@ import { export const FinalColumn = memo(function FinalColumn({ row, focusInsideCell, -}: TableCellProps) { +}: ITableCellProps) { const [userRoles] = useAtom(userRolesAtom, projectScope); const [addRowIdType] = useAtom(tableAddRowIdTypeAtom, projectScope); const confirm = useSetAtom(confirmDialogAtom, projectScope); diff --git a/src/components/Table/FinalColumn/FinalColumnHeader.tsx b/src/components/Table/FinalColumn/FinalColumnHeader.tsx index 416cab5c..a1989f53 100644 --- a/src/components/Table/FinalColumn/FinalColumnHeader.tsx +++ b/src/components/Table/FinalColumn/FinalColumnHeader.tsx @@ -9,12 +9,12 @@ import { spreadSx } from "@src/utils/ui"; export interface IFinalColumnHeaderProps extends Partial { focusInsideCell: boolean; - canAddColumn: boolean; + canAddColumns: boolean; } export default function FinalColumnHeader({ focusInsideCell, - canAddColumn, + canAddColumns, ...props }: IFinalColumnHeaderProps) { const [userRoles] = useAtom(userRolesAtom, projectScope); diff --git a/src/components/Table/Table.tsx b/src/components/Table/Table.tsx index 5271e90d..74c14a40 100644 --- a/src/components/Table/Table.tsx +++ b/src/components/Table/Table.tsx @@ -20,6 +20,7 @@ import { DragVertical } from "@src/assets/icons"; import StyledTable from "./Styled/StyledTable"; import StyledRow from "./Styled/StyledRow"; +import TableHeaderGroup from "./TableHeaderGroup"; import ColumnHeader from "./ColumnHeader"; import StyledResizer from "./Styled/StyledResizer"; import FinalColumnHeader from "./FinalColumn/FinalColumnHeader"; @@ -59,12 +60,6 @@ export const TABLE_PADDING = 16; export const OUT_OF_ORDER_MARGIN = 8; export const DEBOUNCE_DELAY = 500; -export type TableCellProps = CellContext & { - focusInsideCell: boolean; - setFocusInsideCell: (focusInside: boolean) => void; - disabled: boolean; -}; - declare module "@tanstack/table-core" { interface ColumnMeta extends ColumnConfig {} } @@ -73,17 +68,17 @@ const columnHelper = createColumnHelper(); const getRowId = (row: TableRow) => row._rowy_ref.path || row._rowy_ref.id; export interface ITableProps { - canAddColumn: boolean; - canEditColumn: boolean; - canEditCell: boolean; + canAddColumns: boolean; + canEditColumns: boolean; + canEditCells: boolean; hiddenColumns?: string[]; emptyState?: React.ReactNode; } export default function Table({ - canAddColumn, - canEditColumn, - canEditCell, + canAddColumns, + canEditColumns, + canEditCells, hiddenColumns, emptyState, }: ITableProps) { @@ -118,7 +113,7 @@ export default function Table({ }) ); - if (canAddColumn || canEditCell) { + if (canAddColumns || canEditCells) { _columns.push( columnHelper.display({ id: "_rowy_column_actions", @@ -128,7 +123,7 @@ export default function Table({ } return _columns; - }, [tableColumnsOrdered, canAddColumn, canEditCell]); + }, [tableColumnsOrdered, canAddColumns, canEditCells]); // Get user’s hidden columns from props and memoize into a VisibilityState const columnVisibility = useMemo(() => { @@ -183,7 +178,7 @@ export default function Table({ paddingLeft, paddingRight, } = useVirtualization(containerRef, leafColumns); - useSaveColumnSizing(columnSizing, canEditColumn); + useSaveColumnSizing(columnSizing, canEditColumns); const handleDropColumn = useCallback( (result: DropResult) => { @@ -227,7 +222,7 @@ export default function Table({ - - {table.getHeaderGroups().map((headerGroup) => ( - - {(provided) => ( - - {headerGroup.headers.map((header) => { - const isSelectedCell = - (!selectedCell && header.index === 0) || - (selectedCell?.path === "_rowy_header" && - selectedCell?.columnKey === header.id); - - if (header.id === "_rowy_column_actions") - return ( - - ); - - if (!header.column.columnDef.meta) return null; - - return ( - - {(provided, snapshot) => ( - { - 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(); - }} - focusInsideCell={ - isSelectedCell && focusInsideCell - } - > -
- - theme.transitions.create(["opacity"]), - "[role='columnheader']:hover &, [role='columnheader']:focus-within &": - { - opacity: 0.5, - }, - ".column-drag-handle:hover &": { - opacity: 1, - }, - ".column-drag-handle:active &": { - opacity: 1, - color: "primary.main", - }, - ".column-drag-handle:focus &": { - opacity: 1, - color: "primary.main", - outline: "2px solid", - outlineColor: "primary.main", - }, - }} - style={{ width: 8 }} - preserveAspectRatio="xMidYMid slice" - /> -
- - {header.column.getCanResize() && ( - - )} -
- )} -
- ); - })} - {provided.placeholder} -
- )} -
- ))} -
+
@@ -464,95 +301,20 @@ export default function Table({ return ( { - 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(); - }} - onContextMenu={(e) => { - e.preventDefault(); - 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} - validationRegex={ - cell.column.columnDef.meta?.config?.validationRegex - } - > - - {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, - })} - - + isSelectedCell={isSelectedCell} + isReadOnlyCell={isReadOnlyCell} + canEditCells={canEditCells} + lastFrozen={lastFrozen} + rowHeight={tableSchema.rowHeight || DEFAULT_ROW_HEIGHT} + /> ); })} diff --git a/src/components/Table/TableHeaderGroup.tsx b/src/components/Table/TableHeaderGroup.tsx new file mode 100644 index 00000000..9f2e0a23 --- /dev/null +++ b/src/components/Table/TableHeaderGroup.tsx @@ -0,0 +1,193 @@ +import { memo } from "react"; +import { useAtom } from "jotai"; +import { DragDropContext, Droppable, Draggable } from "react-beautiful-dnd"; +import type { DropResult } from "react-beautiful-dnd"; +import type { HeaderGroup } from "@tanstack/react-table"; +import type { TableRow } from "@src/types/table"; + +import StyledRow from "./Styled/StyledRow"; +import ColumnHeader from "./ColumnHeader"; +import StyledResizer from "./Styled/StyledResizer"; +import FinalColumnHeader from "./FinalColumn/FinalColumnHeader"; +import { DragVertical } from "@src/assets/icons"; + +import { tableScope, selectedCellAtom } from "@src/atoms/tableScope"; +import { DEFAULT_ROW_HEIGHT, TABLE_PADDING } from "@src/components/Table"; + +export interface ITableHeaderGroupProps { + headerGroups: HeaderGroup[]; + handleDropColumn: (result: DropResult) => void; + canAddColumns: boolean; + canEditColumns: boolean; + lastFrozen?: string; +} + +export const TableHeaderGroup = memo(function TableHeaderGroup({ + headerGroups, + handleDropColumn, + canAddColumns, + canEditColumns, + lastFrozen, +}: ITableHeaderGroupProps) { + const [selectedCell, setSelectedCell] = useAtom(selectedCellAtom, tableScope); + const focusInsideCell = selectedCell?.focusInside ?? false; + + return ( + + {headerGroups.map((headerGroup) => ( + + {(provided) => ( + + {headerGroup.headers.map((header) => { + const isSelectedCell = + (!selectedCell && header.index === 0) || + (selectedCell?.path === "_rowy_header" && + selectedCell?.columnKey === header.id); + + if (header.id === "_rowy_column_actions") + return ( + + ); + + if (!header.column.columnDef.meta) return null; + + return ( + + {(provided, snapshot) => ( + { + 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(); + }} + focusInsideCell={isSelectedCell && focusInsideCell} + > +
+ + theme.transitions.create(["opacity"]), + "[role='columnheader']:hover &, [role='columnheader']:focus-within &": + { + opacity: 0.5, + }, + ".column-drag-handle:hover &": { + opacity: 1, + }, + ".column-drag-handle:active &": { + opacity: 1, + color: "primary.main", + }, + ".column-drag-handle:focus &": { + opacity: 1, + color: "primary.main", + outline: "2px solid", + outlineColor: "primary.main", + }, + }} + style={{ width: 8 }} + preserveAspectRatio="xMidYMid slice" + /> +
+ + {header.column.getCanResize() && ( + + )} +
+ )} +
+ ); + })} + {provided.placeholder} +
+ )} +
+ ))} +
+ ); +}); + +export default TableHeaderGroup; diff --git a/src/components/Table/formatters/ChipList.tsx b/src/components/Table/formatters/ChipList.tsx index 5a6945d2..42f06264 100644 --- a/src/components/Table/formatters/ChipList.tsx +++ b/src/components/Table/formatters/ChipList.tsx @@ -1,14 +1,9 @@ -import { useAtom } from "jotai"; - import { Grid } from "@mui/material"; -import { tableScope, tableSchemaAtom } from "@src/atoms/tableScope"; -import { DEFAULT_ROW_HEIGHT } from "@src/components/Table"; - -export default function ChipList({ children }: React.PropsWithChildren<{}>) { - const [tableSchema] = useAtom(tableSchemaAtom, tableScope); - - const rowHeight = tableSchema.rowHeight ?? DEFAULT_ROW_HEIGHT; +export default function ChipList({ + children, + rowHeight, +}: React.PropsWithChildren<{ rowHeight: number }>) { const canWrap = rowHeight > 24 * 2 + 4; return ( diff --git a/src/components/Table/useKeyboardNavigation.tsx b/src/components/Table/useKeyboardNavigation.tsx index 57c8829c..3353d23f 100644 --- a/src/components/Table/useKeyboardNavigation.tsx +++ b/src/components/Table/useKeyboardNavigation.tsx @@ -1,3 +1,4 @@ +import { useCallback } from "react"; import { useSetAtom } from "jotai"; import { Column } from "@tanstack/react-table"; @@ -23,123 +24,129 @@ export function useKeyboardNavigation({ }: IUseKeyboardNavigationProps) { const setSelectedCell = useSetAtom(selectedCellAtom, tableScope); - const handleKeyDown = (e: React.KeyboardEvent) => { - // Block default browser behavior for arrow keys (scroll) and other keys - const LISTENED_KEYS = [ - "ArrowUp", - "ArrowDown", - "ArrowLeft", - "ArrowRight", - "Enter", - "Escape", - "Home", - "End", - "PageUp", - "PageDown", - ]; - if (LISTENED_KEYS.includes(e.key)) e.preventDefault(); + const handleKeyDown = useCallback( + (e: React.KeyboardEvent) => { + // Block default browser behavior for arrow keys (scroll) and other keys + const LISTENED_KEYS = [ + "ArrowUp", + "ArrowDown", + "ArrowLeft", + "ArrowRight", + "Enter", + "Escape", + "Home", + "End", + "PageUp", + "PageDown", + ]; + if (LISTENED_KEYS.includes(e.key)) e.preventDefault(); - // Esc: exit cell - if (e.key === "Escape") { - setSelectedCell((c) => ({ ...c!, focusInside: false })); - ( - gridRef.current?.querySelector("[aria-selected=true]") as HTMLDivElement - )?.focus(); - return; - } + // Esc: exit cell + if (e.key === "Escape") { + setSelectedCell((c) => ({ ...c!, focusInside: false })); + ( + gridRef.current?.querySelector( + "[aria-selected=true]" + ) as HTMLDivElement + )?.focus(); + return; + } - // If event target is not a cell, ignore - const target = e.target as HTMLDivElement; - if ( - target.getAttribute("role") !== "columnheader" && - target.getAttribute("role") !== "gridcell" - ) - return; + // If event target is not a cell, ignore + const target = e.target as HTMLDivElement; + if ( + target.getAttribute("role") !== "columnheader" && + target.getAttribute("role") !== "gridcell" + ) + return; - // If Tab, ignore so we can exit the table - if (e.key === "Tab") return; + // If Tab, ignore so we can exit the table + if (e.key === "Tab") return; - // Enter: enter cell - if (e.key === "Enter") { - setSelectedCell((c) => ({ ...c!, focusInside: true })); - (target.querySelector("[tabindex]") as HTMLElement)?.focus(); - return; - } + // Enter: enter cell + if (e.key === "Enter") { + setSelectedCell((c) => ({ ...c!, focusInside: true })); + (target.querySelector("[tabindex]") as HTMLElement)?.focus(); + return; + } - const colIndex = Number(target.getAttribute("aria-colindex")) - 1; - const rowIndex = - Number(target.parentElement!.getAttribute("aria-rowindex")) - 2; + const colIndex = Number(target.getAttribute("aria-colindex")) - 1; + const rowIndex = + Number(target.parentElement!.getAttribute("aria-rowindex")) - 2; - let newColIndex = colIndex; - let newRowIndex = rowIndex; + let newColIndex = colIndex; + let newRowIndex = rowIndex; - switch (e.key) { - case "ArrowUp": - if (e.ctrlKey || e.metaKey) newRowIndex = -1; - else if (rowIndex > -1) newRowIndex = rowIndex - 1; - break; + switch (e.key) { + case "ArrowUp": + if (e.ctrlKey || e.metaKey) newRowIndex = -1; + else if (rowIndex > -1) newRowIndex = rowIndex - 1; + break; - case "ArrowDown": - if (e.ctrlKey || e.metaKey) newRowIndex = tableRows.length - 1; - else if (rowIndex < tableRows.length - 1) newRowIndex = rowIndex + 1; - break; + case "ArrowDown": + if (e.ctrlKey || e.metaKey) newRowIndex = tableRows.length - 1; + else if (rowIndex < tableRows.length - 1) newRowIndex = rowIndex + 1; + break; - case "ArrowLeft": - if (e.ctrlKey || e.metaKey) newColIndex = 0; - else if (colIndex > 0) newColIndex = colIndex - 1; - break; + case "ArrowLeft": + if (e.ctrlKey || e.metaKey) newColIndex = 0; + else if (colIndex > 0) newColIndex = colIndex - 1; + break; - case "ArrowRight": - if (e.ctrlKey || e.metaKey) newColIndex = leafColumns.length - 1; - else if (colIndex < leafColumns.length - 1) newColIndex = colIndex + 1; - break; + case "ArrowRight": + if (e.ctrlKey || e.metaKey) newColIndex = leafColumns.length - 1; + else if (colIndex < leafColumns.length - 1) + newColIndex = colIndex + 1; + break; - case "PageUp": - newRowIndex = Math.max(0, rowIndex - COLLECTION_PAGE_SIZE); - break; + case "PageUp": + newRowIndex = Math.max(0, rowIndex - COLLECTION_PAGE_SIZE); + break; - case "PageDown": - newRowIndex = Math.min( - tableRows.length - 1, - rowIndex + COLLECTION_PAGE_SIZE - ); - break; + case "PageDown": + newRowIndex = Math.min( + tableRows.length - 1, + rowIndex + COLLECTION_PAGE_SIZE + ); + break; - case "Home": - newColIndex = 0; - if (e.ctrlKey || e.metaKey) newRowIndex = -1; - break; + case "Home": + newColIndex = 0; + if (e.ctrlKey || e.metaKey) newRowIndex = -1; + break; - case "End": - newColIndex = leafColumns.length - 1; - if (e.ctrlKey || e.metaKey) newRowIndex = tableRows.length - 1; - break; - } + case "End": + newColIndex = leafColumns.length - 1; + if (e.ctrlKey || e.metaKey) newRowIndex = tableRows.length - 1; + break; + } - // Get `path` and `columnKey` from `tableRows` and `leafColumns` respectively - const newSelectedCell = { - path: - newRowIndex > -1 - ? tableRows[newRowIndex]._rowy_ref.path - : "_rowy_header", - columnKey: leafColumns[newColIndex].id! || leafColumns[0].id!, - // When selected cell changes, exit current cell - focusInside: false, - }; + // Get `path` and `columnKey` from `tableRows` and `leafColumns` respectively + const newSelectedCell = { + path: + newRowIndex > -1 + ? 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 - setSelectedCell(newSelectedCell); + // Store in selectedCellAtom + setSelectedCell(newSelectedCell); - // Find matching DOM element for the cell - const newCellEl = gridRef.current?.querySelector( - `[aria-rowindex="${newRowIndex + 2}"] [aria-colindex="${ - newColIndex + 1 - }"]` - ); + // Find matching DOM element for the cell + const newCellEl = gridRef.current?.querySelector( + `[aria-rowindex="${newRowIndex + 2}"] [aria-colindex="${ + newColIndex + 1 + }"]` + ); - // Focus the cell - if (newCellEl) setTimeout(() => (newCellEl as HTMLDivElement).focus()); - }; + // Focus the cell + if (newCellEl) setTimeout(() => (newCellEl as HTMLDivElement).focus()); + }, + [gridRef, leafColumns, setSelectedCell, tableRows] + ); return { handleKeyDown } as const; } diff --git a/src/components/Table/useSaveColumnSizing.tsx b/src/components/Table/useSaveColumnSizing.tsx index 2ea940ac..d957ac14 100644 --- a/src/components/Table/useSaveColumnSizing.tsx +++ b/src/components/Table/useSaveColumnSizing.tsx @@ -19,7 +19,7 @@ import { ColumnSizingState } from "@tanstack/react-table"; /** Debounces columnSizing and asks admins if they want to save for all users */ export function useSaveColumnSizing( columnSizing: ColumnSizingState, - canEditColumn: boolean + canEditColumns: boolean ) { const { enqueueSnackbar, closeSnackbar } = useSnackbar(); const updateColumn = useSetAtom(updateColumnAtom, tableScope); @@ -30,7 +30,7 @@ export function useSaveColumnSizing( }); // Offer to save when column sizing changes useEffect(() => { - if (!canEditColumn || isEmpty(debouncedColumnSizing)) return; + if (!canEditColumns || isEmpty(debouncedColumnSizing)) return; const snackbarId = enqueueSnackbar("Save column sizes for all users?", { action: ( @@ -45,7 +45,7 @@ export function useSaveColumnSizing( return () => closeSnackbar(snackbarId); }, [ debouncedColumnSizing, - canEditColumn, + canEditColumns, enqueueSnackbar, closeSnackbar, updateColumn, diff --git a/src/components/Table/withTableCell.tsx b/src/components/Table/withTableCell.tsx index 28a7d3bd..bd5b4b41 100644 --- a/src/components/Table/withTableCell.tsx +++ b/src/components/Table/withTableCell.tsx @@ -9,16 +9,17 @@ import { import useStateRef from "react-usestateref"; 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 type { CellContext } from "@tanstack/react-table"; import { Popover, PopoverProps } from "@mui/material"; import { tableScope, updateFieldAtom } from "@src/atoms/tableScope"; import { spreadSx } from "@src/utils/ui"; +import type { TableRow } from "@src/types/table"; +import type { + IDisplayCellProps, + IEditorCellProps, +} from "@src/components/fields/types"; export interface ICellOptions { /** If the rest of the row’s data is used, set this to true for memoization */ @@ -31,6 +32,13 @@ export interface ICellOptions { popoverProps?: Partial; } +export interface ITableCellProps extends CellContext { + focusInsideCell: boolean; + setFocusInsideCell: (focusInside: boolean) => void; + disabled: boolean; + rowHeight: number; +} + /** * HOC to render table cells. * Renders read-only DisplayCell while scrolling for scroll performance. @@ -57,7 +65,8 @@ export default function withTableCell( focusInsideCell, setFocusInsideCell, disabled, - }: TableCellProps) { + rowHeight, + }: ITableCellProps) { const value = getValue(); // Store ref to rendered DisplayCell to get positioning for PopoverCell @@ -93,12 +102,12 @@ export default function withTableCell( type: column.columnDef.meta!.type, row: row.original, column: column.columnDef.meta!, - docRef: row.original._rowy_ref, _rowy_ref: row.original._rowy_ref, disabled: column.columnDef.meta!.editable === false, tabIndex: focusInsideCell ? 0 : -1, showPopoverCell, setFocusInsideCell, + rowHeight, }; // Show display cell, unless if editorMode is inline diff --git a/src/components/fields/ConnectService/DisplayCell.tsx b/src/components/fields/ConnectService/DisplayCell.tsx index e4d08055..e7d267c0 100644 --- a/src/components/fields/ConnectService/DisplayCell.tsx +++ b/src/components/fields/ConnectService/DisplayCell.tsx @@ -12,12 +12,13 @@ export default function ConnectService({ disabled, column, tabIndex, + rowHeight, }: IDisplayCellProps) { const config = column.config ?? {}; const displayKey = config.titleKey ?? config.primaryKey; const rendered = ( - + {Array.isArray(value) && value.map((snapshot) => ( diff --git a/src/components/fields/ConnectTable/DisplayCell.tsx b/src/components/fields/ConnectTable/DisplayCell.tsx index 75c0da2b..efde15bc 100644 --- a/src/components/fields/ConnectTable/DisplayCell.tsx +++ b/src/components/fields/ConnectTable/DisplayCell.tsx @@ -11,11 +11,12 @@ export default function ConnectTable({ disabled, column, tabIndex, + rowHeight, }: IDisplayCellProps) { const config = column.config ?? {}; const rendered = ( - + {Array.isArray(value) ? ( value.map((item: any) => ( diff --git a/src/components/fields/Connector/DisplayCell.tsx b/src/components/fields/Connector/DisplayCell.tsx index 8c6316bc..cd707d80 100644 --- a/src/components/fields/Connector/DisplayCell.tsx +++ b/src/components/fields/Connector/DisplayCell.tsx @@ -13,9 +13,10 @@ export default function Connector({ disabled, column, tabIndex, + rowHeight, }: IDisplayCellProps) { const rendered = ( - + {Array.isArray(value) && value.map((item) => ( diff --git a/src/components/fields/File/DisplayCell.tsx b/src/components/fields/File/DisplayCell.tsx index 73303ddf..ea3a6382 100644 --- a/src/components/fields/File/DisplayCell.tsx +++ b/src/components/fields/File/DisplayCell.tsx @@ -6,9 +6,13 @@ import ChipList from "@src/components/Table/formatters/ChipList"; import { FileIcon } from "."; import { FileValue } from "@src/types/table"; -export default function File_({ value, tabIndex }: IDisplayCellProps) { +export default function File_({ + value, + tabIndex, + rowHeight, +}: IDisplayCellProps) { return ( - + {Array.isArray(value) && value.map((file: FileValue) => ( - + {Array.isArray(value) && value.map((file: FileValue) => ( - {docRef?.id} + {_rowy_ref?.id} ); } diff --git a/src/components/fields/MultiSelect/DisplayCell.tsx b/src/components/fields/MultiSelect/DisplayCell.tsx index eb5e8267..b439c4b9 100644 --- a/src/components/fields/MultiSelect/DisplayCell.tsx +++ b/src/components/fields/MultiSelect/DisplayCell.tsx @@ -13,6 +13,7 @@ export default function MultiSelect({ showPopoverCell, disabled, tabIndex, + rowHeight, }: IDisplayCellProps) { const rendered = typeof value === "string" && value !== "" ? ( @@ -24,7 +25,7 @@ export default function MultiSelect({ {value}
) : ( - + {sanitiseValue(value).map( (item) => typeof item === "string" && ( diff --git a/src/components/fields/SubTable/utils.ts b/src/components/fields/SubTable/utils.ts index e62e0a45..6f6c7746 100644 --- a/src/components/fields/SubTable/utils.ts +++ b/src/components/fields/SubTable/utils.ts @@ -6,7 +6,7 @@ import { ColumnConfig, TableRow, TableRowRef } from "@src/types/table"; export const useSubTableData = ( column: ColumnConfig, row: TableRow, - docRef: TableRowRef + _rowy_ref: TableRowRef ) => { const label = (column.config?.parentLabel ?? []).reduce((acc, curr) => { if (acc !== "") return `${acc} - ${row[curr]}`; @@ -24,7 +24,7 @@ export const useSubTableData = ( let subTablePath = [ rootTablePath, ROUTES.subTable, - encodeURIComponent(docRef.path), + encodeURIComponent(_rowy_ref.path), column.key, ].join("/"); diff --git a/src/components/fields/types.ts b/src/components/fields/types.ts index 8897e003..e4084365 100644 --- a/src/components/fields/types.ts +++ b/src/components/fields/types.ts @@ -1,16 +1,14 @@ 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"; -import { +import type { ITableCellProps } from "@src/components/Table/withTableCell"; +import type { PopoverProps } from "@mui/material"; +import type { ColumnConfig, TableRow, TableRowRef, TableFilter, } from "@src/types/table"; -import { SelectedCell } from "@src/atoms/tableScope"; -import { IContextMenuItem } from "@src/components/Table/ContextMenu/ContextMenuItem"; +import type { SelectedCell } from "@src/atoms/tableScope"; +import type { IContextMenuItem } from "@src/components/Table/ContextMenu/ContextMenuItem"; export { FieldType }; @@ -29,7 +27,7 @@ export interface IFieldConfig { selectedCell: SelectedCell, reset: () => void ) => IContextMenuItem[]; - TableCell: React.ComponentType; + TableCell: React.ComponentType; SideDrawerField: React.ComponentType; settings?: React.ComponentType; settingsValidator?: (config: Record) => Record; @@ -50,14 +48,13 @@ export interface IDisplayCellProps { name: string; row: TableRow; column: ColumnConfig; - /** @deprecated */ - docRef: TableRowRef; /** The row’s _rowy_ref object */ _rowy_ref: TableRowRef; disabled: boolean; tabIndex: number; showPopoverCell: (value: boolean) => void; setFocusInsideCell: (focusInside: boolean) => void; + rowHeight: number; } export interface IEditorCellProps extends IDisplayCellProps { /** Call when the user has input but changes have not been saved */ diff --git a/src/pages/Table/TablePage.tsx b/src/pages/Table/TablePage.tsx index 234be866..da455666 100644 --- a/src/pages/Table/TablePage.tsx +++ b/src/pages/Table/TablePage.tsx @@ -70,9 +70,9 @@ export default function TablePage({ // Set permissions here so we can pass them to the Table component, which // shouldn’t access projectScope at all, to separate concerns. - const canAddColumn = userRoles.includes("ADMIN"); - const canEditColumn = userRoles.includes("ADMIN"); - const canEditCell = + const canAddColumns = userRoles.includes("ADMIN"); + const canEditColumns = userRoles.includes("ADMIN"); + const canEditCells = userRoles.includes("ADMIN") || (!tableSettings.readOnly && intersection(userRoles, tableSettings.roles).length > 0); @@ -135,9 +135,9 @@ export default function TablePage({ }} >
Date: Tue, 15 Nov 2022 17:36:48 +1100 Subject: [PATCH 46/66] move ChipList.tsx --- src/components/Table/{formatters => }/ChipList.tsx | 0 src/components/fields/ConnectService/DisplayCell.tsx | 2 +- src/components/fields/ConnectTable/DisplayCell.tsx | 2 +- src/components/fields/Connector/DisplayCell.tsx | 2 +- src/components/fields/File/DisplayCell.tsx | 2 +- src/components/fields/File/EditorCell.tsx | 2 +- src/components/fields/MultiSelect/DisplayCell.tsx | 2 +- 7 files changed, 6 insertions(+), 6 deletions(-) rename src/components/Table/{formatters => }/ChipList.tsx (100%) diff --git a/src/components/Table/formatters/ChipList.tsx b/src/components/Table/ChipList.tsx similarity index 100% rename from src/components/Table/formatters/ChipList.tsx rename to src/components/Table/ChipList.tsx diff --git a/src/components/fields/ConnectService/DisplayCell.tsx b/src/components/fields/ConnectService/DisplayCell.tsx index e7d267c0..2dd91bd8 100644 --- a/src/components/fields/ConnectService/DisplayCell.tsx +++ b/src/components/fields/ConnectService/DisplayCell.tsx @@ -3,7 +3,7 @@ import { IDisplayCellProps } from "@src/components/fields/types"; import { ButtonBase, Grid, Chip } from "@mui/material"; import { ChevronDown } from "@src/assets/icons"; -import ChipList from "@src/components/Table/formatters/ChipList"; +import ChipList from "@src/components/Table/ChipList"; import { get } from "lodash-es"; export default function ConnectService({ diff --git a/src/components/fields/ConnectTable/DisplayCell.tsx b/src/components/fields/ConnectTable/DisplayCell.tsx index efde15bc..90ca35af 100644 --- a/src/components/fields/ConnectTable/DisplayCell.tsx +++ b/src/components/fields/ConnectTable/DisplayCell.tsx @@ -3,7 +3,7 @@ import { IDisplayCellProps } from "@src/components/fields/types"; import { ButtonBase, Grid, Chip } from "@mui/material"; import { ChevronDown } from "@src/assets/icons"; -import ChipList from "@src/components/Table/formatters/ChipList"; +import ChipList from "@src/components/Table/ChipList"; export default function ConnectTable({ value, diff --git a/src/components/fields/Connector/DisplayCell.tsx b/src/components/fields/Connector/DisplayCell.tsx index cd707d80..6576204a 100644 --- a/src/components/fields/Connector/DisplayCell.tsx +++ b/src/components/fields/Connector/DisplayCell.tsx @@ -3,7 +3,7 @@ import { IDisplayCellProps } from "@src/components/fields/types"; import { ButtonBase, Grid, Chip } from "@mui/material"; import { ChevronDown } from "@src/assets/icons"; -import ChipList from "@src/components/Table/formatters/ChipList"; +import ChipList from "@src/components/Table/ChipList"; import { get } from "lodash-es"; import { getLabel } from "./utils"; diff --git a/src/components/fields/File/DisplayCell.tsx b/src/components/fields/File/DisplayCell.tsx index ea3a6382..2183f902 100644 --- a/src/components/fields/File/DisplayCell.tsx +++ b/src/components/fields/File/DisplayCell.tsx @@ -1,7 +1,7 @@ import { IDisplayCellProps } from "@src/components/fields/types"; import { Grid, Chip } from "@mui/material"; -import ChipList from "@src/components/Table/formatters/ChipList"; +import ChipList from "@src/components/Table/ChipList"; import { FileIcon } from "."; import { FileValue } from "@src/types/table"; diff --git a/src/components/fields/File/EditorCell.tsx b/src/components/fields/File/EditorCell.tsx index 2b8692d3..ed5ba74b 100644 --- a/src/components/fields/File/EditorCell.tsx +++ b/src/components/fields/File/EditorCell.tsx @@ -8,7 +8,7 @@ import { format } from "date-fns"; import { alpha, Stack, Grid, Tooltip, Chip, IconButton } from "@mui/material"; import { Upload as UploadIcon } from "@src/assets/icons"; -import ChipList from "@src/components/Table/formatters/ChipList"; +import ChipList from "@src/components/Table/ChipList"; import CircularProgressOptical from "@src/components/CircularProgressOptical"; import { projectScope, confirmDialogAtom } from "@src/atoms/projectScope"; diff --git a/src/components/fields/MultiSelect/DisplayCell.tsx b/src/components/fields/MultiSelect/DisplayCell.tsx index b439c4b9..7554330a 100644 --- a/src/components/fields/MultiSelect/DisplayCell.tsx +++ b/src/components/fields/MultiSelect/DisplayCell.tsx @@ -5,7 +5,7 @@ import WarningIcon from "@mui/icons-material/WarningAmber"; import { ChevronDown } from "@src/assets/icons"; import { sanitiseValue } from "./utils"; -import ChipList from "@src/components/Table/formatters/ChipList"; +import ChipList from "@src/components/Table/ChipList"; import FormattedChip from "@src/components/FormattedChip"; export default function MultiSelect({ From 1b454de85a0edd33928dc1801da5ad8843f0a840 Mon Sep 17 00:00:00 2001 From: Sidney Alcantara Date: Tue, 15 Nov 2022 17:36:58 +1100 Subject: [PATCH 47/66] extract TableBody out of Table --- src/components/Table/Table.tsx | 130 +++---------------- src/components/Table/TableBody.tsx | 125 ++++++++++++++++++ src/components/Table/useSaveColumnSizing.tsx | 4 +- 3 files changed, 143 insertions(+), 116 deletions(-) create mode 100644 src/components/Table/TableBody.tsx diff --git a/src/components/Table/Table.tsx b/src/components/Table/Table.tsx index 74c14a40..c7395447 100644 --- a/src/components/Table/Table.tsx +++ b/src/components/Table/Table.tsx @@ -3,55 +3,35 @@ import { useAtom, useSetAtom } from "jotai"; import { useThrottledCallback } from "use-debounce"; import { createColumnHelper, - flexRender, getCoreRowModel, useReactTable, - CellContext, } from "@tanstack/react-table"; -import { - DragDropContext, - DropResult, - Droppable, - Draggable, -} from "react-beautiful-dnd"; +import { DropResult } from "react-beautiful-dnd"; import { get } from "lodash-es"; -import { ErrorBoundary } from "react-error-boundary"; -import { DragVertical } from "@src/assets/icons"; import StyledTable from "./Styled/StyledTable"; -import StyledRow from "./Styled/StyledRow"; import TableHeaderGroup from "./TableHeaderGroup"; -import ColumnHeader from "./ColumnHeader"; -import StyledResizer from "./Styled/StyledResizer"; -import FinalColumnHeader from "./FinalColumn/FinalColumnHeader"; +import TableBody from "./TableBody"; 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 { tableScope, - tableSettingsAtom, tableSchemaAtom, tableColumnsOrderedAtom, tableRowsAtom, tableNextPageAtom, tablePageAtom, updateColumnAtom, - updateFieldAtom, - selectedCellAtom, - contextMenuTargetAtom, } from "@src/atoms/tableScope"; import { getFieldType, getFieldProp } from "@src/components/fields"; import { TableRow, ColumnConfig } from "@src/types/table"; import { useKeyboardNavigation } from "./useKeyboardNavigation"; import { useSaveColumnSizing } from "./useSaveColumnSizing"; -import useVirtualization from "./useVirtualization"; export const DEFAULT_ROW_HEIGHT = 41; export const DEFAULT_COL_WIDTH = 150; @@ -61,6 +41,7 @@ export const OUT_OF_ORDER_MARGIN = 8; export const DEBOUNCE_DELAY = 500; declare module "@tanstack/table-core" { + /** The `column.meta` property contains the column config from tableSchema */ interface ColumnMeta extends ColumnConfig {} } @@ -87,9 +68,6 @@ export default function Table({ const [tableRows] = useAtom(tableRowsAtom, tableScope); const [tableNextPage] = useAtom(tableNextPageAtom, tableScope); 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); @@ -170,14 +148,7 @@ export default function Table({ tableRows, leafColumns, }); - const { - virtualRows, - virtualCols, - paddingTop, - paddingBottom, - paddingLeft, - paddingRight, - } = useVirtualization(containerRef, leafColumns); + useSaveColumnSizing(columnSizing, canEditColumns); const handleDropColumn = useCallback( @@ -253,88 +224,17 @@ export default function Table({ /> -
- {paddingTop > 0 && ( -
- )} - - {virtualRows.map((virtualRow) => { - const row = rows[virtualRow.index]; - const outOfOrder = row.original._rowy_outOfOrder; - - return ( - - {paddingLeft > 0 && ( -
- )} - - {outOfOrder && } - - {virtualCols.map((virtualCell) => { - const cellIndex = virtualCell.index; - const cell = row.getVisibleCells()[cellIndex]; - - const isSelectedCell = - selectedCell?.path === row.original._rowy_ref.path && - selectedCell?.columnKey === cell.column.id; - - const fieldTypeGroup = getFieldProp( - "group", - cell.column.columnDef.meta?.type - ); - const isReadOnlyCell = - fieldTypeGroup === "Auditing" || - fieldTypeGroup === "Metadata"; - - return ( - - ); - })} - - {paddingRight > 0 && ( -
- )} - - ); - })} - - {paddingBottom > 0 && ( -
- )} - - {tableRows.length === 0 && - (emptyState ?? )} -
+ {tableRows.length === 0 ? ( + emptyState ?? + ) : ( + + )}
; + leafColumns: Column[]; + rows: Row[]; + + canEditCells: boolean; + lastFrozen?: string; +} + +export default function TableBody({ + containerRef, + leafColumns, + rows, + canEditCells, + lastFrozen, +}: ITableBodyProps) { + const [tableSchema] = useAtom(tableSchemaAtom, tableScope); + const [selectedCell] = useAtom(selectedCellAtom, tableScope); + + const { + virtualRows, + virtualCols, + paddingTop, + paddingBottom, + paddingLeft, + paddingRight, + } = useVirtualization(containerRef, leafColumns); + + return ( +
+ {paddingTop > 0 && ( +
+ )} + + {virtualRows.map((virtualRow) => { + const row = rows[virtualRow.index]; + const outOfOrder = row.original._rowy_outOfOrder; + + return ( + + {paddingLeft > 0 && ( +
+ )} + + {outOfOrder && } + + {virtualCols.map((virtualCell) => { + const cellIndex = virtualCell.index; + const cell = row.getVisibleCells()[cellIndex]; + + const isSelectedCell = + selectedCell?.path === row.original._rowy_ref.path && + selectedCell?.columnKey === cell.column.id; + + const fieldTypeGroup = getFieldProp( + "group", + cell.column.columnDef.meta?.type + ); + const isReadOnlyCell = + fieldTypeGroup === "Auditing" || fieldTypeGroup === "Metadata"; + + return ( + + ); + })} + + {paddingRight > 0 && ( +
+ )} + + ); + })} + + {paddingBottom > 0 && ( +
+ )} +
+ ); +} diff --git a/src/components/Table/useSaveColumnSizing.tsx b/src/components/Table/useSaveColumnSizing.tsx index d957ac14..2bcf9c59 100644 --- a/src/components/Table/useSaveColumnSizing.tsx +++ b/src/components/Table/useSaveColumnSizing.tsx @@ -16,7 +16,9 @@ import { 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 admins if they want to save for all users + */ export function useSaveColumnSizing( columnSizing: ColumnSizingState, canEditColumns: boolean From ea8019e1e3284dc2148c55f4df171307ccb9c02f Mon Sep 17 00:00:00 2001 From: Sidney Alcantara Date: Tue, 15 Nov 2022 18:02:30 +1100 Subject: [PATCH 48/66] render inline editor cell after timeout on mount to improve scroll performance --- src/components/Table/withTableCell.tsx | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/src/components/Table/withTableCell.tsx b/src/components/Table/withTableCell.tsx index bd5b4b41..d959e408 100644 --- a/src/components/Table/withTableCell.tsx +++ b/src/components/Table/withTableCell.tsx @@ -67,8 +67,17 @@ export default function withTableCell( disabled, rowHeight, }: ITableCellProps) { + // Get the latest value on every re-render of this component const value = getValue(); + // Render inline editor cell after timeout on mount + // to improve scroll performance + const [inlineEditorReady, setInlineEditorReady] = useState(false); + useEffect(() => { + if (editorMode === "inline") + setTimeout(() => setInlineEditorReady(true)); + }, []); + // Store ref to rendered DisplayCell to get positioning for PopoverCell const displayCellRef = useRef(null); const parentRef = displayCellRef.current?.parentElement; @@ -110,6 +119,9 @@ export default function withTableCell( rowHeight, }; + // If the inline editor cell is not ready to be rendered, display nothing + if (editorMode === "inline" && !inlineEditorReady) return null; + // Show display cell, unless if editorMode is inline const displayCell = (
Date: Tue, 15 Nov 2022 18:14:44 +1100 Subject: [PATCH 49/66] re-enable cell validation tooltips --- src/components/Table/CellValidation.tsx | 85 +++++++++---------- src/components/Table/Table.tsx | 4 +- .../{TableHeaderGroup.tsx => TableHeader.tsx} | 8 +- 3 files changed, 45 insertions(+), 52 deletions(-) rename src/components/Table/{TableHeaderGroup.tsx => TableHeader.tsx} (97%) diff --git a/src/components/Table/CellValidation.tsx b/src/components/Table/CellValidation.tsx index e19dde1b..6e566487 100644 --- a/src/components/Table/CellValidation.tsx +++ b/src/components/Table/CellValidation.tsx @@ -17,7 +17,6 @@ import { selectedCellAtom, contextMenuTargetAtom, } from "@src/atoms/tableScope"; -import { TABLE_PADDING } from "./Table"; import type { TableRow } from "@src/types/table"; const Dot = styled("div")(({ theme }) => ({ @@ -73,51 +72,29 @@ export const CellValidation = memo(function MemoizedCellValidation({ const isInvalid = validationRegex && !new RegExp(validationRegex).test(value); const isMissing = required && value === undefined; - const renderedCell = ( - - {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: - !canEditCells || cell.column.columnDef.meta?.editable === false, - rowHeight, - })} - - ); + let renderedValidationTooltip = null; - // if (isInvalid) - // return ( - // - // } - // title="Invalid data" - // message="This row will not be saved until all the required fields contain valid data" - // placement="right" - // render={({ openTooltip }) => } - // /> - // {children} - // - // ); - - // if (isMissing) - // return ( - // - // } - // title="Required field" - // message="This row will not be saved until all the required fields contain valid data" - // placement="right" - // render={({ openTooltip }) => } - // /> - // {children} - // - // ); + if (isInvalid) { + renderedValidationTooltip = ( + } + title="Invalid data" + message="This row will not be saved until all the required fields contain valid data" + placement="right" + render={({ openTooltip }) => } + /> + ); + } else if (isMissing) { + renderedValidationTooltip = ( + } + title="Required field" + message="This row will not be saved until all the required fields contain valid data" + placement="right" + render={({ openTooltip }) => } + /> + ); + } return ( - {renderedCell} + {renderedValidationTooltip} + + {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: + !canEditCells || cell.column.columnDef.meta?.editable === false, + rowHeight, + })} + ); }); diff --git a/src/components/Table/Table.tsx b/src/components/Table/Table.tsx index c7395447..886fdcff 100644 --- a/src/components/Table/Table.tsx +++ b/src/components/Table/Table.tsx @@ -10,7 +10,7 @@ import { DropResult } from "react-beautiful-dnd"; import { get } from "lodash-es"; import StyledTable from "./Styled/StyledTable"; -import TableHeaderGroup from "./TableHeaderGroup"; +import TableHeader from "./TableHeader"; import TableBody from "./TableBody"; import FinalColumn from "./FinalColumn/FinalColumn"; import ContextMenu from "./ContextMenu"; @@ -215,7 +215,7 @@ export default function Table({ padding: `0 ${TABLE_PADDING}px`, }} > - []; handleDropColumn: (result: DropResult) => void; canAddColumns: boolean; @@ -22,13 +22,13 @@ export interface ITableHeaderGroupProps { lastFrozen?: string; } -export const TableHeaderGroup = memo(function TableHeaderGroup({ +export const TableHeader = memo(function TableHeader({ headerGroups, handleDropColumn, canAddColumns, canEditColumns, lastFrozen, -}: ITableHeaderGroupProps) { +}: ITableHeaderProps) { const [selectedCell, setSelectedCell] = useAtom(selectedCellAtom, tableScope); const focusInsideCell = selectedCell?.focusInside ?? false; @@ -190,4 +190,4 @@ export const TableHeaderGroup = memo(function TableHeaderGroup({ ); }); -export default TableHeaderGroup; +export default TableHeader; From d81b4fc3c81f994259dee7674244b3a6bd899abf Mon Sep 17 00:00:00 2001 From: Sidney Alcantara Date: Tue, 15 Nov 2022 18:23:57 +1100 Subject: [PATCH 50/66] display RowsSkeleton when loading next page --- src/components/Table/TableBody.tsx | 5 +++++ src/components/Table/TableSkeleton.tsx | 15 +++------------ 2 files changed, 8 insertions(+), 12 deletions(-) diff --git a/src/components/Table/TableBody.tsx b/src/components/Table/TableBody.tsx index 76a1d597..ae119011 100644 --- a/src/components/Table/TableBody.tsx +++ b/src/components/Table/TableBody.tsx @@ -4,11 +4,13 @@ import type { Column, Row } from "@tanstack/react-table"; import StyledRow from "./Styled/StyledRow"; import OutOfOrderIndicator from "./OutOfOrderIndicator"; import CellValidation from "./CellValidation"; +import { RowsSkeleton } from "./TableSkeleton"; import { tableScope, tableSchemaAtom, selectedCellAtom, + tableNextPageAtom, } from "@src/atoms/tableScope"; import { getFieldProp } from "@src/components/fields"; @@ -38,6 +40,7 @@ export default function TableBody({ }: ITableBodyProps) { const [tableSchema] = useAtom(tableSchemaAtom, tableScope); const [selectedCell] = useAtom(selectedCellAtom, tableScope); + const [tableNextPage] = useAtom(tableNextPageAtom, tableScope); const { virtualRows, @@ -117,6 +120,8 @@ export default function TableBody({ ); })} + {tableNextPage.loading && } + {paddingBottom > 0 && (
)} diff --git a/src/components/Table/TableSkeleton.tsx b/src/components/Table/TableSkeleton.tsx index e264fde7..065770fc 100644 --- a/src/components/Table/TableSkeleton.tsx +++ b/src/components/Table/TableSkeleton.tsx @@ -13,7 +13,7 @@ import { tableSchemaAtom, tableColumnsOrderedAtom, } from "@src/atoms/tableScope"; -import { DEFAULT_ROW_HEIGHT, DEFAULT_COL_WIDTH } from "./Table"; +import { DEFAULT_ROW_HEIGHT, DEFAULT_COL_WIDTH, TABLE_PADDING } from "./Table"; import { COLLECTION_PAGE_SIZE } from "@src/config/db"; import { formatSubTableName } from "@src/utils/table"; @@ -129,17 +129,7 @@ export function RowsSkeleton() { div:first-of-type": { - borderBottomLeftRadius: (theme) => theme.shape.borderRadius, - }, - "&:last-of-type > div:last-of-type": { - borderBottomRightRadius: (theme) => theme.shape.borderRadius, - }, - }} + style={{ padding: `0 ${TABLE_PADDING}px`, marginTop: -1 }} > {columns.map((col, j) => ( Date: Tue, 15 Nov 2022 18:39:08 +1100 Subject: [PATCH 51/66] =?UTF-8?q?=F0=9F=8E=89=20remove=20react-data-grid?= =?UTF-8?q?=20dependency?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- package.json | 1 - src/components/SideDrawer/SideDrawer.tsx | 46 ++---------------------- src/pages/Table/TablePage.tsx | 8 ++--- yarn.lock | 7 ---- 4 files changed, 4 insertions(+), 58 deletions(-) diff --git a/package.json b/package.json index baf6cda0..0ea80114 100644 --- a/package.json +++ b/package.json @@ -47,7 +47,6 @@ "react": "^18.0.0", "react-beautiful-dnd": "^13.1.0", "react-color-palette": "^6.2.0", - "react-data-grid": "7.0.0-beta.5", "react-detect-offline": "^2.4.5", "react-div-100vh": "^0.7.0", "react-dnd": "^16.0.1", diff --git a/src/components/SideDrawer/SideDrawer.tsx b/src/components/SideDrawer/SideDrawer.tsx index 04f896bd..ef676bd1 100644 --- a/src/components/SideDrawer/SideDrawer.tsx +++ b/src/components/SideDrawer/SideDrawer.tsx @@ -1,10 +1,8 @@ import { useEffect } from "react"; -import useMemoValue from "use-memo-value"; import clsx from "clsx"; import { useAtom } from "jotai"; -import { find, findIndex, isEqual } from "lodash-es"; +import { find, findIndex } from "lodash-es"; import { ErrorBoundary } from "react-error-boundary"; -import { DataGridHandle } from "react-data-grid"; import { TransitionGroup } from "react-transition-group"; import { Fab, Fade } from "@mui/material"; @@ -16,34 +14,20 @@ import ErrorFallback from "@src/components/ErrorFallback"; import StyledDrawer from "./StyledDrawer"; import SideDrawerFields from "./SideDrawerFields"; -import { projectScope, userSettingsAtom } from "@src/atoms/projectScope"; import { tableScope, - tableIdAtom, - tableColumnsOrderedAtom, tableRowsAtom, sideDrawerOpenAtom, selectedCellAtom, } from "@src/atoms/tableScope"; import { analytics, logEvent } from "@src/analytics"; -import { formatSubTableName } from "@src/utils/table"; export const DRAWER_WIDTH = 512; export const DRAWER_COLLAPSED_WIDTH = 36; -export default function SideDrawer({ - dataGridRef, -}: { - dataGridRef?: React.MutableRefObject; -}) { - const [userSettings] = useAtom(userSettingsAtom, projectScope); - const [tableId] = useAtom(tableIdAtom, tableScope); - const [tableColumnsOrdered] = useAtom(tableColumnsOrderedAtom, tableScope); +export default function SideDrawer() { const [tableRows] = useAtom(tableRowsAtom, tableScope); - const userDocHiddenFields = - userSettings.tables?.[formatSubTableName(tableId)]?.hiddenFields ?? []; - const [cell, setCell] = useAtom(selectedCellAtom, tableScope); const [open, setOpen] = useAtom(sideDrawerOpenAtom, tableScope); const selectedRow = find(tableRows, ["_rowy_ref.path", cell?.path]); @@ -51,26 +35,6 @@ export default function SideDrawer({ "_rowy_ref.path", cell?.path, ]); - // Memo a list of visible column keys for useEffect dependencies - const visibleColumnKeys = useMemoValue( - tableColumnsOrdered - .filter((col) => !userDocHiddenFields.includes(col.key)) - .map((col) => col.key), - isEqual - ); - - // When side drawer is opened, select the cell in the table - // in case we’ve scrolled and selected cell was reset - useEffect(() => { - if (open) { - const columnIndex = visibleColumnKeys.indexOf(cell?.columnKey || ""); - if (columnIndex === -1 || selectedCellRowIndex === -1) return; - dataGridRef?.current?.selectCell( - { rowIdx: selectedCellRowIndex, idx: columnIndex }, - false - ); - } - }, [open, visibleColumnKeys, selectedCellRowIndex, cell, dataGridRef]); const handleNavigate = (direction: "up" | "down") => () => { if (!tableRows || !cell) return; @@ -84,12 +48,6 @@ export default function SideDrawer({ path: newPath, focusInside: false, })); - - const columnIndex = visibleColumnKeys.indexOf(cell!.columnKey || ""); - dataGridRef?.current?.selectCell( - { rowIdx: rowIndex, idx: columnIndex }, - false - ); }; // const [urlDocState, dispatchUrlDoc] = useDoc({}); diff --git a/src/pages/Table/TablePage.tsx b/src/pages/Table/TablePage.tsx index da455666..5959376e 100644 --- a/src/pages/Table/TablePage.tsx +++ b/src/pages/Table/TablePage.tsx @@ -1,6 +1,5 @@ -import { useRef, Suspense, lazy } from "react"; +import { Suspense, lazy } from "react"; import { useAtom } from "jotai"; -import { DataGridHandle } from "react-data-grid"; import { ErrorBoundary } from "react-error-boundary"; import { isEmpty, intersection } from "lodash-es"; @@ -81,9 +80,6 @@ export default function TablePage({ useBeforeUnload(columnModalAtom, tableScope); useBeforeUnload(tableModalAtom, tableScope); - // A ref to the data grid. Contains data grid functions - const dataGridRef = useRef(null); - if (!(tableSchema as any)._rowy_ref) return ( <> @@ -161,7 +157,7 @@ export default function TablePage({ - {!disableSideDrawer && } + {!disableSideDrawer && } diff --git a/yarn.lock b/yarn.lock index aa25b7ed..8f82ba40 100644 --- a/yarn.lock +++ b/yarn.lock @@ -9970,13 +9970,6 @@ react-color-palette@^6.2.0: resolved "https://registry.yarnpkg.com/react-color-palette/-/react-color-palette-6.2.0.tgz#aa3be88f6953d57502c00f4433692129ffbad3e7" integrity sha512-9rIboaRJNoeF8aCI2f3J8wgMyhl74SnGmZLDjor3bKf0iDBhP2EBv0/jGmm0hrj6OackGCqtWl5ZvM89XUc3sg== -react-data-grid@7.0.0-beta.5: - version "7.0.0-beta.5" - resolved "https://registry.yarnpkg.com/react-data-grid/-/react-data-grid-7.0.0-beta.5.tgz#bc39ce45b7a7f42ebfb66840e0ec1c8619d60f10" - integrity sha512-rtN4wnePrQ80UN6lYF/zUQqVVJMT3HW5bTLx9nR5XOKQiG72cGzX2d2+b+e82vUh23zTFBicEnuWSlN9Fa/83Q== - dependencies: - clsx "^1.1.1" - react-detect-offline@^2.4.5: version "2.4.5" resolved "https://registry.yarnpkg.com/react-detect-offline/-/react-detect-offline-2.4.5.tgz#3c242516c37b6789cf89102881031f87e70b80e6" From 2d2cd424f349b948adc5447d38d27008b998a916 Mon Sep 17 00:00:00 2001 From: Sidney Alcantara Date: Wed, 16 Nov 2022 16:14:40 +1100 Subject: [PATCH 52/66] fix table styles --- src/components/Table/Styled/StyledCell.tsx | 5 +++-- src/components/Table/TableHeader.tsx | 4 ++-- src/components/fields/MultiSelect/index.tsx | 2 +- 3 files changed, 6 insertions(+), 5 deletions(-) diff --git a/src/components/Table/Styled/StyledCell.tsx b/src/components/Table/Styled/StyledCell.tsx index c11fee62..5423e8e3 100644 --- a/src/components/Table/Styled/StyledCell.tsx +++ b/src/components/Table/Styled/StyledCell.tsx @@ -33,8 +33,9 @@ export const StyledCell = styled("div")(({ theme }) => ({ borderTop: `1px solid ${theme.palette.divider}`, }, - "&[aria-invalid='true']": { - boxShadow: `inset 0 0 0 2px ${theme.palette.error.main}`, + "&[aria-invalid='true'] .cell-contents": { + outline: `2px dotted ${theme.palette.error.main}`, + outlineOffset: -2, }, })); StyledCell.displayName = "StyledCell"; diff --git a/src/components/Table/TableHeader.tsx b/src/components/Table/TableHeader.tsx index 555b6d82..b90bf178 100644 --- a/src/components/Table/TableHeader.tsx +++ b/src/components/Table/TableHeader.tsx @@ -12,7 +12,7 @@ import FinalColumnHeader from "./FinalColumn/FinalColumnHeader"; import { DragVertical } from "@src/assets/icons"; import { tableScope, selectedCellAtom } from "@src/atoms/tableScope"; -import { DEFAULT_ROW_HEIGHT, TABLE_PADDING } from "@src/components/Table"; +import { DEFAULT_ROW_HEIGHT } from "@src/components/Table"; export interface ITableHeaderProps { headerGroups: HeaderGroup[]; @@ -93,7 +93,7 @@ export const TableHeader = memo(function TableHeader({ style={{ width: header.getSize(), left: header.column.getIsPinned() - ? header.column.getStart() - TABLE_PADDING + ? header.column.getStart() : undefined, ...provided.draggableProps.style, zIndex: header.column.getIsPinned() ? 11 : 10, diff --git a/src/components/fields/MultiSelect/index.tsx b/src/components/fields/MultiSelect/index.tsx index cdda43c0..f6400d1b 100644 --- a/src/components/fields/MultiSelect/index.tsx +++ b/src/components/fields/MultiSelect/index.tsx @@ -34,7 +34,7 @@ export const config: IFieldConfig = { "Multiple values from predefined options. Options are searchable and users can optionally input custom values.", TableCell: withTableCell(DisplayCell, EditorCell, "popover", { disablePadding: true, - transparentPopover: true, + transparentPopover: false, }), SideDrawerField, settings: Settings, From b2747ddddf0ca3791bd2d4b90200d15cc6dbb6d7 Mon Sep 17 00:00:00 2001 From: Sidney Alcantara Date: Wed, 16 Nov 2022 18:12:30 +1100 Subject: [PATCH 53/66] fix editorMode: inline cells waiting to render even when disabled --- src/components/Table/withTableCell.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/components/Table/withTableCell.tsx b/src/components/Table/withTableCell.tsx index d959e408..d91eda98 100644 --- a/src/components/Table/withTableCell.tsx +++ b/src/components/Table/withTableCell.tsx @@ -119,9 +119,6 @@ export default function withTableCell( rowHeight, }; - // If the inline editor cell is not ready to be rendered, display nothing - if (editorMode === "inline" && !inlineEditorReady) return null; - // Show display cell, unless if editorMode is inline const displayCell = (
From 6f8dd8cdc576ea5f2e05f51098e46d54abcaa568 Mon Sep 17 00:00:00 2001 From: Sidney Alcantara Date: Thu, 17 Nov 2022 15:50:26 +1100 Subject: [PATCH 54/66] fix cells not re-rendering when there's a new value from db --- src/components/Table/CellValidation.tsx | 29 ++++++++++++----------- src/components/Table/withTableCell.tsx | 31 +++++++++++++++---------- 2 files changed, 35 insertions(+), 25 deletions(-) diff --git a/src/components/Table/CellValidation.tsx b/src/components/Table/CellValidation.tsx index 6e566487..c7589786 100644 --- a/src/components/Table/CellValidation.tsx +++ b/src/components/Table/CellValidation.tsx @@ -18,6 +18,7 @@ import { contextMenuTargetAtom, } from "@src/atoms/tableScope"; import type { TableRow } from "@src/types/table"; +import type { ITableCellProps } from "./withTableCell"; const Dot = styled("div")(({ theme }) => ({ position: "absolute", @@ -96,6 +97,20 @@ export const CellValidation = memo(function MemoizedCellValidation({ ); } + const tableCellComponentProps: ITableCellProps = { + ...cell.getContext(), + value, + focusInsideCell: isSelectedCell && focusInsideCell, + setFocusInsideCell: (focusInside: boolean) => + setSelectedCell({ + path: row.original._rowy_ref.path, + columnKey: cell.column.id, + focusInside, + }), + disabled: !canEditCells || cell.column.columnDef.meta?.editable === false, + rowHeight, + }; + return ( {renderedValidationTooltip} - {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: - !canEditCells || cell.column.columnDef.meta?.editable === false, - rowHeight, - })} + {flexRender(cell.column.columnDef.cell, tableCellComponentProps)} ); diff --git a/src/components/Table/withTableCell.tsx b/src/components/Table/withTableCell.tsx index d91eda98..c281423d 100644 --- a/src/components/Table/withTableCell.tsx +++ b/src/components/Table/withTableCell.tsx @@ -8,7 +8,7 @@ import { } from "react"; import useStateRef from "react-usestateref"; import { useSetAtom } from "jotai"; -import { get, isEqual } from "lodash-es"; +import { isEqual } from "lodash-es"; import type { CellContext } from "@tanstack/react-table"; import { Popover, PopoverProps } from "@mui/material"; @@ -32,7 +32,9 @@ export interface ICellOptions { popoverProps?: Partial; } -export interface ITableCellProps extends CellContext { +export interface ITableCellProps + extends CellContext { + value: TValue; focusInsideCell: boolean; setFocusInsideCell: (focusInside: boolean) => void; disabled: boolean; @@ -61,15 +63,12 @@ export default function withTableCell( function TableCell({ row, column, - getValue, + value, focusInsideCell, setFocusInsideCell, disabled, rowHeight, }: ITableCellProps) { - // Get the latest value on every re-render of this component - const value = getValue(); - // Render inline editor cell after timeout on mount // to improve scroll performance const [inlineEditorReady, setInlineEditorReady] = useState(false); @@ -203,10 +202,7 @@ export default function withTableCell( 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 valueEqual = isEqual(prev.value, next.value); const columnEqual = isEqual( prev.column.columnDef.meta, next.column.columnDef.meta @@ -234,12 +230,23 @@ interface IEditorCellManagerProps extends IDisplayCellProps { function EditorCellManager({ EditorCellComponent, saveOnUnmount, + value, ...props }: IEditorCellManagerProps) { - const [localValue, setLocalValue, localValueRef] = useStateRef(props.value); - const [, setIsDirty, isDirtyRef] = useStateRef(false); + // 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; From 6f0873ff7de12f663986b937c551fe073725084a Mon Sep 17 00:00:00 2001 From: Sidney Alcantara Date: Thu, 17 Nov 2022 18:17:43 +1100 Subject: [PATCH 55/66] improve Table code organization & fix memo issues --- src/components/ColumnMenu/ColumnMenu.tsx | 2 +- .../{ => Breadcrumbs}/BreadcrumbsSubTable.tsx | 0 .../BreadcrumbsTableRoot.tsx | 2 +- .../Table/ColumnHeader/ColumnHeader.tsx | 294 +++++++++++------- .../ColumnHeader/ColumnHeaderDragHandle.tsx | 58 ++++ .../ColumnHeaderResizer.tsx} | 12 +- .../Table/ColumnHeader/ColumnHeaderSort.tsx | 16 +- .../Table/FinalColumn/FinalColumn.tsx | 4 +- src/components/Table/{ => Mock}/Cell.tsx | 0 src/components/Table/{ => Mock}/Column.tsx | 1 - src/components/Table/Table.tsx | 2 + src/components/Table/TableBody.tsx | 18 +- .../Table/{ => TableCell}/ChipList.tsx | 0 .../{ => TableCell}/EditorCellTextField.tsx | 0 .../TableCell.tsx} | 33 +- src/components/Table/TableCell/index.ts | 2 + .../withRenderTableCell.tsx} | 6 +- src/components/Table/TableHeader.tsx | 118 +------ src/components/Table/TableSkeleton.tsx | 2 +- .../ImportAirtableWizard/Step1Columns.tsx | 4 +- .../ImportAirtableWizard/Step2NewColumns.tsx | 4 +- .../ImportAirtableWizard/Step3Preview.tsx | 4 +- .../ImportCsvWizard/Step1Columns.tsx | 4 +- .../ImportCsvWizard/Step2NewColumns.tsx | 4 +- .../ImportCsvWizard/Step3Preview.tsx | 4 +- .../ImportExistingWizard/Step1Columns.tsx | 2 +- .../ImportExistingWizard/Step2Rename.tsx | 4 +- .../ImportExistingWizard/Step3Types.tsx | 4 +- .../ImportExistingWizard/Step4Preview.tsx | 4 +- src/components/fields/Action/index.tsx | 4 +- src/components/fields/Checkbox/index.tsx | 4 +- src/components/fields/Code/index.tsx | 4 +- src/components/fields/Color/index.tsx | 4 +- .../fields/ConnectService/DisplayCell.tsx | 2 +- .../fields/ConnectService/index.tsx | 4 +- .../fields/ConnectTable/DisplayCell.tsx | 2 +- src/components/fields/ConnectTable/index.tsx | 4 +- .../fields/Connector/DisplayCell.tsx | 2 +- src/components/fields/Connector/index.tsx | 4 +- src/components/fields/CreatedAt/index.tsx | 4 +- src/components/fields/CreatedBy/index.tsx | 4 +- src/components/fields/Date/index.tsx | 4 +- src/components/fields/DateTime/index.tsx | 4 +- src/components/fields/Derivative/index.tsx | 4 +- src/components/fields/Duration/index.tsx | 4 +- src/components/fields/Email/EditorCell.tsx | 2 +- src/components/fields/Email/index.tsx | 4 +- src/components/fields/File/DisplayCell.tsx | 2 +- src/components/fields/File/EditorCell.tsx | 2 +- src/components/fields/File/index.tsx | 4 +- src/components/fields/GeoPoint/index.tsx | 4 +- src/components/fields/Id/index.tsx | 4 +- src/components/fields/Image/index.tsx | 4 +- src/components/fields/Json/index.tsx | 4 +- src/components/fields/LongText/EditorCell.tsx | 2 +- src/components/fields/LongText/index.tsx | 4 +- src/components/fields/Markdown/index.tsx | 4 +- .../fields/MultiSelect/DisplayCell.tsx | 2 +- src/components/fields/MultiSelect/index.tsx | 4 +- src/components/fields/Number/EditorCell.tsx | 2 +- src/components/fields/Number/index.tsx | 4 +- .../fields/Percentage/EditorCell.tsx | 2 +- src/components/fields/Percentage/index.tsx | 4 +- src/components/fields/Phone/EditorCell.tsx | 2 +- src/components/fields/Phone/index.tsx | 4 +- src/components/fields/Rating/index.tsx | 4 +- .../fields/Reference/EditorCell.tsx | 2 +- src/components/fields/Reference/index.tsx | 4 +- src/components/fields/RichText/index.tsx | 4 +- .../fields/ShortText/EditorCell.tsx | 2 +- src/components/fields/ShortText/index.tsx | 4 +- src/components/fields/SingleSelect/index.tsx | 4 +- src/components/fields/Slider/index.tsx | 4 +- src/components/fields/Status/index.tsx | 4 +- src/components/fields/SubTable/index.tsx | 4 +- src/components/fields/UpdatedAt/index.tsx | 4 +- src/components/fields/UpdatedBy/index.tsx | 4 +- src/components/fields/Url/EditorCell.tsx | 2 +- src/components/fields/Url/index.tsx | 4 +- src/components/fields/User/index.tsx | 4 +- src/components/fields/types.ts | 4 +- src/constants/routes.tsx | 2 +- src/pages/Table/ProvidedSubTablePage.tsx | 2 +- 83 files changed, 420 insertions(+), 372 deletions(-) rename src/components/Table/{ => Breadcrumbs}/BreadcrumbsSubTable.tsx (100%) rename src/components/Table/{ => Breadcrumbs}/BreadcrumbsTableRoot.tsx (97%) create mode 100644 src/components/Table/ColumnHeader/ColumnHeaderDragHandle.tsx rename src/components/Table/{Styled/StyledResizer.tsx => ColumnHeader/ColumnHeaderResizer.tsx} (79%) rename src/components/Table/{ => Mock}/Cell.tsx (100%) rename src/components/Table/{ => Mock}/Column.tsx (99%) rename src/components/Table/{ => TableCell}/ChipList.tsx (100%) rename src/components/Table/{ => TableCell}/EditorCellTextField.tsx (100%) rename src/components/Table/{CellValidation.tsx => TableCell/TableCell.tsx} (87%) create mode 100644 src/components/Table/TableCell/index.ts rename src/components/Table/{withTableCell.tsx => TableCell/withRenderTableCell.tsx} (98%) diff --git a/src/components/ColumnMenu/ColumnMenu.tsx b/src/components/ColumnMenu/ColumnMenu.tsx index 54f6e0c3..72b1b765 100644 --- a/src/components/ColumnMenu/ColumnMenu.tsx +++ b/src/components/ColumnMenu/ColumnMenu.tsx @@ -29,7 +29,7 @@ import SettingsIcon from "@mui/icons-material/SettingsOutlined"; import EvalIcon from "@mui/icons-material/PlayCircleOutline"; import MenuContents, { IMenuContentsProps } from "./MenuContents"; -import ColumnHeader from "@src/components/Table/Column"; +import ColumnHeader from "@src/components/Table/Mock/Column"; import { projectScope, diff --git a/src/components/Table/BreadcrumbsSubTable.tsx b/src/components/Table/Breadcrumbs/BreadcrumbsSubTable.tsx similarity index 100% rename from src/components/Table/BreadcrumbsSubTable.tsx rename to src/components/Table/Breadcrumbs/BreadcrumbsSubTable.tsx diff --git a/src/components/Table/BreadcrumbsTableRoot.tsx b/src/components/Table/Breadcrumbs/BreadcrumbsTableRoot.tsx similarity index 97% rename from src/components/Table/BreadcrumbsTableRoot.tsx rename to src/components/Table/Breadcrumbs/BreadcrumbsTableRoot.tsx index dd15526d..676408e3 100644 --- a/src/components/Table/BreadcrumbsTableRoot.tsx +++ b/src/components/Table/Breadcrumbs/BreadcrumbsTableRoot.tsx @@ -1,5 +1,5 @@ import { useAtom } from "jotai"; -import { useLocation, useParams, Link as RouterLink } from "react-router-dom"; +import { useParams, Link as RouterLink } from "react-router-dom"; import { find, camelCase, uniq } from "lodash-es"; import { diff --git a/src/components/Table/ColumnHeader/ColumnHeader.tsx b/src/components/Table/ColumnHeader/ColumnHeader.tsx index 345e856b..84239a40 100644 --- a/src/components/Table/ColumnHeader/ColumnHeader.tsx +++ b/src/components/Table/ColumnHeader/ColumnHeader.tsx @@ -1,5 +1,10 @@ -import { forwardRef, useRef } from "react"; +import { memo, useRef } from "react"; import { useAtom, useSetAtom } from "jotai"; +import type { Header } from "@tanstack/react-table"; +import type { + DraggableProvided, + DraggableStateSnapshot, +} from "react-beautiful-dnd"; import { styled, @@ -7,8 +12,8 @@ import { TooltipProps, tooltipClasses, Fade, - Grid, - GridProps, + Stack, + StackProps, IconButton, Typography, } from "@mui/material"; @@ -16,21 +21,58 @@ import DropdownIcon from "@mui/icons-material/MoreHoriz"; import LockIcon from "@mui/icons-material/LockOutlined"; import ColumnHeaderSort, { SORT_STATES } from "./ColumnHeaderSort"; +import ColumnHeaderDragHandle from "./ColumnHeaderDragHandle"; +import ColumnHeaderResizer from "./ColumnHeaderResizer"; import { projectScope, altPressAtom } from "@src/atoms/projectScope"; import { tableScope, + selectedCellAtom, columnMenuAtom, tableSortsAtom, } from "@src/atoms/tableScope"; import { getFieldProp } from "@src/components/fields"; -import { COLUMN_HEADER_HEIGHT } from "@src/components/Table/Column"; -import { ColumnConfig } from "@src/types/table"; import { FieldType } from "@src/constants/fields"; -import { spreadSx } from "@src/utils/ui"; +import { COLUMN_HEADER_HEIGHT } from "@src/components/Table/Mock/Column"; +import type { ColumnConfig } from "@src/types/table"; +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 }) => ({ @@ -44,18 +86,34 @@ const LightTooltip = styled(({ className, ...props }: TooltipProps) => ( }, })); -export interface IColumnHeaderProps extends Partial { +export interface IColumnHeaderProps + extends Partial> { + header: Header; column: ColumnConfig; + + provided: DraggableProvided; + snapshot: DraggableStateSnapshot; + width: number; + isSelectedCell: boolean; focusInsideCell: boolean; - children: React.ReactNode; + canEditColumns: boolean; + isLastFrozen: boolean; } -export const ColumnHeader = forwardRef(function ColumnHeader( - { column, width, focusInsideCell, children, ...props }: IColumnHeaderProps, - ref: React.Ref -) { +export const ColumnHeader = memo(function ColumnHeader({ + header, + column, + provided, + snapshot, + width, + isSelectedCell, + focusInsideCell, + canEditColumns, + isLastFrozen, +}: IColumnHeaderProps) { const openColumnMenu = useSetAtom(columnMenuAtom, tableScope); + const setSelectedCell = useSetAtom(selectedCellAtom, tableScope); const [altPress] = useAtom(altPressAtom, projectScope); const [tableSorts] = useAtom(tableSortsAtom, tableScope); @@ -74,11 +132,19 @@ export const ColumnHeader = forwardRef(function ColumnHeader( : tableSorts[0]?.direction || "none"; return ( - `1px solid ${theme.palette.divider}`, - - backgroundColor: "background.default", - color: "text.secondary", - transition: (theme) => - theme.transitions.create("color", { - duration: theme.transitions.duration.short, - }), - "&:hover": { color: "text.primary" }, - - position: "relative", - - py: 0, - pr: 0.5, - pl: 1, - width: "100%", - }, - ...spreadSx(props.sx), - ]} + 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(); + }} > + {provided.dragHandleProps && ( + + )} + {width > 140 && ( - { navigator.clipboard.writeText(column.key); }} @@ -139,96 +210,87 @@ export const ColumnHeader = forwardRef(function ColumnHeader( ) : ( getFieldProp("icon", (column as any).type) )} - +
)} - - - {column.name as string} - - } - enterDelay={1000} - placement="bottom-start" - disableInteractive - TransitionComponent={Fade} - sx={{ "& .MuiTooltip-tooltip": { marginTop: "-28px !important" } }} - > + - {altPress ? ( - <> - {column.index} {column.fieldName} - - ) : ( - column.name - )} + {column.name as string} - - + } + enterDelay={1000} + placement="bottom-start" + disableInteractive + TransitionComponent={Fade} + sx={{ "& .MuiTooltip-tooltip": { marginTop: "-28px !important" } }} + > + + {altPress ? ( + <> + {column.index} {column.fieldName} + + ) : ( + column.name + )} + + {column.type !== FieldType.id && ( - - - + )} - - - - theme.transitions.create("color", { - duration: theme.transitions.duration.short, - }), + + + + + - color: "text.disabled", - "[role='columnheader']:hover &, [role='columnheader']:focus &, [role='columnheader']:focus-within &, &:focus": - { color: "text.primary" }, - }} - > - - - - - - {children} - + {header.column.getCanResize() && ( + + )} + ); }); diff --git a/src/components/Table/ColumnHeader/ColumnHeaderDragHandle.tsx b/src/components/Table/ColumnHeader/ColumnHeaderDragHandle.tsx new file mode 100644 index 00000000..a7ae1da9 --- /dev/null +++ b/src/components/Table/ColumnHeader/ColumnHeaderDragHandle.tsx @@ -0,0 +1,58 @@ +import type { DraggableProvidedDragHandleProps } from "react-beautiful-dnd"; +import { DragVertical } from "@src/assets/icons"; + +export interface IColumnHeaderDragHandleProps { + dragHandleProps: DraggableProvidedDragHandleProps; + tabIndex: number; +} + +export default function ColumnHeaderDragHandle({ + dragHandleProps, + tabIndex, +}: IColumnHeaderDragHandleProps) { + return ( +
-1 ? dragHandleProps["aria-describedby"] : undefined + } + style={{ + position: "absolute", + inset: 0, + zIndex: 0, + display: "flex", + alignItems: "center", + outline: "none", + }} + className="column-drag-handle" + > + theme.transitions.create(["opacity"]), + "[role='columnheader']:hover &, [role='columnheader']:focus-within &": + { + opacity: 0.5, + }, + ".column-drag-handle:hover &": { + opacity: 1, + }, + ".column-drag-handle:active &": { + opacity: 1, + color: "primary.main", + }, + ".column-drag-handle:focus &": { + opacity: 1, + color: "primary.main", + outline: "2px solid", + outlineColor: "primary.main", + }, + }} + style={{ width: 8 }} + preserveAspectRatio="xMidYMid slice" + /> +
+ ); +} diff --git a/src/components/Table/Styled/StyledResizer.tsx b/src/components/Table/ColumnHeader/ColumnHeaderResizer.tsx similarity index 79% rename from src/components/Table/Styled/StyledResizer.tsx rename to src/components/Table/ColumnHeader/ColumnHeaderResizer.tsx index 7f23f1da..88b65232 100644 --- a/src/components/Table/Styled/StyledResizer.tsx +++ b/src/components/Table/ColumnHeader/ColumnHeaderResizer.tsx @@ -1,13 +1,13 @@ import { styled } from "@mui/material"; -export interface IStyledResizerProps { +export interface IColumnHeaderResizerProps { isResizing: boolean; } -export const StyledResizer = styled("div", { - name: "StyledResizer", +export const ColumnHeaderResizer = styled("div", { + name: "ColumnHeaderResizer", shouldForwardProp: (prop) => prop !== "isResizing", -})(({ theme, isResizing }) => ({ +})(({ theme, isResizing }) => ({ position: "absolute", zIndex: 5, right: 0, @@ -51,6 +51,6 @@ export const StyledResizer = styled("div", { transform: isResizing ? "scaleY(1.5) !important" : undefined, }, })); -StyledResizer.displayName = "StyledResizer"; +ColumnHeaderResizer.displayName = "ColumnHeaderResizer"; -export default StyledResizer; +export default ColumnHeaderResizer; diff --git a/src/components/Table/ColumnHeader/ColumnHeaderSort.tsx b/src/components/Table/ColumnHeader/ColumnHeaderSort.tsx index c8c4878f..0a5bc049 100644 --- a/src/components/Table/ColumnHeader/ColumnHeaderSort.tsx +++ b/src/components/Table/ColumnHeader/ColumnHeaderSort.tsx @@ -1,3 +1,4 @@ +import { memo } from "react"; import { useSetAtom } from "jotai"; import { colord } from "colord"; @@ -17,7 +18,7 @@ export interface IColumnHeaderSortProps { tabIndex?: number; } -export default function ColumnHeaderSort({ +export const ColumnHeaderSort = memo(function ColumnHeaderSort({ sortKey, currentSort, tabIndex, @@ -34,7 +35,7 @@ export default function ColumnHeaderSort({ return ( - theme.transitions.create(["background-color", "opacity"], { - duration: theme.transitions.duration.short, - }), "& .arrow": { transition: (theme) => @@ -108,4 +102,6 @@ export default function ColumnHeaderSort({ ); -} +}); + +export default ColumnHeaderSort; diff --git a/src/components/Table/FinalColumn/FinalColumn.tsx b/src/components/Table/FinalColumn/FinalColumn.tsx index 6f058eda..28b584e6 100644 --- a/src/components/Table/FinalColumn/FinalColumn.tsx +++ b/src/components/Table/FinalColumn/FinalColumn.tsx @@ -1,6 +1,6 @@ import { memo } from "react"; import { useAtom, useSetAtom } from "jotai"; -import type { ITableCellProps } from "@src/components/Table/withTableCell"; +import type { IRenderedTableCellProps } from "@src/components/Table/TableCell/withRenderTableCell"; import { Stack, Tooltip, IconButton, alpha } from "@mui/material"; import { CopyCells as CopyCellsIcon } from "@src/assets/icons"; @@ -25,7 +25,7 @@ import { export const FinalColumn = memo(function FinalColumn({ row, focusInsideCell, -}: ITableCellProps) { +}: IRenderedTableCellProps) { const [userRoles] = useAtom(userRolesAtom, projectScope); const [addRowIdType] = useAtom(tableAddRowIdTypeAtom, projectScope); const confirm = useSetAtom(confirmDialogAtom, projectScope); diff --git a/src/components/Table/Cell.tsx b/src/components/Table/Mock/Cell.tsx similarity index 100% rename from src/components/Table/Cell.tsx rename to src/components/Table/Mock/Cell.tsx diff --git a/src/components/Table/Column.tsx b/src/components/Table/Mock/Column.tsx similarity index 99% rename from src/components/Table/Column.tsx rename to src/components/Table/Mock/Column.tsx index bc841fc0..fc831d1d 100644 --- a/src/components/Table/Column.tsx +++ b/src/components/Table/Mock/Column.tsx @@ -105,7 +105,6 @@ export const Column = forwardRef(function Column( ml: 0.5, }} - aria-hidden > {label} diff --git a/src/components/Table/Table.tsx b/src/components/Table/Table.tsx index 886fdcff..fe1447c4 100644 --- a/src/components/Table/Table.tsx +++ b/src/components/Table/Table.tsx @@ -221,6 +221,7 @@ export default function Table({ canAddColumns={canAddColumns} canEditColumns={canEditColumns} lastFrozen={lastFrozen} + columnSizing={columnSizing} />
@@ -233,6 +234,7 @@ export default function Table({ rows={rows} canEditCells={canEditCells} lastFrozen={lastFrozen} + columnSizing={columnSizing} /> )} diff --git a/src/components/Table/TableBody.tsx b/src/components/Table/TableBody.tsx index ae119011..674d8d96 100644 --- a/src/components/Table/TableBody.tsx +++ b/src/components/Table/TableBody.tsx @@ -1,9 +1,10 @@ +import { memo } from "react"; import { useAtom } from "jotai"; -import type { Column, Row } from "@tanstack/react-table"; +import type { Column, Row, ColumnSizingState } from "@tanstack/react-table"; import StyledRow from "./Styled/StyledRow"; import OutOfOrderIndicator from "./OutOfOrderIndicator"; -import CellValidation from "./CellValidation"; +import CellValidation from "./TableCell"; import { RowsSkeleton } from "./TableSkeleton"; import { @@ -29,9 +30,12 @@ export interface ITableBodyProps { canEditCells: boolean; lastFrozen?: string; + + /** Re-render when local column sizing changes */ + columnSizing: ColumnSizingState; } -export default function TableBody({ +export const TableBody = memo(function TableBody({ containerRef, leafColumns, rows, @@ -105,9 +109,11 @@ export default function TableBody({ : undefined } isSelectedCell={isSelectedCell} + focusInsideCell={isSelectedCell && selectedCell?.focusInside} isReadOnlyCell={isReadOnlyCell} canEditCells={canEditCells} - lastFrozen={lastFrozen} + isLastFrozen={lastFrozen === cell.column.id} + width={cell.column.getSize()} rowHeight={tableSchema.rowHeight || DEFAULT_ROW_HEIGHT} /> ); @@ -127,4 +133,6 @@ export default function TableBody({ )}
); -} +}); + +export default TableBody; diff --git a/src/components/Table/ChipList.tsx b/src/components/Table/TableCell/ChipList.tsx similarity index 100% rename from src/components/Table/ChipList.tsx rename to src/components/Table/TableCell/ChipList.tsx diff --git a/src/components/Table/EditorCellTextField.tsx b/src/components/Table/TableCell/EditorCellTextField.tsx similarity index 100% rename from src/components/Table/EditorCellTextField.tsx rename to src/components/Table/TableCell/EditorCellTextField.tsx diff --git a/src/components/Table/CellValidation.tsx b/src/components/Table/TableCell/TableCell.tsx similarity index 87% rename from src/components/Table/CellValidation.tsx rename to src/components/Table/TableCell/TableCell.tsx index c7589786..48fb7b93 100644 --- a/src/components/Table/CellValidation.tsx +++ b/src/components/Table/TableCell/TableCell.tsx @@ -1,5 +1,5 @@ import { memo } from "react"; -import { useAtom, useSetAtom } from "jotai"; +import { useSetAtom } from "jotai"; import { ErrorBoundary } from "react-error-boundary"; import { flexRender } from "@tanstack/react-table"; import type { Row, Cell } from "@tanstack/react-table"; @@ -8,7 +8,7 @@ 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 StyledCell from "@src/components/Table/Styled/StyledCell"; import { InlineErrorFallback } from "@src/components/ErrorFallback"; import RichTooltip from "@src/components/RichTooltip"; @@ -18,7 +18,7 @@ import { contextMenuTargetAtom, } from "@src/atoms/tableScope"; import type { TableRow } from "@src/types/table"; -import type { ITableCellProps } from "./withTableCell"; +import type { IRenderedTableCellProps } from "./withRenderTableCell"; const Dot = styled("div")(({ theme }) => ({ position: "absolute", @@ -39,31 +39,34 @@ const Dot = styled("div")(({ theme }) => ({ }, })); -export interface ICellValidationProps { +export interface ITableCellProps { row: Row; cell: Cell; index: number; isSelectedCell: boolean; + focusInsideCell: boolean; isReadOnlyCell: boolean; canEditCells: boolean; rowHeight: number; - lastFrozen?: string; + isLastFrozen: boolean; + width: number; left?: number; } -export const CellValidation = memo(function MemoizedCellValidation({ +export const TableCell = memo(function TableCell({ row, cell, index, isSelectedCell, + focusInsideCell, isReadOnlyCell, canEditCells, rowHeight, - lastFrozen, + isLastFrozen, + width, left, -}: ICellValidationProps) { - const [selectedCell, setSelectedCell] = useAtom(selectedCellAtom, tableScope); - const focusInsideCell = selectedCell?.focusInside ?? false; +}: ITableCellProps) { + const setSelectedCell = useSetAtom(selectedCellAtom, tableScope); const setContextMenuTarget = useSetAtom(contextMenuTargetAtom, tableScope); const value = cell.getValue(); @@ -97,10 +100,10 @@ export const CellValidation = memo(function MemoizedCellValidation({ ); } - const tableCellComponentProps: ITableCellProps = { + const tableCellComponentProps: IRenderedTableCellProps = { ...cell.getContext(), value, - focusInsideCell: isSelectedCell && focusInsideCell, + focusInsideCell, setFocusInsideCell: (focusInside: boolean) => setSelectedCell({ path: row.original._rowy_ref.path, @@ -117,7 +120,7 @@ export const CellValidation = memo(function MemoizedCellValidation({ data-row-id={row.id} data-col-id={cell.column.id} data-frozen={cell.column.getIsPinned() || undefined} - data-frozen-last={lastFrozen === cell.column.id || undefined} + data-frozen-last={isLastFrozen || undefined} role="gridcell" tabIndex={isSelectedCell && !focusInsideCell ? 0 : -1} aria-colindex={index + 1} @@ -133,7 +136,7 @@ export const CellValidation = memo(function MemoizedCellValidation({ } aria-invalid={isInvalid || isMissing} style={{ - width: cell.column.getSize(), + width, height: rowHeight, left, backgroundColor: @@ -178,4 +181,4 @@ export const CellValidation = memo(function MemoizedCellValidation({ ); }); -export default CellValidation; +export default TableCell; diff --git a/src/components/Table/TableCell/index.ts b/src/components/Table/TableCell/index.ts new file mode 100644 index 00000000..f876b4de --- /dev/null +++ b/src/components/Table/TableCell/index.ts @@ -0,0 +1,2 @@ +export { default } from "./TableCell"; +export * from "./TableCell"; diff --git a/src/components/Table/withTableCell.tsx b/src/components/Table/TableCell/withRenderTableCell.tsx similarity index 98% rename from src/components/Table/withTableCell.tsx rename to src/components/Table/TableCell/withRenderTableCell.tsx index c281423d..3a4d2d5f 100644 --- a/src/components/Table/withTableCell.tsx +++ b/src/components/Table/TableCell/withRenderTableCell.tsx @@ -32,7 +32,7 @@ export interface ICellOptions { popoverProps?: Partial; } -export interface ITableCellProps +export interface IRenderedTableCellProps extends CellContext { value: TValue; focusInsideCell: boolean; @@ -60,7 +60,7 @@ export default function withTableCell( options: ICellOptions = {} ) { return memo( - function TableCell({ + function RenderedTableCell({ row, column, value, @@ -68,7 +68,7 @@ export default function withTableCell( setFocusInsideCell, disabled, rowHeight, - }: ITableCellProps) { + }: IRenderedTableCellProps) { // Render inline editor cell after timeout on mount // to improve scroll performance const [inlineEditorReady, setInlineEditorReady] = useState(false); diff --git a/src/components/Table/TableHeader.tsx b/src/components/Table/TableHeader.tsx index b90bf178..ac93d566 100644 --- a/src/components/Table/TableHeader.tsx +++ b/src/components/Table/TableHeader.tsx @@ -2,14 +2,12 @@ import { memo } from "react"; import { useAtom } from "jotai"; import { DragDropContext, Droppable, Draggable } from "react-beautiful-dnd"; import type { DropResult } from "react-beautiful-dnd"; -import type { HeaderGroup } from "@tanstack/react-table"; +import type { ColumnSizingState, HeaderGroup } from "@tanstack/react-table"; import type { TableRow } from "@src/types/table"; import StyledRow from "./Styled/StyledRow"; import ColumnHeader from "./ColumnHeader"; -import StyledResizer from "./Styled/StyledResizer"; import FinalColumnHeader from "./FinalColumn/FinalColumnHeader"; -import { DragVertical } from "@src/assets/icons"; import { tableScope, selectedCellAtom } from "@src/atoms/tableScope"; import { DEFAULT_ROW_HEIGHT } from "@src/components/Table"; @@ -20,6 +18,9 @@ export interface ITableHeaderProps { canAddColumns: boolean; canEditColumns: boolean; lastFrozen?: string; + + /** Re-render when local column sizing changes */ + columnSizing: ColumnSizingState; } export const TableHeader = memo(function TableHeader({ @@ -29,8 +30,8 @@ export const TableHeader = memo(function TableHeader({ canEditColumns, lastFrozen, }: ITableHeaderProps) { - const [selectedCell, setSelectedCell] = useAtom(selectedCellAtom, tableScope); - const focusInsideCell = selectedCell?.focusInside ?? false; + const [selectedCell] = useAtom(selectedCellAtom, tableScope); + const focusInside = selectedCell?.focusInside ?? false; return ( @@ -58,7 +59,7 @@ export const TableHeader = memo(function TableHeader({ data-row-id={"_rowy_header"} data-col-id={header.id} tabIndex={isSelectedCell ? 0 : -1} - focusInsideCell={isSelectedCell && focusInsideCell} + focusInsideCell={isSelectedCell && focusInside} aria-colindex={header.index + 1} aria-readonly={!canEditColumns} aria-selected={isSelectedCell} @@ -78,105 +79,16 @@ export const TableHeader = memo(function TableHeader({ > {(provided, snapshot) => ( { - 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(); - }} - focusInsideCell={isSelectedCell && focusInsideCell} - > -
- - theme.transitions.create(["opacity"]), - "[role='columnheader']:hover &, [role='columnheader']:focus-within &": - { - opacity: 0.5, - }, - ".column-drag-handle:hover &": { - opacity: 1, - }, - ".column-drag-handle:active &": { - opacity: 1, - color: "primary.main", - }, - ".column-drag-handle:focus &": { - opacity: 1, - color: "primary.main", - outline: "2px solid", - outlineColor: "primary.main", - }, - }} - style={{ width: 8 }} - preserveAspectRatio="xMidYMid slice" - /> -
- - {header.column.getCanResize() && ( - - )} -
+ isSelectedCell={isSelectedCell} + focusInsideCell={isSelectedCell && focusInside} + canEditColumns={canEditColumns} + isLastFrozen={lastFrozen === header.id} + /> )} ); diff --git a/src/components/Table/TableSkeleton.tsx b/src/components/Table/TableSkeleton.tsx index 065770fc..6bfaa051 100644 --- a/src/components/Table/TableSkeleton.tsx +++ b/src/components/Table/TableSkeleton.tsx @@ -4,7 +4,7 @@ import { colord } from "colord"; import { Fade, Stack, Skeleton, Button } from "@mui/material"; import { AddColumn as AddColumnIcon } from "@src/assets/icons"; -import Column from "./Column"; +import Column from "./Mock/Column"; import { projectScope, userSettingsAtom } from "@src/atoms/projectScope"; import { diff --git a/src/components/TableModals/ImportAirtableWizard/Step1Columns.tsx b/src/components/TableModals/ImportAirtableWizard/Step1Columns.tsx index ea86e2c4..06235545 100644 --- a/src/components/TableModals/ImportAirtableWizard/Step1Columns.tsx +++ b/src/components/TableModals/ImportAirtableWizard/Step1Columns.tsx @@ -22,7 +22,9 @@ import { TableColumn as TableColumnIcon } from "@src/assets/icons"; import { IStepProps } from "."; import { AirtableConfig } from "@src/components/TableModals/ImportAirtableWizard"; import FadeList from "@src/components/TableModals/ScrollableList"; -import Column, { COLUMN_HEADER_HEIGHT } from "@src/components/Table/Column"; +import Column, { + COLUMN_HEADER_HEIGHT, +} from "@src/components/Table/Mock/Column"; import ColumnSelect from "@src/components/Table/ColumnSelect"; import { diff --git a/src/components/TableModals/ImportAirtableWizard/Step2NewColumns.tsx b/src/components/TableModals/ImportAirtableWizard/Step2NewColumns.tsx index e3b81ece..105d6f58 100644 --- a/src/components/TableModals/ImportAirtableWizard/Step2NewColumns.tsx +++ b/src/components/TableModals/ImportAirtableWizard/Step2NewColumns.tsx @@ -6,8 +6,8 @@ import ChevronRightIcon from "@mui/icons-material/ChevronRight"; import { IStepProps } from "."; import ScrollableList from "@src/components/TableModals/ScrollableList"; -import Column from "@src/components/Table/Column"; -import Cell from "@src/components/Table/Cell"; +import Column from "@src/components/Table/Mock/Column"; +import Cell from "@src/components/Table/Mock/Cell"; import FieldsDropdown from "@src/components/ColumnModals/FieldsDropdown"; import { FieldType } from "@src/constants/fields"; import { SELECTABLE_TYPES } from "@src/components/TableModals/ImportExistingWizard/utils"; diff --git a/src/components/TableModals/ImportAirtableWizard/Step3Preview.tsx b/src/components/TableModals/ImportAirtableWizard/Step3Preview.tsx index ae5c0ee8..fa9f4d4a 100644 --- a/src/components/TableModals/ImportAirtableWizard/Step3Preview.tsx +++ b/src/components/TableModals/ImportAirtableWizard/Step3Preview.tsx @@ -2,8 +2,8 @@ import { useAtom } from "jotai"; import { find } from "lodash-es"; import { styled, Grid } from "@mui/material"; -import Column from "@src/components/Table/Column"; -import Cell from "@src/components/Table/Cell"; +import Column from "@src/components/Table/Mock/Column"; +import Cell from "@src/components/Table/Mock/Cell"; import { IStepProps } from "."; import { tableScope, tableSchemaAtom } from "@src/atoms/tableScope"; diff --git a/src/components/TableModals/ImportCsvWizard/Step1Columns.tsx b/src/components/TableModals/ImportCsvWizard/Step1Columns.tsx index 3189cf3e..684584ec 100644 --- a/src/components/TableModals/ImportCsvWizard/Step1Columns.tsx +++ b/src/components/TableModals/ImportCsvWizard/Step1Columns.tsx @@ -26,7 +26,9 @@ import { TableColumn as TableColumnIcon } from "@src/assets/icons"; import { IStepProps } from "."; import { CsvConfig } from "@src/components/TableModals/ImportCsvWizard"; import FadeList from "@src/components/TableModals/ScrollableList"; -import Column, { COLUMN_HEADER_HEIGHT } from "@src/components/Table/Column"; +import Column, { + COLUMN_HEADER_HEIGHT, +} from "@src/components/Table/Mock/Column"; import ColumnSelect from "@src/components/Table/ColumnSelect"; import { diff --git a/src/components/TableModals/ImportCsvWizard/Step2NewColumns.tsx b/src/components/TableModals/ImportCsvWizard/Step2NewColumns.tsx index b47f6afe..6bd3a264 100644 --- a/src/components/TableModals/ImportCsvWizard/Step2NewColumns.tsx +++ b/src/components/TableModals/ImportCsvWizard/Step2NewColumns.tsx @@ -7,8 +7,8 @@ import ChevronRightIcon from "@mui/icons-material/ChevronRight"; import { IStepProps } from "."; import ScrollableList from "@src/components/TableModals/ScrollableList"; -import Column from "@src/components/Table/Column"; -import Cell from "@src/components/Table/Cell"; +import Column from "@src/components/Table/Mock/Column"; +import Cell from "@src/components/Table/Mock/Cell"; import FieldsDropdown from "@src/components/ColumnModals/FieldsDropdown"; import { FieldType } from "@src/constants/fields"; diff --git a/src/components/TableModals/ImportCsvWizard/Step3Preview.tsx b/src/components/TableModals/ImportCsvWizard/Step3Preview.tsx index 8c1addb3..c17608fc 100644 --- a/src/components/TableModals/ImportCsvWizard/Step3Preview.tsx +++ b/src/components/TableModals/ImportCsvWizard/Step3Preview.tsx @@ -3,8 +3,8 @@ import { find } from "lodash-es"; import { parseJSON } from "date-fns"; import { styled, Grid } from "@mui/material"; -import Column from "@src/components/Table/Column"; -import Cell from "@src/components/Table/Cell"; +import Column from "@src/components/Table/Mock/Column"; +import Cell from "@src/components/Table/Mock/Cell"; import { IStepProps } from "."; import { tableScope, tableSchemaAtom } from "@src/atoms/tableScope"; diff --git a/src/components/TableModals/ImportExistingWizard/Step1Columns.tsx b/src/components/TableModals/ImportExistingWizard/Step1Columns.tsx index 97563c10..29d5903f 100644 --- a/src/components/TableModals/ImportExistingWizard/Step1Columns.tsx +++ b/src/components/TableModals/ImportExistingWizard/Step1Columns.tsx @@ -20,7 +20,7 @@ import DragHandleIcon from "@mui/icons-material/DragHandle"; import { AddColumn as AddColumnIcon } from "@src/assets/icons"; import ScrollableList from "@src/components/TableModals/ScrollableList"; -import Column from "@src/components/Table/Column"; +import Column from "@src/components/Table/Mock/Column"; import EmptyState from "@src/components/EmptyState"; import { tableScope, tableRowsAtom } from "@src/atoms/tableScope"; diff --git a/src/components/TableModals/ImportExistingWizard/Step2Rename.tsx b/src/components/TableModals/ImportExistingWizard/Step2Rename.tsx index 0b13ed11..d4bf3faf 100644 --- a/src/components/TableModals/ImportExistingWizard/Step2Rename.tsx +++ b/src/components/TableModals/ImportExistingWizard/Step2Rename.tsx @@ -14,7 +14,9 @@ import DoneIcon from "@mui/icons-material/Done"; import { IStepProps } from "."; import ScrollableList from "@src/components/TableModals/ScrollableList"; -import Column, { COLUMN_HEADER_HEIGHT } from "@src/components/Table/Column"; +import Column, { + COLUMN_HEADER_HEIGHT, +} from "@src/components/Table/Mock/Column"; export default function Step2Rename({ config, diff --git a/src/components/TableModals/ImportExistingWizard/Step3Types.tsx b/src/components/TableModals/ImportExistingWizard/Step3Types.tsx index 2cebf092..f4c9be37 100644 --- a/src/components/TableModals/ImportExistingWizard/Step3Types.tsx +++ b/src/components/TableModals/ImportExistingWizard/Step3Types.tsx @@ -6,8 +6,8 @@ import ChevronRightIcon from "@mui/icons-material/ChevronRight"; import { IStepProps } from "."; import ScrollableList from "@src/components/TableModals/ScrollableList"; -import Column from "@src/components/Table/Column"; -import Cell from "@src/components/Table/Cell"; +import Column from "@src/components/Table/Mock/Column"; +import Cell from "@src/components/Table/Mock/Cell"; import FieldsDropdown from "@src/components/ColumnModals/FieldsDropdown"; import { tableScope, tableRowsAtom } from "@src/atoms/tableScope"; diff --git a/src/components/TableModals/ImportExistingWizard/Step4Preview.tsx b/src/components/TableModals/ImportExistingWizard/Step4Preview.tsx index 3852f8a5..8fb9be9e 100644 --- a/src/components/TableModals/ImportExistingWizard/Step4Preview.tsx +++ b/src/components/TableModals/ImportExistingWizard/Step4Preview.tsx @@ -2,8 +2,8 @@ import { useAtom } from "jotai"; import { IStepProps } from "."; import { styled, Grid } from "@mui/material"; -import Column from "@src/components/Table/Column"; -import Cell from "@src/components/Table/Cell"; +import Column from "@src/components/Table/Mock/Column"; +import Cell from "@src/components/Table/Mock/Cell"; import { tableScope, tableRowsAtom } from "@src/atoms/tableScope"; diff --git a/src/components/fields/Action/index.tsx b/src/components/fields/Action/index.tsx index 0677e7e4..e2e8c1bd 100644 --- a/src/components/fields/Action/index.tsx +++ b/src/components/fields/Action/index.tsx @@ -1,6 +1,6 @@ import { lazy } from "react"; import { IFieldConfig, FieldType } from "@src/components/fields/types"; -import withTableCell from "@src/components/Table/withTableCell"; +import withRenderTableCell from "@src/components/Table/TableCell/withRenderTableCell"; import ActionIcon from "@mui/icons-material/TouchAppOutlined"; import DisplayCell from "./DisplayCell"; @@ -24,7 +24,7 @@ export const config: IFieldConfig = { icon: , description: "Button with pre-defined action script or triggers a Cloud Function. Optionally supports Undo and Redo.", - TableCell: withTableCell(DisplayCell, EditorCell, "inline", { + TableCell: withRenderTableCell(DisplayCell, EditorCell, "inline", { disablePadding: true, }), SideDrawerField, diff --git a/src/components/fields/Checkbox/index.tsx b/src/components/fields/Checkbox/index.tsx index 291d074c..24039351 100644 --- a/src/components/fields/Checkbox/index.tsx +++ b/src/components/fields/Checkbox/index.tsx @@ -1,6 +1,6 @@ import { lazy } from "react"; import { IFieldConfig, FieldType } from "@src/components/fields/types"; -import withTableCell from "@src/components/Table/withTableCell"; +import withRenderTableCell from "@src/components/Table/TableCell/withRenderTableCell"; import CheckboxIcon from "@mui/icons-material/ToggleOnOutlined"; import DisplayCell from "./DisplayCell"; @@ -24,7 +24,7 @@ export const config: IFieldConfig = { initializable: true, icon: , description: "True/false value. Default: false.", - TableCell: withTableCell(DisplayCell, EditorCell, "inline", { + TableCell: withRenderTableCell(DisplayCell, EditorCell, "inline", { usesRowData: true, }), csvImportParser: (value: string) => { diff --git a/src/components/fields/Code/index.tsx b/src/components/fields/Code/index.tsx index 72738586..d3ca01ec 100644 --- a/src/components/fields/Code/index.tsx +++ b/src/components/fields/Code/index.tsx @@ -1,6 +1,6 @@ import { lazy } from "react"; import { IFieldConfig, FieldType } from "@src/components/fields/types"; -import withTableCell from "@src/components/Table/withTableCell"; +import withRenderTableCell from "@src/components/Table/TableCell/withRenderTableCell"; import CodeIcon from "@mui/icons-material/Code"; import DisplayCell from "./DisplayCell"; @@ -23,7 +23,7 @@ export const config: IFieldConfig = { initializable: true, icon: , description: "Raw code edited with the Monaco Editor.", - TableCell: withTableCell(DisplayCell, SideDrawerField, "popover", { + TableCell: withRenderTableCell(DisplayCell, SideDrawerField, "popover", { popoverProps: { anchorOrigin: { vertical: "top", horizontal: "center" }, PaperProps: { sx: { borderRadius: 1 } }, diff --git a/src/components/fields/Color/index.tsx b/src/components/fields/Color/index.tsx index 140593a3..ad626bd5 100644 --- a/src/components/fields/Color/index.tsx +++ b/src/components/fields/Color/index.tsx @@ -1,6 +1,6 @@ import { lazy } from "react"; import { IFieldConfig, FieldType } from "@src/components/fields/types"; -import withTableCell from "@src/components/Table/withTableCell"; +import withRenderTableCell from "@src/components/Table/TableCell/withRenderTableCell"; import { toColor } from "react-color-palette"; import ColorIcon from "@mui/icons-material/Colorize"; @@ -25,7 +25,7 @@ export const config: IFieldConfig = { icon: , description: "Color stored as Hex, RGB, and HSV. Edited with a visual picker.", - TableCell: withTableCell(DisplayCell, EditorCell, "popover", { + TableCell: withRenderTableCell(DisplayCell, EditorCell, "popover", { disablePadding: true, }), SideDrawerField, diff --git a/src/components/fields/ConnectService/DisplayCell.tsx b/src/components/fields/ConnectService/DisplayCell.tsx index 2dd91bd8..9e8177bf 100644 --- a/src/components/fields/ConnectService/DisplayCell.tsx +++ b/src/components/fields/ConnectService/DisplayCell.tsx @@ -3,7 +3,7 @@ import { IDisplayCellProps } from "@src/components/fields/types"; import { ButtonBase, Grid, Chip } from "@mui/material"; import { ChevronDown } from "@src/assets/icons"; -import ChipList from "@src/components/Table/ChipList"; +import ChipList from "@src/components/Table/TableCell/ChipList"; import { get } from "lodash-es"; export default function ConnectService({ diff --git a/src/components/fields/ConnectService/index.tsx b/src/components/fields/ConnectService/index.tsx index 2495b867..f58219ca 100644 --- a/src/components/fields/ConnectService/index.tsx +++ b/src/components/fields/ConnectService/index.tsx @@ -1,6 +1,6 @@ import { lazy } from "react"; import { IFieldConfig, FieldType } from "@src/components/fields/types"; -import withTableCell from "@src/components/Table/withTableCell"; +import withRenderTableCell from "@src/components/Table/TableCell/withRenderTableCell"; import ConnectServiceIcon from "@mui/icons-material/Http"; import DisplayCell from "./DisplayCell"; @@ -28,7 +28,7 @@ export const config: IFieldConfig = { icon: , description: "Connects to an external web service to fetch a list of results.", - TableCell: withTableCell(DisplayCell, EditorCell, "popover", { + TableCell: withRenderTableCell(DisplayCell, EditorCell, "popover", { disablePadding: true, transparentPopover: true, }), diff --git a/src/components/fields/ConnectTable/DisplayCell.tsx b/src/components/fields/ConnectTable/DisplayCell.tsx index 90ca35af..de750aa8 100644 --- a/src/components/fields/ConnectTable/DisplayCell.tsx +++ b/src/components/fields/ConnectTable/DisplayCell.tsx @@ -3,7 +3,7 @@ import { IDisplayCellProps } from "@src/components/fields/types"; import { ButtonBase, Grid, Chip } from "@mui/material"; import { ChevronDown } from "@src/assets/icons"; -import ChipList from "@src/components/Table/ChipList"; +import ChipList from "@src/components/Table/TableCell/ChipList"; export default function ConnectTable({ value, diff --git a/src/components/fields/ConnectTable/index.tsx b/src/components/fields/ConnectTable/index.tsx index e5cd002b..f4a0ca38 100644 --- a/src/components/fields/ConnectTable/index.tsx +++ b/src/components/fields/ConnectTable/index.tsx @@ -1,6 +1,6 @@ import { lazy } from "react"; import { IFieldConfig, FieldType } from "@src/components/fields/types"; -import withTableCell from "@src/components/Table/withTableCell"; +import withRenderTableCell from "@src/components/Table/TableCell/withRenderTableCell"; import { ConnectTable as ConnectTableIcon } from "@src/assets/icons"; import DisplayCell from "./DisplayCell"; @@ -28,7 +28,7 @@ export const config: IFieldConfig = { icon: , description: "Connects to an existing table to fetch a snapshot of values from a row. Requires Rowy Run and Algolia setup.", - TableCell: withTableCell(DisplayCell, EditorCell, "popover", { + TableCell: withRenderTableCell(DisplayCell, EditorCell, "popover", { disablePadding: true, transparentPopover: true, }), diff --git a/src/components/fields/Connector/DisplayCell.tsx b/src/components/fields/Connector/DisplayCell.tsx index 6576204a..e31e3d18 100644 --- a/src/components/fields/Connector/DisplayCell.tsx +++ b/src/components/fields/Connector/DisplayCell.tsx @@ -3,7 +3,7 @@ import { IDisplayCellProps } from "@src/components/fields/types"; import { ButtonBase, Grid, Chip } from "@mui/material"; import { ChevronDown } from "@src/assets/icons"; -import ChipList from "@src/components/Table/ChipList"; +import ChipList from "@src/components/Table/TableCell/ChipList"; import { get } from "lodash-es"; import { getLabel } from "./utils"; diff --git a/src/components/fields/Connector/index.tsx b/src/components/fields/Connector/index.tsx index 7946e82b..a0e239d1 100644 --- a/src/components/fields/Connector/index.tsx +++ b/src/components/fields/Connector/index.tsx @@ -1,6 +1,6 @@ import { lazy } from "react"; import { IFieldConfig, FieldType } from "@src/components/fields/types"; -import withTableCell from "@src/components/Table/withTableCell"; +import withRenderTableCell from "@src/components/Table/TableCell/withRenderTableCell"; import ConnectorIcon from "@mui/icons-material/Cable"; import DisplayCell from "./DisplayCell"; @@ -29,7 +29,7 @@ export const config: IFieldConfig = { icon: , description: "Connects to any table or API to fetch a list of results based on a text query or row data.", - TableCell: withTableCell(DisplayCell, EditorCell, "popover", { + TableCell: withRenderTableCell(DisplayCell, EditorCell, "popover", { disablePadding: true, }), SideDrawerField, diff --git a/src/components/fields/CreatedAt/index.tsx b/src/components/fields/CreatedAt/index.tsx index 151466fe..cffa0ea8 100644 --- a/src/components/fields/CreatedAt/index.tsx +++ b/src/components/fields/CreatedAt/index.tsx @@ -1,6 +1,6 @@ import { lazy } from "react"; import { IFieldConfig, FieldType } from "@src/components/fields/types"; -import withTableCell from "@src/components/Table/withTableCell"; +import withRenderTableCell from "@src/components/Table/TableCell/withRenderTableCell"; import { CreatedAt as CreatedAtIcon } from "@src/assets/icons"; import DisplayCell from "./DisplayCell"; @@ -24,7 +24,7 @@ export const config: IFieldConfig = { initialValue: null, icon: , description: "Displays the timestamp of when the row was created. Read-only.", - TableCell: withTableCell(DisplayCell, null), + TableCell: withRenderTableCell(DisplayCell, null), SideDrawerField, settings: Settings, }; diff --git a/src/components/fields/CreatedBy/index.tsx b/src/components/fields/CreatedBy/index.tsx index 8bb0329c..98fe8cb6 100644 --- a/src/components/fields/CreatedBy/index.tsx +++ b/src/components/fields/CreatedBy/index.tsx @@ -1,6 +1,6 @@ import { lazy } from "react"; import { IFieldConfig, FieldType } from "@src/components/fields/types"; -import withTableCell from "@src/components/Table/withTableCell"; +import withRenderTableCell from "@src/components/Table/TableCell/withRenderTableCell"; import { CreatedBy as CreatedByIcon } from "@src/assets/icons"; import DisplayCell from "./DisplayCell"; @@ -25,7 +25,7 @@ export const config: IFieldConfig = { icon: , description: "Displays the user that created the row and timestamp. Read-only.", - TableCell: withTableCell(DisplayCell, null), + TableCell: withRenderTableCell(DisplayCell, null), SideDrawerField, settings: Settings, }; diff --git a/src/components/fields/Date/index.tsx b/src/components/fields/Date/index.tsx index 1ff9b01a..7078e1f1 100644 --- a/src/components/fields/Date/index.tsx +++ b/src/components/fields/Date/index.tsx @@ -1,6 +1,6 @@ import { lazy } from "react"; import { IFieldConfig, FieldType } from "@src/components/fields/types"; -import withTableCell from "@src/components/Table/withTableCell"; +import withRenderTableCell from "@src/components/Table/TableCell/withRenderTableCell"; import { parse, format } from "date-fns"; import { DATE_FORMAT } from "@src/constants/dates"; @@ -28,7 +28,7 @@ export const config: IFieldConfig = { initializable: true, icon: , description: `Formatted date. Format is configurable, default: ${DATE_FORMAT}. Edited with a visual picker.`, - TableCell: withTableCell(DisplayCell, EditorCell, "focus", { + TableCell: withRenderTableCell(DisplayCell, EditorCell, "focus", { disablePadding: true, }), SideDrawerField, diff --git a/src/components/fields/DateTime/index.tsx b/src/components/fields/DateTime/index.tsx index cc247b97..d414fb6e 100644 --- a/src/components/fields/DateTime/index.tsx +++ b/src/components/fields/DateTime/index.tsx @@ -1,6 +1,6 @@ import { lazy } from "react"; import { IFieldConfig, FieldType } from "@src/components/fields/types"; -import withTableCell from "@src/components/Table/withTableCell"; +import withRenderTableCell from "@src/components/Table/TableCell/withRenderTableCell"; import { parseJSON, format } from "date-fns"; import { DATE_TIME_FORMAT } from "@src/constants/dates"; @@ -36,7 +36,7 @@ export const config: IFieldConfig = { initializable: true, icon: , description: `Formatted date & time. Format is configurable, default: ${DATE_TIME_FORMAT}. Edited with a visual picker.`, - TableCell: withTableCell(DisplayCell, EditorCell, "focus", { + TableCell: withRenderTableCell(DisplayCell, EditorCell, "focus", { disablePadding: true, }), SideDrawerField, diff --git a/src/components/fields/Derivative/index.tsx b/src/components/fields/Derivative/index.tsx index f37897ed..d046b4d3 100644 --- a/src/components/fields/Derivative/index.tsx +++ b/src/components/fields/Derivative/index.tsx @@ -1,5 +1,5 @@ import { IFieldConfig, FieldType } from "@src/components/fields/types"; -import withTableCell from "@src/components/Table/withTableCell"; +import withRenderTableCell from "@src/components/Table/TableCell/withRenderTableCell"; import { Derivative as DerivativeIcon } from "@src/assets/icons"; import Settings, { settingsValidator } from "./Settings"; @@ -15,7 +15,7 @@ export const config: IFieldConfig = { icon: , description: "Value derived from the rest of the row’s values. Displayed using any other field type. Requires Rowy Run set up.", - TableCell: withTableCell(() => null, null), + TableCell: withRenderTableCell(() => null, null), SideDrawerField: () => null as any, contextMenuActions: ContextMenuActions, settings: Settings, diff --git a/src/components/fields/Duration/index.tsx b/src/components/fields/Duration/index.tsx index 56f05132..225906ba 100644 --- a/src/components/fields/Duration/index.tsx +++ b/src/components/fields/Duration/index.tsx @@ -1,6 +1,6 @@ import { lazy } from "react"; import { IFieldConfig, FieldType } from "@src/components/fields/types"; -import withTableCell from "@src/components/Table/withTableCell"; +import withRenderTableCell from "@src/components/Table/TableCell/withRenderTableCell"; import DurationIcon from "@mui/icons-material/TimerOutlined"; import DisplayCell from "./DisplayCell"; @@ -20,7 +20,7 @@ export const config: IFieldConfig = { initialValue: {}, icon: , description: "Duration calculated from two timestamps.", - TableCell: withTableCell(DisplayCell, SideDrawerField, "popover", { + TableCell: withRenderTableCell(DisplayCell, SideDrawerField, "popover", { popoverProps: { PaperProps: { sx: { p: 1 } } }, }), SideDrawerField, diff --git a/src/components/fields/Email/EditorCell.tsx b/src/components/fields/Email/EditorCell.tsx index c14a114a..bdc0d823 100644 --- a/src/components/fields/Email/EditorCell.tsx +++ b/src/components/fields/Email/EditorCell.tsx @@ -1,5 +1,5 @@ import type { IEditorCellProps } from "@src/components/fields/types"; -import EditorCellTextField from "@src/components/Table/EditorCellTextField"; +import EditorCellTextField from "@src/components/Table/TableCell/EditorCellTextField"; export default function Email(props: IEditorCellProps) { return ; diff --git a/src/components/fields/Email/index.tsx b/src/components/fields/Email/index.tsx index 1450f513..3ea395d8 100644 --- a/src/components/fields/Email/index.tsx +++ b/src/components/fields/Email/index.tsx @@ -1,6 +1,6 @@ import { lazy } from "react"; import { IFieldConfig, FieldType } from "@src/components/fields/types"; -import withTableCell from "@src/components/Table/withTableCell"; +import withRenderTableCell from "@src/components/Table/TableCell/withRenderTableCell"; import EmailIcon from "@mui/icons-material/MailOutlined"; import DisplayCell from "@src/components/fields/ShortText/DisplayCell"; @@ -23,7 +23,7 @@ export const config: IFieldConfig = { icon: , description: "Email address. Not validated.", contextMenuActions: BasicContextMenuActions, - TableCell: withTableCell(DisplayCell, EditorCell), + TableCell: withRenderTableCell(DisplayCell, EditorCell), SideDrawerField, filter: { operators: filterOperators, diff --git a/src/components/fields/File/DisplayCell.tsx b/src/components/fields/File/DisplayCell.tsx index 2183f902..36d82d64 100644 --- a/src/components/fields/File/DisplayCell.tsx +++ b/src/components/fields/File/DisplayCell.tsx @@ -1,7 +1,7 @@ import { IDisplayCellProps } from "@src/components/fields/types"; import { Grid, Chip } from "@mui/material"; -import ChipList from "@src/components/Table/ChipList"; +import ChipList from "@src/components/Table/TableCell/ChipList"; import { FileIcon } from "."; import { FileValue } from "@src/types/table"; diff --git a/src/components/fields/File/EditorCell.tsx b/src/components/fields/File/EditorCell.tsx index ed5ba74b..c738f9d4 100644 --- a/src/components/fields/File/EditorCell.tsx +++ b/src/components/fields/File/EditorCell.tsx @@ -8,7 +8,7 @@ import { format } from "date-fns"; import { alpha, Stack, Grid, Tooltip, Chip, IconButton } from "@mui/material"; import { Upload as UploadIcon } from "@src/assets/icons"; -import ChipList from "@src/components/Table/ChipList"; +import ChipList from "@src/components/Table/TableCell/ChipList"; import CircularProgressOptical from "@src/components/CircularProgressOptical"; import { projectScope, confirmDialogAtom } from "@src/atoms/projectScope"; diff --git a/src/components/fields/File/index.tsx b/src/components/fields/File/index.tsx index 2f883954..33ab5021 100644 --- a/src/components/fields/File/index.tsx +++ b/src/components/fields/File/index.tsx @@ -1,6 +1,6 @@ import { lazy } from "react"; import { IFieldConfig, FieldType } from "@src/components/fields/types"; -import withTableCell from "@src/components/Table/withTableCell"; +import withRenderTableCell from "@src/components/Table/TableCell/withRenderTableCell"; import FileIcon from "@mui/icons-material/AttachFile"; import DisplayCell from "./DisplayCell"; @@ -22,7 +22,7 @@ export const config: IFieldConfig = { initialValue: [], icon: , description: "File uploaded to Firebase Storage. Supports any file type.", - TableCell: withTableCell(DisplayCell, EditorCell, "inline", { + TableCell: withRenderTableCell(DisplayCell, EditorCell, "inline", { disablePadding: true, }), SideDrawerField, diff --git a/src/components/fields/GeoPoint/index.tsx b/src/components/fields/GeoPoint/index.tsx index 481198f6..17dc20fb 100644 --- a/src/components/fields/GeoPoint/index.tsx +++ b/src/components/fields/GeoPoint/index.tsx @@ -1,7 +1,7 @@ import { lazy } from "react"; import { GeoPoint } from "firebase/firestore"; import { IFieldConfig, FieldType } from "@src/components/fields/types"; -import withTableCell from "@src/components/Table/withTableCell"; +import withRenderTableCell from "@src/components/Table/TableCell/withRenderTableCell"; import GeoPointIcon from "@mui/icons-material/PinDropOutlined"; import DisplayCell from "./DisplayCell"; @@ -21,7 +21,7 @@ export const config: IFieldConfig = { initialValue: {}, icon: , description: "Geo point is represented as latitude/longitude pair.", - TableCell: withTableCell(DisplayCell, SideDrawerField, "popover", { + TableCell: withRenderTableCell(DisplayCell, SideDrawerField, "popover", { popoverProps: { PaperProps: { sx: { p: 1, pt: 0 } } }, }), SideDrawerField, diff --git a/src/components/fields/Id/index.tsx b/src/components/fields/Id/index.tsx index 0b2a5b85..0215e7fe 100644 --- a/src/components/fields/Id/index.tsx +++ b/src/components/fields/Id/index.tsx @@ -1,6 +1,6 @@ import { lazy } from "react"; import { IFieldConfig, FieldType } from "@src/components/fields/types"; -import withTableCell from "@src/components/Table/withTableCell"; +import withRenderTableCell from "@src/components/Table/TableCell/withRenderTableCell"; import DisplayCell from "./DisplayCell"; import { Id as IdIcon } from "@src/assets/icons"; @@ -17,7 +17,7 @@ export const config: IFieldConfig = { initialValue: "", icon: , description: "Displays the row’s ID. Read-only. Cannot be sorted.", - TableCell: withTableCell(DisplayCell, null), + TableCell: withRenderTableCell(DisplayCell, null), SideDrawerField, }; export default config; diff --git a/src/components/fields/Image/index.tsx b/src/components/fields/Image/index.tsx index 1dbcde26..71d5a32d 100644 --- a/src/components/fields/Image/index.tsx +++ b/src/components/fields/Image/index.tsx @@ -1,6 +1,6 @@ import { lazy } from "react"; import { IFieldConfig, FieldType } from "@src/components/fields/types"; -import withTableCell from "@src/components/Table/withTableCell"; +import withRenderTableCell from "@src/components/Table/TableCell/withRenderTableCell"; import { Image as ImageIcon } from "@src/assets/icons"; import DisplayCell from "./DisplayCell"; @@ -23,7 +23,7 @@ export const config: IFieldConfig = { icon: , description: "Image file uploaded to Firebase Storage. Supports JPEG, PNG, SVG, GIF, WebP, AVIF, JPEG XL.", - TableCell: withTableCell(DisplayCell, EditorCell, "inline", { + TableCell: withRenderTableCell(DisplayCell, EditorCell, "inline", { disablePadding: true, }), SideDrawerField, diff --git a/src/components/fields/Json/index.tsx b/src/components/fields/Json/index.tsx index a847f009..6804b7c6 100644 --- a/src/components/fields/Json/index.tsx +++ b/src/components/fields/Json/index.tsx @@ -1,6 +1,6 @@ import { lazy } from "react"; import { IFieldConfig, FieldType } from "@src/components/fields/types"; -import withTableCell from "@src/components/Table/withTableCell"; +import withRenderTableCell from "@src/components/Table/TableCell/withRenderTableCell"; import { Json as JsonIcon } from "@src/assets/icons"; import DisplayCell from "./DisplayCell"; @@ -24,7 +24,7 @@ export const config: IFieldConfig = { initializable: true, icon: , description: "Object edited with a visual JSON editor.", - TableCell: withTableCell(DisplayCell, SideDrawerField, "popover", { + TableCell: withRenderTableCell(DisplayCell, SideDrawerField, "popover", { popoverProps: { PaperProps: { sx: { p: 1 } } }, }), csvImportParser: (value) => { diff --git a/src/components/fields/LongText/EditorCell.tsx b/src/components/fields/LongText/EditorCell.tsx index ba2824bd..581ce96a 100644 --- a/src/components/fields/LongText/EditorCell.tsx +++ b/src/components/fields/LongText/EditorCell.tsx @@ -1,5 +1,5 @@ import type { IEditorCellProps } from "@src/components/fields/types"; -import EditorCellTextField from "@src/components/Table/EditorCellTextField"; +import EditorCellTextField from "@src/components/Table/TableCell/EditorCellTextField"; export default function LongText(props: IEditorCellProps) { return ; diff --git a/src/components/fields/LongText/index.tsx b/src/components/fields/LongText/index.tsx index 00c79f31..6b926126 100644 --- a/src/components/fields/LongText/index.tsx +++ b/src/components/fields/LongText/index.tsx @@ -1,6 +1,6 @@ import { lazy } from "react"; import { IFieldConfig, FieldType } from "@src/components/fields/types"; -import withTableCell from "@src/components/Table/withTableCell"; +import withRenderTableCell from "@src/components/Table/TableCell/withRenderTableCell"; import LongTextIcon from "@mui/icons-material/Notes"; import DisplayCell from "./DisplayCell"; @@ -29,7 +29,7 @@ export const config: IFieldConfig = { icon: , description: "Text displayed on multiple lines.", contextMenuActions: BasicContextMenuActions, - TableCell: withTableCell(DisplayCell, EditorCell), + TableCell: withRenderTableCell(DisplayCell, EditorCell), SideDrawerField, settings: Settings, filter: { diff --git a/src/components/fields/Markdown/index.tsx b/src/components/fields/Markdown/index.tsx index 23776019..26de107b 100644 --- a/src/components/fields/Markdown/index.tsx +++ b/src/components/fields/Markdown/index.tsx @@ -1,6 +1,6 @@ import { lazy } from "react"; import { IFieldConfig, FieldType } from "@src/components/fields/types"; -import withTableCell from "@src/components/Table/withTableCell"; +import withRenderTableCell from "@src/components/Table/TableCell/withRenderTableCell"; import { Markdown as MarkdownIcon } from "@src/assets/icons"; import DisplayCell from "./DisplayCell"; @@ -21,7 +21,7 @@ export const config: IFieldConfig = { initializable: true, icon: , description: "Markdown editor with preview", - TableCell: withTableCell(DisplayCell, SideDrawerField, "popover"), + TableCell: withRenderTableCell(DisplayCell, SideDrawerField, "popover"), SideDrawerField, }; export default config; diff --git a/src/components/fields/MultiSelect/DisplayCell.tsx b/src/components/fields/MultiSelect/DisplayCell.tsx index 7554330a..fe7d6b87 100644 --- a/src/components/fields/MultiSelect/DisplayCell.tsx +++ b/src/components/fields/MultiSelect/DisplayCell.tsx @@ -5,7 +5,7 @@ import WarningIcon from "@mui/icons-material/WarningAmber"; import { ChevronDown } from "@src/assets/icons"; import { sanitiseValue } from "./utils"; -import ChipList from "@src/components/Table/ChipList"; +import ChipList from "@src/components/Table/TableCell/ChipList"; import FormattedChip from "@src/components/FormattedChip"; export default function MultiSelect({ diff --git a/src/components/fields/MultiSelect/index.tsx b/src/components/fields/MultiSelect/index.tsx index f6400d1b..19a5cecd 100644 --- a/src/components/fields/MultiSelect/index.tsx +++ b/src/components/fields/MultiSelect/index.tsx @@ -1,6 +1,6 @@ import { lazy } from "react"; import { IFieldConfig, FieldType } from "@src/components/fields/types"; -import withTableCell from "@src/components/Table/withTableCell"; +import withRenderTableCell from "@src/components/Table/TableCell/withRenderTableCell"; import { MultiSelect as MultiSelectIcon } from "@src/assets/icons"; import DisplayCell from "./DisplayCell"; @@ -32,7 +32,7 @@ export const config: IFieldConfig = { icon: , description: "Multiple values from predefined options. Options are searchable and users can optionally input custom values.", - TableCell: withTableCell(DisplayCell, EditorCell, "popover", { + TableCell: withRenderTableCell(DisplayCell, EditorCell, "popover", { disablePadding: true, transparentPopover: false, }), diff --git a/src/components/fields/Number/EditorCell.tsx b/src/components/fields/Number/EditorCell.tsx index 5ae634a2..e3594eac 100644 --- a/src/components/fields/Number/EditorCell.tsx +++ b/src/components/fields/Number/EditorCell.tsx @@ -1,5 +1,5 @@ import type { IEditorCellProps } from "@src/components/fields/types"; -import EditorCellTextField from "@src/components/Table/EditorCellTextField"; +import EditorCellTextField from "@src/components/Table/TableCell/EditorCellTextField"; export default function Number_(props: IEditorCellProps) { return ( diff --git a/src/components/fields/Number/index.tsx b/src/components/fields/Number/index.tsx index 0a25fca8..2f04cadd 100644 --- a/src/components/fields/Number/index.tsx +++ b/src/components/fields/Number/index.tsx @@ -1,6 +1,6 @@ import { lazy } from "react"; import { IFieldConfig, FieldType } from "@src/components/fields/types"; -import withTableCell from "@src/components/Table/withTableCell"; +import withRenderTableCell from "@src/components/Table/TableCell/withRenderTableCell"; import { Number as NumberIcon } from "@src/assets/icons"; import DisplayCell from "./DisplayCell"; @@ -22,7 +22,7 @@ export const config: IFieldConfig = { icon: , description: "Numeric value.", contextMenuActions: BasicContextMenuActions, - TableCell: withTableCell(DisplayCell, EditorCell), + TableCell: withRenderTableCell(DisplayCell, EditorCell), SideDrawerField, filter: { operators: filterOperators, diff --git a/src/components/fields/Percentage/EditorCell.tsx b/src/components/fields/Percentage/EditorCell.tsx index 16dd9a88..1e3a7e96 100644 --- a/src/components/fields/Percentage/EditorCell.tsx +++ b/src/components/fields/Percentage/EditorCell.tsx @@ -1,5 +1,5 @@ import type { IEditorCellProps } from "@src/components/fields/types"; -import EditorCellTextField from "@src/components/Table/EditorCellTextField"; +import EditorCellTextField from "@src/components/Table/TableCell/EditorCellTextField"; export default function Percentage(props: IEditorCellProps) { return ( diff --git a/src/components/fields/Percentage/index.tsx b/src/components/fields/Percentage/index.tsx index 86ff6fe6..b571b50f 100644 --- a/src/components/fields/Percentage/index.tsx +++ b/src/components/fields/Percentage/index.tsx @@ -1,6 +1,6 @@ import { lazy } from "react"; import { IFieldConfig, FieldType } from "@src/components/fields/types"; -import withTableCell from "@src/components/Table/withTableCell"; +import withRenderTableCell from "@src/components/Table/TableCell/withRenderTableCell"; import { Percentage as PercentageIcon } from "@src/assets/icons"; import DisplayCell from "./DisplayCell"; @@ -30,7 +30,7 @@ export const config: IFieldConfig = { requireConfiguration: true, description: "Percentage stored as a number between 0 and 1.", contextMenuActions: BasicContextMenuActions, - TableCell: withTableCell(DisplayCell, EditorCell), + TableCell: withRenderTableCell(DisplayCell, EditorCell), SideDrawerField, settings: Settings, filter: { diff --git a/src/components/fields/Phone/EditorCell.tsx b/src/components/fields/Phone/EditorCell.tsx index 825fd00f..4db1bf0c 100644 --- a/src/components/fields/Phone/EditorCell.tsx +++ b/src/components/fields/Phone/EditorCell.tsx @@ -1,5 +1,5 @@ import type { IEditorCellProps } from "@src/components/fields/types"; -import EditorCellTextField from "@src/components/Table/EditorCellTextField"; +import EditorCellTextField from "@src/components/Table/TableCell/EditorCellTextField"; export default function Phone(props: IEditorCellProps) { return ; diff --git a/src/components/fields/Phone/index.tsx b/src/components/fields/Phone/index.tsx index ec8792a6..0d72c31a 100644 --- a/src/components/fields/Phone/index.tsx +++ b/src/components/fields/Phone/index.tsx @@ -1,6 +1,6 @@ import { lazy } from "react"; import { IFieldConfig, FieldType } from "@src/components/fields/types"; -import withTableCell from "@src/components/Table/withTableCell"; +import withRenderTableCell from "@src/components/Table/TableCell/withRenderTableCell"; import PhoneIcon from "@mui/icons-material/PhoneOutlined"; import DisplayCell from "@src/components/fields/ShortText/DisplayCell"; @@ -23,7 +23,7 @@ export const config: IFieldConfig = { icon: , description: "Phone number stored as text. Not validated.", contextMenuActions: BasicContextMenuActions, - TableCell: withTableCell(DisplayCell, EditorCell), + TableCell: withRenderTableCell(DisplayCell, EditorCell), SideDrawerField, filter: { operators: filterOperators, diff --git a/src/components/fields/Rating/index.tsx b/src/components/fields/Rating/index.tsx index eac10ed3..3712ffe7 100644 --- a/src/components/fields/Rating/index.tsx +++ b/src/components/fields/Rating/index.tsx @@ -1,6 +1,6 @@ import { lazy } from "react"; import { IFieldConfig, FieldType } from "@src/components/fields/types"; -import withTableCell from "@src/components/Table/withTableCell"; +import withRenderTableCell from "@src/components/Table/TableCell/withRenderTableCell"; import RatingIcon from "@mui/icons-material/StarBorder"; import DisplayCell from "./DisplayCell"; @@ -26,7 +26,7 @@ export const config: IFieldConfig = { requireConfiguration: true, description: "Rating displayed as stars. Max stars is configurable, default: 5 stars.", - TableCell: withTableCell(DisplayCell, EditorCell, "inline"), + TableCell: withRenderTableCell(DisplayCell, EditorCell, "inline"), settings: Settings, SideDrawerField, filter: { diff --git a/src/components/fields/Reference/EditorCell.tsx b/src/components/fields/Reference/EditorCell.tsx index 27232cff..07115fb8 100644 --- a/src/components/fields/Reference/EditorCell.tsx +++ b/src/components/fields/Reference/EditorCell.tsx @@ -3,7 +3,7 @@ import { useAtom } from "jotai"; import { doc, deleteField } from "firebase/firestore"; import type { IEditorCellProps } from "@src/components/fields/types"; -import EditorCellTextField from "@src/components/Table/EditorCellTextField"; +import EditorCellTextField from "@src/components/Table/TableCell/EditorCellTextField"; import { InputAdornment, Tooltip } from "@mui/material"; import ErrorIcon from "@mui/icons-material/ErrorOutline"; diff --git a/src/components/fields/Reference/index.tsx b/src/components/fields/Reference/index.tsx index fcf0345d..c4fdce52 100644 --- a/src/components/fields/Reference/index.tsx +++ b/src/components/fields/Reference/index.tsx @@ -1,6 +1,6 @@ import { lazy } from "react"; import { IFieldConfig, FieldType } from "@src/components/fields/types"; -import withTableCell from "@src/components/Table/withTableCell"; +import withRenderTableCell from "@src/components/Table/TableCell/withRenderTableCell"; import { Reference } from "@src/assets/icons"; import DisplayCell from "./DisplayCell"; @@ -23,7 +23,7 @@ export const config: IFieldConfig = { initializable: true, icon: , description: "Firestore document reference", - TableCell: withTableCell(DisplayCell, EditorCell, "focus", { + TableCell: withRenderTableCell(DisplayCell, EditorCell, "focus", { disablePadding: true, }), SideDrawerField, diff --git a/src/components/fields/RichText/index.tsx b/src/components/fields/RichText/index.tsx index bd53d610..c9c1ddbe 100644 --- a/src/components/fields/RichText/index.tsx +++ b/src/components/fields/RichText/index.tsx @@ -1,6 +1,6 @@ import { lazy } from "react"; import { IFieldConfig, FieldType } from "@src/components/fields/types"; -import withTableCell from "@src/components/Table/withTableCell"; +import withRenderTableCell from "@src/components/Table/TableCell/withRenderTableCell"; import RichTextIcon from "@mui/icons-material/TextFormat"; import DisplayCell from "./DisplayCell"; @@ -23,7 +23,7 @@ export const config: IFieldConfig = { icon: , description: "HTML edited with a rich text editor.", contextMenuActions: BasicContextMenuActions, - TableCell: withTableCell(DisplayCell, SideDrawerField, "popover"), + TableCell: withRenderTableCell(DisplayCell, SideDrawerField, "popover"), SideDrawerField, }; export default config; diff --git a/src/components/fields/ShortText/EditorCell.tsx b/src/components/fields/ShortText/EditorCell.tsx index 0e34e5a8..123d7892 100644 --- a/src/components/fields/ShortText/EditorCell.tsx +++ b/src/components/fields/ShortText/EditorCell.tsx @@ -1,5 +1,5 @@ import type { IEditorCellProps } from "@src/components/fields/types"; -import EditorCellTextField from "@src/components/Table/EditorCellTextField"; +import EditorCellTextField from "@src/components/Table/TableCell/EditorCellTextField"; export default function ShortText(props: IEditorCellProps) { return ; diff --git a/src/components/fields/ShortText/index.tsx b/src/components/fields/ShortText/index.tsx index 0160a64a..1288f9bb 100644 --- a/src/components/fields/ShortText/index.tsx +++ b/src/components/fields/ShortText/index.tsx @@ -1,6 +1,6 @@ import { lazy } from "react"; import { IFieldConfig, FieldType } from "@src/components/fields/types"; -import withTableCell from "@src/components/Table/withTableCell"; +import withRenderTableCell from "@src/components/Table/TableCell/withRenderTableCell"; import ShortTextIcon from "@mui/icons-material/ShortText"; import DisplayCell from "@src/components/fields/ShortText/DisplayCell"; @@ -30,7 +30,7 @@ export const config: IFieldConfig = { icon: , description: "Text displayed on a single line.", contextMenuActions: BasicContextMenuActions, - TableCell: withTableCell(DisplayCell, EditorCell), + TableCell: withRenderTableCell(DisplayCell, EditorCell), SideDrawerField, settings: Settings, filter: { diff --git a/src/components/fields/SingleSelect/index.tsx b/src/components/fields/SingleSelect/index.tsx index 187be997..83a23e3e 100644 --- a/src/components/fields/SingleSelect/index.tsx +++ b/src/components/fields/SingleSelect/index.tsx @@ -1,6 +1,6 @@ import { lazy } from "react"; import { IFieldConfig, FieldType } from "@src/components/fields/types"; -import withTableCell from "@src/components/Table/withTableCell"; +import withRenderTableCell from "@src/components/Table/TableCell/withRenderTableCell"; import { SingleSelect as SingleSelectIcon } from "@src/assets/icons"; import DisplayCell from "./DisplayCell"; @@ -27,7 +27,7 @@ export const config: IFieldConfig = { icon: , description: "Single value from predefined options. Options are searchable and users can optionally input custom values.", - TableCell: withTableCell(DisplayCell, EditorCell, "popover", { + TableCell: withRenderTableCell(DisplayCell, EditorCell, "popover", { disablePadding: true, transparentPopover: true, }), diff --git a/src/components/fields/Slider/index.tsx b/src/components/fields/Slider/index.tsx index 6d21dc9d..06f7bce0 100644 --- a/src/components/fields/Slider/index.tsx +++ b/src/components/fields/Slider/index.tsx @@ -1,6 +1,6 @@ import { lazy } from "react"; import { IFieldConfig, FieldType } from "@src/components/fields/types"; -import withTableCell from "@src/components/Table/withTableCell"; +import withRenderTableCell from "@src/components/Table/TableCell/withRenderTableCell"; import { Slider as SliderIcon } from "@src/assets/icons"; import DisplayCell from "./DisplayCell"; @@ -24,7 +24,7 @@ export const config: IFieldConfig = { icon: , requireConfiguration: true, description: "Numeric value edited with a Slider. Range is configurable.", - TableCell: withTableCell(DisplayCell, SideDrawerField, "popover", { + TableCell: withRenderTableCell(DisplayCell, SideDrawerField, "popover", { popoverProps: { PaperProps: { sx: { p: 1, pt: 5 } } }, }), settings: Settings, diff --git a/src/components/fields/Status/index.tsx b/src/components/fields/Status/index.tsx index d4d0e978..420a5e11 100644 --- a/src/components/fields/Status/index.tsx +++ b/src/components/fields/Status/index.tsx @@ -1,6 +1,6 @@ import { lazy } from "react"; import { IFieldConfig, FieldType } from "@src/components/fields/types"; -import withTableCell from "@src/components/Table/withTableCell"; +import withRenderTableCell from "@src/components/Table/TableCell/withRenderTableCell"; import { Status as StatusIcon } from "@src/assets/icons"; import DisplayCell from "./DisplayCell"; @@ -24,7 +24,7 @@ export const config: IFieldConfig = { initializable: true, icon: , description: "Displays field value as custom status text.", - TableCell: withTableCell(DisplayCell, EditorCell, "popover", { + TableCell: withRenderTableCell(DisplayCell, EditorCell, "popover", { disablePadding: true, transparentPopover: true, }), diff --git a/src/components/fields/SubTable/index.tsx b/src/components/fields/SubTable/index.tsx index 84df815d..7e153771 100644 --- a/src/components/fields/SubTable/index.tsx +++ b/src/components/fields/SubTable/index.tsx @@ -1,6 +1,6 @@ import { lazy } from "react"; import { IFieldConfig, FieldType } from "@src/components/fields/types"; -import withTableCell from "@src/components/Table/withTableCell"; +import withRenderTableCell from "@src/components/Table/TableCell/withRenderTableCell"; import { SubTable as SubTableIcon } from "@src/assets/icons"; import DisplayCell from "./DisplayCell"; @@ -24,7 +24,7 @@ export const config: IFieldConfig = { settings: Settings, description: "Connects to a sub-table in the current row. Also displays number of rows inside the sub-table. Max sub-table depth: 100.", - TableCell: withTableCell(DisplayCell, null, "focus", { + TableCell: withRenderTableCell(DisplayCell, null, "focus", { usesRowData: true, disablePadding: true, }), diff --git a/src/components/fields/UpdatedAt/index.tsx b/src/components/fields/UpdatedAt/index.tsx index 9b7476eb..d6e5eb92 100644 --- a/src/components/fields/UpdatedAt/index.tsx +++ b/src/components/fields/UpdatedAt/index.tsx @@ -1,6 +1,6 @@ import { lazy } from "react"; import { IFieldConfig, FieldType } from "@src/components/fields/types"; -import withTableCell from "@src/components/Table/withTableCell"; +import withRenderTableCell from "@src/components/Table/TableCell/withRenderTableCell"; import { UpdatedAt as UpdatedAtIcon } from "@src/assets/icons"; import DisplayCell from "./DisplayCell"; @@ -25,7 +25,7 @@ export const config: IFieldConfig = { icon: , description: "Displays the timestamp of the last update to the row. Read-only.", - TableCell: withTableCell(DisplayCell, null), + TableCell: withRenderTableCell(DisplayCell, null), SideDrawerField, settings: Settings, }; diff --git a/src/components/fields/UpdatedBy/index.tsx b/src/components/fields/UpdatedBy/index.tsx index 5ddfb877..4b1c3a42 100644 --- a/src/components/fields/UpdatedBy/index.tsx +++ b/src/components/fields/UpdatedBy/index.tsx @@ -1,6 +1,6 @@ import { lazy } from "react"; import { IFieldConfig, FieldType } from "@src/components/fields/types"; -import withTableCell from "@src/components/Table/withTableCell"; +import withRenderTableCell from "@src/components/Table/TableCell/withRenderTableCell"; import { UpdatedBy as UpdatedByIcon } from "@src/assets/icons"; import DisplayCell from "./DisplayCell"; @@ -26,7 +26,7 @@ export const config: IFieldConfig = { icon: , description: "Displays the user that last updated the row, timestamp, and updated field key. Read-only.", - TableCell: withTableCell(DisplayCell, null), + TableCell: withRenderTableCell(DisplayCell, null), SideDrawerField, settings: Settings, }; diff --git a/src/components/fields/Url/EditorCell.tsx b/src/components/fields/Url/EditorCell.tsx index 6f39d2d9..d22c4880 100644 --- a/src/components/fields/Url/EditorCell.tsx +++ b/src/components/fields/Url/EditorCell.tsx @@ -1,5 +1,5 @@ import type { IEditorCellProps } from "@src/components/fields/types"; -import EditorCellTextField from "@src/components/Table/EditorCellTextField"; +import EditorCellTextField from "@src/components/Table/TableCell/EditorCellTextField"; export default function Url(props: IEditorCellProps) { return ; diff --git a/src/components/fields/Url/index.tsx b/src/components/fields/Url/index.tsx index 612423d0..be276388 100644 --- a/src/components/fields/Url/index.tsx +++ b/src/components/fields/Url/index.tsx @@ -1,6 +1,6 @@ import { lazy } from "react"; import { IFieldConfig, FieldType } from "@src/components/fields/types"; -import withTableCell from "@src/components/Table/withTableCell"; +import withRenderTableCell from "@src/components/Table/TableCell/withRenderTableCell"; import UrlIcon from "@mui/icons-material/Link"; import DisplayCell from "./DisplayCell"; @@ -23,7 +23,7 @@ export const config: IFieldConfig = { icon: , description: "Web address. Not validated.", contextMenuActions: BasicContextMenuActions, - TableCell: withTableCell(DisplayCell, EditorCell, "focus", { + TableCell: withRenderTableCell(DisplayCell, EditorCell, "focus", { disablePadding: true, }), SideDrawerField, diff --git a/src/components/fields/User/index.tsx b/src/components/fields/User/index.tsx index 29f996d8..b06680df 100644 --- a/src/components/fields/User/index.tsx +++ b/src/components/fields/User/index.tsx @@ -1,6 +1,6 @@ import { lazy } from "react"; import { IFieldConfig, FieldType } from "@src/components/fields/types"; -import withTableCell from "@src/components/Table/withTableCell"; +import withRenderTableCell from "@src/components/Table/TableCell/withRenderTableCell"; import UserIcon from "@mui/icons-material/PersonOutlined"; import DisplayCell from "./DisplayCell"; @@ -23,7 +23,7 @@ export const config: IFieldConfig = { initialValue: null, icon: , description: "User information and optionally, timestamp. Read-only.", - TableCell: withTableCell(DisplayCell, null), + TableCell: withRenderTableCell(DisplayCell, null), SideDrawerField, settings: Settings, }; diff --git a/src/components/fields/types.ts b/src/components/fields/types.ts index e4084365..b43b6d82 100644 --- a/src/components/fields/types.ts +++ b/src/components/fields/types.ts @@ -1,5 +1,5 @@ import { FieldType } from "@src/constants/fields"; -import type { ITableCellProps } from "@src/components/Table/withTableCell"; +import type { IRenderedTableCellProps } from "@src/components/Table/TableCell/withRenderTableCell"; import type { PopoverProps } from "@mui/material"; import type { ColumnConfig, @@ -27,7 +27,7 @@ export interface IFieldConfig { selectedCell: SelectedCell, reset: () => void ) => IContextMenuItem[]; - TableCell: React.ComponentType; + TableCell: React.ComponentType; SideDrawerField: React.ComponentType; settings?: React.ComponentType; settingsValidator?: (config: Record) => Record; diff --git a/src/constants/routes.tsx b/src/constants/routes.tsx index 4b59b687..af23e2cd 100644 --- a/src/constants/routes.tsx +++ b/src/constants/routes.tsx @@ -1,5 +1,5 @@ import Logo from "@src/assets/Logo"; -import BreadcrumbsTableRoot from "@src/components/Table/BreadcrumbsTableRoot"; +import BreadcrumbsTableRoot from "@src/components/Table/Breadcrumbs/BreadcrumbsTableRoot"; import { FadeProps, Typography } from "@mui/material"; export enum ROUTES { diff --git a/src/pages/Table/ProvidedSubTablePage.tsx b/src/pages/Table/ProvidedSubTablePage.tsx index 58380d27..0769d445 100644 --- a/src/pages/Table/ProvidedSubTablePage.tsx +++ b/src/pages/Table/ProvidedSubTablePage.tsx @@ -7,7 +7,7 @@ import { useLocation, useNavigate, useParams } from "react-router-dom"; import { find, isEqual } from "lodash-es"; import Modal from "@src/components/Modal"; -import BreadcrumbsSubTable from "@src/components/Table/BreadcrumbsSubTable"; +import BreadcrumbsSubTable from "@src/components/Table/Breadcrumbs/BreadcrumbsSubTable"; import ErrorFallback from "@src/components/ErrorFallback"; import TableSourceFirestore from "@src/sources/TableSourceFirestore"; import TableToolbarSkeleton from "@src/components/TableToolbar/TableToolbarSkeleton"; From 86b2004215043652f2e8918996a01583f11b554e Mon Sep 17 00:00:00 2001 From: Sidney Alcantara Date: Fri, 18 Nov 2022 14:27:08 +1100 Subject: [PATCH 56/66] fix disabled cell styles --- .../Table/TableCell/EditorCellTextField.tsx | 3 ++- .../Table/TableCell/withRenderTableCell.tsx | 2 +- src/components/fields/Action/DisplayCell.tsx | 17 ++++++++++++++++- src/components/fields/Checkbox/DisplayCell.tsx | 13 ++++++++++++- src/components/fields/Code/DisplayCell.tsx | 2 +- src/components/fields/File/DisplayCell.tsx | 12 +++++++----- src/components/fields/File/EditorCell.tsx | 12 +++++++----- src/components/fields/Json/DisplayCell.tsx | 2 +- src/components/fields/LongText/DisplayCell.tsx | 2 +- src/components/fields/Status/DisplayCell.tsx | 1 + 10 files changed, 49 insertions(+), 17 deletions(-) diff --git a/src/components/Table/TableCell/EditorCellTextField.tsx b/src/components/Table/TableCell/EditorCellTextField.tsx index 3d89d8be..1f3cb155 100644 --- a/src/components/Table/TableCell/EditorCellTextField.tsx +++ b/src/components/Table/TableCell/EditorCellTextField.tsx @@ -55,6 +55,7 @@ export default function EditorCellTextField({ width: "100%", height: "calc(100% - 1px)", marginTop: "1px", + padding: 0, paddingBottom: "1px", backgroundColor: "var(--cell-background-color)", @@ -70,7 +71,7 @@ export default function EditorCellTextField({ lineHeight: (theme) => theme.typography.body2.lineHeight, maxHeight: "100%", boxSizing: "border-box", - py: 3 / 8, + py: 2 / 8, }, }, ...spreadSx(InputProps.sx), diff --git a/src/components/Table/TableCell/withRenderTableCell.tsx b/src/components/Table/TableCell/withRenderTableCell.tsx index 3a4d2d5f..a8f10f83 100644 --- a/src/components/Table/TableCell/withRenderTableCell.tsx +++ b/src/components/Table/TableCell/withRenderTableCell.tsx @@ -111,7 +111,7 @@ export default function withTableCell( row: row.original, column: column.columnDef.meta!, _rowy_ref: row.original._rowy_ref, - disabled: column.columnDef.meta!.editable === false, + disabled, tabIndex: focusInsideCell ? 0 : -1, showPopoverCell, setFocusInsideCell, diff --git a/src/components/fields/Action/DisplayCell.tsx b/src/components/fields/Action/DisplayCell.tsx index 78871401..264f55be 100644 --- a/src/components/fields/Action/DisplayCell.tsx +++ b/src/components/fields/Action/DisplayCell.tsx @@ -1,5 +1,6 @@ import { IDisplayCellProps } from "@src/components/fields/types"; import { get } from "lodash-es"; +import { sanitiseCallableName, isUrl } from "./utils"; export const getActionName = (column: IDisplayCellProps["column"]) => { const config = get(column, "config"); @@ -10,5 +11,19 @@ export const getActionName = (column: IDisplayCellProps["column"]) => { }; export default function Action({ value, column }: IDisplayCellProps) { - return <>{value ? value.status : getActionName(column)}; + const hasRan = value && ![null, undefined].includes(value.status); + + return ( +
+ {hasRan && isUrl(value.status) ? ( + + {value.status} + + ) : hasRan ? ( + value.status + ) : ( + sanitiseCallableName(getActionName(column)) + )} +
+ ); } diff --git a/src/components/fields/Checkbox/DisplayCell.tsx b/src/components/fields/Checkbox/DisplayCell.tsx index 67943586..65af2709 100644 --- a/src/components/fields/Checkbox/DisplayCell.tsx +++ b/src/components/fields/Checkbox/DisplayCell.tsx @@ -6,7 +6,16 @@ export default function Checkbox({ column, value }: IDisplayCellProps) { return ( + } label={column.name as string} labelPlacement="start" @@ -15,6 +24,8 @@ export default function Checkbox({ column, value }: IDisplayCellProps) { width: "100%", alignItems: "center", + cursor: "default", + "& .MuiFormControlLabel-label": { font: "inherit", letterSpacing: "inherit", diff --git a/src/components/fields/Code/DisplayCell.tsx b/src/components/fields/Code/DisplayCell.tsx index 215266f2..dc9b5bd1 100644 --- a/src/components/fields/Code/DisplayCell.tsx +++ b/src/components/fields/Code/DisplayCell.tsx @@ -12,7 +12,7 @@ export default function Code({ value }: IDisplayCellProps) { style={{ width: "100%", maxHeight: "100%", - padding: theme.spacing(1, 0), + padding: "3px 0", whiteSpace: "pre-wrap", lineHeight: theme.typography.body2.lineHeight, diff --git a/src/components/fields/File/DisplayCell.tsx b/src/components/fields/File/DisplayCell.tsx index 36d82d64..f92d21e6 100644 --- a/src/components/fields/File/DisplayCell.tsx +++ b/src/components/fields/File/DisplayCell.tsx @@ -26,11 +26,13 @@ export default function File_({ } label={file.name} - onClick={(e) => { - window.open(file.downloadURL); - e.stopPropagation(); - }} - style={{ width: "100%" }} + onClick={(e: any) => e.stopPropagation()} + component="a" + href={file.downloadURL} + target="_blank" + rel="noopener noreferrer" + clickable + style={{ width: "100%", cursor: "pointer" }} tabIndex={tabIndex} /> diff --git a/src/components/fields/File/EditorCell.tsx b/src/components/fields/File/EditorCell.tsx index c738f9d4..adcea801 100644 --- a/src/components/fields/File/EditorCell.tsx +++ b/src/components/fields/File/EditorCell.tsx @@ -117,10 +117,12 @@ export default function File_({ } label={file.name} - onClick={(e) => { - window.open(file.downloadURL); - e.stopPropagation(); - }} + onClick={(e: any) => e.stopPropagation()} + component="a" + href={file.downloadURL} + target="_blank" + rel="noopener noreferrer" + clickable onDelete={ disabled ? undefined @@ -134,7 +136,7 @@ export default function File_({ }) } tabIndex={tabIndex} - style={{ width: "100%" }} + style={{ width: "100%", cursor: "pointer" }} /> diff --git a/src/components/fields/Json/DisplayCell.tsx b/src/components/fields/Json/DisplayCell.tsx index 82a1eea1..bc97afa0 100644 --- a/src/components/fields/Json/DisplayCell.tsx +++ b/src/components/fields/Json/DisplayCell.tsx @@ -15,7 +15,7 @@ export default function Json({ value }: IDisplayCellProps) { style={{ width: "100%", maxHeight: "100%", - padding: theme.spacing(1, 0), + padding: "3px 0", whiteSpace: "pre-wrap", lineHeight: theme.typography.body2.lineHeight, diff --git a/src/components/fields/LongText/DisplayCell.tsx b/src/components/fields/LongText/DisplayCell.tsx index 1e8e0fc2..cc285625 100644 --- a/src/components/fields/LongText/DisplayCell.tsx +++ b/src/components/fields/LongText/DisplayCell.tsx @@ -9,7 +9,7 @@ export default function LongText({ value }: IDisplayCellProps) {
+ {value} {getLabel(value, sortedConditions)}
); From c6b10d337897c7c7b89797d8dbf52ddab8e02803 Mon Sep 17 00:00:00 2001 From: Sidney Alcantara Date: Fri, 18 Nov 2022 14:27:23 +1100 Subject: [PATCH 57/66] fix sub-table opening appearing to crash the page --- src/App.tsx | 17 ++++++++++++++++- src/pages/Table/ProvidedSubTablePage.tsx | 1 + 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/src/App.tsx b/src/App.tsx index 79162f2d..ea283a01 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -2,6 +2,7 @@ import { lazy, Suspense } from "react"; import { Routes, Route, Navigate } from "react-router-dom"; import { useAtom } from "jotai"; +import { Backdrop } from "@mui/material"; import Loading from "@src/components/Loading"; import ProjectSourceFirebase from "@src/sources/ProjectSourceFirebase"; import MembersSourceFirebase from "@src/sources/MembersSourceFirebase"; @@ -116,7 +117,21 @@ export default function App() { } /> } + element={ + + + + } + > + + + } /> diff --git a/src/pages/Table/ProvidedSubTablePage.tsx b/src/pages/Table/ProvidedSubTablePage.tsx index 0769d445..860748c2 100644 --- a/src/pages/Table/ProvidedSubTablePage.tsx +++ b/src/pages/Table/ProvidedSubTablePage.tsx @@ -110,6 +110,7 @@ export default function ProvidedSubTablePage() { disableBottomDivider: true, style: { "--dialog-spacing": 0, "--dialog-contents-spacing": 0 } as any, }} + BackdropProps={{ key: "sub-table-modal-backdrop" }} > Date: Fri, 18 Nov 2022 16:53:45 +1100 Subject: [PATCH 58/66] ColumnMenu: get user permissions as props instead of checking userRolesAtom --- src/components/ColumnMenu/ColumnMenu.tsx | 21 +++++++++++++++++---- src/pages/Table/TablePage.tsx | 16 +++++++++++----- 2 files changed, 28 insertions(+), 9 deletions(-) diff --git a/src/components/ColumnMenu/ColumnMenu.tsx b/src/components/ColumnMenu/ColumnMenu.tsx index 72b1b765..cf025217 100644 --- a/src/components/ColumnMenu/ColumnMenu.tsx +++ b/src/components/ColumnMenu/ColumnMenu.tsx @@ -33,7 +33,6 @@ import ColumnHeader from "@src/components/Table/Mock/Column"; import { projectScope, - userRolesAtom, userSettingsAtom, updateUserSettingsAtom, confirmDialogAtom, @@ -80,8 +79,17 @@ export interface IMenuModalProps { ) => void; } -export default function ColumnMenu() { - const [userRoles] = useAtom(userRolesAtom, projectScope); +export interface IColumnMenuProps { + canAddColumns: boolean; + canEditColumns: boolean; + canDeleteColumns: boolean; +} + +export default function ColumnMenu({ + canAddColumns, + canEditColumns, + canDeleteColumns, +}: IColumnMenuProps) { const [userSettings] = useAtom(userSettingsAtom, projectScope); const [updateUserSettings] = useAtom(updateUserSettingsAtom, projectScope); const confirm = useSetAtom(confirmDialogAtom, projectScope); @@ -387,6 +395,7 @@ export default function ColumnMenu() { openColumnModal({ type: "new", index: column.index - 1 }); handleClose(); }, + disabled: !canAddColumns, }, { label: "Insert to the right…", @@ -396,6 +405,7 @@ export default function ColumnMenu() { openColumnModal({ type: "new", index: column.index + 1 }); handleClose(); }, + disabled: !canAddColumns, }, { label: `Delete column${altPress ? "" : "…"}`, @@ -450,16 +460,19 @@ export default function ColumnMenu() { }); }, color: "error" as "error", + disabled: !canDeleteColumns, }, ]; let menuItems = [...localViewActions]; - if (userRoles.includes("ADMIN") || userRoles.includes("OPS")) { + if (canEditColumns) { menuItems.push.apply(menuItems, configActions); if (column.type === FieldType.derivative) { menuItems.push.apply(menuItems, derivativeActions); } + } + if (canAddColumns || canDeleteColumns) { menuItems.push.apply(menuItems, columnActions); } diff --git a/src/pages/Table/TablePage.tsx b/src/pages/Table/TablePage.tsx index 5959376e..3dce7445 100644 --- a/src/pages/Table/TablePage.tsx +++ b/src/pages/Table/TablePage.tsx @@ -67,10 +67,12 @@ export default function TablePage({ const [tableSchema] = useAtom(tableSchemaAtom, tableScope); const snackLogContext = useSnackLogContext(); - // Set permissions here so we can pass them to the Table component, which - // shouldn’t access projectScope at all, to separate concerns. - const canAddColumns = userRoles.includes("ADMIN"); - const canEditColumns = userRoles.includes("ADMIN"); + // Set permissions here so we can pass them to the `Table` component, which + // shouldn’t access `projectScope` at all, to separate concerns. + const canAddColumns = + userRoles.includes("ADMIN") || userRoles.includes("OPS"); + const canEditColumns = canAddColumns; + const canDeleteColumns = canAddColumns; const canEditCells = userRoles.includes("ADMIN") || (!tableSettings.readOnly && @@ -170,7 +172,11 @@ export default function TablePage({ {!disableModals && ( - + From edb70164cf6b1639f9db96da9c029cd0cb853c7f Mon Sep 17 00:00:00 2001 From: Sidney Alcantara Date: Fri, 18 Nov 2022 17:31:03 +1100 Subject: [PATCH 59/66] add code comments & explanations --- .../Table/ColumnHeader/ColumnHeader.tsx | 71 +++------ .../Table/ColumnHeader/ColumnHeaderSort.tsx | 4 + .../Table/Styled/StyledColumnHeader.tsx | 58 +++++++ src/components/Table/Styled/StyledDot.tsx | 22 +++ src/components/Table/Table.tsx | 33 +++- src/components/Table/TableBody.tsx | 21 ++- .../Table/TableCell/EditorCellController.tsx | 90 +++++++++++ src/components/Table/TableCell/TableCell.tsx | 58 ++++--- .../Table/TableCell/withRenderTableCell.tsx | 145 ++++++++---------- src/components/Table/TableHeader.tsx | 16 +- src/components/Table/useSaveColumnSizing.tsx | 3 +- src/components/fields/types.ts | 6 + src/pages/Table/ProvidedSubTablePage.tsx | 7 +- src/pages/Table/ProvidedTablePage.tsx | 7 + src/pages/Table/TablePage.tsx | 14 +- 15 files changed, 380 insertions(+), 175 deletions(-) create mode 100644 src/components/Table/Styled/StyledColumnHeader.tsx create mode 100644 src/components/Table/Styled/StyledDot.tsx create mode 100644 src/components/Table/TableCell/EditorCellController.tsx 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, From dd3f5553ab2ab9c6ea5b39937aa2f882089ee710 Mon Sep 17 00:00:00 2001 From: Sidney Alcantara Date: Fri, 18 Nov 2022 18:36:41 +1100 Subject: [PATCH 60/66] fix Rating cells not saving to db & add warning in comments --- src/components/Table/TableCell/withRenderTableCell.tsx | 8 ++++---- src/components/fields/Rating/EditorCell.tsx | 6 +++++- src/components/fields/types.ts | 2 +- 3 files changed, 10 insertions(+), 6 deletions(-) diff --git a/src/components/Table/TableCell/withRenderTableCell.tsx b/src/components/Table/TableCell/withRenderTableCell.tsx index 0d215b19..60935e07 100644 --- a/src/components/Table/TableCell/withRenderTableCell.tsx +++ b/src/components/Table/TableCell/withRenderTableCell.tsx @@ -67,12 +67,12 @@ export interface IRenderedTableCellProps * * - `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`. + * - ⚠️ If it’s displayed inline, you must call `onSubmit` to save the value + * to the database, because it never unmounts. + * - ✨ You can reuse your `SideDrawerField` as they take the same props. It + * should probably be displayed in a popover. * - ⚠️ Make sure to use the `tabIndex` prop for buttons, text fields, and * other interactive elements. * - {@link IEditorCellProps} diff --git a/src/components/fields/Rating/EditorCell.tsx b/src/components/fields/Rating/EditorCell.tsx index 4d3c0c23..89a48f2d 100644 --- a/src/components/fields/Rating/EditorCell.tsx +++ b/src/components/fields/Rating/EditorCell.tsx @@ -4,6 +4,7 @@ import DisplayCell from "./DisplayCell"; export default function Rating({ onChange, + onSubmit, tabIndex, ...props }: IEditorCellProps) { @@ -20,7 +21,10 @@ export default function Rating({ onChange(newValue)} + onChange={(_, newValue) => { + onChange(newValue); + onSubmit(); + }} ref={ref} /> ); diff --git a/src/components/fields/types.ts b/src/components/fields/types.ts index 50a05f8a..63374d81 100644 --- a/src/components/fields/types.ts +++ b/src/components/fields/types.ts @@ -1,5 +1,5 @@ import { FieldType } from "@src/constants/fields"; -import type { IRenderedTableCellProps } from "@src/components/Table/TableCell/withRenderTableCell"; +import { IRenderedTableCellProps } from "@src/components/Table/TableCell/withRenderTableCell"; import type { PopoverProps } from "@mui/material"; import type { ColumnConfig, From 763c0a20d0a2b191411ae2d5c0263dd2e7cd8d02 Mon Sep 17 00:00:00 2001 From: Sidney Alcantara Date: Mon, 21 Nov 2022 14:17:42 +1100 Subject: [PATCH 61/66] Rating Icon: remove console.log --- src/components/fields/Rating/Icon.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/src/components/fields/Rating/Icon.tsx b/src/components/fields/Rating/Icon.tsx index e65b8f3d..a34bdc24 100644 --- a/src/components/fields/Rating/Icon.tsx +++ b/src/components/fields/Rating/Icon.tsx @@ -20,7 +20,6 @@ const getStateIcon = (config: any) => { if (!get(config, "customIcons.enabled")) { return ; } - console.log(get(config, "customIcons.rating")); return get(config, "customIcons.rating") || ; }; const getStateOutline = (config: any) => { From 9829fdebc1938a65dabfe0a92b85ace7f2c85939 Mon Sep 17 00:00:00 2001 From: Sidney Alcantara Date: Mon, 21 Nov 2022 17:16:32 +1100 Subject: [PATCH 62/66] fix virtualization occasionally not detecting scroll --- src/components/Table/Table.tsx | 17 ++++++++++++++--- src/components/Table/TableBody.tsx | 6 ++++++ 2 files changed, 20 insertions(+), 3 deletions(-) diff --git a/src/components/Table/Table.tsx b/src/components/Table/Table.tsx index 2d412787..412e0535 100644 --- a/src/components/Table/Table.tsx +++ b/src/components/Table/Table.tsx @@ -1,4 +1,5 @@ import { useMemo, useRef, useState, useEffect, useCallback } from "react"; +import useStateRef from "react-usestateref"; import { useAtom, useSetAtom } from "jotai"; import { useThrottledCallback } from "use-debounce"; import { @@ -92,7 +93,11 @@ export default function Table({ const updateColumn = useSetAtom(updateColumnAtom, tableScope); - const containerRef = useRef(null); + // Store a **state** and reference to the container element + // so the state can re-render `TableBody`, preventing virtualization + // not detecting scroll if the container element was initially `null` + const [containerEl, setContainerEl, containerRef] = + useStateRef(null); const gridRef = useRef(null); // Get column defs from table schema @@ -205,11 +210,16 @@ export default function Table({ // for large screen heights useEffect(() => { fetchMoreOnBottomReached(containerRef.current); - }, [fetchMoreOnBottomReached, tablePage, tableNextPage.loading]); + }, [ + fetchMoreOnBottomReached, + tablePage, + tableNextPage.loading, + containerRef, + ]); return (
setContainerEl(el)} onScroll={(e) => fetchMoreOnBottomReached(e.target as HTMLDivElement)} style={{ overflow: "auto", width: "100%", height: "100%" }} > @@ -252,6 +262,7 @@ export default function Table({ emptyState ?? ) : ( ; /** Used in `useVirtualization` */ From 74cdae7195649509902602f00a61dfa4a1c87a54 Mon Sep 17 00:00:00 2001 From: Sidney Alcantara Date: Mon, 21 Nov 2022 18:13:33 +1100 Subject: [PATCH 63/66] fix frozen columns disappearing when scrolled right --- src/components/Table/TableBody.tsx | 29 ++++++-------------- src/components/Table/TableCell/TableCell.tsx | 8 ++++-- src/components/Table/useVirtualization.tsx | 18 +++++++++++- 3 files changed, 32 insertions(+), 23 deletions(-) diff --git a/src/components/Table/TableBody.tsx b/src/components/Table/TableBody.tsx index 118dbece..74efe194 100644 --- a/src/components/Table/TableBody.tsx +++ b/src/components/Table/TableBody.tsx @@ -17,11 +17,7 @@ import { import { getFieldProp } from "@src/components/fields"; import type { TableRow } from "@src/types/table"; import useVirtualization from "./useVirtualization"; -import { - TABLE_PADDING, - DEFAULT_ROW_HEIGHT, - OUT_OF_ORDER_MARGIN, -} from "./Table"; +import { DEFAULT_ROW_HEIGHT, OUT_OF_ORDER_MARGIN } from "./Table"; export interface ITableBodyProps { /** @@ -72,6 +68,8 @@ export const TableBody = memo(function TableBody({ paddingRight, } = useVirtualization(containerRef, leafColumns); + const rowHeight = tableSchema.rowHeight || DEFAULT_ROW_HEIGHT; + return (
{paddingTop > 0 && ( @@ -88,15 +86,13 @@ export const TableBody = memo(function TableBody({ role="row" aria-rowindex={row.index + 2} style={{ - height: "auto", + height: rowHeight, marginBottom: outOfOrder ? OUT_OF_ORDER_MARGIN : 0, + paddingLeft, + paddingRight, }} data-out-of-order={outOfOrder || undefined} > - {paddingLeft > 0 && ( -
- )} - {outOfOrder && } {virtualCols.map((virtualCell) => { @@ -120,25 +116,18 @@ export const TableBody = memo(function TableBody({ row={row} cell={cell} index={cellIndex} - left={ - cell.column.getIsPinned() - ? virtualCell.start - TABLE_PADDING - : undefined - } isSelectedCell={isSelectedCell} focusInsideCell={isSelectedCell && selectedCell?.focusInside} isReadOnlyCell={isReadOnlyCell} canEditCells={canEditCells} isLastFrozen={lastFrozen === cell.column.id} width={cell.column.getSize()} - rowHeight={tableSchema.rowHeight || DEFAULT_ROW_HEIGHT} + rowHeight={rowHeight} + left={virtualCell.start} + isPinned={cell.column.getIsPinned() === "left"} /> ); })} - - {paddingRight > 0 && ( -
- )} ); })} diff --git a/src/components/Table/TableCell/TableCell.tsx b/src/components/Table/TableCell/TableCell.tsx index 1842602b..4ea51433 100644 --- a/src/components/Table/TableCell/TableCell.tsx +++ b/src/components/Table/TableCell/TableCell.tsx @@ -17,6 +17,7 @@ import { selectedCellAtom, contextMenuTargetAtom, } from "@src/atoms/tableScope"; +import { TABLE_PADDING } from "@src/components/Table"; import type { TableRow } from "@src/types/table"; import type { IRenderedTableCellProps } from "./withRenderTableCell"; @@ -52,7 +53,8 @@ export interface ITableCellProps { * If provided, cell is pinned/frozen, and this value is used for * `position: sticky`. */ - left?: number; + left: number; + isPinned: boolean; } /** @@ -79,6 +81,7 @@ export const TableCell = memo(function TableCell({ isLastFrozen, width, left, + isPinned, }: ITableCellProps) { const setSelectedCell = useSetAtom(selectedCellAtom, tableScope); const setContextMenuTarget = useSetAtom(contextMenuTargetAtom, tableScope); @@ -152,7 +155,8 @@ export const TableCell = memo(function TableCell({ style={{ width, height: rowHeight, - left, + position: isPinned ? "sticky" : "absolute", + left: left - (isPinned ? TABLE_PADDING : 0), backgroundColor: cell.column.id === "_rowy_column_actions" ? "transparent" : undefined, borderBottomWidth: diff --git a/src/components/Table/useVirtualization.tsx b/src/components/Table/useVirtualization.tsx index b9b3ca55..90df0329 100644 --- a/src/components/Table/useVirtualization.tsx +++ b/src/components/Table/useVirtualization.tsx @@ -1,6 +1,7 @@ import { useEffect, useCallback } from "react"; import { useAtom } from "jotai"; -import { useVirtual } from "react-virtual"; +import { useVirtual, defaultRangeExtractor } from "react-virtual"; +import type { Range } from "react-virtual"; import { tableScope, @@ -69,6 +70,21 @@ export function useVirtualization( ), [leafColumns] ), + rangeExtractor: useCallback( + (range: Range) => { + const defaultRange = defaultRangeExtractor(range); + const frozenColumns = leafColumns + .filter((c) => c.getIsPinned()) + .map((c) => c.getPinnedIndex()); + + const combinedRange = Array.from( + new Set([...defaultRange, ...frozenColumns]) + ).sort((a, b) => a - b); + + return combinedRange; + }, + [leafColumns] + ), }); // Scroll to selected cell From e2bed9e4b0f1c8581edcd8c9867bb7f39a5efb89 Mon Sep 17 00:00:00 2001 From: Sidney Alcantara Date: Tue, 22 Nov 2022 14:59:02 +1100 Subject: [PATCH 64/66] temp fix vercel builds breaking on node 18 ``` The prop value with an expression type of JSXFragment could not be resolved. Please file an issue ( https://github.com/jsx-eslint/jsx-ast-utils/issues/new ) to get this fixed immediately. Error: error:0308010C:digital envelope routines::unsupported at String.replace () ``` --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 0ea80114..4b331667 100644 --- a/package.json +++ b/package.json @@ -92,7 +92,7 @@ "typedoc": "typedoc src/atoms/tableScope/index.ts src/atoms/globalScope/index.ts --out typedoc" }, "engines": { - "node": ">=16" + "node": "^16" }, "eslintConfig": { "plugins": [ From 9a7b225afae509a281f957e15a42c4aa75bd63c2 Mon Sep 17 00:00:00 2001 From: Sidney Alcantara Date: Tue, 22 Nov 2022 16:20:10 +1100 Subject: [PATCH 65/66] fix final column header appearing to be draggable --- src/components/Table/TableHeader.tsx | 45 ++++++++++++++++++---------- 1 file changed, 29 insertions(+), 16 deletions(-) diff --git a/src/components/Table/TableHeader.tsx b/src/components/Table/TableHeader.tsx index 43eb8833..ab813275 100644 --- a/src/components/Table/TableHeader.tsx +++ b/src/components/Table/TableHeader.tsx @@ -1,4 +1,4 @@ -import { memo } from "react"; +import { memo, Fragment } from "react"; import { useAtom } from "jotai"; import { DragDropContext, Droppable, Draggable } from "react-beautiful-dnd"; import type { DropResult } from "react-beautiful-dnd"; @@ -57,30 +57,36 @@ export const TableHeader = memo(function TableHeader({ {...provided.droppableProps} ref={provided.innerRef} > - {headerGroup.headers.map((header) => { + {headerGroup.headers.map((header, i) => { const isSelectedCell = (!selectedCell && header.index === 0) || (selectedCell?.path === "_rowy_header" && selectedCell?.columnKey === header.id); + const isLastHeader = i === headerGroup.headers.length - 1; + + // Render later, after the drag & drop placeholder if (header.id === "_rowy_column_actions") return ( - + + {provided.placeholder} + + ); if (!header.column.columnDef.meta) return null; - return ( + const draggableHeader = ( ); + + if (isLastHeader) + return ( + + {draggableHeader} + {provided.placeholder} + + ); + else return draggableHeader; })} - {/* Required by react-beautiful-dnd */} - {provided.placeholder} )} From 3115a9c917344dbf22e36522600e65ba890292fc Mon Sep 17 00:00:00 2001 From: Sidney Alcantara Date: Tue, 22 Nov 2022 16:24:46 +1100 Subject: [PATCH 66/66] fix table spacing when side drawer is opened --- src/pages/Table/TablePage.tsx | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/pages/Table/TablePage.tsx b/src/pages/Table/TablePage.tsx index 6008c835..75f1fc28 100644 --- a/src/pages/Table/TablePage.tsx +++ b/src/pages/Table/TablePage.tsx @@ -13,7 +13,7 @@ import TableSkeleton from "@src/components/Table/TableSkeleton"; import EmptyTable from "@src/components/Table/EmptyTable"; import TableToolbar from "@src/components/TableToolbar"; import Table from "@src/components/Table"; -import SideDrawer from "@src/components/SideDrawer"; +import SideDrawer, { DRAWER_WIDTH } from "@src/components/SideDrawer"; import ColumnMenu from "@src/components/ColumnMenu"; import ColumnModals from "@src/components/ColumnModals"; import TableModals from "@src/components/TableModals"; @@ -140,7 +140,9 @@ export default function TablePage({ '& [role="grid"]': { marginBottom: `env(safe-area-inset-bottom)`, marginLeft: `env(safe-area-inset-left)`, - marginRight: `env(safe-area-inset-right)`, + // Ensure there’s enough space so that all columns are + // still visible when the side drawer is open + marginRight: `max(env(safe-area-inset-right), ${DRAWER_WIDTH}px)`, }, }} >