mirror of
https://github.com/docmost/docmost.git
synced 2026-07-24 07:22:49 +10:00
refactor(base): migrate column reorder from dnd-kit to pragmatic-drag-and-drop
This commit is contained in:
@@ -4,7 +4,7 @@ import { notifications } from "@mantine/notifications";
|
|||||||
import { useAtom } from "jotai";
|
import { useAtom } from "jotai";
|
||||||
import { IconDatabase } from "@tabler/icons-react";
|
import { IconDatabase } from "@tabler/icons-react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { arrayMove } from "@dnd-kit/sortable";
|
import { reorder } from "@atlaskit/pragmatic-drag-and-drop/reorder";
|
||||||
import { generateJitteredKeyBetween } from "fractional-indexing-jittered";
|
import { generateJitteredKeyBetween } from "fractional-indexing-jittered";
|
||||||
import { useBaseQuery } from "@/features/base/queries/base-query";
|
import { useBaseQuery } from "@/features/base/queries/base-query";
|
||||||
import { useBaseSocket } from "@/features/base/hooks/use-base-socket";
|
import { useBaseSocket } from "@/features/base/hooks/use-base-socket";
|
||||||
@@ -207,14 +207,11 @@ export function BaseTable({ pageId, embedded }: BaseTableProps) {
|
|||||||
}, [pageId, createViewMutation, t]);
|
}, [pageId, createViewMutation, t]);
|
||||||
|
|
||||||
const handleColumnReorder = useCallback(
|
const handleColumnReorder = useCallback(
|
||||||
(activeId: string, overId: string) => {
|
(columnId: string, finishIndex: number) => {
|
||||||
const currentOrder = table.getState().columnOrder;
|
const order = table.getState().columnOrder;
|
||||||
const oldIndex = currentOrder.indexOf(activeId);
|
const startIndex = order.indexOf(columnId);
|
||||||
const newIndex = currentOrder.indexOf(overId);
|
if (startIndex === -1 || startIndex === finishIndex) return;
|
||||||
if (oldIndex === -1 || newIndex === -1) return;
|
table.setColumnOrder(reorder({ list: order, startIndex, finishIndex }));
|
||||||
|
|
||||||
const newOrder = arrayMove(currentOrder, oldIndex, newIndex);
|
|
||||||
table.setColumnOrder(newOrder);
|
|
||||||
persistViewConfig();
|
persistViewConfig();
|
||||||
},
|
},
|
||||||
[table, persistViewConfig],
|
[table, persistViewConfig],
|
||||||
|
|||||||
@@ -7,20 +7,6 @@ import {
|
|||||||
windowScroll,
|
windowScroll,
|
||||||
} from "@tanstack/react-virtual";
|
} from "@tanstack/react-virtual";
|
||||||
import { useAtom } from "jotai";
|
import { useAtom } from "jotai";
|
||||||
import {
|
|
||||||
DndContext,
|
|
||||||
closestCenter,
|
|
||||||
KeyboardSensor,
|
|
||||||
PointerSensor,
|
|
||||||
useSensor,
|
|
||||||
useSensors,
|
|
||||||
DragEndEvent,
|
|
||||||
} from "@dnd-kit/core";
|
|
||||||
import {
|
|
||||||
SortableContext,
|
|
||||||
horizontalListSortingStrategy,
|
|
||||||
} from "@dnd-kit/sortable";
|
|
||||||
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 { editingCellAtomFamily, activePropertyMenuAtomFamily, propertyMenuDirtyAtomFamily, propertyMenuCloseRequestAtomFamily } 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";
|
||||||
@@ -54,7 +40,7 @@ type GridContainerProps = {
|
|||||||
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, finishIndex: number) => void;
|
||||||
onResizeEnd?: () => void;
|
onResizeEnd?: () => void;
|
||||||
onRowReorder?: (rowId: string, targetRowId: string, position: "above" | "below") => void;
|
onRowReorder?: (rowId: string, targetRowId: string, position: "above" | "below") => void;
|
||||||
hasNextPage?: boolean;
|
hasNextPage?: boolean;
|
||||||
@@ -314,63 +300,34 @@ export function GridContainer({
|
|||||||
});
|
});
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const sensors = useSensors(
|
const getColumnOrder = useCallback(
|
||||||
useSensor(PointerSensor, {
|
() => table.getState().columnOrder,
|
||||||
activationConstraint: { distance: 8 },
|
[table],
|
||||||
}),
|
|
||||||
useSensor(KeyboardSensor),
|
|
||||||
);
|
);
|
||||||
|
|
||||||
const sortableColumnIds = useMemo(() => {
|
|
||||||
return table
|
|
||||||
.getVisibleLeafColumns()
|
|
||||||
.filter((col) => col.id !== "__row_number")
|
|
||||||
.map((col) => col.id);
|
|
||||||
}, [table, table.getState().columnOrder]);
|
|
||||||
|
|
||||||
const handleDragEnd = useCallback(
|
|
||||||
(event: DragEndEvent) => {
|
|
||||||
const { active, over } = event;
|
|
||||||
if (!over || active.id === over.id) return;
|
|
||||||
onColumnReorder?.(active.id as string, over.id as string);
|
|
||||||
},
|
|
||||||
[onColumnReorder],
|
|
||||||
);
|
|
||||||
|
|
||||||
const modifiers = useMemo(() => [restrictToHorizontalAxis], []);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<DndContext
|
<div role="grid">
|
||||||
sensors={sensors}
|
<div className={classes.stickyBand}>
|
||||||
collisionDetection={closestCenter}
|
{stickyBandPrelude}
|
||||||
onDragEnd={handleDragEnd}
|
<div
|
||||||
modifiers={modifiers}
|
className={classes.headerGrid}
|
||||||
>
|
ref={headerRef}
|
||||||
<div role="grid">
|
style={{ gridTemplateColumns }}
|
||||||
<div className={classes.stickyBand}>
|
role="row"
|
||||||
{stickyBandPrelude}
|
>
|
||||||
<div
|
<GridHeader
|
||||||
className={classes.headerGrid}
|
table={table}
|
||||||
ref={headerRef}
|
pageId={pageId}
|
||||||
style={{ gridTemplateColumns }}
|
columnOrder={table.getState().columnOrder}
|
||||||
role="row"
|
columnVisibility={table.getState().columnVisibility}
|
||||||
>
|
properties={properties}
|
||||||
<SortableContext
|
loadedRowIds={rowIds}
|
||||||
items={sortableColumnIds}
|
onPropertyCreated={handlePropertyCreated}
|
||||||
strategy={horizontalListSortingStrategy}
|
getColumnOrder={getColumnOrder}
|
||||||
>
|
onColumnReorder={onColumnReorder}
|
||||||
<GridHeader
|
/>
|
||||||
table={table}
|
|
||||||
pageId={pageId}
|
|
||||||
columnOrder={table.getState().columnOrder}
|
|
||||||
columnVisibility={table.getState().columnVisibility}
|
|
||||||
properties={properties}
|
|
||||||
loadedRowIds={rowIds}
|
|
||||||
onPropertyCreated={handlePropertyCreated}
|
|
||||||
/>
|
|
||||||
</SortableContext>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
<div
|
<div
|
||||||
className={classes.bodyGrid}
|
className={classes.bodyGrid}
|
||||||
ref={bodyRef}
|
ref={bodyRef}
|
||||||
@@ -415,7 +372,6 @@ export function GridContainer({
|
|||||||
<AddRowButton onClick={handleAddRow} />
|
<AddRowButton onClick={handleAddRow} />
|
||||||
{pageId && <SelectionActionBar pageId={pageId} />}
|
{pageId && <SelectionActionBar pageId={pageId} />}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</DndContext>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,11 +1,27 @@
|
|||||||
import { memo, useCallback, useEffect, useRef } from "react";
|
import { memo, useCallback, useEffect, useRef, useState } from "react";
|
||||||
import { Header, flexRender } from "@tanstack/react-table";
|
import { Header, flexRender } from "@tanstack/react-table";
|
||||||
import { useSortable } from "@dnd-kit/sortable";
|
|
||||||
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 { combine } from "@atlaskit/pragmatic-drag-and-drop/combine";
|
||||||
|
import {
|
||||||
|
draggable,
|
||||||
|
dropTargetForElements,
|
||||||
|
} from "@atlaskit/pragmatic-drag-and-drop/element/adapter";
|
||||||
|
import {
|
||||||
|
attachClosestEdge,
|
||||||
|
extractClosestEdge,
|
||||||
|
type Edge,
|
||||||
|
} from "@atlaskit/pragmatic-drag-and-drop-hitbox/closest-edge";
|
||||||
|
import { getReorderDestinationIndex } from "@atlaskit/pragmatic-drag-and-drop-hitbox/util/get-reorder-destination-index";
|
||||||
|
import { triggerPostMoveFlash } from "@atlaskit/pragmatic-drag-and-drop-flourish/trigger-post-move-flash";
|
||||||
|
import * as liveRegion from "@atlaskit/pragmatic-drag-and-drop-live-region";
|
||||||
import { IBaseRow, IBaseProperty, EditingCell } from "@/features/base/types/base.types";
|
import { IBaseRow, IBaseProperty, EditingCell } from "@/features/base/types/base.types";
|
||||||
import { activePropertyMenuAtomFamily, propertyMenuDirtyAtomFamily, propertyMenuCloseRequestAtomFamily, editingCellAtomFamily } from "@/features/base/atoms/base-atoms";
|
import {
|
||||||
|
activePropertyMenuAtomFamily,
|
||||||
|
propertyMenuDirtyAtomFamily,
|
||||||
|
propertyMenuCloseRequestAtomFamily,
|
||||||
|
editingCellAtomFamily,
|
||||||
|
} from "@/features/base/atoms/base-atoms";
|
||||||
import {
|
import {
|
||||||
IconLetterT,
|
IconLetterT,
|
||||||
IconHash,
|
IconHash,
|
||||||
@@ -24,9 +40,12 @@ import {
|
|||||||
} from "@tabler/icons-react";
|
} from "@tabler/icons-react";
|
||||||
import { PropertyMenuContent } from "@/features/base/components/property/property-menu";
|
import { PropertyMenuContent } from "@/features/base/components/property/property-menu";
|
||||||
import { RowNumberHeaderCell } from "./row-number-header-cell";
|
import { RowNumberHeaderCell } from "./row-number-header-cell";
|
||||||
|
import { BaseDropEdgeIndicator } from "./base-drop-edge-indicator";
|
||||||
import { useRowSelection } from "@/features/base/hooks/use-row-selection";
|
import { useRowSelection } from "@/features/base/hooks/use-row-selection";
|
||||||
import classes from "@/features/base/styles/grid.module.css";
|
import classes from "@/features/base/styles/grid.module.css";
|
||||||
|
|
||||||
|
const COLUMN_DRAG_TYPE = "base-column";
|
||||||
|
|
||||||
const typeIcons: Record<string, typeof IconLetterT> = {
|
const typeIcons: Record<string, typeof IconLetterT> = {
|
||||||
text: IconLetterT,
|
text: IconLetterT,
|
||||||
number: IconHash,
|
number: IconHash,
|
||||||
@@ -49,6 +68,8 @@ type GridHeaderCellProps = {
|
|||||||
property: IBaseProperty | undefined;
|
property: IBaseProperty | undefined;
|
||||||
loadedRowIds: string[];
|
loadedRowIds: string[];
|
||||||
pageId: string;
|
pageId: string;
|
||||||
|
getColumnOrder: () => string[];
|
||||||
|
onColumnReorder?: (columnId: string, finishIndex: number) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const GridHeaderCell = memo(function GridHeaderCell({
|
export const GridHeaderCell = memo(function GridHeaderCell({
|
||||||
@@ -56,6 +77,8 @@ export const GridHeaderCell = memo(function GridHeaderCell({
|
|||||||
property,
|
property,
|
||||||
loadedRowIds,
|
loadedRowIds,
|
||||||
pageId,
|
pageId,
|
||||||
|
getColumnOrder,
|
||||||
|
onColumnReorder,
|
||||||
}: 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();
|
||||||
@@ -70,31 +93,62 @@ export const GridHeaderCell = memo(function GridHeaderCell({
|
|||||||
const [closeRequest, setCloseRequest] = useAtom(propertyMenuCloseRequestAtomFamily(pageId)) as unknown as [number, (val: number) => void];
|
const [closeRequest, setCloseRequest] = useAtom(propertyMenuCloseRequestAtomFamily(pageId)) as unknown as [number, (val: number) => void];
|
||||||
const [, setEditingCell] = useAtom(editingCellAtomFamily(pageId)) as unknown as [EditingCell, (val: EditingCell) => void];
|
const [, setEditingCell] = useAtom(editingCellAtomFamily(pageId)) as unknown as [EditingCell, (val: EditingCell) => void];
|
||||||
|
|
||||||
|
const [isDragging, setIsDragging] = useState(false);
|
||||||
|
const [closestEdge, setClosestEdge] = useState<Edge | null>(null);
|
||||||
|
|
||||||
const handleDirtyChange = useCallback((dirty: boolean) => {
|
const handleDirtyChange = useCallback((dirty: boolean) => {
|
||||||
setPropertyMenuDirty(dirty);
|
setPropertyMenuDirty(dirty);
|
||||||
}, [setPropertyMenuDirty]);
|
}, [setPropertyMenuDirty]);
|
||||||
|
|
||||||
const isSortableDisabled = isRowNumber || isPinned === "left";
|
const isSortableDisabled = isRowNumber || isPinned === "left";
|
||||||
|
|
||||||
const {
|
useEffect(() => {
|
||||||
attributes,
|
const el = cellRef.current;
|
||||||
listeners,
|
if (!el || isSortableDisabled) return;
|
||||||
setNodeRef,
|
return combine(
|
||||||
transform,
|
draggable({
|
||||||
transition,
|
element: el,
|
||||||
isDragging,
|
getInitialData: () => ({
|
||||||
} = useSortable({
|
type: COLUMN_DRAG_TYPE,
|
||||||
id: header.column.id,
|
columnId: header.column.id,
|
||||||
disabled: isSortableDisabled,
|
}),
|
||||||
});
|
onDragStart: () => setIsDragging(true),
|
||||||
|
onDrop: () => setIsDragging(false),
|
||||||
const combinedRef = useCallback(
|
}),
|
||||||
(node: HTMLDivElement | null) => {
|
dropTargetForElements({
|
||||||
setNodeRef(node);
|
element: el,
|
||||||
(cellRef as React.MutableRefObject<HTMLDivElement | null>).current = node;
|
canDrop: ({ source }) =>
|
||||||
},
|
source.data.type === COLUMN_DRAG_TYPE &&
|
||||||
[setNodeRef],
|
source.data.columnId !== header.column.id,
|
||||||
);
|
getData: ({ input, element }) =>
|
||||||
|
attachClosestEdge(
|
||||||
|
{ columnId: header.column.id },
|
||||||
|
{ input, element, allowedEdges: ["left", "right"] },
|
||||||
|
),
|
||||||
|
onDrag: ({ self }) => setClosestEdge(extractClosestEdge(self.data)),
|
||||||
|
onDragLeave: () => setClosestEdge(null),
|
||||||
|
onDrop: ({ source, self }) => {
|
||||||
|
setClosestEdge(null);
|
||||||
|
const edge = extractClosestEdge(self.data);
|
||||||
|
if (!edge) return;
|
||||||
|
const order = getColumnOrder();
|
||||||
|
const startIndex = order.indexOf(source.data.columnId as string);
|
||||||
|
const indexOfTarget = order.indexOf(header.column.id);
|
||||||
|
if (startIndex === -1 || indexOfTarget === -1) return;
|
||||||
|
const finishIndex = getReorderDestinationIndex({
|
||||||
|
startIndex,
|
||||||
|
indexOfTarget,
|
||||||
|
closestEdgeOfTarget: edge,
|
||||||
|
axis: "horizontal",
|
||||||
|
});
|
||||||
|
if (finishIndex === startIndex) return;
|
||||||
|
onColumnReorder?.(source.data.columnId as string, finishIndex);
|
||||||
|
triggerPostMoveFlash(el);
|
||||||
|
liveRegion.announce(`Moved column to position ${finishIndex + 1}`);
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}, [header.column.id, isSortableDisabled, onColumnReorder, getColumnOrder]);
|
||||||
|
|
||||||
const handleHeaderClick = useCallback(() => {
|
const handleHeaderClick = useCallback(() => {
|
||||||
setEditingCell(null);
|
setEditingCell(null);
|
||||||
@@ -108,11 +162,6 @@ export const GridHeaderCell = memo(function GridHeaderCell({
|
|||||||
setActivePropertyMenu(null);
|
setActivePropertyMenu(null);
|
||||||
}, [setActivePropertyMenu]);
|
}, [setActivePropertyMenu]);
|
||||||
|
|
||||||
// Mantine's built-in `closeOnEscape` only fires when focus is inside the
|
|
||||||
// dropdown, but opening the property menu (clicking the header) leaves
|
|
||||||
// focus on the header itself. Mirror the click-outside path: when dirty,
|
|
||||||
// bump `propertyMenuCloseRequestAtomFamily` so property-menu shows its
|
|
||||||
// "Unsaved changes" confirmation panel; otherwise close directly.
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!menuOpened) return;
|
if (!menuOpened) return;
|
||||||
const handler = (e: KeyboardEvent) => {
|
const handler = (e: KeyboardEvent) => {
|
||||||
@@ -129,33 +178,19 @@ export const GridHeaderCell = memo(function GridHeaderCell({
|
|||||||
|
|
||||||
const TypeIcon = property ? typeIcons[property.type] : undefined;
|
const TypeIcon = property ? typeIcons[property.type] : undefined;
|
||||||
|
|
||||||
const sortableStyle = transform
|
|
||||||
? {
|
|
||||||
transform: CSS.Transform.toString({
|
|
||||||
...transform,
|
|
||||||
scaleX: 1,
|
|
||||||
scaleY: 1,
|
|
||||||
}),
|
|
||||||
transition,
|
|
||||||
opacity: isDragging ? 0.5 : 1,
|
|
||||||
zIndex: isDragging ? 10 : undefined,
|
|
||||||
}
|
|
||||||
: {};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
ref={combinedRef}
|
ref={cellRef}
|
||||||
className={`${classes.headerCell} ${isPinned ? classes.headerCellPinned : ""} ${hasSelection ? classes.hasSelection : ""}`}
|
className={`${classes.headerCell} ${isPinned ? classes.headerCellPinned : ""} ${hasSelection ? classes.hasSelection : ""}`}
|
||||||
style={{
|
style={{
|
||||||
...(isPinned
|
...(isPinned
|
||||||
? ({ "--pin-offset": `${pinOffset}px` } as React.CSSProperties)
|
? ({ "--pin-offset": `${pinOffset}px` } as React.CSSProperties)
|
||||||
: {}),
|
: {}),
|
||||||
...(isRowNumber ? {} : { cursor: "pointer" }),
|
...(isRowNumber ? {} : { cursor: "pointer" }),
|
||||||
...sortableStyle,
|
opacity: isDragging ? 0.4 : 1,
|
||||||
}}
|
}}
|
||||||
onClick={handleHeaderClick}
|
onClick={handleHeaderClick}
|
||||||
{...(isSortableDisabled ? {} : attributes)}
|
data-dragging={isDragging || undefined}
|
||||||
{...(isSortableDisabled ? {} : listeners)}
|
|
||||||
>
|
>
|
||||||
{isRowNumber ? (
|
{isRowNumber ? (
|
||||||
<RowNumberHeaderCell loadedRowIds={loadedRowIds} pageId={pageId} />
|
<RowNumberHeaderCell loadedRowIds={loadedRowIds} pageId={pageId} />
|
||||||
@@ -186,6 +221,7 @@ export const GridHeaderCell = memo(function GridHeaderCell({
|
|||||||
onClick={(e) => e.stopPropagation()}
|
onClick={(e) => e.stopPropagation()}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
{closestEdge && <BaseDropEdgeIndicator edge={closestEdge} />}
|
||||||
{property && !isRowNumber && (
|
{property && !isRowNumber && (
|
||||||
<Popover
|
<Popover
|
||||||
opened={menuOpened}
|
opened={menuOpened}
|
||||||
|
|||||||
@@ -8,13 +8,13 @@ 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
|
|
||||||
// (table ref is stable from useReactTable, so memo won't fire without these)
|
|
||||||
columnOrder: ColumnOrderState;
|
columnOrder: ColumnOrderState;
|
||||||
columnVisibility: VisibilityState;
|
columnVisibility: VisibilityState;
|
||||||
properties: IBaseProperty[];
|
properties: IBaseProperty[];
|
||||||
loadedRowIds: string[];
|
loadedRowIds: string[];
|
||||||
onPropertyCreated?: () => void;
|
onPropertyCreated?: () => void;
|
||||||
|
getColumnOrder: () => string[];
|
||||||
|
onColumnReorder?: (columnId: string, finishIndex: number) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const GridHeader = memo(function GridHeader({
|
export const GridHeader = memo(function GridHeader({
|
||||||
@@ -27,6 +27,8 @@ export const GridHeader = memo(function GridHeader({
|
|||||||
properties,
|
properties,
|
||||||
loadedRowIds,
|
loadedRowIds,
|
||||||
onPropertyCreated,
|
onPropertyCreated,
|
||||||
|
getColumnOrder,
|
||||||
|
onColumnReorder,
|
||||||
}: GridHeaderProps) {
|
}: GridHeaderProps) {
|
||||||
const headerGroups = table.getHeaderGroups();
|
const headerGroups = table.getHeaderGroups();
|
||||||
const propertyById = useMemo(() => {
|
const propertyById = useMemo(() => {
|
||||||
@@ -44,6 +46,8 @@ export const GridHeader = memo(function GridHeader({
|
|||||||
property={propertyById.get(header.column.id)}
|
property={propertyById.get(header.column.id)}
|
||||||
loadedRowIds={loadedRowIds}
|
loadedRowIds={loadedRowIds}
|
||||||
pageId={pageId}
|
pageId={pageId}
|
||||||
|
getColumnOrder={getColumnOrder}
|
||||||
|
onColumnReorder={onColumnReorder}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
<CreatePropertyPopover
|
<CreatePropertyPopover
|
||||||
|
|||||||
Reference in New Issue
Block a user