mirror of
https://github.com/rowyio/rowy.git
synced 2025-12-29 00:16:39 +01:00
337 lines
11 KiB
TypeScript
337 lines
11 KiB
TypeScript
import React, { useMemo, useState, Suspense } from "react";
|
|
import { useAtom, useSetAtom } from "jotai";
|
|
import { useDebouncedCallback, useThrottledCallback } from "use-debounce";
|
|
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 { 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 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,
|
|
userRolesAtom,
|
|
userSettingsAtom,
|
|
} from "@src/atoms/projectScope";
|
|
import {
|
|
tableScope,
|
|
tableIdAtom,
|
|
tableSettingsAtom,
|
|
tableSchemaAtom,
|
|
tableColumnsOrderedAtom,
|
|
tableRowsAtom,
|
|
tableNextPageAtom,
|
|
tablePageAtom,
|
|
updateColumnAtom,
|
|
updateFieldAtom,
|
|
selectedCellAtom,
|
|
} from "@src/atoms/tableScope";
|
|
|
|
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";
|
|
|
|
export type DataGridColumn = ColumnConfig & Column<any> & { 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 };
|
|
|
|
export default function Table({
|
|
dataGridRef,
|
|
}: {
|
|
dataGridRef?: React.MutableRefObject<DataGridHandle | null>;
|
|
}) {
|
|
const [userRoles] = useAtom(userRolesAtom, projectScope);
|
|
const [userSettings] = useAtom(userSettingsAtom, projectScope);
|
|
|
|
const [tableId] = useAtom(tableIdAtom, tableScope);
|
|
const [tableSettings] = useAtom(tableSettingsAtom, tableScope);
|
|
const [tableSchema] = useAtom(tableSchemaAtom, tableScope);
|
|
const [tableColumnsOrdered] = useAtom(tableColumnsOrderedAtom, tableScope);
|
|
const [tableRows] = useAtom(tableRowsAtom, tableScope);
|
|
const [tableNextPage] = useAtom(tableNextPageAtom, tableScope);
|
|
const setTablePage = useSetAtom(tablePageAtom, tableScope);
|
|
const [selectedCell, setSelectedCell] = useAtom(selectedCellAtom, tableScope);
|
|
|
|
const updateColumn = useSetAtom(updateColumnAtom, tableScope);
|
|
const updateField = useSetAtom(updateFieldAtom, tableScope);
|
|
|
|
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
|
|
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 ?? 100,
|
|
}));
|
|
|
|
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,
|
|
});
|
|
}
|
|
|
|
return _columns;
|
|
}, [
|
|
tableColumnsOrdered,
|
|
userDocHiddenFields,
|
|
tableSettings.readOnly,
|
|
userRoles,
|
|
]);
|
|
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("."));
|
|
|
|
// 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<Set<React.Key>>();
|
|
// const [selectedRows, setSelectedRows] = useState<any[]>([]);
|
|
|
|
// 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<HTMLDivElement>) => {
|
|
// Select corresponding header cell when scrolled to prevent jumping
|
|
dataGridRef?.current?.selectCell({
|
|
idx: selectedColumnIndex || 0,
|
|
rowIdx: -1,
|
|
});
|
|
|
|
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
|
|
);
|
|
|
|
return (
|
|
<Suspense fallback={<Loading message="Loading fields" />}>
|
|
{/* <Hotkeys selectedCell={selectedCell} /> */}
|
|
<TableContainer rowHeight={rowHeight} className="table-container">
|
|
<DndProvider backend={HTML5Backend}>
|
|
{showLeftScrollDivider && <div className="left-scroll-divider" />}
|
|
|
|
<DataGrid
|
|
onColumnResize={handleResize}
|
|
onScroll={handleScroll}
|
|
ref={(handle) => {
|
|
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 });
|
|
}}
|
|
/>
|
|
</DndProvider>
|
|
|
|
{tableRows.length === 0 && (
|
|
<EmptyState
|
|
Icon={AddRowIcon}
|
|
message="Add a row to get started"
|
|
description={
|
|
<div>
|
|
<br />
|
|
<AddRow />
|
|
</div>
|
|
}
|
|
style={{
|
|
position: "absolute",
|
|
inset: 0,
|
|
top: COLUMN_HEADER_HEIGHT,
|
|
height: "auto",
|
|
}}
|
|
/>
|
|
)}
|
|
{tableNextPage.loading && <LinearProgress />}
|
|
</TableContainer>
|
|
|
|
<ContextMenu />
|
|
{/*
|
|
<BulkActions
|
|
selectedRows={selectedRows}
|
|
columns={columns}
|
|
clearSelection={() => {
|
|
setSelectedRowsSet(new Set());
|
|
setSelectedRows([]);
|
|
}}
|
|
/> */}
|
|
</Suspense>
|
|
);
|
|
}
|