mirror of
https://github.com/docmost/docmost.git
synced 2026-07-25 23:14:46 +10:00
fix(base): scope per-base UI atoms by pageId to prevent embed render loops
Two BaseTable instances on the same page (e.g. multiple base embeds in one document) shared the same global jotai atoms for activeViewId, editingCell, property menu state, and row selection. Each instance's useEffect that synced activeViewId would clobber the other's value every render, pinning React into a "Maximum update depth exceeded" loop. Convert every UI atom in base-atoms.ts to an atomFamily keyed by pageId so each base owns its own scope, and thread pageId through the grid component tree (GridRow, GridCell, GridHeaderCell, RowNumberCell, RowNumberHeaderCell, PropertyMenuContent) plus useRowSelection so each consumer reaches the per-base atom. use-base-socket already had pageId in scope; its store.get/store.set calls now resolve through selectedRowIdsAtomFamily(pageId) too.
This commit is contained in:
@@ -1,15 +1,38 @@
|
|||||||
import { atom } from "jotai";
|
import { atom } from "jotai";
|
||||||
|
import { atomFamily } from "jotai/utils";
|
||||||
import { EditingCell } from "@/features/base/types/base.types";
|
import { EditingCell } from "@/features/base/types/base.types";
|
||||||
|
|
||||||
export const activeViewIdAtom = atom<string | null>(null);
|
// Atoms are scoped per-base via `pageId` so that two BaseTable instances
|
||||||
|
// rendered on the same page (e.g. multiple base embeds inside one
|
||||||
|
// document) don't share UI state. A global atom would otherwise cause
|
||||||
|
// each instance's `useEffect` writers to clobber the other's value
|
||||||
|
// every render — pinning React into a "Maximum update depth exceeded"
|
||||||
|
// loop.
|
||||||
|
|
||||||
export const editingCellAtom = atom<EditingCell>(null);
|
export const activeViewIdAtomFamily = atomFamily((_pageId: string) =>
|
||||||
|
atom<string | null>(null),
|
||||||
|
);
|
||||||
|
|
||||||
export const activePropertyMenuAtom = atom<string | null>(null);
|
export const editingCellAtomFamily = atomFamily((_pageId: string) =>
|
||||||
|
atom<EditingCell>(null),
|
||||||
|
);
|
||||||
|
|
||||||
export const propertyMenuDirtyAtom = atom<boolean>(false);
|
export const activePropertyMenuAtomFamily = atomFamily((_pageId: string) =>
|
||||||
|
atom<string | null>(null),
|
||||||
|
);
|
||||||
|
|
||||||
export const propertyMenuCloseRequestAtom = atom<number>(0);
|
export const propertyMenuDirtyAtomFamily = atomFamily((_pageId: string) =>
|
||||||
|
atom<boolean>(false),
|
||||||
|
);
|
||||||
|
|
||||||
export const selectedRowIdsAtom = atom<Set<string>>(new Set<string>());
|
export const propertyMenuCloseRequestAtomFamily = atomFamily((_pageId: string) =>
|
||||||
export const lastToggledRowIndexAtom = atom<number | null>(null);
|
atom<number>(0),
|
||||||
|
);
|
||||||
|
|
||||||
|
export const selectedRowIdsAtomFamily = atomFamily((_pageId: string) =>
|
||||||
|
atom<Set<string>>(new Set<string>()),
|
||||||
|
);
|
||||||
|
|
||||||
|
export const lastToggledRowIndexAtomFamily = atomFamily((_pageId: string) =>
|
||||||
|
atom<number | null>(null),
|
||||||
|
);
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ import {
|
|||||||
useCreateViewMutation,
|
useCreateViewMutation,
|
||||||
useUpdateViewMutation,
|
useUpdateViewMutation,
|
||||||
} from "@/features/base/queries/base-view-query";
|
} from "@/features/base/queries/base-view-query";
|
||||||
import { activeViewIdAtom } from "@/features/base/atoms/base-atoms";
|
import { activeViewIdAtomFamily } from "@/features/base/atoms/base-atoms";
|
||||||
import { useBaseTable } from "@/features/base/hooks/use-base-table";
|
import { useBaseTable } from "@/features/base/hooks/use-base-table";
|
||||||
import { useRowSelection } from "@/features/base/hooks/use-row-selection";
|
import { useRowSelection } from "@/features/base/hooks/use-row-selection";
|
||||||
import useCurrentUser from "@/features/user/hooks/use-current-user";
|
import useCurrentUser from "@/features/user/hooks/use-current-user";
|
||||||
@@ -53,7 +53,7 @@ export function BaseTable({ pageId, embedded }: BaseTableProps) {
|
|||||||
useBaseSocket(pageId);
|
useBaseSocket(pageId);
|
||||||
const { data: base, isLoading: baseLoading, error: baseError } = useBaseQuery(pageId);
|
const { data: base, isLoading: baseLoading, error: baseError } = useBaseQuery(pageId);
|
||||||
|
|
||||||
const [activeViewId, setActiveViewId] = useAtom(activeViewIdAtom) as unknown as [string | null, (val: string | null) => void];
|
const [activeViewId, setActiveViewId] = useAtom(activeViewIdAtomFamily(pageId)) as unknown as [string | null, (val: string | null) => void];
|
||||||
|
|
||||||
const views = base?.views ?? [];
|
const views = base?.views ?? [];
|
||||||
const activeView = useMemo(() => {
|
const activeView = useMemo(() => {
|
||||||
@@ -146,7 +146,7 @@ export function BaseTable({ pageId, embedded }: BaseTableProps) {
|
|||||||
}
|
}
|
||||||
}, [activeView, activeViewId, setActiveViewId]);
|
}, [activeView, activeViewId, setActiveViewId]);
|
||||||
|
|
||||||
const { clear: clearSelection } = useRowSelection();
|
const { clear: clearSelection } = useRowSelection(pageId);
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
clearSelection();
|
clearSelection();
|
||||||
}, [pageId, activeView?.id, clearSelection]);
|
}, [pageId, activeView?.id, clearSelection]);
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { memo, useCallback } from "react";
|
|||||||
import { Cell } from "@tanstack/react-table";
|
import { Cell } from "@tanstack/react-table";
|
||||||
import { useAtom } from "jotai";
|
import { useAtom } from "jotai";
|
||||||
import { IBaseRow, IBaseProperty, EditingCell } from "@/features/base/types/base.types";
|
import { IBaseRow, IBaseProperty, EditingCell } from "@/features/base/types/base.types";
|
||||||
import { editingCellAtom } from "@/features/base/atoms/base-atoms";
|
import { editingCellAtomFamily } from "@/features/base/atoms/base-atoms";
|
||||||
import { isSystemPropertyType } from "@/features/base/hooks/use-base-table";
|
import { isSystemPropertyType } from "@/features/base/hooks/use-base-table";
|
||||||
import { CellText } from "@/features/base/components/cells/cell-text";
|
import { CellText } from "@/features/base/components/cells/cell-text";
|
||||||
import { CellNumber } from "@/features/base/components/cells/cell-number";
|
import { CellNumber } from "@/features/base/components/cells/cell-number";
|
||||||
@@ -65,6 +65,7 @@ type GridCellProps = {
|
|||||||
onCellUpdate: (rowId: string, propertyId: string, value: unknown) => void;
|
onCellUpdate: (rowId: string, propertyId: string, value: unknown) => void;
|
||||||
rowDragProps?: RowDragProps;
|
rowDragProps?: RowDragProps;
|
||||||
orderedRowIds?: string[];
|
orderedRowIds?: string[];
|
||||||
|
pageId: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const GridCell = memo(function GridCell({
|
export const GridCell = memo(function GridCell({
|
||||||
@@ -73,13 +74,14 @@ export const GridCell = memo(function GridCell({
|
|||||||
onCellUpdate,
|
onCellUpdate,
|
||||||
rowDragProps,
|
rowDragProps,
|
||||||
orderedRowIds,
|
orderedRowIds,
|
||||||
|
pageId,
|
||||||
}: GridCellProps) {
|
}: GridCellProps) {
|
||||||
const property = cell.column.columnDef.meta?.property;
|
const property = cell.column.columnDef.meta?.property;
|
||||||
const isRowNumber = cell.column.id === "__row_number";
|
const isRowNumber = cell.column.id === "__row_number";
|
||||||
const isPinned = cell.column.getIsPinned();
|
const isPinned = cell.column.getIsPinned();
|
||||||
const pinOffset = isPinned ? cell.column.getStart("left") : undefined;
|
const pinOffset = isPinned ? cell.column.getStart("left") : undefined;
|
||||||
|
|
||||||
const [editingCell, setEditingCell] = useAtom(editingCellAtom) as unknown as [EditingCell, (val: EditingCell) => void];
|
const [editingCell, setEditingCell] = useAtom(editingCellAtomFamily(pageId)) as unknown as [EditingCell, (val: EditingCell) => void];
|
||||||
|
|
||||||
const rowId = cell.row.id;
|
const rowId = cell.row.id;
|
||||||
const isEditing =
|
const isEditing =
|
||||||
@@ -121,6 +123,7 @@ export const GridCell = memo(function GridCell({
|
|||||||
isPinned={Boolean(isPinned)}
|
isPinned={Boolean(isPinned)}
|
||||||
pinOffset={pinOffset}
|
pinOffset={pinOffset}
|
||||||
rowDragProps={rowDragProps}
|
rowDragProps={rowDragProps}
|
||||||
|
pageId={pageId}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ import {
|
|||||||
} from "@dnd-kit/sortable";
|
} from "@dnd-kit/sortable";
|
||||||
import { restrictToHorizontalAxis } from "@dnd-kit/modifiers";
|
import { restrictToHorizontalAxis } from "@dnd-kit/modifiers";
|
||||||
import { IBaseRow, IBaseProperty, EditingCell } from "@/features/base/types/base.types";
|
import { IBaseRow, IBaseProperty, EditingCell } from "@/features/base/types/base.types";
|
||||||
import { editingCellAtom, activePropertyMenuAtom, propertyMenuDirtyAtom, propertyMenuCloseRequestAtom } from "@/features/base/atoms/base-atoms";
|
import { editingCellAtomFamily, activePropertyMenuAtomFamily, propertyMenuDirtyAtomFamily, propertyMenuCloseRequestAtomFamily } from "@/features/base/atoms/base-atoms";
|
||||||
import { useColumnResize } from "@/features/base/hooks/use-column-resize";
|
import { useColumnResize } from "@/features/base/hooks/use-column-resize";
|
||||||
import { useGridKeyboardNav } from "@/features/base/hooks/use-grid-keyboard-nav";
|
import { useGridKeyboardNav } from "@/features/base/hooks/use-grid-keyboard-nav";
|
||||||
import { useRowDrag } from "@/features/base/hooks/use-row-drag";
|
import { useRowDrag } from "@/features/base/hooks/use-row-drag";
|
||||||
@@ -53,7 +53,7 @@ type GridContainerProps = {
|
|||||||
properties: IBaseProperty[];
|
properties: IBaseProperty[];
|
||||||
onCellUpdate: (rowId: string, propertyId: string, value: unknown) => void;
|
onCellUpdate: (rowId: string, propertyId: string, value: unknown) => void;
|
||||||
onAddRow?: () => void;
|
onAddRow?: () => void;
|
||||||
pageId?: string;
|
pageId: string;
|
||||||
onColumnReorder?: (columnId: string, overColumnId: string) => void;
|
onColumnReorder?: (columnId: string, overColumnId: string) => void;
|
||||||
onResizeEnd?: () => void;
|
onResizeEnd?: () => void;
|
||||||
onRowReorder?: (rowId: string, targetRowId: string, position: "above" | "below") => void;
|
onRowReorder?: (rowId: string, targetRowId: string, position: "above" | "below") => void;
|
||||||
@@ -96,16 +96,16 @@ export function GridContainer({
|
|||||||
const lastTriggeredRowsLenRef = useRef(0);
|
const lastTriggeredRowsLenRef = useRef(0);
|
||||||
const rows = table.getRowModel().rows;
|
const rows = table.getRowModel().rows;
|
||||||
|
|
||||||
const [editingCell, setEditingCell] = useAtom(editingCellAtom) as unknown as [EditingCell, (val: EditingCell) => void];
|
const [editingCell, setEditingCell] = useAtom(editingCellAtomFamily(pageId)) as unknown as [EditingCell, (val: EditingCell) => void];
|
||||||
const [, setActivePropertyMenu] = useAtom(activePropertyMenuAtom) as unknown as [string | null, (val: string | null) => void];
|
const [, setActivePropertyMenu] = useAtom(activePropertyMenuAtomFamily(pageId)) as unknown as [string | null, (val: string | null) => void];
|
||||||
const [propertyMenuDirty] = useAtom(propertyMenuDirtyAtom) as unknown as [boolean];
|
const [propertyMenuDirty] = useAtom(propertyMenuDirtyAtomFamily(pageId)) as unknown as [boolean];
|
||||||
const [, setCloseRequest] = useAtom(propertyMenuCloseRequestAtom) as unknown as [number, (val: number) => void];
|
const [, setCloseRequest] = useAtom(propertyMenuCloseRequestAtomFamily(pageId)) as unknown as [number, (val: number) => void];
|
||||||
const propertyMenuDirtyRef = useRef(propertyMenuDirty);
|
const propertyMenuDirtyRef = useRef(propertyMenuDirty);
|
||||||
propertyMenuDirtyRef.current = propertyMenuDirty;
|
propertyMenuDirtyRef.current = propertyMenuDirty;
|
||||||
const closeRequestCounterRef = useRef(0);
|
const closeRequestCounterRef = useRef(0);
|
||||||
|
|
||||||
const { selectionCount, clear: clearSelection } = useRowSelection();
|
const { selectionCount, clear: clearSelection } = useRowSelection(pageId);
|
||||||
const { deleteSelected } = useDeleteSelectedRows(pageId ?? "");
|
const { deleteSelected } = useDeleteSelectedRows(pageId);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const handleMouseDown = (e: MouseEvent) => {
|
const handleMouseDown = (e: MouseEvent) => {
|
||||||
@@ -366,6 +366,7 @@ export function GridContainer({
|
|||||||
onCellUpdate={onCellUpdate}
|
onCellUpdate={onCellUpdate}
|
||||||
orderedRowIds={rowIds}
|
orderedRowIds={rowIds}
|
||||||
columnVisibility={table.getState().columnVisibility}
|
columnVisibility={table.getState().columnVisibility}
|
||||||
|
pageId={pageId}
|
||||||
dragHandlers={
|
dragHandlers={
|
||||||
onRowReorder
|
onRowReorder
|
||||||
? {
|
? {
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import { CSS } from "@dnd-kit/utilities";
|
|||||||
import { Popover } from "@mantine/core";
|
import { Popover } from "@mantine/core";
|
||||||
import { useAtom } from "jotai";
|
import { useAtom } from "jotai";
|
||||||
import { IBaseRow, IBaseProperty, EditingCell } from "@/features/base/types/base.types";
|
import { IBaseRow, IBaseProperty, EditingCell } from "@/features/base/types/base.types";
|
||||||
import { activePropertyMenuAtom, propertyMenuDirtyAtom, propertyMenuCloseRequestAtom, editingCellAtom } from "@/features/base/atoms/base-atoms";
|
import { activePropertyMenuAtomFamily, propertyMenuDirtyAtomFamily, propertyMenuCloseRequestAtomFamily, editingCellAtomFamily } from "@/features/base/atoms/base-atoms";
|
||||||
import {
|
import {
|
||||||
IconLetterT,
|
IconLetterT,
|
||||||
IconHash,
|
IconHash,
|
||||||
@@ -48,25 +48,27 @@ type GridHeaderCellProps = {
|
|||||||
header: Header<IBaseRow, unknown>;
|
header: Header<IBaseRow, unknown>;
|
||||||
property: IBaseProperty | undefined;
|
property: IBaseProperty | undefined;
|
||||||
loadedRowIds: string[];
|
loadedRowIds: string[];
|
||||||
|
pageId: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const GridHeaderCell = memo(function GridHeaderCell({
|
export const GridHeaderCell = memo(function GridHeaderCell({
|
||||||
header,
|
header,
|
||||||
property,
|
property,
|
||||||
loadedRowIds,
|
loadedRowIds,
|
||||||
|
pageId,
|
||||||
}: GridHeaderCellProps) {
|
}: GridHeaderCellProps) {
|
||||||
const isRowNumber = header.column.id === "__row_number";
|
const isRowNumber = header.column.id === "__row_number";
|
||||||
const isPinned = header.column.getIsPinned();
|
const isPinned = header.column.getIsPinned();
|
||||||
const pinOffset = isPinned ? header.column.getStart("left") : undefined;
|
const pinOffset = isPinned ? header.column.getStart("left") : undefined;
|
||||||
const { selectionCount } = useRowSelection();
|
const { selectionCount } = useRowSelection(pageId);
|
||||||
const hasSelection = selectionCount > 0;
|
const hasSelection = selectionCount > 0;
|
||||||
|
|
||||||
const [activePropertyMenu, setActivePropertyMenu] = useAtom(activePropertyMenuAtom) as unknown as [string | null, (val: string | null) => void];
|
const [activePropertyMenu, setActivePropertyMenu] = useAtom(activePropertyMenuAtomFamily(pageId)) as unknown as [string | null, (val: string | null) => void];
|
||||||
const menuOpened = activePropertyMenu === header.column.id;
|
const menuOpened = activePropertyMenu === header.column.id;
|
||||||
const cellRef = useRef<HTMLDivElement>(null);
|
const cellRef = useRef<HTMLDivElement>(null);
|
||||||
const [propertyMenuDirty, setPropertyMenuDirty] = useAtom(propertyMenuDirtyAtom) as unknown as [boolean, (val: boolean) => void];
|
const [propertyMenuDirty, setPropertyMenuDirty] = useAtom(propertyMenuDirtyAtomFamily(pageId)) as unknown as [boolean, (val: boolean) => void];
|
||||||
const [closeRequest, setCloseRequest] = useAtom(propertyMenuCloseRequestAtom) as unknown as [number, (val: number) => void];
|
const [closeRequest, setCloseRequest] = useAtom(propertyMenuCloseRequestAtomFamily(pageId)) as unknown as [number, (val: number) => void];
|
||||||
const [, setEditingCell] = useAtom(editingCellAtom) as unknown as [EditingCell, (val: EditingCell) => void];
|
const [, setEditingCell] = useAtom(editingCellAtomFamily(pageId)) as unknown as [EditingCell, (val: EditingCell) => void];
|
||||||
|
|
||||||
const handleDirtyChange = useCallback((dirty: boolean) => {
|
const handleDirtyChange = useCallback((dirty: boolean) => {
|
||||||
setPropertyMenuDirty(dirty);
|
setPropertyMenuDirty(dirty);
|
||||||
@@ -109,7 +111,7 @@ export const GridHeaderCell = memo(function GridHeaderCell({
|
|||||||
// Mantine's built-in `closeOnEscape` only fires when focus is inside the
|
// Mantine's built-in `closeOnEscape` only fires when focus is inside the
|
||||||
// dropdown, but opening the property menu (clicking the header) leaves
|
// dropdown, but opening the property menu (clicking the header) leaves
|
||||||
// focus on the header itself. Mirror the click-outside path: when dirty,
|
// focus on the header itself. Mirror the click-outside path: when dirty,
|
||||||
// bump `propertyMenuCloseRequestAtom` so property-menu shows its
|
// bump `propertyMenuCloseRequestAtomFamily` so property-menu shows its
|
||||||
// "Unsaved changes" confirmation panel; otherwise close directly.
|
// "Unsaved changes" confirmation panel; otherwise close directly.
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!menuOpened) return;
|
if (!menuOpened) return;
|
||||||
@@ -156,7 +158,7 @@ export const GridHeaderCell = memo(function GridHeaderCell({
|
|||||||
{...(isSortableDisabled ? {} : listeners)}
|
{...(isSortableDisabled ? {} : listeners)}
|
||||||
>
|
>
|
||||||
{isRowNumber ? (
|
{isRowNumber ? (
|
||||||
<RowNumberHeaderCell loadedRowIds={loadedRowIds} />
|
<RowNumberHeaderCell loadedRowIds={loadedRowIds} pageId={pageId} />
|
||||||
) : (
|
) : (
|
||||||
<div className={classes.headerCellContent}>
|
<div className={classes.headerCellContent}>
|
||||||
{TypeIcon && (
|
{TypeIcon && (
|
||||||
@@ -207,6 +209,7 @@ export const GridHeaderCell = memo(function GridHeaderCell({
|
|||||||
opened={menuOpened}
|
opened={menuOpened}
|
||||||
onClose={handleMenuClose}
|
onClose={handleMenuClose}
|
||||||
onDirtyChange={handleDirtyChange}
|
onDirtyChange={handleDirtyChange}
|
||||||
|
pageId={pageId}
|
||||||
/>
|
/>
|
||||||
</Popover.Dropdown>
|
</Popover.Dropdown>
|
||||||
</Popover>
|
</Popover>
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import classes from "@/features/base/styles/grid.module.css";
|
|||||||
|
|
||||||
type GridHeaderProps = {
|
type GridHeaderProps = {
|
||||||
table: Table<IBaseRow>;
|
table: Table<IBaseRow>;
|
||||||
pageId?: string;
|
pageId: string;
|
||||||
// Passed explicitly to break memo when columns change
|
// Passed explicitly to break memo when columns change
|
||||||
// (table ref is stable from useReactTable, so memo won't fire without these)
|
// (table ref is stable from useReactTable, so memo won't fire without these)
|
||||||
columnOrder: ColumnOrderState;
|
columnOrder: ColumnOrderState;
|
||||||
@@ -43,15 +43,14 @@ export const GridHeader = memo(function GridHeader({
|
|||||||
header={header}
|
header={header}
|
||||||
property={propertyById.get(header.column.id)}
|
property={propertyById.get(header.column.id)}
|
||||||
loadedRowIds={loadedRowIds}
|
loadedRowIds={loadedRowIds}
|
||||||
|
pageId={pageId}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
{pageId && (
|
<CreatePropertyPopover
|
||||||
<CreatePropertyPopover
|
pageId={pageId}
|
||||||
pageId={pageId}
|
properties={properties}
|
||||||
properties={properties}
|
onPropertyCreated={onPropertyCreated}
|
||||||
onPropertyCreated={onPropertyCreated}
|
/>
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ type GridRowProps = {
|
|||||||
dragHandlers?: RowDragHandlers;
|
dragHandlers?: RowDragHandlers;
|
||||||
orderedRowIds: string[];
|
orderedRowIds: string[];
|
||||||
columnVisibility: VisibilityState;
|
columnVisibility: VisibilityState;
|
||||||
|
pageId: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const GridRow = memo(function GridRow({
|
export const GridRow = memo(function GridRow({
|
||||||
@@ -32,8 +33,9 @@ export const GridRow = memo(function GridRow({
|
|||||||
orderedRowIds,
|
orderedRowIds,
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
columnVisibility: _columnVisibility,
|
columnVisibility: _columnVisibility,
|
||||||
|
pageId,
|
||||||
}: GridRowProps) {
|
}: GridRowProps) {
|
||||||
const isSelected = useRowSelection().isSelected(row.id);
|
const isSelected = useRowSelection(pageId).isSelected(row.id);
|
||||||
const handleDragStart = useCallback(
|
const handleDragStart = useCallback(
|
||||||
(e: React.DragEvent) => {
|
(e: React.DragEvent) => {
|
||||||
e.dataTransfer.effectAllowed = "move";
|
e.dataTransfer.effectAllowed = "move";
|
||||||
@@ -76,6 +78,7 @@ export const GridRow = memo(function GridRow({
|
|||||||
rowIndex={rowIndex}
|
rowIndex={rowIndex}
|
||||||
onCellUpdate={onCellUpdate}
|
onCellUpdate={onCellUpdate}
|
||||||
orderedRowIds={orderedRowIds}
|
orderedRowIds={orderedRowIds}
|
||||||
|
pageId={pageId}
|
||||||
rowDragProps={
|
rowDragProps={
|
||||||
isRowNumber && dragHandlers
|
isRowNumber && dragHandlers
|
||||||
? {
|
? {
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ type RowNumberCellProps = {
|
|||||||
isPinned: boolean;
|
isPinned: boolean;
|
||||||
pinOffset?: number;
|
pinOffset?: number;
|
||||||
rowDragProps?: RowDragProps;
|
rowDragProps?: RowDragProps;
|
||||||
|
pageId: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const RowNumberCell = memo(function RowNumberCell({
|
export const RowNumberCell = memo(function RowNumberCell({
|
||||||
@@ -25,8 +26,9 @@ export const RowNumberCell = memo(function RowNumberCell({
|
|||||||
isPinned,
|
isPinned,
|
||||||
pinOffset,
|
pinOffset,
|
||||||
rowDragProps,
|
rowDragProps,
|
||||||
|
pageId,
|
||||||
}: RowNumberCellProps) {
|
}: RowNumberCellProps) {
|
||||||
const { isSelected, toggle } = useRowSelection();
|
const { isSelected, toggle } = useRowSelection(pageId);
|
||||||
const selected = isSelected(rowId);
|
const selected = isSelected(rowId);
|
||||||
|
|
||||||
const handleCheckboxChange = useCallback(
|
const handleCheckboxChange = useCallback(
|
||||||
|
|||||||
@@ -5,12 +5,14 @@ import classes from "@/features/base/styles/grid.module.css";
|
|||||||
|
|
||||||
type RowNumberHeaderCellProps = {
|
type RowNumberHeaderCellProps = {
|
||||||
loadedRowIds: string[];
|
loadedRowIds: string[];
|
||||||
|
pageId: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const RowNumberHeaderCell = memo(function RowNumberHeaderCell({
|
export const RowNumberHeaderCell = memo(function RowNumberHeaderCell({
|
||||||
loadedRowIds,
|
loadedRowIds,
|
||||||
|
pageId,
|
||||||
}: RowNumberHeaderCellProps) {
|
}: RowNumberHeaderCellProps) {
|
||||||
const { selectedIds, toggleAll } = useRowSelection();
|
const { selectedIds, toggleAll } = useRowSelection(pageId);
|
||||||
|
|
||||||
const { checked, indeterminate } = useMemo(() => {
|
const { checked, indeterminate } = useMemo(() => {
|
||||||
if (loadedRowIds.length === 0) {
|
if (loadedRowIds.length === 0) {
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ export const SelectionActionBar = memo(function SelectionActionBar({
|
|||||||
pageId,
|
pageId,
|
||||||
}: SelectionActionBarProps) {
|
}: SelectionActionBarProps) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const { selectionCount, clear } = useRowSelection();
|
const { selectionCount, clear } = useRowSelection(pageId);
|
||||||
const { deleteSelected, isPending } = useDeleteSelectedRows(pageId);
|
const { deleteSelected, isPending } = useDeleteSelectedRows(pageId);
|
||||||
|
|
||||||
const isOpen = selectionCount > 0;
|
const isOpen = selectionCount > 0;
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ import {
|
|||||||
} from "@tabler/icons-react";
|
} from "@tabler/icons-react";
|
||||||
import { IBaseProperty } from "@/features/base/types/base.types";
|
import { IBaseProperty } from "@/features/base/types/base.types";
|
||||||
import { useAtom } from "jotai";
|
import { useAtom } from "jotai";
|
||||||
import { propertyMenuCloseRequestAtom } from "@/features/base/atoms/base-atoms";
|
import { propertyMenuCloseRequestAtomFamily } from "@/features/base/atoms/base-atoms";
|
||||||
import {
|
import {
|
||||||
useUpdatePropertyMutation,
|
useUpdatePropertyMutation,
|
||||||
useDeletePropertyMutation,
|
useDeletePropertyMutation,
|
||||||
@@ -34,6 +34,7 @@ type PropertyMenuContentProps = {
|
|||||||
opened: boolean;
|
opened: boolean;
|
||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
onDirtyChange?: (dirty: boolean) => void;
|
onDirtyChange?: (dirty: boolean) => void;
|
||||||
|
pageId: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
type MenuPanel = "main" | "rename" | "options" | "confirmDelete" | "confirmDiscard";
|
type MenuPanel = "main" | "rename" | "options" | "confirmDelete" | "confirmDiscard";
|
||||||
@@ -43,6 +44,7 @@ export function PropertyMenuContent({
|
|||||||
opened,
|
opened,
|
||||||
onClose,
|
onClose,
|
||||||
onDirtyChange,
|
onDirtyChange,
|
||||||
|
pageId,
|
||||||
}: PropertyMenuContentProps) {
|
}: PropertyMenuContentProps) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const [panel, setPanel] = useState<MenuPanel>("main");
|
const [panel, setPanel] = useState<MenuPanel>("main");
|
||||||
@@ -51,7 +53,7 @@ export function PropertyMenuContent({
|
|||||||
const [optionsDirty, setOptionsDirty] = useState(false);
|
const [optionsDirty, setOptionsDirty] = useState(false);
|
||||||
const pendingActionRef = useRef<"back" | "close" | null>(null);
|
const pendingActionRef = useRef<"back" | "close" | null>(null);
|
||||||
const sourcePanelRef = useRef<"rename" | "options" | null>(null);
|
const sourcePanelRef = useRef<"rename" | "options" | null>(null);
|
||||||
const [closeRequest] = useAtom(propertyMenuCloseRequestAtom) as unknown as [number];
|
const [closeRequest] = useAtom(propertyMenuCloseRequestAtomFamily(pageId)) as unknown as [number];
|
||||||
const closeRequestRef = useRef(closeRequest);
|
const closeRequestRef = useRef(closeRequest);
|
||||||
|
|
||||||
const renameDirty = renameValue !== property.name;
|
const renameDirty = renameValue !== property.name;
|
||||||
@@ -79,7 +81,7 @@ export function PropertyMenuContent({
|
|||||||
|
|
||||||
// Single dirty signal to the outside — reflects whichever panel is
|
// Single dirty signal to the outside — reflects whichever panel is
|
||||||
// currently accumulating unsaved work. Keeps rename and options in
|
// currently accumulating unsaved work. Keeps rename and options in
|
||||||
// lockstep with the `propertyMenuDirtyAtom` so the grid-container's
|
// lockstep with the `propertyMenuDirtyAtomFamily` so the grid-container's
|
||||||
// outside-click handler and the header's ESC handler both prompt
|
// outside-click handler and the header's ESC handler both prompt
|
||||||
// "Unsaved changes" consistently.
|
// "Unsaved changes" consistently.
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import {
|
|||||||
IBaseRow,
|
IBaseRow,
|
||||||
IBaseView,
|
IBaseView,
|
||||||
} from "@/features/base/types/base.types";
|
} from "@/features/base/types/base.types";
|
||||||
import { selectedRowIdsAtom } from "@/features/base/atoms/base-atoms";
|
import { selectedRowIdsAtomFamily } from "@/features/base/atoms/base-atoms";
|
||||||
import { formulaRecomputeAtom } from "@/features/base/atoms/formula-recompute-atom";
|
import { formulaRecomputeAtom } from "@/features/base/atoms/formula-recompute-atom";
|
||||||
import { IPagination } from "@/lib/types";
|
import { IPagination } from "@/lib/types";
|
||||||
|
|
||||||
@@ -211,11 +211,12 @@ export function useBaseSocket(pageId: string | undefined): void {
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
const store = getDefaultStore();
|
const store = getDefaultStore();
|
||||||
const current = store.get(selectedRowIdsAtom);
|
const selectedIdsAtom = selectedRowIdsAtomFamily(pageId);
|
||||||
|
const current = store.get(selectedIdsAtom);
|
||||||
if (current.has(e.rowId)) {
|
if (current.has(e.rowId)) {
|
||||||
const next = new Set(current);
|
const next = new Set(current);
|
||||||
next.delete(e.rowId);
|
next.delete(e.rowId);
|
||||||
store.set(selectedRowIdsAtom, next);
|
store.set(selectedIdsAtom, next);
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@@ -236,14 +237,15 @@ export function useBaseSocket(pageId: string | undefined): void {
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
const store = getDefaultStore();
|
const store = getDefaultStore();
|
||||||
const current = store.get(selectedRowIdsAtom);
|
const selectedIdsAtom = selectedRowIdsAtomFamily(pageId);
|
||||||
|
const current = store.get(selectedIdsAtom);
|
||||||
if (current.size > 0) {
|
if (current.size > 0) {
|
||||||
let changed = false;
|
let changed = false;
|
||||||
const next = new Set(current);
|
const next = new Set(current);
|
||||||
for (const id of e.rowIds) {
|
for (const id of e.rowIds) {
|
||||||
if (next.delete(id)) changed = true;
|
if (next.delete(id)) changed = true;
|
||||||
}
|
}
|
||||||
if (changed) store.set(selectedRowIdsAtom, next);
|
if (changed) store.set(selectedIdsAtom, next);
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ const BATCH_SIZE = 500;
|
|||||||
|
|
||||||
export function useDeleteSelectedRows(pageId: string) {
|
export function useDeleteSelectedRows(pageId: string) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const { selectedIds, clear } = useRowSelection();
|
const { selectedIds, clear } = useRowSelection(pageId);
|
||||||
const mutation = useDeleteRowsMutation();
|
const mutation = useDeleteRowsMutation();
|
||||||
|
|
||||||
const runDelete = useCallback(
|
const runDelete = useCallback(
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import { useCallback } from "react";
|
import { useCallback } from "react";
|
||||||
import { useAtom } from "jotai";
|
import { useAtom } from "jotai";
|
||||||
import {
|
import {
|
||||||
selectedRowIdsAtom,
|
selectedRowIdsAtomFamily,
|
||||||
lastToggledRowIndexAtom,
|
lastToggledRowIndexAtomFamily,
|
||||||
} from "@/features/base/atoms/base-atoms";
|
} from "@/features/base/atoms/base-atoms";
|
||||||
|
|
||||||
type ToggleOpts = {
|
type ToggleOpts = {
|
||||||
@@ -11,13 +11,15 @@ type ToggleOpts = {
|
|||||||
orderedRowIds: string[];
|
orderedRowIds: string[];
|
||||||
};
|
};
|
||||||
|
|
||||||
export function useRowSelection() {
|
export function useRowSelection(pageId: string) {
|
||||||
const [selectedIds, setSelectedIds] = useAtom(selectedRowIdsAtom) as unknown as [
|
const [selectedIds, setSelectedIds] = useAtom(
|
||||||
|
selectedRowIdsAtomFamily(pageId),
|
||||||
|
) as unknown as [
|
||||||
Set<string>,
|
Set<string>,
|
||||||
(val: Set<string> | ((prev: Set<string>) => Set<string>)) => void,
|
(val: Set<string> | ((prev: Set<string>) => Set<string>)) => void,
|
||||||
];
|
];
|
||||||
const [lastToggledIndex, setLastToggledIndex] = useAtom(
|
const [lastToggledIndex, setLastToggledIndex] = useAtom(
|
||||||
lastToggledRowIndexAtom,
|
lastToggledRowIndexAtomFamily(pageId),
|
||||||
) as unknown as [number | null, (val: number | null) => void];
|
) as unknown as [number | null, (val: number | null) => void];
|
||||||
|
|
||||||
const isSelected = useCallback(
|
const isSelected = useCallback(
|
||||||
|
|||||||
Reference in New Issue
Block a user