Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import useKeyPressWithAtom from "@src/hooks/useKeyPressWithAtom";
import TableGroupRedirectPage from "./pages/TableGroupRedirectPage";
import SignOutPage from "@src/pages/Auth/SignOutPage";
import ProvidedArraySubTablePage from "./pages/Table/ProvidedArraySubTablePage";
import DirectTableTestPage from "./pages/Test/DirectTableTestPage";

// prettier-ignore
const AuthPage = lazy(() => import("@src/pages/Auth/AuthPage" /* webpackChunkName: "AuthPage" */));
Expand Down Expand Up @@ -82,6 +83,7 @@ export default function App() {
<Loading fullScreen message="Authenticating" />
) : (
<Routes>
<Route path="/test" element={<DirectTableTestPage />} />
<Route path="*" element={<NotFound />} />

<Route path={ROUTES.auth} element={<AuthPage />} />
Expand Down
7 changes: 7 additions & 0 deletions src/atoms/tableScope/ui.ts
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,13 @@ export type SelectedCell = {
/** Store selected cell in table. Used in side drawer and context menu */
export const selectedCellAtom = atom<SelectedCell | null>(null);

export type FillRange = {
startCell: SelectedCell;
endCell: SelectedCell;
};
/** Store fill range during drag-to-fill operation */
export const fillRangeAtom = atom<FillRange | null>(null);

/** Store context menu target atom for positioning. If not null, menu open. */
export const contextMenuTargetAtom = atom<HTMLElement | null>(null);

Expand Down
73 changes: 70 additions & 3 deletions src/components/Table/Table.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -32,10 +32,12 @@ import {
tableNextPageAtom,
tablePageAtom,
updateColumnAtom,
selectedCellAtom,
tableSortsAtom,
tableIdAtom,
serverDocCountAtom,
selectedCellAtom,
tableSortsAtom,
fillRangeAtom,
updateFieldAtom,
} from "@src/atoms/tableScope";
import { projectScope, userSettingsAtom } from "@src/atoms/projectScope";
import { getFieldType, getFieldProp } from "@src/components/fields";
Expand Down Expand Up @@ -63,7 +65,7 @@ const columnHelper = createColumnHelper<TableRow>();
const getRowId = (row: TableRow) => row._rowy_ref.path || row._rowy_ref.id;

export interface ITableProps {
/** Determines if Add column button is displayed */
/** Determines if Add column button is displayed */
canAddColumns: boolean;
/** Determines if columns can be rearranged */
canEditColumns: boolean;
Expand Down Expand Up @@ -282,6 +284,8 @@ export default function Table({
leafColumns,
});
const [selectedCell] = useAtom(selectedCellAtom, tableScope);
const [fillRange, setFillRange] = useAtom(fillRangeAtom, tableScope);
const updateField = useSetAtom(updateFieldAtom, tableScope);
const { handleCopy, handlePaste, handleCut } = useMenuAction(selectedCell);
const { handler: hotKeysHandler } = useHotKeys([
["mod+C", handleCopy],
Expand Down Expand Up @@ -335,6 +339,69 @@ export default function Table({
};
}, [handlePaste]);

useEffect(() => {
const handleMouseUp = async () => {
if (!fillRange) return;

const { startCell, endCell } = fillRange;
if (
startCell.path === endCell.path &&
startCell.columnKey === endCell.columnKey
) {
setFillRange(null);
return;
}

// Find start and end row indices
const startIndex = tableRows.findIndex(
(r) => r._rowy_ref.path === startCell.path
);
const endIndex = tableRows.findIndex(
(r) => r._rowy_ref.path === endCell.path
);

if (startIndex === -1 || endIndex === -1) {
setFillRange(null);
return;
}

const minRow = Math.min(startIndex, endIndex);
const maxRow = Math.max(startIndex, endIndex);

// Get source value
const sourceRow = tableRows[startIndex];
const columnConfig = tableSchema.columns?.[startCell.columnKey];
if (!columnConfig) {
setFillRange(null);
return;
}
const value = get(sourceRow, columnConfig.fieldName);

// Apply fill
const promises: Promise<any>[] = [];
for (let i = minRow; i <= maxRow; i++) {
if (i === startIndex) continue; // Skip source
const targetRow = tableRows[i];
promises.push(
updateField({
path: targetRow._rowy_ref.path,
fieldName: columnConfig.fieldName,
value,
arrayTableData: targetRow._rowy_ref.arrayTableData
? { index: targetRow._rowy_ref.arrayTableData.index }
: undefined,
})
);
}

await Promise.all(promises);
setFillRange(null);
};

window.addEventListener("mouseup", handleMouseUp);
return () => window.removeEventListener("mouseup", handleMouseUp);
}, [fillRange, tableRows, tableSchema, updateField, setFillRange]);

// apply user default sort on first render
const [applySort, setApplySort] = useState(true);
useEffect(() => {
Expand Down
53 changes: 51 additions & 2 deletions src/components/Table/TableCell/TableCell.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { memo } from "react";
import { useSetAtom } from "jotai";
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";
Expand All @@ -16,7 +16,9 @@ import {
tableScope,
selectedCellAtom,
contextMenuTargetAtom,
fillRangeAtom,
} from "@src/atoms/tableScope";
import { styled } from "@mui/material";
import { TABLE_PADDING } from "@src/components/Table";
import type { TableRow } from "@src/types/table";
import type { IRenderedTableCellProps } from "./withRenderTableCell";
Expand All @@ -33,7 +35,7 @@ export interface ITableCellProps {
/** 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
* 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;
Expand All @@ -57,6 +59,18 @@ export interface ITableCellProps {
isPinned: boolean;
}

const FillHandle = styled("div")(({ theme }) => ({
position: "absolute",
bottom: -5,
right: -5,
width: 9,
height: 9,
backgroundColor: theme.palette.primary.main,
border: "1px solid white",
cursor: "crosshair",
zIndex: 10,
}));

/**
* Renders the container div for each cell with accessibility attributes for
* keyboard navigation.
Expand Down Expand Up @@ -85,6 +99,7 @@ export const TableCell = memo(function TableCell({
}: ITableCellProps) {
const setSelectedCell = useSetAtom(selectedCellAtom, tableScope);
const setContextMenuTarget = useSetAtom(contextMenuTargetAtom, tableScope);
const [fillRange, setFillRange] = useAtom(fillRangeAtom, tableScope);

const value = cell.getValue();
const required = cell.column.columnDef.meta?.config?.required;
Expand Down Expand Up @@ -201,7 +216,41 @@ export const TableCell = memo(function TableCell({
setContextMenuTarget(e.target as HTMLElement);
}
}}
onMouseEnter={() => {
if (fillRange) {
setFillRange((prev) => ({
...prev!,
endCell: {
path: row.original._rowy_ref.path,
columnKey: cell.column.id,
focusInside: false,
arrayIndex: row.original._rowy_ref.arrayTableData?.index,
},
}));
}
}}
>
{isSelectedCell && !focusInsideCell && !isReadOnlyCell && canEditCells && (
<FillHandle
onMouseDown={(e) => {
e.stopPropagation();
setFillRange({
startCell: {
path: row.original._rowy_ref.path,
columnKey: cell.column.id,
focusInside: false,
arrayIndex: row.original._rowy_ref.arrayTableData?.index,
},
endCell: {
path: row.original._rowy_ref.path,
columnKey: cell.column.id,
focusInside: false,
arrayIndex: row.original._rowy_ref.arrayTableData?.index,
},
});
}}
/>
)}
{renderedValidationTooltip}
<ErrorBoundary fallbackRender={InlineErrorFallback}>
{flexRender(cell.column.columnDef.cell, tableCellComponentProps)}
Expand Down
63 changes: 63 additions & 0 deletions src/pages/Test/DirectTableTestPage.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import { Provider } from "jotai";
import {
tableScope,
tableIdAtom,
tableRowsAtom,
tableSchemaAtom,
tableColumnsOrderedAtom,
} from "@src/atoms/tableScope";
import Table from "@src/components/Table";

const mockRows: any[] = [
{ _rowy_ref: { id: "1", path: "test/1" }, name: "Alice", value: 10 },
{ _rowy_ref: { id: "2", path: "test/2" }, name: "Bob", value: 20 },
{ _rowy_ref: { id: "3", path: "test/3" }, name: "Charlie", value: 30 },
];

const mockColumns: any[] = [
{
key: "name",
id: "name",
fieldName: "name",
label: "Name",
type: "shortText",
index: 0,
width: 200,
config: {},
},
{
key: "value",
id: "value",
fieldName: "value",
label: "Value",
type: "number",
index: 1,
width: 200,
config: {},
},
];

const mockSchema = {
columns: {
name: mockColumns[0],
value: mockColumns[1],
},
};

export default function DirectTableTestPage() {
return (
<Provider
scope={tableScope}
initialValues={[
[tableIdAtom, "test"],
[tableRowsAtom, mockRows],
[tableSchemaAtom, mockSchema],
[tableColumnsOrderedAtom, mockColumns],
]}
>
<div style={{ height: "100vh" }}>
<Table />
</div>
</Provider>
);
}