@rebasepro/admin 0.6.1 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{CollectionEditorDialog-HNXGxXdO.js → CollectionEditorDialog-iGgS4rLX.js} +233 -152
- package/dist/CollectionEditorDialog-iGgS4rLX.js.map +1 -0
- package/dist/{PropertyEditView-C2wp8kQ9.js → PropertyEditView-C9yhLyja.js} +129 -15
- package/dist/PropertyEditView-C9yhLyja.js.map +1 -0
- package/dist/{CollectionsStudioView-B1mN33GB.js → RouterCollectionsStudioView-Dqx27GD8.js} +61 -39
- package/dist/RouterCollectionsStudioView-Dqx27GD8.js.map +1 -0
- package/dist/collection_editor/extensibility_types.d.ts +135 -0
- package/dist/collection_editor/index.d.ts +6 -0
- package/dist/collection_editor/serializable_types.d.ts +374 -0
- package/dist/collection_editor/serializable_utils.d.ts +50 -0
- package/dist/collection_editor/ui/collection_editor/CollectionEditorDialog.d.ts +2 -1
- package/dist/collection_editor/ui/collection_editor/CollectionPropertiesEditorForm.d.ts +12 -1
- package/dist/collection_editor/ui/collection_editor/CollectionStudioView.d.ts +13 -1
- package/dist/collection_editor/ui/collection_editor/CollectionsStudioView.d.ts +24 -2
- package/dist/collection_editor/ui/collection_editor/DisplaySettingsForm.d.ts +2 -1
- package/dist/collection_editor/ui/collection_editor/GeneralSettingsForm.d.ts +4 -1
- package/dist/collection_editor/ui/collection_editor/PropertyEditView.d.ts +12 -1
- package/dist/collection_editor/ui/collection_editor/RouterCollectionStudioView.d.ts +15 -0
- package/dist/collection_editor/ui/collection_editor/RouterCollectionsStudioView.d.ts +14 -0
- package/dist/collection_editor/useJsonCollectionsConfigController.d.ts +37 -0
- package/dist/collection_editor/useSafeSnackbarController.d.ts +2 -0
- package/dist/collection_editor_ui.d.ts +4 -0
- package/dist/collection_editor_ui.js +33 -4
- package/dist/collection_editor_ui.js.map +1 -0
- package/dist/components/EntityCollectionView/EntityBoardCard.d.ts +3 -3
- package/dist/components/EntityCollectionView/hooks/useKanbanDragAndDrop.d.ts +3 -3
- package/dist/components/EntityEditView.d.ts +3 -2
- package/dist/components/RebaseCMS.d.ts +1 -1
- package/dist/form/EntityForm.d.ts +10 -9
- package/dist/form/EntityFormBinding.d.ts +27 -0
- package/dist/form/form_utils.d.ts +8 -0
- package/dist/form/index.d.ts +5 -1
- package/dist/{history-BO6VT85a.js → history-ChHgyyvM.js} +2 -2
- package/dist/{history-BO6VT85a.js.map → history-ChHgyyvM.js.map} +1 -1
- package/dist/hooks/navigation/contexts/CollectionRegistryContext.d.ts +1 -1
- package/dist/hooks/navigation/useAsyncResolver.d.ts +26 -0
- package/dist/hooks/navigation/useBuildCollectionRegistryController.d.ts +2 -0
- package/dist/hooks/navigation/useTopLevelNavigation.d.ts +1 -1
- package/dist/hooks/navigation/utils.d.ts +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.js +2 -2
- package/dist/routes/CustomViewRoute.d.ts +2 -1
- package/dist/{src-BYniefVu.js → src-DOX2nOyh.js} +1586 -1283
- package/dist/src-DOX2nOyh.js.map +1 -0
- package/dist/types/components/EntityFormActionsProps.d.ts +1 -1
- package/dist/types/components/EntityFormProps.d.ts +59 -0
- package/dist/types/fields.d.ts +6 -1
- package/dist/util/dataControllerAdapter.d.ts +30 -0
- package/dist/util/navigation_utils.d.ts +1 -1
- package/dist/util/propertyConfigMapper.d.ts +17 -0
- package/package.json +18 -18
- package/src/collection_editor/extensibility_types.ts +168 -0
- package/src/collection_editor/index.ts +46 -0
- package/src/collection_editor/serializable_types.ts +487 -0
- package/src/collection_editor/serializable_utils.ts +588 -0
- package/src/collection_editor/ui/collection_editor/AICollectionGeneratorPopover.tsx +4 -4
- package/src/collection_editor/ui/collection_editor/CollectionEditorDialog.tsx +134 -70
- package/src/collection_editor/ui/collection_editor/CollectionPropertiesEditorForm.tsx +95 -78
- package/src/collection_editor/ui/collection_editor/CollectionStudioView.tsx +23 -20
- package/src/collection_editor/ui/collection_editor/CollectionsStudioView.tsx +79 -27
- package/src/collection_editor/ui/collection_editor/DisplaySettingsForm.tsx +9 -7
- package/src/collection_editor/ui/collection_editor/GeneralSettingsForm.tsx +22 -5
- package/src/collection_editor/ui/collection_editor/GetCodeDialog.tsx +3 -3
- package/src/collection_editor/ui/collection_editor/PropertyEditView.tsx +155 -10
- package/src/collection_editor/ui/collection_editor/PropertyFieldPreview.tsx +6 -6
- package/src/collection_editor/ui/collection_editor/PropertyTree.tsx +1 -0
- package/src/collection_editor/ui/collection_editor/RouterCollectionStudioView.tsx +44 -0
- package/src/collection_editor/ui/collection_editor/RouterCollectionsStudioView.tsx +41 -0
- package/src/collection_editor/ui/collection_editor/properties/EnumPropertyField.tsx +4 -3
- package/src/collection_editor/useJsonCollectionsConfigController.tsx +286 -0
- package/src/collection_editor/useSafeSnackbarController.ts +9 -0
- package/src/collection_editor_ui.ts +6 -0
- package/src/components/DefaultDrawer.tsx +13 -3
- package/src/components/DrawerNavigationGroup.tsx +2 -2
- package/src/components/EntityCollectionView/EntityBoardCard.tsx +6 -6
- package/src/components/EntityCollectionView/EntityCollectionBoardView.tsx +12 -11
- package/src/components/EntityCollectionView/EntityCollectionCardView.tsx +40 -190
- package/src/components/EntityCollectionView/EntityCollectionListView.tsx +54 -183
- package/src/components/EntityCollectionView/hooks/useKanbanDragAndDrop.ts +6 -6
- package/src/components/EntityEditView.tsx +5 -4
- package/src/components/EntityEditViewFormActions.tsx +32 -26
- package/src/components/HomePage/ContentHomePage.tsx +13 -4
- package/src/components/RebaseCMS.tsx +3 -2
- package/src/components/RebaseNavigation.tsx +71 -7
- package/src/components/SelectableTable/SelectableTable.tsx +2 -2
- package/src/components/SideDialogs.tsx +0 -7
- package/src/components/common/default_entity_actions.tsx +9 -9
- package/src/contexts/BreacrumbsContext.tsx +15 -6
- package/src/editor/selectors/ai-selector.tsx +0 -1
- package/src/form/EntityForm.tsx +191 -428
- package/src/form/EntityFormActions.tsx +33 -47
- package/src/form/EntityFormBinding.tsx +323 -0
- package/src/form/components/FormLayout.tsx +1 -1
- package/src/form/field_bindings/ReadOnlyFieldBinding.tsx +7 -2
- package/src/form/form_utils.ts +163 -0
- package/src/form/index.tsx +14 -5
- package/src/hooks/navigation/contexts/CollectionRegistryContext.tsx +2 -2
- package/src/hooks/navigation/contexts/NavigationStateContext.tsx +2 -2
- package/src/hooks/navigation/contexts/UrlContext.tsx +1 -2
- package/src/hooks/navigation/useAsyncResolver.ts +100 -0
- package/src/hooks/navigation/useBuildCollectionRegistryController.tsx +15 -8
- package/src/hooks/navigation/useBuildNavigationStateController.tsx +12 -8
- package/src/hooks/navigation/useNavigationResolution.ts +33 -0
- package/src/hooks/navigation/useResolvedCollections.ts +29 -71
- package/src/hooks/navigation/useResolvedViews.tsx +40 -70
- package/src/hooks/navigation/useTopLevelNavigation.ts +20 -16
- package/src/hooks/navigation/utils.ts +7 -4
- package/src/index.ts +5 -0
- package/src/routes/CustomViewRoute.tsx +14 -6
- package/src/types/components/EntityFormActionsProps.tsx +1 -1
- package/src/types/components/EntityFormProps.tsx +68 -0
- package/src/types/fields.tsx +7 -1
- package/src/util/dataControllerAdapter.ts +93 -0
- package/src/util/navigation_utils.ts +1 -1
- package/src/util/propertyConfigMapper.ts +134 -0
- package/LICENSE +0 -21
- package/dist/CollectionEditorDialog-HNXGxXdO.js.map +0 -1
- package/dist/CollectionsStudioView-B1mN33GB.js.map +0 -1
- package/dist/PropertyEditView-C2wp8kQ9.js.map +0 -1
- package/dist/components/EntityCollectionView/Board.d.ts +0 -3
- package/dist/components/EntityCollectionView/BoardColumn.d.ts +0 -42
- package/dist/components/EntityCollectionView/BoardColumnTitle.d.ts +0 -9
- package/dist/components/EntityCollectionView/BoardSortableList.d.ts +0 -14
- package/dist/components/EntityCollectionView/board_types.d.ts +0 -105
- package/dist/hooks/navigation/useNavigationRegistry.d.ts +0 -12
- package/dist/hooks/navigation/useNavigationURLs.d.ts +0 -11
- package/dist/src-BYniefVu.js.map +0 -1
- package/src/components/EntityCollectionView/Board.tsx +0 -475
- package/src/components/EntityCollectionView/BoardColumn.tsx +0 -165
- package/src/components/EntityCollectionView/BoardColumnTitle.tsx +0 -46
- package/src/components/EntityCollectionView/BoardSortableList.tsx +0 -177
- package/src/components/EntityCollectionView/board_types.ts +0 -113
- package/src/hooks/navigation/useNavigationRegistry.ts +0 -164
- package/src/hooks/navigation/useNavigationURLs.ts +0 -56
|
@@ -1,475 +0,0 @@
|
|
|
1
|
-
import React, { useCallback, useEffect, useRef, useState } from "react";
|
|
2
|
-
import { createPortal } from "react-dom";
|
|
3
|
-
import {
|
|
4
|
-
DndContext,
|
|
5
|
-
DragEndEvent,
|
|
6
|
-
DragOverEvent,
|
|
7
|
-
DragOverlay,
|
|
8
|
-
DragStartEvent,
|
|
9
|
-
PointerSensor,
|
|
10
|
-
pointerWithin,
|
|
11
|
-
rectIntersection,
|
|
12
|
-
CollisionDetection,
|
|
13
|
-
useSensor,
|
|
14
|
-
useSensors
|
|
15
|
-
} from "@dnd-kit/core";
|
|
16
|
-
import { arrayMove, horizontalListSortingStrategy, SortableContext } from "@dnd-kit/sortable";
|
|
17
|
-
import { BoardColumn } from "./BoardColumn";
|
|
18
|
-
import { BoardItem, BoardItemMap, BoardItemViewProps, BoardProps } from "./board_types";
|
|
19
|
-
import { cls } from "@rebasepro/ui";
|
|
20
|
-
|
|
21
|
-
export function Board<M extends Record<string, unknown>, COLUMN extends string>({
|
|
22
|
-
data,
|
|
23
|
-
columns: columnsProp,
|
|
24
|
-
columnLabels,
|
|
25
|
-
columnColors,
|
|
26
|
-
className,
|
|
27
|
-
assignColumn,
|
|
28
|
-
allowColumnReorder = false,
|
|
29
|
-
onColumnReorder,
|
|
30
|
-
onItemsReorder,
|
|
31
|
-
ItemComponent,
|
|
32
|
-
columnLoadingState,
|
|
33
|
-
onLoadMoreColumn,
|
|
34
|
-
onAddItemToColumn,
|
|
35
|
-
AddColumnComponent
|
|
36
|
-
}: BoardProps<M, COLUMN>) {
|
|
37
|
-
|
|
38
|
-
const [activeItem, setActiveItem] = useState<BoardItem<M> | null>(null);
|
|
39
|
-
const [activeColumn, setActiveColumn] = useState<COLUMN | null>(null);
|
|
40
|
-
const [isDragging, setIsDragging] = useState(false);
|
|
41
|
-
const [dragOverColumnId, setDragOverColumnId] = useState<string | null>(null);
|
|
42
|
-
|
|
43
|
-
// Custom overlay positioning — bypasses position:fixed containment issues
|
|
44
|
-
const grabOffsetRef = useRef<{ x: number; y: number }>({ x: 0,
|
|
45
|
-
y: 0 });
|
|
46
|
-
const [overlayPos, setOverlayPos] = useState<{ x: number; y: number } | null>(null);
|
|
47
|
-
|
|
48
|
-
const handleMouseMove = useCallback((e: MouseEvent) => {
|
|
49
|
-
setOverlayPos({
|
|
50
|
-
x: e.clientX - grabOffsetRef.current.x,
|
|
51
|
-
y: e.clientY - grabOffsetRef.current.y
|
|
52
|
-
});
|
|
53
|
-
}, []);
|
|
54
|
-
// Clean up window listeners on unmount
|
|
55
|
-
useEffect(() => {
|
|
56
|
-
return () => {
|
|
57
|
-
window.removeEventListener("mousemove", handleMouseMove);
|
|
58
|
-
window.removeEventListener("pointermove", handleMouseMove);
|
|
59
|
-
};
|
|
60
|
-
}, [handleMouseMove]);
|
|
61
|
-
|
|
62
|
-
const [itemMapState, setItemMapState] = useState<BoardItemMap<M>>(() => {
|
|
63
|
-
const dataColumnMap: Record<string, COLUMN> = data.reduce((prev, item: BoardItem<M>) => ({
|
|
64
|
-
...prev,
|
|
65
|
-
[item.id]: assignColumn(item)
|
|
66
|
-
}), {});
|
|
67
|
-
return columnsProp.reduce(
|
|
68
|
-
(previous: BoardItemMap<M>, column: COLUMN) => ({
|
|
69
|
-
...previous,
|
|
70
|
-
[column]: data.filter((item: BoardItem<M>) => dataColumnMap[item.id] === column)
|
|
71
|
-
}),
|
|
72
|
-
{}
|
|
73
|
-
);
|
|
74
|
-
});
|
|
75
|
-
|
|
76
|
-
const sensors = useSensors(
|
|
77
|
-
useSensor(PointerSensor, {
|
|
78
|
-
activationConstraint: {
|
|
79
|
-
distance: 5
|
|
80
|
-
}
|
|
81
|
-
})
|
|
82
|
-
);
|
|
83
|
-
|
|
84
|
-
useEffect(() => {
|
|
85
|
-
if (isDragging) return;
|
|
86
|
-
|
|
87
|
-
const dataColumnMap: Record<string, COLUMN> = data.reduce((prev, item) => ({
|
|
88
|
-
...prev,
|
|
89
|
-
[item.id]: assignColumn(item)
|
|
90
|
-
}), {});
|
|
91
|
-
|
|
92
|
-
const newItemMap = columnsProp.reduce(
|
|
93
|
-
(previous: BoardItemMap<M>, column: COLUMN) => ({
|
|
94
|
-
...previous,
|
|
95
|
-
[column]: data.filter((item: BoardItem<M>) => dataColumnMap[item.id] === column)
|
|
96
|
-
}),
|
|
97
|
-
{} as BoardItemMap<M>
|
|
98
|
-
);
|
|
99
|
-
|
|
100
|
-
// Sync Board's itemMapState with incoming data.
|
|
101
|
-
// The incoming data is ordered by the fractional-index order key (from columnData).
|
|
102
|
-
// We accept that order as the source of truth, but avoid triggering re-renders
|
|
103
|
-
// when the data is structurally identical (same IDs in same order, same entity refs).
|
|
104
|
-
setItemMapState(prevMap => {
|
|
105
|
-
let changed = false;
|
|
106
|
-
|
|
107
|
-
for (const col of columnsProp) {
|
|
108
|
-
const prevItems = prevMap[col] ?? [];
|
|
109
|
-
const newItems = newItemMap[col] ?? [];
|
|
110
|
-
|
|
111
|
-
// Quick structural check: same length, same IDs in same order
|
|
112
|
-
if (prevItems.length !== newItems.length) {
|
|
113
|
-
changed = true;
|
|
114
|
-
continue;
|
|
115
|
-
}
|
|
116
|
-
for (let i = 0; i < prevItems.length; i++) {
|
|
117
|
-
if (prevItems[i].id !== newItems[i].id || prevItems[i].entity !== newItems[i].entity) {
|
|
118
|
-
changed = true;
|
|
119
|
-
break;
|
|
120
|
-
}
|
|
121
|
-
}
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
if (!changed) {
|
|
125
|
-
// Nothing changed — return same reference, no re-render
|
|
126
|
-
return prevMap;
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
// Something changed — accept the new data.
|
|
130
|
-
// Preserve entity refs that haven't changed to minimize downstream re-renders.
|
|
131
|
-
const updated: BoardItemMap<M> = {};
|
|
132
|
-
for (const col of columnsProp) {
|
|
133
|
-
const prevItems = prevMap[col] ?? [];
|
|
134
|
-
const newItems = newItemMap[col] ?? [];
|
|
135
|
-
|
|
136
|
-
// Build prev lookup for entity ref reuse
|
|
137
|
-
const prevById = new Map<string, BoardItem<M>>();
|
|
138
|
-
for (const item of prevItems) {
|
|
139
|
-
prevById.set(item.id, item);
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
updated[col] = newItems.map(newItem => {
|
|
143
|
-
const prev = prevById.get(newItem.id);
|
|
144
|
-
if (prev && prev.entity === newItem.entity) {
|
|
145
|
-
return prev; // Reuse existing object reference
|
|
146
|
-
}
|
|
147
|
-
return newItem;
|
|
148
|
-
});
|
|
149
|
-
}
|
|
150
|
-
return updated;
|
|
151
|
-
});
|
|
152
|
-
}, [data, columnsProp, assignColumn]);
|
|
153
|
-
|
|
154
|
-
const findColumnByItemId = (id: string): string | undefined => {
|
|
155
|
-
return Object.keys(itemMapState).find(col => itemMapState[col]?.some(i => i.id === id));
|
|
156
|
-
};
|
|
157
|
-
|
|
158
|
-
const handleDragStart = (event: DragStartEvent) => {
|
|
159
|
-
setIsDragging(true);
|
|
160
|
-
setDragOverColumnId(null);
|
|
161
|
-
const { active } = event;
|
|
162
|
-
|
|
163
|
-
// Compute grab offset: distance from pointer to element's top-left.
|
|
164
|
-
// active.rect.current.initial may be null at this point (measured async),
|
|
165
|
-
// so we find the DOM element directly from the activator event target.
|
|
166
|
-
const activatorEvt = event.activatorEvent as PointerEvent | MouseEvent;
|
|
167
|
-
if (activatorEvt) {
|
|
168
|
-
const target = activatorEvt.target as HTMLElement;
|
|
169
|
-
// Walk up to the sortable wrapper (the element with role="button"
|
|
170
|
-
// set by useSortable's attributes, or the closest [data-sortable])
|
|
171
|
-
const draggableEl = target.closest<HTMLElement>("[role='button']") ?? target;
|
|
172
|
-
const rect = draggableEl.getBoundingClientRect();
|
|
173
|
-
grabOffsetRef.current = {
|
|
174
|
-
x: activatorEvt.clientX - rect.left,
|
|
175
|
-
y: activatorEvt.clientY - rect.top
|
|
176
|
-
};
|
|
177
|
-
setOverlayPos({
|
|
178
|
-
x: rect.left,
|
|
179
|
-
y: rect.top
|
|
180
|
-
});
|
|
181
|
-
}
|
|
182
|
-
window.addEventListener("mousemove", handleMouseMove);
|
|
183
|
-
window.addEventListener("pointermove", handleMouseMove);
|
|
184
|
-
|
|
185
|
-
if (active.data.current?.type === "COLUMN") {
|
|
186
|
-
const columnId = active.id as string;
|
|
187
|
-
const column = columnsProp.find(col => String(col) === columnId);
|
|
188
|
-
if (column) {
|
|
189
|
-
setActiveColumn(column);
|
|
190
|
-
}
|
|
191
|
-
} else if (active.data.current?.type === "ITEM") {
|
|
192
|
-
const columnId = findColumnByItemId(active.id as string);
|
|
193
|
-
if (columnId) {
|
|
194
|
-
const item = itemMapState[columnId]?.find(i => i.id === active.id);
|
|
195
|
-
setActiveItem(item || null);
|
|
196
|
-
}
|
|
197
|
-
}
|
|
198
|
-
};
|
|
199
|
-
|
|
200
|
-
const handleDragOver = (event: DragOverEvent) => {
|
|
201
|
-
const {
|
|
202
|
-
active,
|
|
203
|
-
over
|
|
204
|
-
} = event;
|
|
205
|
-
|
|
206
|
-
if (!over) {
|
|
207
|
-
setDragOverColumnId(null);
|
|
208
|
-
return;
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
let currentHoveredColumnId: string | null = null;
|
|
212
|
-
const overId = over.id as string;
|
|
213
|
-
const overDataType = over.data.current?.type as string | undefined;
|
|
214
|
-
|
|
215
|
-
if (overDataType === "ITEM-LIST" || overDataType === "COLUMN") {
|
|
216
|
-
currentHoveredColumnId = overId;
|
|
217
|
-
} else if (overDataType === "ITEM") {
|
|
218
|
-
currentHoveredColumnId = findColumnByItemId(overId) || null;
|
|
219
|
-
} else if (columnsProp.includes(overId as COLUMN)) {
|
|
220
|
-
currentHoveredColumnId = overId;
|
|
221
|
-
}
|
|
222
|
-
|
|
223
|
-
setDragOverColumnId(currentHoveredColumnId);
|
|
224
|
-
|
|
225
|
-
// Skip item reordering if dragging a column
|
|
226
|
-
if (active.data.current?.type !== "ITEM") {
|
|
227
|
-
return;
|
|
228
|
-
}
|
|
229
|
-
|
|
230
|
-
const activeId = active.id as string;
|
|
231
|
-
const activeColumn = findColumnByItemId(activeId);
|
|
232
|
-
let overColumnForMove = findColumnByItemId(overId);
|
|
233
|
-
|
|
234
|
-
if (!overColumnForMove && overDataType === "ITEM-LIST") {
|
|
235
|
-
overColumnForMove = overId;
|
|
236
|
-
}
|
|
237
|
-
if (!overColumnForMove && columnsProp.includes(overId as COLUMN)) {
|
|
238
|
-
overColumnForMove = overId;
|
|
239
|
-
}
|
|
240
|
-
|
|
241
|
-
if (!activeColumn || !overColumnForMove) return;
|
|
242
|
-
if (activeColumn === overColumnForMove) return;
|
|
243
|
-
|
|
244
|
-
// Prevent moving to a column if item with same ID already exists there
|
|
245
|
-
if (itemMapState[overColumnForMove]?.some(i => i.id === activeId)) {
|
|
246
|
-
return;
|
|
247
|
-
}
|
|
248
|
-
|
|
249
|
-
setItemMapState(currentMap => {
|
|
250
|
-
const activeItems = [...(currentMap[activeColumn] || [])];
|
|
251
|
-
const overItems = [...(currentMap[overColumnForMove!] || [])];
|
|
252
|
-
const activeIndex = activeItems.findIndex(i => i.id === activeId);
|
|
253
|
-
|
|
254
|
-
if (activeIndex === -1) return currentMap;
|
|
255
|
-
|
|
256
|
-
let overIndex;
|
|
257
|
-
if (overDataType === "ITEM-LIST" || (columnsProp.includes(overId as COLUMN) && !findColumnByItemId(overId))) {
|
|
258
|
-
overIndex = overItems.length;
|
|
259
|
-
} else {
|
|
260
|
-
overIndex = overItems.findIndex(i => i.id === overId);
|
|
261
|
-
if (overIndex !== -1) {
|
|
262
|
-
const activeTop = active.rect.current.translated?.top ?? 0;
|
|
263
|
-
const activeHeight = active.rect.current.translated?.height ?? 0;
|
|
264
|
-
const activeCenter = activeTop + activeHeight / 2;
|
|
265
|
-
|
|
266
|
-
const overTop = over?.rect.top ?? 0;
|
|
267
|
-
const overHeight = over?.rect.height ?? 0;
|
|
268
|
-
const overCenter = overTop + overHeight / 2;
|
|
269
|
-
|
|
270
|
-
const isBelowOverItem = activeCenter > overCenter;
|
|
271
|
-
|
|
272
|
-
const modifier = isBelowOverItem ? 1 : 0;
|
|
273
|
-
overIndex = overIndex >= 0 ? overIndex + modifier : overItems.length;
|
|
274
|
-
} else {
|
|
275
|
-
overIndex = overItems.length;
|
|
276
|
-
}
|
|
277
|
-
}
|
|
278
|
-
|
|
279
|
-
const newItemMap = { ...currentMap };
|
|
280
|
-
const [moved] = activeItems.splice(activeIndex, 1);
|
|
281
|
-
overItems.splice(overIndex, 0, moved);
|
|
282
|
-
newItemMap[activeColumn] = activeItems;
|
|
283
|
-
newItemMap[overColumnForMove!] = overItems;
|
|
284
|
-
return newItemMap;
|
|
285
|
-
});
|
|
286
|
-
};
|
|
287
|
-
|
|
288
|
-
const handleDragEnd = (event: DragEndEvent) => {
|
|
289
|
-
const {
|
|
290
|
-
active,
|
|
291
|
-
over
|
|
292
|
-
} = event;
|
|
293
|
-
|
|
294
|
-
// Clean up custom overlay tracking
|
|
295
|
-
window.removeEventListener("mousemove", handleMouseMove);
|
|
296
|
-
window.removeEventListener("pointermove", handleMouseMove);
|
|
297
|
-
setOverlayPos(null);
|
|
298
|
-
|
|
299
|
-
setIsDragging(false);
|
|
300
|
-
setActiveItem(null);
|
|
301
|
-
setActiveColumn(null);
|
|
302
|
-
setDragOverColumnId(null);
|
|
303
|
-
|
|
304
|
-
if (!over) return;
|
|
305
|
-
|
|
306
|
-
const activeId = active.id as string;
|
|
307
|
-
const overId = over.id as string;
|
|
308
|
-
|
|
309
|
-
if (active.data.current?.type === "COLUMN" &&
|
|
310
|
-
over.data.current?.type === "COLUMN" &&
|
|
311
|
-
activeId !== overId) {
|
|
312
|
-
|
|
313
|
-
const oldIndex = columnsProp.findIndex(col => String(col) === activeId);
|
|
314
|
-
const newIndex = columnsProp.findIndex(col => String(col) === overId);
|
|
315
|
-
|
|
316
|
-
if (oldIndex !== -1 && newIndex !== -1 && onColumnReorder && allowColumnReorder) {
|
|
317
|
-
const newOrder = arrayMove([...columnsProp], oldIndex, newIndex);
|
|
318
|
-
onColumnReorder(newOrder);
|
|
319
|
-
}
|
|
320
|
-
} else if (active.data.current?.type === "ITEM" && onItemsReorder) {
|
|
321
|
-
// Find the original column assignment from the input data
|
|
322
|
-
const originalColumn = data.find(item => item.id === activeId)
|
|
323
|
-
? assignColumn(data.find(item => item.id === activeId)!)
|
|
324
|
-
: undefined;
|
|
325
|
-
|
|
326
|
-
// Find the current column assignment from our internal state
|
|
327
|
-
const currentColumn = findColumnByItemId(activeId) as COLUMN | undefined;
|
|
328
|
-
const overColumn = findColumnByItemId(overId) || currentColumn;
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
const finalItemMapState = { ...itemMapState };
|
|
332
|
-
|
|
333
|
-
if (currentColumn && currentColumn === overColumn) {
|
|
334
|
-
// Use dnd-kit's sortable indexes if available to avoid race conditions with mutating handleDragOver
|
|
335
|
-
const activeIndex = finalItemMapState[currentColumn].findIndex(i => i.id === activeId);
|
|
336
|
-
|
|
337
|
-
let overIndex = finalItemMapState[overColumn].findIndex(i => i.id === overId);
|
|
338
|
-
const overType = over.data.current?.type;
|
|
339
|
-
if (overIndex === -1 && (overType === "COLUMN" || overType === "ITEM-LIST")) {
|
|
340
|
-
if (originalColumn === currentColumn) {
|
|
341
|
-
overIndex = finalItemMapState[overColumn].length;
|
|
342
|
-
} else {
|
|
343
|
-
// For cross-column, handleDragOver already placed it at the correct index (top/bottom)
|
|
344
|
-
// when dragging over the column background.
|
|
345
|
-
overIndex = activeIndex;
|
|
346
|
-
}
|
|
347
|
-
} else if (overIndex === -1 && typeof over.data.current?.sortable?.index === "number") {
|
|
348
|
-
overIndex = over.data.current.sortable.index;
|
|
349
|
-
}
|
|
350
|
-
|
|
351
|
-
if (activeIndex !== -1 && overIndex !== -1 && activeIndex !== overIndex) {
|
|
352
|
-
finalItemMapState[currentColumn] = arrayMove(finalItemMapState[currentColumn], activeIndex, overIndex);
|
|
353
|
-
setItemMapState(finalItemMapState);
|
|
354
|
-
}
|
|
355
|
-
}
|
|
356
|
-
|
|
357
|
-
// Notify parent component of the change, including column movement information
|
|
358
|
-
// Pass ONLY the items in the target column, in their new visual order
|
|
359
|
-
if (originalColumn !== currentColumn && originalColumn && currentColumn) {
|
|
360
|
-
// Item has moved between columns
|
|
361
|
-
onItemsReorder(finalItemMapState[currentColumn] as BoardItem<M>[], {
|
|
362
|
-
itemId: activeId,
|
|
363
|
-
sourceColumn: originalColumn,
|
|
364
|
-
targetColumn: currentColumn
|
|
365
|
-
});
|
|
366
|
-
} else if (currentColumn) {
|
|
367
|
-
// Reordering within the same column
|
|
368
|
-
onItemsReorder(finalItemMapState[currentColumn] as BoardItem<M>[], {
|
|
369
|
-
itemId: activeId,
|
|
370
|
-
sourceColumn: currentColumn,
|
|
371
|
-
targetColumn: currentColumn
|
|
372
|
-
});
|
|
373
|
-
}
|
|
374
|
-
}
|
|
375
|
-
};
|
|
376
|
-
|
|
377
|
-
const customCollisionDetection: CollisionDetection = (args) => {
|
|
378
|
-
const pointerCollisions = pointerWithin(args);
|
|
379
|
-
|
|
380
|
-
if (pointerCollisions.length > 0) {
|
|
381
|
-
return pointerCollisions;
|
|
382
|
-
}
|
|
383
|
-
|
|
384
|
-
return rectIntersection(args);
|
|
385
|
-
};
|
|
386
|
-
|
|
387
|
-
return (
|
|
388
|
-
<DndContext
|
|
389
|
-
sensors={sensors}
|
|
390
|
-
collisionDetection={customCollisionDetection}
|
|
391
|
-
onDragStart={handleDragStart}
|
|
392
|
-
onDragOver={handleDragOver}
|
|
393
|
-
onDragEnd={handleDragEnd}
|
|
394
|
-
>
|
|
395
|
-
{/* Hidden DragOverlay — keeps dnd-kit's internal collision detection
|
|
396
|
-
and drag tracking working (it needs dragOverlay.rect to be set) */}
|
|
397
|
-
<DragOverlay dropAnimation={null} style={{ opacity: 0 }}>
|
|
398
|
-
{(activeItem || activeColumn) ? <div style={{ width: 1,
|
|
399
|
-
height: 1 }}/> : null}
|
|
400
|
-
</DragOverlay>
|
|
401
|
-
|
|
402
|
-
{/* Visual overlay — rendered at document.body via portal to bypass
|
|
403
|
-
any ancestor transforms/containment that break position:fixed */}
|
|
404
|
-
{overlayPos && (activeItem || activeColumn) && createPortal(
|
|
405
|
-
<div
|
|
406
|
-
className={document.documentElement.classList.contains("dark") ? "dark" : ""}
|
|
407
|
-
style={{
|
|
408
|
-
position: "fixed",
|
|
409
|
-
top: overlayPos.y,
|
|
410
|
-
left: overlayPos.x,
|
|
411
|
-
zIndex: 9999,
|
|
412
|
-
pointerEvents: "none",
|
|
413
|
-
touchAction: "none"
|
|
414
|
-
}}
|
|
415
|
-
>
|
|
416
|
-
{activeItem ? (
|
|
417
|
-
<ItemComponent
|
|
418
|
-
item={activeItem}
|
|
419
|
-
isDragging={true}
|
|
420
|
-
index={-1}
|
|
421
|
-
style={{
|
|
422
|
-
boxShadow: "0 4px 16px rgba(0,0,0,0.15)",
|
|
423
|
-
opacity: 0.9,
|
|
424
|
-
width: "304px"
|
|
425
|
-
}}
|
|
426
|
-
/>
|
|
427
|
-
) : activeColumn ? (
|
|
428
|
-
<BoardColumn
|
|
429
|
-
key={String(activeColumn)}
|
|
430
|
-
index={-1}
|
|
431
|
-
id={String(activeColumn)}
|
|
432
|
-
title={columnLabels?.[activeColumn] ?? String(activeColumn)}
|
|
433
|
-
items={itemMapState[String(activeColumn)] || []}
|
|
434
|
-
ItemComponent={ItemComponent}
|
|
435
|
-
isDragging={true}
|
|
436
|
-
isDragOverColumn={false}
|
|
437
|
-
/>
|
|
438
|
-
) : null}
|
|
439
|
-
</div>,
|
|
440
|
-
document.body
|
|
441
|
-
)}
|
|
442
|
-
|
|
443
|
-
<SortableContext
|
|
444
|
-
items={columnsProp.map(String)}
|
|
445
|
-
strategy={horizontalListSortingStrategy}
|
|
446
|
-
>
|
|
447
|
-
<div className={cls("p-2 md:p-3 lg:p-4 h-full min-w-full inline-flex", className)}>
|
|
448
|
-
{columnsProp.map((key: COLUMN, index: number) => (
|
|
449
|
-
<BoardColumn
|
|
450
|
-
key={String(key)}
|
|
451
|
-
index={index}
|
|
452
|
-
id={String(key)}
|
|
453
|
-
title={columnLabels?.[key] ?? String(key)}
|
|
454
|
-
color={columnColors?.[key]}
|
|
455
|
-
items={itemMapState[String(key)] || []}
|
|
456
|
-
ItemComponent={ItemComponent}
|
|
457
|
-
isDragging={isDragging}
|
|
458
|
-
isDragOverColumn={String(key) === dragOverColumnId}
|
|
459
|
-
allowReorder={allowColumnReorder}
|
|
460
|
-
loading={columnLoadingState?.[String(key)]?.loading}
|
|
461
|
-
hasMore={columnLoadingState?.[String(key)]?.hasMore}
|
|
462
|
-
totalCount={columnLoadingState?.[String(key)]?.totalCount}
|
|
463
|
-
onLoadMore={onLoadMoreColumn ? () => onLoadMoreColumn(key) : undefined}
|
|
464
|
-
onAddItem={onAddItemToColumn ? () => onAddItemToColumn(key) : undefined}
|
|
465
|
-
style={{
|
|
466
|
-
opacity: activeColumn === key ? 0 : 1
|
|
467
|
-
}}
|
|
468
|
-
/>
|
|
469
|
-
))}
|
|
470
|
-
{AddColumnComponent}
|
|
471
|
-
</div>
|
|
472
|
-
</SortableContext>
|
|
473
|
-
</DndContext>
|
|
474
|
-
);
|
|
475
|
-
}
|
|
@@ -1,165 +0,0 @@
|
|
|
1
|
-
import React, { memo, useMemo } from "react";
|
|
2
|
-
import { SortableContext, useSortable, verticalListSortingStrategy } from "@dnd-kit/sortable";
|
|
3
|
-
import { CSS } from "@dnd-kit/utilities";
|
|
4
|
-
import { BoardSortableList } from "./BoardSortableList";
|
|
5
|
-
import { BoardColumnTitle } from "./BoardColumnTitle";
|
|
6
|
-
import { BoardItem, BoardItemViewProps } from "./board_types";
|
|
7
|
-
import {
|
|
8
|
-
ChipColorKey,
|
|
9
|
-
ChipColorScheme,
|
|
10
|
-
cls,
|
|
11
|
-
defaultBorderMixin,
|
|
12
|
-
IconButton,
|
|
13
|
-
iconSize,
|
|
14
|
-
PlusIcon
|
|
15
|
-
} from "@rebasepro/ui";
|
|
16
|
-
|
|
17
|
-
export interface BoardColumnProps<M extends Record<string, unknown>> {
|
|
18
|
-
id: string;
|
|
19
|
-
title: string;
|
|
20
|
-
items: BoardItem<M>[];
|
|
21
|
-
index: number;
|
|
22
|
-
ItemComponent: React.ComponentType<BoardItemViewProps<M>>;
|
|
23
|
-
isDragging: boolean;
|
|
24
|
-
isDragOverColumn: boolean;
|
|
25
|
-
/**
|
|
26
|
-
* Whether column reordering is allowed (shows drag handle)
|
|
27
|
-
*/
|
|
28
|
-
allowReorder?: boolean;
|
|
29
|
-
/**
|
|
30
|
-
* Whether items are loading for this column
|
|
31
|
-
*/
|
|
32
|
-
loading?: boolean;
|
|
33
|
-
/**
|
|
34
|
-
* Whether there are more items to load
|
|
35
|
-
*/
|
|
36
|
-
hasMore?: boolean;
|
|
37
|
-
/**
|
|
38
|
-
* Callback to load more items
|
|
39
|
-
*/
|
|
40
|
-
onLoadMore?: () => void;
|
|
41
|
-
/**
|
|
42
|
-
* Callback to add a new item to this column
|
|
43
|
-
*/
|
|
44
|
-
onAddItem?: () => void;
|
|
45
|
-
/**
|
|
46
|
-
* Total count of entities in this column
|
|
47
|
-
*/
|
|
48
|
-
totalCount?: number;
|
|
49
|
-
/**
|
|
50
|
-
* Color of the column header indicator
|
|
51
|
-
*/
|
|
52
|
-
color?: ChipColorKey | ChipColorScheme;
|
|
53
|
-
style?: React.CSSProperties;
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
// Memoized to prevent unnecessary re-renders when other columns change
|
|
57
|
-
export const BoardColumn = memo(function BoardColumn<M extends Record<string, unknown>>({
|
|
58
|
-
id,
|
|
59
|
-
title,
|
|
60
|
-
items,
|
|
61
|
-
ItemComponent,
|
|
62
|
-
isDragging,
|
|
63
|
-
isDragOverColumn,
|
|
64
|
-
allowReorder = false,
|
|
65
|
-
loading = false,
|
|
66
|
-
hasMore = false,
|
|
67
|
-
onLoadMore,
|
|
68
|
-
onAddItem,
|
|
69
|
-
totalCount,
|
|
70
|
-
color,
|
|
71
|
-
style
|
|
72
|
-
}: BoardColumnProps<M>) {
|
|
73
|
-
const {
|
|
74
|
-
setNodeRef,
|
|
75
|
-
attributes,
|
|
76
|
-
listeners,
|
|
77
|
-
isDragging: isColumnBeingDragged,
|
|
78
|
-
transform,
|
|
79
|
-
transition
|
|
80
|
-
} = useSortable({
|
|
81
|
-
id,
|
|
82
|
-
data: { type: "COLUMN" },
|
|
83
|
-
disabled: !allowReorder
|
|
84
|
-
});
|
|
85
|
-
|
|
86
|
-
const combinedStyle = useMemo(() => ({
|
|
87
|
-
...style,
|
|
88
|
-
transform: CSS.Translate.toString(transform),
|
|
89
|
-
transition,
|
|
90
|
-
zIndex: isColumnBeingDragged ? 2 : 1
|
|
91
|
-
}), [style, transform, transition, isColumnBeingDragged]);
|
|
92
|
-
|
|
93
|
-
// Only apply drag listeners if reordering is allowed
|
|
94
|
-
const dragListeners = allowReorder ? listeners : {};
|
|
95
|
-
|
|
96
|
-
// Memoize className to avoid recomputation
|
|
97
|
-
const columnClassName = useMemo(() => cls(
|
|
98
|
-
"border h-full w-80 min-w-80 mx-2 flex flex-col rounded-md",
|
|
99
|
-
defaultBorderMixin,
|
|
100
|
-
isColumnBeingDragged ? "ring-2 ring-primary" : ""
|
|
101
|
-
), [isColumnBeingDragged]);
|
|
102
|
-
|
|
103
|
-
const headerClassName = useMemo(() => cls(
|
|
104
|
-
"flex items-center justify-between px-2 rounded-t-md transition-colors duration-200 ease-in-out",
|
|
105
|
-
isColumnBeingDragged
|
|
106
|
-
? "bg-surface-100 dark:bg-surface-700"
|
|
107
|
-
: "bg-surface-50 hover:bg-surface-100 dark:bg-surface-800 dark:hover:bg-surface-700",
|
|
108
|
-
allowReorder ? "cursor-grab" : ""
|
|
109
|
-
), [isColumnBeingDragged, allowReorder]);
|
|
110
|
-
|
|
111
|
-
// Memoize items IDs array to avoid recreating on each render
|
|
112
|
-
const itemIds = useMemo(() => items.map(i => i.id), [items]);
|
|
113
|
-
|
|
114
|
-
return (
|
|
115
|
-
<div
|
|
116
|
-
ref={setNodeRef}
|
|
117
|
-
style={combinedStyle}
|
|
118
|
-
{...attributes}
|
|
119
|
-
className={columnClassName}
|
|
120
|
-
>
|
|
121
|
-
<div
|
|
122
|
-
{...dragListeners}
|
|
123
|
-
className={headerClassName}
|
|
124
|
-
>
|
|
125
|
-
<div className="flex items-center gap-2">
|
|
126
|
-
<BoardColumnTitle aria-label={`${title} item list`} color={color}>
|
|
127
|
-
{title}
|
|
128
|
-
</BoardColumnTitle>
|
|
129
|
-
{totalCount !== undefined && (
|
|
130
|
-
<span className="text-xs text-surface-500 dark:text-surface-400">
|
|
131
|
-
{totalCount}
|
|
132
|
-
</span>
|
|
133
|
-
)}
|
|
134
|
-
</div>
|
|
135
|
-
{onAddItem && (
|
|
136
|
-
<IconButton
|
|
137
|
-
size="small"
|
|
138
|
-
onClick={(e: React.MouseEvent) => {
|
|
139
|
-
e.stopPropagation();
|
|
140
|
-
onAddItem();
|
|
141
|
-
}}
|
|
142
|
-
className="opacity-60 hover:opacity-100"
|
|
143
|
-
>
|
|
144
|
-
<PlusIcon size={iconSize.small}/>
|
|
145
|
-
</IconButton>
|
|
146
|
-
)}
|
|
147
|
-
</div>
|
|
148
|
-
<SortableContext
|
|
149
|
-
items={itemIds}
|
|
150
|
-
strategy={verticalListSortingStrategy}
|
|
151
|
-
>
|
|
152
|
-
<BoardSortableList
|
|
153
|
-
columnId={id}
|
|
154
|
-
items={items}
|
|
155
|
-
ItemComponent={ItemComponent}
|
|
156
|
-
isDragging={isDragging}
|
|
157
|
-
isDragOverColumn={isDragOverColumn}
|
|
158
|
-
loading={loading}
|
|
159
|
-
hasMore={hasMore}
|
|
160
|
-
onLoadMore={onLoadMore}
|
|
161
|
-
/>
|
|
162
|
-
</SortableContext>
|
|
163
|
-
</div>
|
|
164
|
-
);
|
|
165
|
-
}) as <M extends Record<string, unknown>>(props: BoardColumnProps<M>) => React.ReactElement;
|
|
@@ -1,46 +0,0 @@
|
|
|
1
|
-
import { ChipColorKey, ChipColorScheme, getColorSchemeForKey, cls, Typography } from "@rebasepro/ui";
|
|
2
|
-
import React, { useMemo } from "react";
|
|
3
|
-
|
|
4
|
-
export interface BoardColumnTitleProps {
|
|
5
|
-
children: React.ReactNode;
|
|
6
|
-
className?: string;
|
|
7
|
-
"aria-label"?: string;
|
|
8
|
-
color?: ChipColorKey | ChipColorScheme;
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
export function BoardColumnTitle({
|
|
12
|
-
children,
|
|
13
|
-
className,
|
|
14
|
-
color,
|
|
15
|
-
...props
|
|
16
|
-
}: BoardColumnTitleProps) {
|
|
17
|
-
const colorScheme = useMemo(() => {
|
|
18
|
-
if (!color) return undefined;
|
|
19
|
-
if (typeof color === "string") {
|
|
20
|
-
return getColorSchemeForKey(color);
|
|
21
|
-
}
|
|
22
|
-
return color;
|
|
23
|
-
}, [color]);
|
|
24
|
-
|
|
25
|
-
return (
|
|
26
|
-
<Typography
|
|
27
|
-
variant="subtitle2"
|
|
28
|
-
component="h4"
|
|
29
|
-
className={
|
|
30
|
-
cls("py-3 px-3 transition-colors duration-200 flex-grow select-none relative outline-none focus:outline focus:outline-2 focus:outline-offset-2 flex items-center gap-3",
|
|
31
|
-
className)
|
|
32
|
-
}
|
|
33
|
-
{...props}
|
|
34
|
-
>
|
|
35
|
-
{colorScheme && (
|
|
36
|
-
<div
|
|
37
|
-
className="w-3 h-3 rounded-full flex-shrink-0"
|
|
38
|
-
style={{
|
|
39
|
-
backgroundColor: colorScheme.darkColor ?? colorScheme.color
|
|
40
|
-
}}
|
|
41
|
-
/>
|
|
42
|
-
)}
|
|
43
|
-
{children}
|
|
44
|
-
</Typography>
|
|
45
|
-
);
|
|
46
|
-
}
|