@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.
Files changed (134) hide show
  1. package/dist/{CollectionEditorDialog-HNXGxXdO.js → CollectionEditorDialog-iGgS4rLX.js} +233 -152
  2. package/dist/CollectionEditorDialog-iGgS4rLX.js.map +1 -0
  3. package/dist/{PropertyEditView-C2wp8kQ9.js → PropertyEditView-C9yhLyja.js} +129 -15
  4. package/dist/PropertyEditView-C9yhLyja.js.map +1 -0
  5. package/dist/{CollectionsStudioView-B1mN33GB.js → RouterCollectionsStudioView-Dqx27GD8.js} +61 -39
  6. package/dist/RouterCollectionsStudioView-Dqx27GD8.js.map +1 -0
  7. package/dist/collection_editor/extensibility_types.d.ts +135 -0
  8. package/dist/collection_editor/index.d.ts +6 -0
  9. package/dist/collection_editor/serializable_types.d.ts +374 -0
  10. package/dist/collection_editor/serializable_utils.d.ts +50 -0
  11. package/dist/collection_editor/ui/collection_editor/CollectionEditorDialog.d.ts +2 -1
  12. package/dist/collection_editor/ui/collection_editor/CollectionPropertiesEditorForm.d.ts +12 -1
  13. package/dist/collection_editor/ui/collection_editor/CollectionStudioView.d.ts +13 -1
  14. package/dist/collection_editor/ui/collection_editor/CollectionsStudioView.d.ts +24 -2
  15. package/dist/collection_editor/ui/collection_editor/DisplaySettingsForm.d.ts +2 -1
  16. package/dist/collection_editor/ui/collection_editor/GeneralSettingsForm.d.ts +4 -1
  17. package/dist/collection_editor/ui/collection_editor/PropertyEditView.d.ts +12 -1
  18. package/dist/collection_editor/ui/collection_editor/RouterCollectionStudioView.d.ts +15 -0
  19. package/dist/collection_editor/ui/collection_editor/RouterCollectionsStudioView.d.ts +14 -0
  20. package/dist/collection_editor/useJsonCollectionsConfigController.d.ts +37 -0
  21. package/dist/collection_editor/useSafeSnackbarController.d.ts +2 -0
  22. package/dist/collection_editor_ui.d.ts +4 -0
  23. package/dist/collection_editor_ui.js +33 -4
  24. package/dist/collection_editor_ui.js.map +1 -0
  25. package/dist/components/EntityCollectionView/EntityBoardCard.d.ts +3 -3
  26. package/dist/components/EntityCollectionView/hooks/useKanbanDragAndDrop.d.ts +3 -3
  27. package/dist/components/EntityEditView.d.ts +3 -2
  28. package/dist/components/RebaseCMS.d.ts +1 -1
  29. package/dist/form/EntityForm.d.ts +10 -9
  30. package/dist/form/EntityFormBinding.d.ts +27 -0
  31. package/dist/form/form_utils.d.ts +8 -0
  32. package/dist/form/index.d.ts +5 -1
  33. package/dist/{history-BO6VT85a.js → history-ChHgyyvM.js} +2 -2
  34. package/dist/{history-BO6VT85a.js.map → history-ChHgyyvM.js.map} +1 -1
  35. package/dist/hooks/navigation/contexts/CollectionRegistryContext.d.ts +1 -1
  36. package/dist/hooks/navigation/useAsyncResolver.d.ts +26 -0
  37. package/dist/hooks/navigation/useBuildCollectionRegistryController.d.ts +2 -0
  38. package/dist/hooks/navigation/useTopLevelNavigation.d.ts +1 -1
  39. package/dist/hooks/navigation/utils.d.ts +1 -1
  40. package/dist/index.d.ts +2 -0
  41. package/dist/index.js +2 -2
  42. package/dist/routes/CustomViewRoute.d.ts +2 -1
  43. package/dist/{src-BYniefVu.js → src-DOX2nOyh.js} +1586 -1283
  44. package/dist/src-DOX2nOyh.js.map +1 -0
  45. package/dist/types/components/EntityFormActionsProps.d.ts +1 -1
  46. package/dist/types/components/EntityFormProps.d.ts +59 -0
  47. package/dist/types/fields.d.ts +6 -1
  48. package/dist/util/dataControllerAdapter.d.ts +30 -0
  49. package/dist/util/navigation_utils.d.ts +1 -1
  50. package/dist/util/propertyConfigMapper.d.ts +17 -0
  51. package/package.json +18 -18
  52. package/src/collection_editor/extensibility_types.ts +168 -0
  53. package/src/collection_editor/index.ts +46 -0
  54. package/src/collection_editor/serializable_types.ts +487 -0
  55. package/src/collection_editor/serializable_utils.ts +588 -0
  56. package/src/collection_editor/ui/collection_editor/AICollectionGeneratorPopover.tsx +4 -4
  57. package/src/collection_editor/ui/collection_editor/CollectionEditorDialog.tsx +134 -70
  58. package/src/collection_editor/ui/collection_editor/CollectionPropertiesEditorForm.tsx +95 -78
  59. package/src/collection_editor/ui/collection_editor/CollectionStudioView.tsx +23 -20
  60. package/src/collection_editor/ui/collection_editor/CollectionsStudioView.tsx +79 -27
  61. package/src/collection_editor/ui/collection_editor/DisplaySettingsForm.tsx +9 -7
  62. package/src/collection_editor/ui/collection_editor/GeneralSettingsForm.tsx +22 -5
  63. package/src/collection_editor/ui/collection_editor/GetCodeDialog.tsx +3 -3
  64. package/src/collection_editor/ui/collection_editor/PropertyEditView.tsx +155 -10
  65. package/src/collection_editor/ui/collection_editor/PropertyFieldPreview.tsx +6 -6
  66. package/src/collection_editor/ui/collection_editor/PropertyTree.tsx +1 -0
  67. package/src/collection_editor/ui/collection_editor/RouterCollectionStudioView.tsx +44 -0
  68. package/src/collection_editor/ui/collection_editor/RouterCollectionsStudioView.tsx +41 -0
  69. package/src/collection_editor/ui/collection_editor/properties/EnumPropertyField.tsx +4 -3
  70. package/src/collection_editor/useJsonCollectionsConfigController.tsx +286 -0
  71. package/src/collection_editor/useSafeSnackbarController.ts +9 -0
  72. package/src/collection_editor_ui.ts +6 -0
  73. package/src/components/DefaultDrawer.tsx +13 -3
  74. package/src/components/DrawerNavigationGroup.tsx +2 -2
  75. package/src/components/EntityCollectionView/EntityBoardCard.tsx +6 -6
  76. package/src/components/EntityCollectionView/EntityCollectionBoardView.tsx +12 -11
  77. package/src/components/EntityCollectionView/EntityCollectionCardView.tsx +40 -190
  78. package/src/components/EntityCollectionView/EntityCollectionListView.tsx +54 -183
  79. package/src/components/EntityCollectionView/hooks/useKanbanDragAndDrop.ts +6 -6
  80. package/src/components/EntityEditView.tsx +5 -4
  81. package/src/components/EntityEditViewFormActions.tsx +32 -26
  82. package/src/components/HomePage/ContentHomePage.tsx +13 -4
  83. package/src/components/RebaseCMS.tsx +3 -2
  84. package/src/components/RebaseNavigation.tsx +71 -7
  85. package/src/components/SelectableTable/SelectableTable.tsx +2 -2
  86. package/src/components/SideDialogs.tsx +0 -7
  87. package/src/components/common/default_entity_actions.tsx +9 -9
  88. package/src/contexts/BreacrumbsContext.tsx +15 -6
  89. package/src/editor/selectors/ai-selector.tsx +0 -1
  90. package/src/form/EntityForm.tsx +191 -428
  91. package/src/form/EntityFormActions.tsx +33 -47
  92. package/src/form/EntityFormBinding.tsx +323 -0
  93. package/src/form/components/FormLayout.tsx +1 -1
  94. package/src/form/field_bindings/ReadOnlyFieldBinding.tsx +7 -2
  95. package/src/form/form_utils.ts +163 -0
  96. package/src/form/index.tsx +14 -5
  97. package/src/hooks/navigation/contexts/CollectionRegistryContext.tsx +2 -2
  98. package/src/hooks/navigation/contexts/NavigationStateContext.tsx +2 -2
  99. package/src/hooks/navigation/contexts/UrlContext.tsx +1 -2
  100. package/src/hooks/navigation/useAsyncResolver.ts +100 -0
  101. package/src/hooks/navigation/useBuildCollectionRegistryController.tsx +15 -8
  102. package/src/hooks/navigation/useBuildNavigationStateController.tsx +12 -8
  103. package/src/hooks/navigation/useNavigationResolution.ts +33 -0
  104. package/src/hooks/navigation/useResolvedCollections.ts +29 -71
  105. package/src/hooks/navigation/useResolvedViews.tsx +40 -70
  106. package/src/hooks/navigation/useTopLevelNavigation.ts +20 -16
  107. package/src/hooks/navigation/utils.ts +7 -4
  108. package/src/index.ts +5 -0
  109. package/src/routes/CustomViewRoute.tsx +14 -6
  110. package/src/types/components/EntityFormActionsProps.tsx +1 -1
  111. package/src/types/components/EntityFormProps.tsx +68 -0
  112. package/src/types/fields.tsx +7 -1
  113. package/src/util/dataControllerAdapter.ts +93 -0
  114. package/src/util/navigation_utils.ts +1 -1
  115. package/src/util/propertyConfigMapper.ts +134 -0
  116. package/LICENSE +0 -21
  117. package/dist/CollectionEditorDialog-HNXGxXdO.js.map +0 -1
  118. package/dist/CollectionsStudioView-B1mN33GB.js.map +0 -1
  119. package/dist/PropertyEditView-C2wp8kQ9.js.map +0 -1
  120. package/dist/components/EntityCollectionView/Board.d.ts +0 -3
  121. package/dist/components/EntityCollectionView/BoardColumn.d.ts +0 -42
  122. package/dist/components/EntityCollectionView/BoardColumnTitle.d.ts +0 -9
  123. package/dist/components/EntityCollectionView/BoardSortableList.d.ts +0 -14
  124. package/dist/components/EntityCollectionView/board_types.d.ts +0 -105
  125. package/dist/hooks/navigation/useNavigationRegistry.d.ts +0 -12
  126. package/dist/hooks/navigation/useNavigationURLs.d.ts +0 -11
  127. package/dist/src-BYniefVu.js.map +0 -1
  128. package/src/components/EntityCollectionView/Board.tsx +0 -475
  129. package/src/components/EntityCollectionView/BoardColumn.tsx +0 -165
  130. package/src/components/EntityCollectionView/BoardColumnTitle.tsx +0 -46
  131. package/src/components/EntityCollectionView/BoardSortableList.tsx +0 -177
  132. package/src/components/EntityCollectionView/board_types.ts +0 -113
  133. package/src/hooks/navigation/useNavigationRegistry.ts +0 -164
  134. 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
- }