@firecms/core 3.0.1 → 3.1.0-canary.1df3b2c
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/README.md +1 -1
- package/dist/components/AIIcon.d.ts +16 -0
- package/dist/components/EntityCollectionTable/EntityCollectionRowActions.d.ts +7 -1
- package/dist/components/EntityCollectionTable/EntityCollectionTable.d.ts +1 -1
- package/dist/components/EntityCollectionTable/EntityCollectionTableProps.d.ts +14 -0
- package/dist/components/EntityCollectionTable/PropertyTableCell.d.ts +6 -0
- package/dist/components/EntityCollectionTable/internal/CollectionTableToolbar.d.ts +5 -4
- package/dist/components/EntityCollectionTable/internal/EntityTableCell.d.ts +6 -0
- package/dist/components/EntityCollectionView/Board.d.ts +2 -0
- package/dist/components/EntityCollectionView/BoardColumn.d.ts +42 -0
- package/dist/components/EntityCollectionView/BoardColumnTitle.d.ts +9 -0
- package/dist/components/EntityCollectionView/BoardSortableList.d.ts +14 -0
- package/dist/components/EntityCollectionView/EntityBoardCard.d.ts +26 -0
- package/dist/components/EntityCollectionView/EntityCard.d.ts +19 -0
- package/dist/components/EntityCollectionView/EntityCollectionBoardView.d.ts +20 -0
- package/dist/components/EntityCollectionView/EntityCollectionCardView.d.ts +31 -0
- package/dist/components/EntityCollectionView/EntityCollectionViewActions.d.ts +2 -2
- package/dist/components/EntityCollectionView/EntityCollectionViewStartActions.d.ts +7 -3
- package/dist/components/EntityCollectionView/FiltersDialog.d.ts +14 -0
- package/dist/components/EntityCollectionView/ViewModeToggle.d.ts +49 -0
- package/dist/components/EntityCollectionView/board_types.d.ts +105 -0
- package/dist/components/EntityCollectionView/useBoardDataController.d.ts +60 -0
- package/dist/components/SelectableTable/SelectableTable.d.ts +5 -1
- package/dist/components/SelectableTable/filters/DateTimeFilterField.d.ts +2 -1
- package/dist/components/VirtualTable/VirtualTableCell.d.ts +6 -0
- package/dist/components/VirtualTable/VirtualTableHeader.d.ts +2 -0
- package/dist/components/VirtualTable/VirtualTableHeaderRow.d.ts +1 -1
- package/dist/components/VirtualTable/VirtualTableProps.d.ts +11 -0
- package/dist/components/VirtualTable/fields/VirtualTableDateField.d.ts +1 -0
- package/dist/components/VirtualTable/types.d.ts +2 -0
- package/dist/components/index.d.ts +3 -0
- package/dist/contexts/index.d.ts +10 -0
- package/dist/core/DrawerNavigationGroup.d.ts +45 -0
- package/dist/core/index.d.ts +1 -0
- package/dist/form/validation.d.ts +3 -2
- package/dist/hooks/useBreadcrumbsController.d.ts +16 -0
- package/dist/hooks/useCollapsedGroups.d.ts +4 -1
- package/dist/index.es.js +5239 -1590
- package/dist/index.es.js.map +1 -1
- package/dist/index.umd.js +5233 -1585
- package/dist/index.umd.js.map +1 -1
- package/dist/preview/PropertyPreviewProps.d.ts +5 -0
- package/dist/preview/components/DatePreview.d.ts +13 -3
- package/dist/preview/components/ImagePreview.d.ts +5 -1
- package/dist/preview/components/StorageThumbnail.d.ts +2 -1
- package/dist/preview/components/UrlComponentPreview.d.ts +2 -1
- package/dist/preview/property_previews/ArrayOfStorageComponentsPreview.d.ts +1 -1
- package/dist/preview/property_previews/ArrayOfStringsPreview.d.ts +1 -1
- package/dist/preview/property_previews/SkeletonPropertyComponent.d.ts +1 -1
- package/dist/types/collections.d.ts +42 -2
- package/dist/types/datasource.d.ts +0 -1
- package/dist/types/plugins.d.ts +46 -1
- package/dist/types/properties.d.ts +259 -4
- package/dist/util/__tests__/conditions.test.d.ts +1 -0
- package/dist/util/__tests__/objects.test.d.ts +1 -0
- package/dist/util/conditions.d.ts +26 -0
- package/dist/util/entities.d.ts +1 -2
- package/dist/util/index.d.ts +2 -1
- package/dist/util/property_utils.d.ts +2 -1
- package/dist/util/resolutions.d.ts +1 -1
- package/package.json +10 -7
- package/src/app/Scaffold.tsx +14 -15
- package/src/components/AIIcon.tsx +39 -0
- package/src/components/ArrayContainer.tsx +1 -4
- package/src/components/ClearFilterSortButton.tsx +19 -16
- package/src/components/ConfirmationDialog.tsx +0 -2
- package/src/components/DeleteEntityDialog.tsx +2 -4
- package/src/components/EntityCollectionTable/EntityCollectionRowActions.tsx +74 -41
- package/src/components/EntityCollectionTable/EntityCollectionTable.tsx +130 -79
- package/src/components/EntityCollectionTable/EntityCollectionTableProps.tsx +121 -104
- package/src/components/EntityCollectionTable/PropertyTableCell.tsx +132 -103
- package/src/components/EntityCollectionTable/internal/CollectionTableToolbar.tsx +20 -42
- package/src/components/EntityCollectionTable/internal/EntityTableCell.tsx +90 -49
- package/src/components/EntityCollectionView/Board.tsx +324 -0
- package/src/components/EntityCollectionView/BoardColumn.tsx +158 -0
- package/src/components/EntityCollectionView/BoardColumnTitle.tsx +45 -0
- package/src/components/EntityCollectionView/BoardSortableList.tsx +172 -0
- package/src/components/EntityCollectionView/EntityBoardCard.tsx +212 -0
- package/src/components/EntityCollectionView/EntityCard.tsx +231 -0
- package/src/components/EntityCollectionView/EntityCollectionBoardView.tsx +713 -0
- package/src/components/EntityCollectionView/EntityCollectionCardView.tsx +244 -0
- package/src/components/EntityCollectionView/EntityCollectionView.tsx +485 -203
- package/src/components/EntityCollectionView/EntityCollectionViewActions.tsx +31 -19
- package/src/components/EntityCollectionView/EntityCollectionViewStartActions.tsx +84 -15
- package/src/components/EntityCollectionView/FiltersDialog.tsx +249 -0
- package/src/components/EntityCollectionView/ViewModeToggle.tsx +202 -0
- package/src/components/EntityCollectionView/board_types.ts +113 -0
- package/src/components/EntityCollectionView/useBoardDataController.tsx +490 -0
- package/src/components/ErrorTooltip.tsx +2 -1
- package/src/components/HomePage/DefaultHomePage.tsx +47 -10
- package/src/components/HomePage/HomePageDnD.tsx +56 -41
- package/src/components/HomePage/NavigationCard.tsx +20 -18
- package/src/components/HomePage/NavigationGroup.tsx +17 -16
- package/src/components/HomePage/RenameGroupDialog.tsx +0 -2
- package/src/components/HomePage/SmallNavigationCard.tsx +10 -9
- package/src/components/ReferenceTable/ReferenceSelectionTable.tsx +3 -10
- package/src/components/ReferenceWidget.tsx +2 -4
- package/src/components/SelectableTable/SelectableTable.tsx +75 -67
- package/src/components/SelectableTable/filters/BooleanFilterField.tsx +7 -6
- package/src/components/SelectableTable/filters/DateTimeFilterField.tsx +39 -40
- package/src/components/SelectableTable/filters/ReferenceFilterField.tsx +38 -38
- package/src/components/SelectableTable/filters/StringNumberFilterField.tsx +49 -58
- package/src/components/UnsavedChangesDialog.tsx +0 -2
- package/src/components/UserDisplay.tsx +4 -4
- package/src/components/VirtualTable/VirtualTable.tsx +170 -19
- package/src/components/VirtualTable/VirtualTableCell.tsx +18 -2
- package/src/components/VirtualTable/VirtualTableHeader.tsx +20 -11
- package/src/components/VirtualTable/VirtualTableHeaderRow.tsx +158 -42
- package/src/components/VirtualTable/VirtualTableProps.tsx +14 -1
- package/src/components/VirtualTable/VirtualTableRow.tsx +1 -1
- package/src/components/VirtualTable/fields/VirtualTableDateField.tsx +3 -0
- package/src/components/VirtualTable/fields/VirtualTableSelect.tsx +17 -4
- package/src/components/VirtualTable/types.tsx +2 -0
- package/src/components/common/useColumnsIds.tsx +95 -3
- package/src/components/index.tsx +4 -0
- package/src/contexts/BreacrumbsContext.tsx +15 -8
- package/src/contexts/index.ts +10 -0
- package/src/core/DefaultAppBar.tsx +39 -26
- package/src/core/DefaultDrawer.tsx +42 -56
- package/src/core/DrawerNavigationGroup.tsx +118 -0
- package/src/core/DrawerNavigationItem.tsx +4 -3
- package/src/core/EntityEditView.tsx +41 -43
- package/src/core/SideDialogs.tsx +4 -2
- package/src/core/index.tsx +1 -0
- package/src/form/PropertyFieldBinding.tsx +58 -43
- package/src/form/components/StorageItemPreview.tsx +2 -1
- package/src/form/field_bindings/ArrayOfReferencesFieldBinding.tsx +0 -1
- package/src/form/field_bindings/DateTimeFieldBinding.tsx +17 -16
- package/src/form/field_bindings/KeyValueFieldBinding.tsx +0 -1
- package/src/form/field_bindings/MapFieldBinding.tsx +69 -67
- package/src/form/field_bindings/MarkdownEditorFieldBinding.tsx +21 -17
- package/src/form/field_bindings/TextFieldBinding.tsx +71 -35
- package/src/form/validation.ts +245 -160
- package/src/hooks/useBreadcrumbsController.tsx +18 -0
- package/src/hooks/useBuildNavigationController.tsx +42 -19
- package/src/hooks/useCollapsedGroups.ts +12 -4
- package/src/internal/useBuildDataSource.ts +69 -34
- package/src/internal/useBuildSideDialogsController.tsx +11 -8
- package/src/internal/useBuildSideEntityController.tsx +2 -4
- package/src/internal/useRestoreScroll.tsx +26 -14
- package/src/preview/PropertyPreview.tsx +40 -32
- package/src/preview/PropertyPreviewProps.tsx +6 -0
- package/src/preview/components/DatePreview.tsx +72 -4
- package/src/preview/components/EmptyValue.tsx +1 -1
- package/src/preview/components/ImagePreview.tsx +37 -21
- package/src/preview/components/StorageThumbnail.tsx +16 -12
- package/src/preview/components/UrlComponentPreview.tsx +28 -25
- package/src/preview/property_previews/ArrayOfStorageComponentsPreview.tsx +9 -7
- package/src/preview/property_previews/ArrayOfStringsPreview.tsx +11 -9
- package/src/preview/property_previews/ArrayPropertyPreview.tsx +26 -24
- package/src/preview/property_previews/SkeletonPropertyComponent.tsx +61 -56
- package/src/routes/CustomCMSRoute.tsx +1 -0
- package/src/routes/FireCMSRoute.tsx +26 -13
- package/src/types/collections.ts +48 -3
- package/src/types/datasource.ts +54 -56
- package/src/types/plugins.tsx +51 -1
- package/src/types/properties.ts +347 -27
- package/src/util/__tests__/conditions.test.ts +506 -0
- package/src/util/__tests__/objects.test.ts +196 -0
- package/src/util/callbacks.ts +6 -3
- package/src/util/collections.ts +51 -6
- package/src/util/conditions.ts +339 -0
- package/src/util/entities.ts +28 -29
- package/src/util/entity_cache.ts +2 -1
- package/src/util/index.ts +2 -1
- package/src/util/objects.ts +31 -13
- package/src/util/{references.ts → previews.ts} +14 -0
- package/src/util/property_utils.tsx +36 -10
- package/src/util/resolutions.ts +57 -55
- /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;
|