@firecms/core 3.0.1 → 3.1.0-canary.9e89e98

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 (170) hide show
  1. package/README.md +1 -1
  2. package/dist/components/AIIcon.d.ts +16 -0
  3. package/dist/components/EntityCollectionTable/EntityCollectionRowActions.d.ts +7 -1
  4. package/dist/components/EntityCollectionTable/EntityCollectionTable.d.ts +1 -1
  5. package/dist/components/EntityCollectionTable/EntityCollectionTableProps.d.ts +14 -0
  6. package/dist/components/EntityCollectionTable/PropertyTableCell.d.ts +6 -0
  7. package/dist/components/EntityCollectionTable/internal/CollectionTableToolbar.d.ts +5 -4
  8. package/dist/components/EntityCollectionTable/internal/EntityTableCell.d.ts +6 -0
  9. package/dist/components/EntityCollectionView/Board.d.ts +2 -0
  10. package/dist/components/EntityCollectionView/BoardColumn.d.ts +42 -0
  11. package/dist/components/EntityCollectionView/BoardColumnTitle.d.ts +9 -0
  12. package/dist/components/EntityCollectionView/BoardSortableList.d.ts +14 -0
  13. package/dist/components/EntityCollectionView/EntityBoardCard.d.ts +26 -0
  14. package/dist/components/EntityCollectionView/EntityCard.d.ts +19 -0
  15. package/dist/components/EntityCollectionView/EntityCollectionBoardView.d.ts +20 -0
  16. package/dist/components/EntityCollectionView/EntityCollectionCardView.d.ts +31 -0
  17. package/dist/components/EntityCollectionView/EntityCollectionViewActions.d.ts +2 -2
  18. package/dist/components/EntityCollectionView/EntityCollectionViewStartActions.d.ts +7 -3
  19. package/dist/components/EntityCollectionView/FiltersDialog.d.ts +14 -0
  20. package/dist/components/EntityCollectionView/ViewModeToggle.d.ts +44 -0
  21. package/dist/components/EntityCollectionView/board_types.d.ts +105 -0
  22. package/dist/components/EntityCollectionView/useBoardDataController.d.ts +60 -0
  23. package/dist/components/SelectableTable/SelectableTable.d.ts +5 -1
  24. package/dist/components/SelectableTable/filters/DateTimeFilterField.d.ts +2 -1
  25. package/dist/components/VirtualTable/VirtualTableCell.d.ts +6 -0
  26. package/dist/components/VirtualTable/VirtualTableHeader.d.ts +2 -0
  27. package/dist/components/VirtualTable/VirtualTableHeaderRow.d.ts +1 -1
  28. package/dist/components/VirtualTable/VirtualTableProps.d.ts +11 -0
  29. package/dist/components/VirtualTable/fields/VirtualTableDateField.d.ts +1 -0
  30. package/dist/components/VirtualTable/types.d.ts +2 -0
  31. package/dist/components/index.d.ts +3 -0
  32. package/dist/contexts/index.d.ts +10 -0
  33. package/dist/core/DrawerNavigationGroup.d.ts +45 -0
  34. package/dist/core/index.d.ts +1 -0
  35. package/dist/form/validation.d.ts +3 -2
  36. package/dist/hooks/useBreadcrumbsController.d.ts +16 -0
  37. package/dist/hooks/useCollapsedGroups.d.ts +4 -1
  38. package/dist/index.es.js +5185 -1561
  39. package/dist/index.es.js.map +1 -1
  40. package/dist/index.umd.js +5179 -1556
  41. package/dist/index.umd.js.map +1 -1
  42. package/dist/preview/PropertyPreviewProps.d.ts +5 -0
  43. package/dist/preview/components/DatePreview.d.ts +13 -3
  44. package/dist/preview/components/ImagePreview.d.ts +5 -1
  45. package/dist/preview/components/StorageThumbnail.d.ts +2 -1
  46. package/dist/preview/components/UrlComponentPreview.d.ts +2 -1
  47. package/dist/preview/property_previews/ArrayOfStorageComponentsPreview.d.ts +1 -1
  48. package/dist/preview/property_previews/ArrayOfStringsPreview.d.ts +1 -1
  49. package/dist/preview/property_previews/SkeletonPropertyComponent.d.ts +1 -1
  50. package/dist/types/collections.d.ts +50 -2
  51. package/dist/types/datasource.d.ts +0 -1
  52. package/dist/types/plugins.d.ts +46 -1
  53. package/dist/types/properties.d.ts +259 -4
  54. package/dist/util/__tests__/conditions.test.d.ts +1 -0
  55. package/dist/util/__tests__/objects.test.d.ts +1 -0
  56. package/dist/util/conditions.d.ts +26 -0
  57. package/dist/util/entities.d.ts +1 -2
  58. package/dist/util/index.d.ts +2 -1
  59. package/dist/util/property_utils.d.ts +2 -1
  60. package/dist/util/resolutions.d.ts +1 -1
  61. package/package.json +10 -7
  62. package/src/app/Scaffold.tsx +14 -15
  63. package/src/components/AIIcon.tsx +39 -0
  64. package/src/components/ArrayContainer.tsx +1 -4
  65. package/src/components/ClearFilterSortButton.tsx +19 -16
  66. package/src/components/ConfirmationDialog.tsx +0 -2
  67. package/src/components/DeleteEntityDialog.tsx +2 -4
  68. package/src/components/EntityCollectionTable/EntityCollectionRowActions.tsx +74 -41
  69. package/src/components/EntityCollectionTable/EntityCollectionTable.tsx +130 -79
  70. package/src/components/EntityCollectionTable/EntityCollectionTableProps.tsx +121 -104
  71. package/src/components/EntityCollectionTable/PropertyTableCell.tsx +132 -103
  72. package/src/components/EntityCollectionTable/internal/CollectionTableToolbar.tsx +20 -42
  73. package/src/components/EntityCollectionTable/internal/EntityTableCell.tsx +90 -49
  74. package/src/components/EntityCollectionView/Board.tsx +324 -0
  75. package/src/components/EntityCollectionView/BoardColumn.tsx +158 -0
  76. package/src/components/EntityCollectionView/BoardColumnTitle.tsx +45 -0
  77. package/src/components/EntityCollectionView/BoardSortableList.tsx +172 -0
  78. package/src/components/EntityCollectionView/EntityBoardCard.tsx +212 -0
  79. package/src/components/EntityCollectionView/EntityCard.tsx +231 -0
  80. package/src/components/EntityCollectionView/EntityCollectionBoardView.tsx +713 -0
  81. package/src/components/EntityCollectionView/EntityCollectionCardView.tsx +244 -0
  82. package/src/components/EntityCollectionView/EntityCollectionView.tsx +490 -203
  83. package/src/components/EntityCollectionView/EntityCollectionViewActions.tsx +31 -19
  84. package/src/components/EntityCollectionView/EntityCollectionViewStartActions.tsx +84 -15
  85. package/src/components/EntityCollectionView/FiltersDialog.tsx +249 -0
  86. package/src/components/EntityCollectionView/ViewModeToggle.tsx +199 -0
  87. package/src/components/EntityCollectionView/board_types.ts +113 -0
  88. package/src/components/EntityCollectionView/useBoardDataController.tsx +490 -0
  89. package/src/components/ErrorTooltip.tsx +2 -1
  90. package/src/components/HomePage/DefaultHomePage.tsx +47 -10
  91. package/src/components/HomePage/HomePageDnD.tsx +56 -41
  92. package/src/components/HomePage/NavigationCard.tsx +20 -18
  93. package/src/components/HomePage/NavigationGroup.tsx +17 -16
  94. package/src/components/HomePage/RenameGroupDialog.tsx +0 -2
  95. package/src/components/HomePage/SmallNavigationCard.tsx +10 -9
  96. package/src/components/ReferenceTable/ReferenceSelectionTable.tsx +3 -10
  97. package/src/components/ReferenceWidget.tsx +2 -4
  98. package/src/components/SelectableTable/SelectableTable.tsx +75 -67
  99. package/src/components/SelectableTable/filters/BooleanFilterField.tsx +7 -6
  100. package/src/components/SelectableTable/filters/DateTimeFilterField.tsx +39 -40
  101. package/src/components/SelectableTable/filters/ReferenceFilterField.tsx +38 -38
  102. package/src/components/SelectableTable/filters/StringNumberFilterField.tsx +49 -58
  103. package/src/components/UnsavedChangesDialog.tsx +0 -2
  104. package/src/components/UserDisplay.tsx +4 -4
  105. package/src/components/VirtualTable/VirtualTable.tsx +170 -19
  106. package/src/components/VirtualTable/VirtualTableCell.tsx +18 -2
  107. package/src/components/VirtualTable/VirtualTableHeader.tsx +20 -11
  108. package/src/components/VirtualTable/VirtualTableHeaderRow.tsx +158 -42
  109. package/src/components/VirtualTable/VirtualTableProps.tsx +14 -1
  110. package/src/components/VirtualTable/VirtualTableRow.tsx +1 -1
  111. package/src/components/VirtualTable/fields/VirtualTableDateField.tsx +3 -0
  112. package/src/components/VirtualTable/fields/VirtualTableSelect.tsx +17 -4
  113. package/src/components/VirtualTable/types.tsx +2 -0
  114. package/src/components/common/useColumnsIds.tsx +95 -3
  115. package/src/components/index.tsx +4 -0
  116. package/src/contexts/BreacrumbsContext.tsx +15 -8
  117. package/src/contexts/index.ts +10 -0
  118. package/src/core/DefaultAppBar.tsx +39 -26
  119. package/src/core/DefaultDrawer.tsx +42 -56
  120. package/src/core/DrawerNavigationGroup.tsx +118 -0
  121. package/src/core/DrawerNavigationItem.tsx +4 -3
  122. package/src/core/EntityEditView.tsx +41 -43
  123. package/src/core/SideDialogs.tsx +4 -2
  124. package/src/core/index.tsx +1 -0
  125. package/src/form/PropertyFieldBinding.tsx +58 -43
  126. package/src/form/components/StorageItemPreview.tsx +2 -1
  127. package/src/form/field_bindings/ArrayOfReferencesFieldBinding.tsx +0 -1
  128. package/src/form/field_bindings/DateTimeFieldBinding.tsx +17 -16
  129. package/src/form/field_bindings/KeyValueFieldBinding.tsx +0 -1
  130. package/src/form/field_bindings/MapFieldBinding.tsx +69 -67
  131. package/src/form/field_bindings/MarkdownEditorFieldBinding.tsx +21 -17
  132. package/src/form/field_bindings/TextFieldBinding.tsx +71 -35
  133. package/src/form/validation.ts +245 -160
  134. package/src/hooks/useBreadcrumbsController.tsx +18 -0
  135. package/src/hooks/useBuildNavigationController.tsx +42 -19
  136. package/src/hooks/useCollapsedGroups.ts +12 -4
  137. package/src/internal/useBuildDataSource.ts +69 -34
  138. package/src/internal/useBuildSideDialogsController.tsx +11 -8
  139. package/src/internal/useBuildSideEntityController.tsx +2 -4
  140. package/src/internal/useRestoreScroll.tsx +26 -14
  141. package/src/preview/PropertyPreview.tsx +40 -32
  142. package/src/preview/PropertyPreviewProps.tsx +6 -0
  143. package/src/preview/components/DatePreview.tsx +72 -4
  144. package/src/preview/components/EmptyValue.tsx +1 -1
  145. package/src/preview/components/ImagePreview.tsx +37 -21
  146. package/src/preview/components/StorageThumbnail.tsx +16 -12
  147. package/src/preview/components/UrlComponentPreview.tsx +28 -25
  148. package/src/preview/property_previews/ArrayOfStorageComponentsPreview.tsx +9 -7
  149. package/src/preview/property_previews/ArrayOfStringsPreview.tsx +11 -9
  150. package/src/preview/property_previews/ArrayPropertyPreview.tsx +26 -24
  151. package/src/preview/property_previews/SkeletonPropertyComponent.tsx +61 -56
  152. package/src/routes/CustomCMSRoute.tsx +1 -0
  153. package/src/routes/FireCMSRoute.tsx +26 -13
  154. package/src/types/collections.ts +57 -3
  155. package/src/types/datasource.ts +54 -56
  156. package/src/types/plugins.tsx +51 -1
  157. package/src/types/properties.ts +347 -27
  158. package/src/util/__tests__/conditions.test.ts +506 -0
  159. package/src/util/__tests__/objects.test.ts +196 -0
  160. package/src/util/callbacks.ts +6 -3
  161. package/src/util/collections.ts +51 -6
  162. package/src/util/conditions.ts +339 -0
  163. package/src/util/entities.ts +28 -29
  164. package/src/util/entity_cache.ts +2 -1
  165. package/src/util/index.ts +2 -1
  166. package/src/util/objects.ts +31 -13
  167. package/src/util/{references.ts → previews.ts} +14 -0
  168. package/src/util/property_utils.tsx +36 -10
  169. package/src/util/resolutions.ts +57 -55
  170. /package/dist/util/{references.d.ts → previews.d.ts} +0 -0
@@ -0,0 +1,324 @@
1
+ import React, { useEffect, useState } from "react";
2
+ import {
3
+ DndContext,
4
+ DragEndEvent,
5
+ DragOverEvent,
6
+ DragOverlay,
7
+ DragStartEvent,
8
+ PointerSensor,
9
+ pointerWithin,
10
+ useSensor,
11
+ useSensors
12
+ } from "@dnd-kit/core";
13
+ import { arrayMove, horizontalListSortingStrategy, SortableContext } from "@dnd-kit/sortable";
14
+ import { BoardColumn } from "./BoardColumn";
15
+ import { BoardItem, BoardItemMap, BoardItemViewProps, BoardProps } from "./board_types";
16
+ import { cls } from "@firecms/ui";
17
+
18
+ export function Board<M extends Record<string, any>, COLUMN extends string>({
19
+ data,
20
+ columns: columnsProp,
21
+ columnLabels,
22
+ columnColors,
23
+ className,
24
+ assignColumn,
25
+ allowColumnReorder = false,
26
+ onColumnReorder,
27
+ onItemsReorder,
28
+ ItemComponent,
29
+ columnLoadingState,
30
+ onLoadMoreColumn,
31
+ onAddItemToColumn,
32
+ AddColumnComponent,
33
+ }: BoardProps<M, COLUMN>) {
34
+
35
+ const [activeItem, setActiveItem] = useState<BoardItem<M> | null>(null);
36
+ const [activeColumn, setActiveColumn] = useState<COLUMN | null>(null);
37
+ const [isDragging, setIsDragging] = useState(false);
38
+ const [dragOverColumnId, setDragOverColumnId] = useState<string | null>(null);
39
+ const [itemMapState, setItemMapState] = useState<BoardItemMap<M>>(() => {
40
+ const dataColumnMap: Record<string, COLUMN> = data.reduce((prev, item: BoardItem<M>) => ({
41
+ ...prev,
42
+ [item.id]: assignColumn(item)
43
+ }), {});
44
+ return columnsProp.reduce(
45
+ (previous: BoardItemMap<M>, column: COLUMN) => ({
46
+ ...previous,
47
+ [column]: data.filter((item: BoardItem<M>) => dataColumnMap[item.id] === column)
48
+ }),
49
+ {}
50
+ );
51
+ });
52
+
53
+ const sensors = useSensors(
54
+ useSensor(PointerSensor, {
55
+ activationConstraint: {
56
+ distance: 5,
57
+ },
58
+ })
59
+ );
60
+
61
+ useEffect(() => {
62
+ const dataColumnMap: Record<string, COLUMN> = data.reduce((prev, item) => ({
63
+ ...prev,
64
+ [item.id]: assignColumn(item)
65
+ }), {});
66
+
67
+ setItemMapState(() => 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
+ }, [data, columnsProp, assignColumn]);
75
+
76
+ const findColumnByItemId = (id: string): string | undefined => {
77
+ return Object.keys(itemMapState).find(col => itemMapState[col]?.some(i => i.id === id));
78
+ };
79
+
80
+ const handleDragStart = (event: DragStartEvent) => {
81
+ setIsDragging(true);
82
+ setDragOverColumnId(null);
83
+ const { active } = event;
84
+
85
+ if (active.data.current?.type === "COLUMN") {
86
+ const columnId = active.id as string;
87
+ const column = columnsProp.find(col => String(col) === columnId);
88
+ if (column) {
89
+ setActiveColumn(column);
90
+ }
91
+ } else if (active.data.current?.type === "ITEM") {
92
+ const columnId = findColumnByItemId(active.id as string);
93
+ if (columnId) {
94
+ const item = itemMapState[columnId]?.find(i => i.id === active.id);
95
+ setActiveItem(item || null);
96
+ }
97
+ }
98
+ };
99
+
100
+ const handleDragOver = (event: DragOverEvent) => {
101
+ const {
102
+ active,
103
+ over
104
+ } = event;
105
+
106
+ if (!over) {
107
+ setDragOverColumnId(null);
108
+ return;
109
+ }
110
+
111
+ let currentHoveredColumnId: string | null = null;
112
+ const overId = over.id as string;
113
+ const overDataType = over.data.current?.type as string | undefined;
114
+
115
+ if (overDataType === "ITEM-LIST" || overDataType === "COLUMN") {
116
+ currentHoveredColumnId = overId;
117
+ } else if (overDataType === "ITEM") {
118
+ currentHoveredColumnId = findColumnByItemId(overId) || null;
119
+ } else if (columnsProp.includes(overId as COLUMN)) {
120
+ currentHoveredColumnId = overId;
121
+ }
122
+
123
+ setDragOverColumnId(currentHoveredColumnId);
124
+
125
+ // Skip item reordering if dragging a column
126
+ if (active.data.current?.type !== "ITEM") {
127
+ return;
128
+ }
129
+
130
+ const activeId = active.id as string;
131
+ const activeColumn = findColumnByItemId(activeId);
132
+ let overColumnForMove = findColumnByItemId(overId);
133
+
134
+ if (!overColumnForMove && overDataType === "ITEM-LIST") {
135
+ overColumnForMove = overId;
136
+ }
137
+ if (!overColumnForMove && columnsProp.includes(overId as COLUMN)) {
138
+ overColumnForMove = overId;
139
+ }
140
+
141
+ if (!activeColumn || !overColumnForMove) return;
142
+ if (activeColumn === overColumnForMove && activeId === overId && overDataType !== "ITEM-LIST") return;
143
+
144
+ // Prevent moving to a column if item with same ID already exists there
145
+ if (overColumnForMove !== activeColumn &&
146
+ itemMapState[overColumnForMove]?.some(i => i.id === activeId)) {
147
+ return;
148
+ }
149
+
150
+ setItemMapState(currentMap => {
151
+ const activeItems = [...(currentMap[activeColumn] || [])];
152
+ const overItems = [...(currentMap[overColumnForMove!] || [])];
153
+ const activeIndex = activeItems.findIndex(i => i.id === activeId);
154
+
155
+ if (activeIndex === -1) return currentMap;
156
+
157
+ let overIndex;
158
+ if (overDataType === "ITEM-LIST" || (columnsProp.includes(overId as COLUMN) && !findColumnByItemId(overId))) {
159
+ overIndex = overItems.length;
160
+ } else {
161
+ overIndex = overItems.findIndex(i => i.id === overId);
162
+ if (overIndex !== -1) {
163
+ const { y } = event.delta;
164
+ const overRect = over.rect;
165
+ if (overRect) {
166
+ const threshold = overRect.height / 2;
167
+ if (y > threshold) {
168
+ overIndex += 1;
169
+ }
170
+ }
171
+ } else {
172
+ overIndex = overItems.length;
173
+ }
174
+ }
175
+
176
+ if (activeColumn === overColumnForMove && activeIndex === overIndex) return currentMap;
177
+
178
+ const newItemMap = { ...currentMap };
179
+ if (activeColumn === overColumnForMove) {
180
+ newItemMap[activeColumn] = arrayMove(activeItems, activeIndex, overIndex);
181
+ } else {
182
+ const [moved] = activeItems.splice(activeIndex, 1);
183
+ overItems.splice(overIndex, 0, moved);
184
+ newItemMap[activeColumn] = activeItems;
185
+ newItemMap[overColumnForMove] = overItems;
186
+ }
187
+ return newItemMap;
188
+ });
189
+ };
190
+
191
+ const handleDragEnd = (event: DragEndEvent) => {
192
+ const {
193
+ active,
194
+ over
195
+ } = event;
196
+
197
+ setIsDragging(false);
198
+ setActiveItem(null);
199
+ setActiveColumn(null);
200
+ setDragOverColumnId(null);
201
+
202
+ if (!over) return;
203
+
204
+ const activeId = active.id as string;
205
+ const overId = over.id as string;
206
+
207
+ if (active.data.current?.type === "COLUMN" &&
208
+ over.data.current?.type === "COLUMN" &&
209
+ activeId !== overId) {
210
+
211
+ const oldIndex = columnsProp.findIndex(col => String(col) === activeId);
212
+ const newIndex = columnsProp.findIndex(col => String(col) === overId);
213
+
214
+ if (oldIndex !== -1 && newIndex !== -1 && onColumnReorder && allowColumnReorder) {
215
+ const newOrder = arrayMove([...columnsProp], oldIndex, newIndex);
216
+ onColumnReorder(newOrder);
217
+ }
218
+ } else if (active.data.current?.type === "ITEM" && onItemsReorder) {
219
+ // Find the original column assignment from the input data
220
+ const originalColumn = data.find(item => item.id === activeId)
221
+ ? assignColumn(data.find(item => item.id === activeId)!)
222
+ : undefined;
223
+
224
+ // Find the current column assignment from our internal state
225
+ const currentColumn = findColumnByItemId(activeId) as COLUMN | undefined;
226
+
227
+ // When items have been reordered, convert itemMapState to a flat list
228
+ const allItems: BoardItem<M>[] = [];
229
+
230
+ // Collect all items from all columns in their current order
231
+ Object.entries(itemMapState).forEach(([, columnItems]) => {
232
+ if (columnItems && (columnItems as BoardItem<M>[]).length > 0) {
233
+ allItems.push(...(columnItems as BoardItem<M>[]));
234
+ }
235
+ });
236
+
237
+ // Notify parent component of the change, including column movement information
238
+ if (originalColumn !== currentColumn && originalColumn && currentColumn) {
239
+ // Item has moved between columns - provide this context to parent
240
+ onItemsReorder(allItems, {
241
+ itemId: activeId,
242
+ sourceColumn: originalColumn,
243
+ targetColumn: currentColumn
244
+ });
245
+ } else if (currentColumn) {
246
+ // Reordering within the same column - still need to provide moveInfo
247
+ onItemsReorder(allItems, {
248
+ itemId: activeId,
249
+ sourceColumn: currentColumn,
250
+ targetColumn: currentColumn
251
+ });
252
+ }
253
+ }
254
+ };
255
+
256
+ return (
257
+ <DndContext
258
+ sensors={sensors}
259
+ collisionDetection={pointerWithin}
260
+ onDragStart={handleDragStart}
261
+ onDragOver={handleDragOver}
262
+ onDragEnd={handleDragEnd}
263
+ >
264
+ <DragOverlay dropAnimation={{
265
+ duration: 300,
266
+ easing: "cubic-bezier(0.18, 0.67, 0.6, 1.22)",
267
+ }}>
268
+ {activeItem ? (
269
+ <ItemComponent
270
+ item={activeItem}
271
+ isDragging={true}
272
+ index={-1}
273
+ style={{
274
+ boxShadow: "0 4px 16px rgba(0,0,0,0.15)",
275
+ opacity: 0.9,
276
+ }}
277
+ />
278
+ ) : activeColumn ? (
279
+ <BoardColumn
280
+ key={String(activeColumn)}
281
+ index={-1}
282
+ id={String(activeColumn)}
283
+ title={columnLabels?.[activeColumn] ?? String(activeColumn)}
284
+ items={itemMapState[String(activeColumn)] || []}
285
+ ItemComponent={ItemComponent}
286
+ isDragging={true}
287
+ isDragOverColumn={false}
288
+ />
289
+ ) : null}
290
+ </DragOverlay>
291
+
292
+ <SortableContext
293
+ items={columnsProp.map(String)}
294
+ strategy={horizontalListSortingStrategy}
295
+ >
296
+ <div className={cls("p-2 md:p-3 lg:p-4 h-full min-w-full inline-flex", className)}>
297
+ {columnsProp.map((key: COLUMN, index: number) => (
298
+ <BoardColumn
299
+ key={String(key)}
300
+ index={index}
301
+ id={String(key)}
302
+ title={columnLabels?.[key] ?? String(key)}
303
+ color={columnColors?.[key]}
304
+ items={itemMapState[String(key)] || []}
305
+ ItemComponent={ItemComponent}
306
+ isDragging={isDragging}
307
+ isDragOverColumn={String(key) === dragOverColumnId}
308
+ allowReorder={allowColumnReorder}
309
+ loading={columnLoadingState?.[String(key)]?.loading}
310
+ hasMore={columnLoadingState?.[String(key)]?.hasMore}
311
+ totalCount={columnLoadingState?.[String(key)]?.totalCount}
312
+ onLoadMore={onLoadMoreColumn ? () => onLoadMoreColumn(key) : undefined}
313
+ onAddItem={onAddItemToColumn ? () => onAddItemToColumn(key) : undefined}
314
+ style={{
315
+ opacity: activeColumn === key ? 0 : 1
316
+ }}
317
+ />
318
+ ))}
319
+ {AddColumnComponent}
320
+ </div>
321
+ </SortableContext>
322
+ </DndContext>
323
+ );
324
+ }
@@ -0,0 +1,158 @@
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 { AddIcon, ChipColorKey, ChipColorScheme, cls, defaultBorderMixin, IconButton } from "@firecms/ui";
8
+
9
+ export interface BoardColumnProps<M extends Record<string, any>> {
10
+ id: string;
11
+ title: string;
12
+ items: BoardItem<M>[];
13
+ index: number;
14
+ ItemComponent: React.ComponentType<BoardItemViewProps<M>>;
15
+ isDragging: boolean;
16
+ isDragOverColumn: boolean;
17
+ /**
18
+ * Whether column reordering is allowed (shows drag handle)
19
+ */
20
+ allowReorder?: boolean;
21
+ /**
22
+ * Whether items are loading for this column
23
+ */
24
+ loading?: boolean;
25
+ /**
26
+ * Whether there are more items to load
27
+ */
28
+ hasMore?: boolean;
29
+ /**
30
+ * Callback to load more items
31
+ */
32
+ onLoadMore?: () => void;
33
+ /**
34
+ * Callback to add a new item to this column
35
+ */
36
+ onAddItem?: () => void;
37
+ /**
38
+ * Total count of entities in this column
39
+ */
40
+ totalCount?: number;
41
+ /**
42
+ * Color of the column header indicator
43
+ */
44
+ color?: ChipColorKey | ChipColorScheme;
45
+ style?: React.CSSProperties;
46
+ }
47
+
48
+ // Memoized to prevent unnecessary re-renders when other columns change
49
+ export const BoardColumn = memo(function BoardColumn<M extends Record<string, any>>({
50
+ id,
51
+ title,
52
+ items,
53
+ ItemComponent,
54
+ isDragging,
55
+ isDragOverColumn,
56
+ allowReorder = false,
57
+ loading = false,
58
+ hasMore = false,
59
+ onLoadMore,
60
+ onAddItem,
61
+ totalCount,
62
+ color,
63
+ style
64
+ }: BoardColumnProps<M>) {
65
+ const {
66
+ setNodeRef,
67
+ attributes,
68
+ listeners,
69
+ isDragging: isColumnBeingDragged,
70
+ transform,
71
+ transition,
72
+ } = useSortable({
73
+ id,
74
+ data: { type: "COLUMN" },
75
+ disabled: !allowReorder
76
+ });
77
+
78
+ // Memoize combined style to avoid object recreation
79
+ const combinedStyle = useMemo(() => ({
80
+ ...style,
81
+ transform: CSS.Transform.toString(transform),
82
+ transition,
83
+ zIndex: isColumnBeingDragged ? 2 : 1,
84
+ }), [style, transform, transition, isColumnBeingDragged]);
85
+
86
+ // Only apply drag listeners if reordering is allowed
87
+ const dragListeners = allowReorder ? listeners : {};
88
+
89
+ // Memoize className to avoid recomputation
90
+ const columnClassName = useMemo(() => cls(
91
+ "border h-full w-80 min-w-80 mx-2 flex flex-col rounded-md",
92
+ defaultBorderMixin,
93
+ isColumnBeingDragged ? "ring-2 ring-primary" : ""
94
+ ), [isColumnBeingDragged]);
95
+
96
+ const headerClassName = useMemo(() => cls(
97
+ "flex items-center justify-between px-2 rounded-t-md transition-colors duration-200 ease-in-out",
98
+ isColumnBeingDragged
99
+ ? "bg-surface-100 dark:bg-surface-900"
100
+ : "bg-surface-50 hover:bg-surface-100 dark:bg-surface-950 dark:hover:bg-surface-900",
101
+ allowReorder ? "cursor-grab" : ""
102
+ ), [isColumnBeingDragged, allowReorder]);
103
+
104
+ // Memoize items IDs array to avoid recreating on each render
105
+ const itemIds = useMemo(() => items.map(i => i.id), [items]);
106
+
107
+ return (
108
+ <div
109
+ ref={setNodeRef}
110
+ style={combinedStyle}
111
+ {...attributes}
112
+ className={columnClassName}
113
+ >
114
+ <div
115
+ {...dragListeners}
116
+ className={headerClassName}
117
+ >
118
+ <div className="flex items-center gap-2">
119
+ <BoardColumnTitle aria-label={`${title} item list`} color={color}>
120
+ {title}
121
+ </BoardColumnTitle>
122
+ {totalCount !== undefined && (
123
+ <span className="text-xs text-surface-500 dark:text-surface-400">
124
+ {totalCount}
125
+ </span>
126
+ )}
127
+ </div>
128
+ {onAddItem && (
129
+ <IconButton
130
+ size="small"
131
+ onClick={(e: React.MouseEvent) => {
132
+ e.stopPropagation();
133
+ onAddItem();
134
+ }}
135
+ className="opacity-60 hover:opacity-100"
136
+ >
137
+ <AddIcon size="small"/>
138
+ </IconButton>
139
+ )}
140
+ </div>
141
+ <SortableContext
142
+ items={itemIds}
143
+ strategy={verticalListSortingStrategy}
144
+ >
145
+ <BoardSortableList
146
+ columnId={id}
147
+ items={items}
148
+ ItemComponent={ItemComponent}
149
+ isDragging={isDragging}
150
+ isDragOverColumn={isDragOverColumn}
151
+ loading={loading}
152
+ hasMore={hasMore}
153
+ onLoadMore={onLoadMore}
154
+ />
155
+ </SortableContext>
156
+ </div>
157
+ );
158
+ }) as <M extends Record<string, any>>(props: BoardColumnProps<M>) => React.ReactElement;
@@ -0,0 +1,45 @@
1
+ import { ChipColorKey, ChipColorScheme, getColorSchemeForKey, cls } from "@firecms/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
+ <h4
27
+ className={
28
+ 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",
29
+ "text-sm font-semibold text-surface-800 dark:text-surface-200",
30
+ className)
31
+ }
32
+ {...props}
33
+ >
34
+ {colorScheme && (
35
+ <div
36
+ className="w-3 h-3 rounded-full flex-shrink-0"
37
+ style={{
38
+ backgroundColor: colorScheme.color
39
+ }}
40
+ />
41
+ )}
42
+ {children}
43
+ </h4>
44
+ );
45
+ }
@@ -0,0 +1,172 @@
1
+ import React, { memo, useEffect, useMemo, useRef } from "react";
2
+ import { useDroppable } from "@dnd-kit/core";
3
+ import { useSortable } from "@dnd-kit/sortable";
4
+ import { CSS } from "@dnd-kit/utilities";
5
+ import { CircularProgress, cls } from "@firecms/ui";
6
+ import { BoardItem, BoardItemViewProps } from "./board_types";
7
+
8
+ interface BoardSortableListProps<M extends Record<string, any>> {
9
+ columnId: string;
10
+ items: BoardItem<M>[];
11
+ ItemComponent: React.ComponentType<BoardItemViewProps<M>>;
12
+ isDragging: boolean;
13
+ isDragOverColumn: boolean;
14
+ loading?: boolean;
15
+ hasMore?: boolean;
16
+ onLoadMore?: () => void;
17
+ }
18
+
19
+ export function BoardSortableList<M extends Record<string, any>>({
20
+ columnId,
21
+ items,
22
+ ItemComponent,
23
+ isDragging,
24
+ isDragOverColumn,
25
+ loading = false,
26
+ hasMore = false,
27
+ onLoadMore,
28
+ }: BoardSortableListProps<M>) {
29
+ const {
30
+ setNodeRef,
31
+ } = useDroppable({
32
+ id: columnId,
33
+ data: { type: "ITEM-LIST" }
34
+ });
35
+
36
+ // Infinite scroll sentinel ref - must be inside scrollable container
37
+ const sentinelRef = useRef<HTMLDivElement>(null);
38
+ const isLoadingRef = useRef(false);
39
+ isLoadingRef.current = loading;
40
+ const lastLoadTimeRef = useRef(0);
41
+
42
+ // Set up IntersectionObserver for infinite scroll
43
+ useEffect(() => {
44
+ if (!sentinelRef.current || !hasMore || !onLoadMore) return;
45
+
46
+ const sentinel = sentinelRef.current;
47
+
48
+ const observer = new IntersectionObserver(
49
+ (entries) => {
50
+ const now = Date.now();
51
+ if (
52
+ entries[0].isIntersecting &&
53
+ hasMore &&
54
+ !isLoadingRef.current &&
55
+ now - lastLoadTimeRef.current > 500
56
+ ) {
57
+ lastLoadTimeRef.current = now;
58
+ onLoadMore();
59
+ }
60
+ },
61
+ { threshold: 0.1 }
62
+ );
63
+
64
+ observer.observe(sentinel);
65
+
66
+ // Check if sentinel is already visible when effect runs
67
+ // This handles the case where the sentinel was visible before the observer was created
68
+ const rect = sentinel.getBoundingClientRect();
69
+ const containerRect = sentinel.parentElement?.getBoundingClientRect();
70
+ if (containerRect && rect.top < containerRect.bottom && rect.bottom > containerRect.top) {
71
+ const now = Date.now();
72
+ if (hasMore && !isLoadingRef.current && now - lastLoadTimeRef.current > 500) {
73
+ lastLoadTimeRef.current = now;
74
+ onLoadMore();
75
+ }
76
+ }
77
+
78
+ return () => observer.disconnect();
79
+ }, [hasMore, onLoadMore]);
80
+
81
+ // Memoize className to avoid recomputation on every render
82
+ const containerClassName = useMemo(() => cls(
83
+ "flex flex-col p-2 transition-opacity duration-100 transition-bg ease-linear w-full overflow-y-auto no-scrollbar flex-1 rounded-md",
84
+ isDragging && isDragOverColumn
85
+ ? "bg-surface-accent-200 dark:bg-surface-800"
86
+ : isDragging
87
+ ? "bg-surface-50 dark:bg-surface-950 hover:bg-surface-accent-100 dark:hover:bg-surface-800"
88
+ : "bg-surface-50 dark:bg-surface-950"
89
+ ), [isDragging, isDragOverColumn]);
90
+
91
+ return (
92
+ <div
93
+ ref={setNodeRef}
94
+ className={containerClassName}
95
+ style={{ minHeight: 80 }}
96
+ >
97
+ {items.length === 0 && !loading ? (
98
+ <div className="flex-1 flex items-center justify-center">
99
+ <span className="text-xs text-surface-400 dark:text-surface-500">
100
+ No items
101
+ </span>
102
+ </div>
103
+ ) : (
104
+ <>
105
+ {items.map((item, index) => (
106
+ <SortableItem
107
+ key={item.id}
108
+ item={item}
109
+ index={index}
110
+ columnId={columnId}
111
+ ItemComponent={ItemComponent}
112
+ />
113
+ ))}
114
+ {/* Infinite scroll sentinel - inside scrollable container */}
115
+ {(loading || hasMore) && (
116
+ <div ref={sentinelRef} className="flex items-center justify-center py-2 min-h-6">
117
+ {loading && <CircularProgress size="smallest" />}
118
+ </div>
119
+ )}
120
+ </>
121
+ )}
122
+ </div>
123
+ );
124
+ }
125
+
126
+ interface SortableItemProps<M extends Record<string, any>> {
127
+ item: BoardItem<M>;
128
+ index: number;
129
+ columnId: string;
130
+ ItemComponent: React.ComponentType<BoardItemViewProps<M>>;
131
+ }
132
+
133
+ // Memoized to prevent unnecessary re-renders when other items in the list change
134
+ const SortableItem = memo(function SortableItem<M extends Record<string, any>>({
135
+ item,
136
+ index,
137
+ columnId,
138
+ ItemComponent,
139
+ }: SortableItemProps<M>) {
140
+ const {
141
+ setNodeRef,
142
+ attributes,
143
+ listeners,
144
+ isDragging: isItemBeingDragged,
145
+ transform,
146
+ transition,
147
+ } = useSortable({
148
+ id: item.id,
149
+ data: {
150
+ type: "ITEM",
151
+ columnId
152
+ }
153
+ });
154
+
155
+ // Memoize style object to prevent object recreation on each render
156
+ const sortableStyle = useMemo(() => ({
157
+ transform: CSS.Transform.toString(transform),
158
+ transition,
159
+ zIndex: isItemBeingDragged ? 2 : 1,
160
+ opacity: isItemBeingDragged ? 0 : 1,
161
+ }), [transform, transition, isItemBeingDragged]);
162
+
163
+ return (
164
+ <div ref={setNodeRef} style={sortableStyle} {...attributes} {...listeners}>
165
+ <ItemComponent
166
+ item={item}
167
+ isDragging={isItemBeingDragged}
168
+ index={index}
169
+ />
170
+ </div>
171
+ );
172
+ }) as <M extends Record<string, any>>(props: SortableItemProps<M>) => React.ReactElement;