@rebasepro/ui 0.6.0 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/icons/index.d.ts +1 -1
- package/dist/index.css +13 -13
- package/dist/index.d.ts +1 -0
- package/dist/index.es.js +2119 -74
- package/dist/index.es.js.map +1 -1
- package/dist/src/index.css +13 -13
- package/dist/styles.d.ts +9 -9
- package/dist/views/CardView.d.ts +32 -0
- package/dist/views/CollectionView/CollectionCardView.d.ts +30 -0
- package/dist/views/CollectionView/CollectionKanbanView.d.ts +31 -0
- package/dist/views/CollectionView/CollectionListView.d.ts +30 -0
- package/dist/views/CollectionView/CollectionTableView.d.ts +37 -0
- package/dist/views/CollectionView/CollectionView.d.ts +121 -0
- package/dist/views/CollectionView/CollectionViewToolbar.d.ts +31 -0
- package/dist/views/CollectionView/CollectionViewTypes.d.ts +106 -0
- package/dist/views/CollectionView/DefaultCellRenderer.d.ts +9 -0
- package/dist/views/CollectionView/index.d.ts +24 -0
- package/dist/views/CollectionView/utils.d.ts +5 -0
- package/dist/views/Kanban/Board.d.ts +3 -0
- package/dist/views/Kanban/BoardColumn.d.ts +20 -0
- package/dist/views/Kanban/BoardColumnTitle.d.ts +8 -0
- package/dist/views/Kanban/BoardSortableList.d.ts +14 -0
- package/dist/views/Kanban/board_types.d.ts +61 -0
- package/dist/views/Kanban/index.d.ts +5 -0
- package/dist/views/KanbanView.d.ts +24 -0
- package/dist/views/ListView.d.ts +40 -0
- package/dist/views/TableView.d.ts +6 -0
- package/dist/views/index.d.ts +5 -0
- package/package.json +1 -1
- package/src/components/Alert.tsx +1 -1
- package/src/components/BooleanSwitchWithLabel.tsx +1 -1
- package/src/components/Button.tsx +5 -5
- package/src/components/Chip.tsx +39 -37
- package/src/components/DateTimeField.tsx +2 -2
- package/src/components/Dialog.tsx +1 -1
- package/src/components/ExpandablePanel.tsx +1 -1
- package/src/components/FileUpload.tsx +1 -1
- package/src/components/FilterChip.tsx +2 -2
- package/src/components/IconButton.tsx +1 -1
- package/src/components/MultiSelect.tsx +1 -1
- package/src/components/Select.tsx +2 -2
- package/src/components/Sheet.tsx +8 -4
- package/src/components/Table.tsx +2 -2
- package/src/components/TextField.tsx +3 -3
- package/src/components/VirtualTable/VirtualTableRow.tsx +1 -1
- package/src/icons/index.ts +3 -0
- package/src/index.css +13 -13
- package/src/index.ts +2 -0
- package/src/styles.ts +9 -9
- package/src/views/CardView.tsx +281 -0
- package/src/views/CollectionView/CollectionCardView.tsx +270 -0
- package/src/views/CollectionView/CollectionKanbanView.tsx +251 -0
- package/src/views/CollectionView/CollectionListView.tsx +245 -0
- package/src/views/CollectionView/CollectionTableView.tsx +229 -0
- package/src/views/CollectionView/CollectionView.tsx +388 -0
- package/src/views/CollectionView/CollectionViewToolbar.tsx +229 -0
- package/src/views/CollectionView/CollectionViewTypes.ts +137 -0
- package/src/views/CollectionView/DefaultCellRenderer.tsx +404 -0
- package/src/views/CollectionView/index.ts +44 -0
- package/src/views/CollectionView/utils.ts +14 -0
- package/src/views/Kanban/Board.tsx +421 -0
- package/src/views/Kanban/BoardColumn.tsx +135 -0
- package/src/views/Kanban/BoardColumnTitle.tsx +47 -0
- package/src/views/Kanban/BoardSortableList.tsx +170 -0
- package/src/views/Kanban/board_types.ts +70 -0
- package/src/views/Kanban/index.tsx +5 -0
- package/src/views/KanbanView.tsx +32 -0
- package/src/views/ListView.tsx +281 -0
- package/src/views/TableView.tsx +7 -0
- package/src/views/index.ts +5 -0
- package/dist/index.umd.js +0 -6958
- package/dist/index.umd.js.map +0 -1
|
@@ -0,0 +1,421 @@
|
|
|
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
|
+
useSensor,
|
|
11
|
+
useSensors
|
|
12
|
+
} from "@dnd-kit/core";
|
|
13
|
+
import { arrayMove, SortableContext } from "@dnd-kit/sortable";
|
|
14
|
+
import { BoardColumn } from "./BoardColumn";
|
|
15
|
+
import { BoardItem, BoardItemMap, BoardItemViewProps, BoardProps } from "./board_types";
|
|
16
|
+
import { cls } from "../../util";
|
|
17
|
+
|
|
18
|
+
export function Board<T, 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<T, COLUMN>) {
|
|
34
|
+
|
|
35
|
+
const [activeItem, setActiveItem] = useState<BoardItem<T> | 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
|
+
|
|
40
|
+
const grabOffsetRef = useRef<{ x: number; y: number }>({ x: 0, y: 0 });
|
|
41
|
+
const [overlayPos, setOverlayPos] = useState<{ x: number; y: number } | null>(null);
|
|
42
|
+
|
|
43
|
+
const handleMouseMove = useCallback((e: MouseEvent) => {
|
|
44
|
+
setOverlayPos({
|
|
45
|
+
x: e.clientX - grabOffsetRef.current.x,
|
|
46
|
+
y: e.clientY - grabOffsetRef.current.y
|
|
47
|
+
});
|
|
48
|
+
}, []);
|
|
49
|
+
|
|
50
|
+
useEffect(() => {
|
|
51
|
+
return () => {
|
|
52
|
+
window.removeEventListener("mousemove", handleMouseMove);
|
|
53
|
+
window.removeEventListener("pointermove", handleMouseMove);
|
|
54
|
+
};
|
|
55
|
+
}, [handleMouseMove]);
|
|
56
|
+
|
|
57
|
+
const [itemMapState, setItemMapState] = useState<BoardItemMap<T>>(() => {
|
|
58
|
+
const dataColumnMap: Record<string, COLUMN> = data.reduce((prev, item: BoardItem<T>) => ({
|
|
59
|
+
...prev,
|
|
60
|
+
[item.id]: assignColumn(item)
|
|
61
|
+
}), {});
|
|
62
|
+
return columnsProp.reduce(
|
|
63
|
+
(previous: BoardItemMap<T>, column: COLUMN) => ({
|
|
64
|
+
...previous,
|
|
65
|
+
[column]: data.filter((item: BoardItem<T>) => dataColumnMap[item.id] === column)
|
|
66
|
+
}),
|
|
67
|
+
{} as BoardItemMap<T>
|
|
68
|
+
);
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
const sensors = useSensors(
|
|
72
|
+
useSensor(PointerSensor, {
|
|
73
|
+
activationConstraint: {
|
|
74
|
+
distance: 5
|
|
75
|
+
}
|
|
76
|
+
})
|
|
77
|
+
);
|
|
78
|
+
|
|
79
|
+
useEffect(() => {
|
|
80
|
+
if (isDragging) return;
|
|
81
|
+
|
|
82
|
+
const dataColumnMap: Record<string, COLUMN> = data.reduce((prev, item) => ({
|
|
83
|
+
...prev,
|
|
84
|
+
[item.id]: assignColumn(item)
|
|
85
|
+
}), {});
|
|
86
|
+
|
|
87
|
+
const newItemMap = columnsProp.reduce(
|
|
88
|
+
(previous: BoardItemMap<T>, column: COLUMN) => ({
|
|
89
|
+
...previous,
|
|
90
|
+
[column]: data.filter((item: BoardItem<T>) => dataColumnMap[item.id] === column)
|
|
91
|
+
}),
|
|
92
|
+
{} as BoardItemMap<T>
|
|
93
|
+
);
|
|
94
|
+
|
|
95
|
+
setItemMapState(prevMap => {
|
|
96
|
+
let changed = false;
|
|
97
|
+
|
|
98
|
+
for (const col of columnsProp) {
|
|
99
|
+
const prevItems = prevMap[col] ?? [];
|
|
100
|
+
const newItems = newItemMap[col] ?? [];
|
|
101
|
+
|
|
102
|
+
if (prevItems.length !== newItems.length) {
|
|
103
|
+
changed = true;
|
|
104
|
+
continue;
|
|
105
|
+
}
|
|
106
|
+
for (let i = 0; i < prevItems.length; i++) {
|
|
107
|
+
if (prevItems[i].id !== newItems[i].id || prevItems[i].data !== newItems[i].data) {
|
|
108
|
+
changed = true;
|
|
109
|
+
break;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
if (!changed) {
|
|
115
|
+
return prevMap;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const updated: BoardItemMap<T> = {};
|
|
119
|
+
for (const col of columnsProp) {
|
|
120
|
+
const prevItems = prevMap[col] ?? [];
|
|
121
|
+
const newItems = newItemMap[col] ?? [];
|
|
122
|
+
|
|
123
|
+
const prevById = new Map<string, BoardItem<T>>();
|
|
124
|
+
for (const item of prevItems) {
|
|
125
|
+
prevById.set(item.id, item);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
updated[col] = newItems.map(newItem => {
|
|
129
|
+
const prev = prevById.get(newItem.id);
|
|
130
|
+
if (prev && prev.data === newItem.data) {
|
|
131
|
+
return prev;
|
|
132
|
+
}
|
|
133
|
+
return newItem;
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
return updated;
|
|
137
|
+
});
|
|
138
|
+
}, [data, columnsProp, assignColumn]);
|
|
139
|
+
|
|
140
|
+
const findColumnByItemId = (id: string): string | undefined => {
|
|
141
|
+
return Object.keys(itemMapState).find(col => itemMapState[col]?.some(i => i.id === id));
|
|
142
|
+
};
|
|
143
|
+
|
|
144
|
+
const handleDragStart = (event: DragStartEvent) => {
|
|
145
|
+
setIsDragging(true);
|
|
146
|
+
setDragOverColumnId(null);
|
|
147
|
+
const { active } = event;
|
|
148
|
+
|
|
149
|
+
const activatorEvt = event.activatorEvent as PointerEvent | MouseEvent;
|
|
150
|
+
if (activatorEvt) {
|
|
151
|
+
const target = activatorEvt.target as HTMLElement;
|
|
152
|
+
const draggableEl = target.closest<HTMLElement>("[role='button']") ?? target;
|
|
153
|
+
const rect = draggableEl.getBoundingClientRect();
|
|
154
|
+
grabOffsetRef.current = {
|
|
155
|
+
x: activatorEvt.clientX - rect.left,
|
|
156
|
+
y: activatorEvt.clientY - rect.top
|
|
157
|
+
};
|
|
158
|
+
setOverlayPos({
|
|
159
|
+
x: rect.left,
|
|
160
|
+
y: rect.top
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
window.addEventListener("mousemove", handleMouseMove);
|
|
164
|
+
window.addEventListener("pointermove", handleMouseMove);
|
|
165
|
+
|
|
166
|
+
if (active.data.current?.type === "COLUMN") {
|
|
167
|
+
const columnId = active.id as string;
|
|
168
|
+
const column = columnsProp.find(col => String(col) === columnId);
|
|
169
|
+
if (column) {
|
|
170
|
+
setActiveColumn(column);
|
|
171
|
+
}
|
|
172
|
+
} else if (active.data.current?.type === "ITEM") {
|
|
173
|
+
const columnId = findColumnByItemId(active.id as string);
|
|
174
|
+
if (columnId) {
|
|
175
|
+
const item = itemMapState[columnId]?.find(i => i.id === active.id);
|
|
176
|
+
setActiveItem(item || null);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
};
|
|
180
|
+
|
|
181
|
+
const handleDragOver = (event: DragOverEvent) => {
|
|
182
|
+
const {
|
|
183
|
+
active,
|
|
184
|
+
over
|
|
185
|
+
} = event;
|
|
186
|
+
|
|
187
|
+
if (!over) {
|
|
188
|
+
setDragOverColumnId(null);
|
|
189
|
+
return;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
let currentHoveredColumnId: string | null = null;
|
|
193
|
+
const overId = over.id as string;
|
|
194
|
+
const overDataType = over.data.current?.type as string | undefined;
|
|
195
|
+
|
|
196
|
+
if (overDataType === "ITEM-LIST" || overDataType === "COLUMN") {
|
|
197
|
+
currentHoveredColumnId = overId;
|
|
198
|
+
} else if (overDataType === "ITEM") {
|
|
199
|
+
currentHoveredColumnId = findColumnByItemId(overId) || null;
|
|
200
|
+
} else if (columnsProp.includes(overId as COLUMN)) {
|
|
201
|
+
currentHoveredColumnId = overId;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
setDragOverColumnId(currentHoveredColumnId);
|
|
205
|
+
|
|
206
|
+
if (active.data.current?.type !== "ITEM") {
|
|
207
|
+
return;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
const activeId = active.id as string;
|
|
211
|
+
const activeColumn = findColumnByItemId(activeId);
|
|
212
|
+
let overColumnForMove = findColumnByItemId(overId);
|
|
213
|
+
|
|
214
|
+
if (!overColumnForMove && overDataType === "ITEM-LIST") {
|
|
215
|
+
overColumnForMove = overId;
|
|
216
|
+
}
|
|
217
|
+
if (!overColumnForMove && columnsProp.includes(overId as COLUMN)) {
|
|
218
|
+
overColumnForMove = overId;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
if (!activeColumn || !overColumnForMove) return;
|
|
222
|
+
if (activeColumn === overColumnForMove) return;
|
|
223
|
+
|
|
224
|
+
if (itemMapState[overColumnForMove]?.some(i => i.id === activeId)) {
|
|
225
|
+
return;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
setItemMapState(currentMap => {
|
|
229
|
+
const activeItems = [...(currentMap[activeColumn] || [])];
|
|
230
|
+
const overItems = [...(currentMap[overColumnForMove!] || [])];
|
|
231
|
+
const activeIndex = activeItems.findIndex(i => i.id === activeId);
|
|
232
|
+
|
|
233
|
+
if (activeIndex === -1) return currentMap;
|
|
234
|
+
|
|
235
|
+
let overIndex;
|
|
236
|
+
if (overDataType === "ITEM-LIST" || (columnsProp.includes(overId as COLUMN) && !findColumnByItemId(overId))) {
|
|
237
|
+
overIndex = overItems.length;
|
|
238
|
+
} else {
|
|
239
|
+
overIndex = overItems.findIndex(i => i.id === overId);
|
|
240
|
+
if (overIndex !== -1) {
|
|
241
|
+
const activeTop = active.rect.current.translated?.top ?? 0;
|
|
242
|
+
const activeHeight = active.rect.current.translated?.height ?? 0;
|
|
243
|
+
const activeCenter = activeTop + activeHeight / 2;
|
|
244
|
+
|
|
245
|
+
const overTop = over?.rect.top ?? 0;
|
|
246
|
+
const overHeight = over?.rect.height ?? 0;
|
|
247
|
+
const overCenter = overTop + overHeight / 2;
|
|
248
|
+
|
|
249
|
+
const isBelowOverItem = activeCenter > overCenter;
|
|
250
|
+
|
|
251
|
+
const modifier = isBelowOverItem ? 1 : 0;
|
|
252
|
+
overIndex = overIndex >= 0 ? overIndex + modifier : overItems.length;
|
|
253
|
+
} else {
|
|
254
|
+
overIndex = overItems.length;
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
const newItemMap = { ...currentMap };
|
|
259
|
+
const [moved] = activeItems.splice(activeIndex, 1);
|
|
260
|
+
overItems.splice(overIndex, 0, moved);
|
|
261
|
+
newItemMap[activeColumn] = activeItems;
|
|
262
|
+
newItemMap[overColumnForMove!] = overItems;
|
|
263
|
+
return newItemMap;
|
|
264
|
+
});
|
|
265
|
+
};
|
|
266
|
+
|
|
267
|
+
const handleDragEnd = (event: DragEndEvent) => {
|
|
268
|
+
const {
|
|
269
|
+
active,
|
|
270
|
+
over
|
|
271
|
+
} = event;
|
|
272
|
+
|
|
273
|
+
window.removeEventListener("mousemove", handleMouseMove);
|
|
274
|
+
window.removeEventListener("pointermove", handleMouseMove);
|
|
275
|
+
setOverlayPos(null);
|
|
276
|
+
|
|
277
|
+
setIsDragging(false);
|
|
278
|
+
setActiveItem(null);
|
|
279
|
+
setActiveColumn(null);
|
|
280
|
+
setDragOverColumnId(null);
|
|
281
|
+
|
|
282
|
+
if (!over) return;
|
|
283
|
+
|
|
284
|
+
const activeId = active.id as string;
|
|
285
|
+
const overId = over.id as string;
|
|
286
|
+
|
|
287
|
+
if (active.data.current?.type === "COLUMN") {
|
|
288
|
+
if (activeId !== overId) {
|
|
289
|
+
const oldIndex = columnsProp.indexOf(activeId as COLUMN);
|
|
290
|
+
const newIndex = columnsProp.indexOf(overId as COLUMN);
|
|
291
|
+
if (oldIndex !== -1 && newIndex !== -1) {
|
|
292
|
+
const reordered = arrayMove(columnsProp, oldIndex, newIndex);
|
|
293
|
+
onColumnReorder?.(reordered);
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
return;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
const activeCol = findColumnByItemId(activeId) as COLUMN | undefined;
|
|
300
|
+
let overCol = findColumnByItemId(overId) as COLUMN | undefined;
|
|
301
|
+
|
|
302
|
+
if (!overCol) {
|
|
303
|
+
const overDataType = over.data.current?.type;
|
|
304
|
+
if (overDataType === "ITEM-LIST" || columnsProp.includes(overId as COLUMN)) {
|
|
305
|
+
overCol = overId as COLUMN;
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
if (!activeCol || !overCol) return;
|
|
310
|
+
|
|
311
|
+
const isSameColumn = activeCol === overCol;
|
|
312
|
+
const activeItems = itemMapState[activeCol] || [];
|
|
313
|
+
const overItems = itemMapState[overCol] || [];
|
|
314
|
+
|
|
315
|
+
const activeIndex = activeItems.findIndex(i => i.id === activeId);
|
|
316
|
+
|
|
317
|
+
let overIndex;
|
|
318
|
+
if (over.id === overCol) {
|
|
319
|
+
overIndex = overItems.length;
|
|
320
|
+
} else {
|
|
321
|
+
overIndex = overItems.findIndex(i => i.id === overId);
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
if (activeIndex === -1 || overIndex === -1) return;
|
|
325
|
+
|
|
326
|
+
let finalItems: BoardItem<T>[] = [];
|
|
327
|
+
|
|
328
|
+
if (isSameColumn) {
|
|
329
|
+
finalItems = arrayMove(activeItems, activeIndex, overIndex);
|
|
330
|
+
} else {
|
|
331
|
+
const newActiveItems = [...activeItems];
|
|
332
|
+
const newOverItems = [...overItems];
|
|
333
|
+
const [moved] = newActiveItems.splice(activeIndex, 1);
|
|
334
|
+
newOverItems.splice(overIndex, 0, moved);
|
|
335
|
+
finalItems = [...newActiveItems, ...newOverItems];
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
const fullFlattenedList: BoardItem<T>[] = [];
|
|
339
|
+
columnsProp.forEach(col => {
|
|
340
|
+
if (col === activeCol) {
|
|
341
|
+
if (isSameColumn) {
|
|
342
|
+
fullFlattenedList.push(...finalItems);
|
|
343
|
+
} else {
|
|
344
|
+
fullFlattenedList.push(...activeItems.filter(i => i.id !== activeId));
|
|
345
|
+
}
|
|
346
|
+
} else if (col === overCol) {
|
|
347
|
+
const newOverItems = [...overItems];
|
|
348
|
+
const [moved] = [...activeItems].splice(activeIndex, 1);
|
|
349
|
+
newOverItems.splice(overIndex, 0, moved);
|
|
350
|
+
fullFlattenedList.push(...newOverItems);
|
|
351
|
+
} else {
|
|
352
|
+
fullFlattenedList.push(...(itemMapState[col] || []));
|
|
353
|
+
}
|
|
354
|
+
});
|
|
355
|
+
|
|
356
|
+
onItemsReorder?.(fullFlattenedList, {
|
|
357
|
+
itemId: activeId,
|
|
358
|
+
sourceColumn: activeCol,
|
|
359
|
+
targetColumn: overCol
|
|
360
|
+
});
|
|
361
|
+
};
|
|
362
|
+
|
|
363
|
+
return (
|
|
364
|
+
<DndContext
|
|
365
|
+
sensors={sensors}
|
|
366
|
+
onDragStart={handleDragStart}
|
|
367
|
+
onDragOver={handleDragOver}
|
|
368
|
+
onDragEnd={handleDragEnd}
|
|
369
|
+
>
|
|
370
|
+
<div className={cls("flex flex-row h-full w-full overflow-x-auto p-4 select-none items-start", className)}>
|
|
371
|
+
<SortableContext items={columnsProp}>
|
|
372
|
+
{columnsProp.map((col, index) => {
|
|
373
|
+
const colItems = itemMapState[col] || [];
|
|
374
|
+
const loadingState = columnLoadingState?.[col];
|
|
375
|
+
return (
|
|
376
|
+
<BoardColumn
|
|
377
|
+
key={String(col)}
|
|
378
|
+
id={String(col)}
|
|
379
|
+
title={columnLabels?.[col] ?? String(col)}
|
|
380
|
+
items={colItems}
|
|
381
|
+
index={index}
|
|
382
|
+
ItemComponent={ItemComponent}
|
|
383
|
+
isDragging={isDragging}
|
|
384
|
+
isDragOverColumn={dragOverColumnId === String(col)}
|
|
385
|
+
allowReorder={allowColumnReorder}
|
|
386
|
+
loading={loadingState?.loading}
|
|
387
|
+
hasMore={loadingState?.hasMore}
|
|
388
|
+
totalCount={loadingState?.totalCount}
|
|
389
|
+
color={columnColors?.[col]}
|
|
390
|
+
onLoadMore={onLoadMoreColumn ? () => onLoadMoreColumn(col) : undefined}
|
|
391
|
+
onAddItem={onAddItemToColumn ? () => onAddItemToColumn(col) : undefined}
|
|
392
|
+
/>
|
|
393
|
+
);
|
|
394
|
+
})}
|
|
395
|
+
</SortableContext>
|
|
396
|
+
{AddColumnComponent}
|
|
397
|
+
</div>
|
|
398
|
+
|
|
399
|
+
{typeof document !== "undefined" && createPortal(
|
|
400
|
+
<DragOverlay dropAnimation={null}>
|
|
401
|
+
{isDragging && activeItem && (
|
|
402
|
+
<div style={{
|
|
403
|
+
position: "fixed",
|
|
404
|
+
left: overlayPos?.x ?? 0,
|
|
405
|
+
top: overlayPos?.y ?? 0,
|
|
406
|
+
pointerEvents: "none",
|
|
407
|
+
zIndex: 9999
|
|
408
|
+
}}>
|
|
409
|
+
<ItemComponent
|
|
410
|
+
item={activeItem}
|
|
411
|
+
isDragging={true}
|
|
412
|
+
isClone={true}
|
|
413
|
+
/>
|
|
414
|
+
</div>
|
|
415
|
+
)}
|
|
416
|
+
</DragOverlay>,
|
|
417
|
+
document.body
|
|
418
|
+
)}
|
|
419
|
+
</DndContext>
|
|
420
|
+
);
|
|
421
|
+
}
|
|
@@ -0,0 +1,135 @@
|
|
|
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
|
+
IconButton
|
|
9
|
+
} from "../../components";
|
|
10
|
+
import { cls } from "../../util";
|
|
11
|
+
import { iconSize, PlusIcon } from "../../icons";
|
|
12
|
+
|
|
13
|
+
export interface BoardColumnProps<T> {
|
|
14
|
+
id: string;
|
|
15
|
+
title: string;
|
|
16
|
+
items: BoardItem<T>[];
|
|
17
|
+
index: number;
|
|
18
|
+
ItemComponent: React.ComponentType<BoardItemViewProps<T>>;
|
|
19
|
+
isDragging: boolean;
|
|
20
|
+
isDragOverColumn: boolean;
|
|
21
|
+
allowReorder?: boolean;
|
|
22
|
+
loading?: boolean;
|
|
23
|
+
hasMore?: boolean;
|
|
24
|
+
onLoadMore?: () => void;
|
|
25
|
+
onAddItem?: () => void;
|
|
26
|
+
totalCount?: number;
|
|
27
|
+
color?: any;
|
|
28
|
+
style?: React.CSSProperties;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export const BoardColumn = memo(function BoardColumn<T>({
|
|
32
|
+
id,
|
|
33
|
+
title,
|
|
34
|
+
items,
|
|
35
|
+
ItemComponent,
|
|
36
|
+
isDragging,
|
|
37
|
+
isDragOverColumn,
|
|
38
|
+
allowReorder = false,
|
|
39
|
+
loading = false,
|
|
40
|
+
hasMore = false,
|
|
41
|
+
onLoadMore,
|
|
42
|
+
onAddItem,
|
|
43
|
+
totalCount,
|
|
44
|
+
color,
|
|
45
|
+
style
|
|
46
|
+
}: BoardColumnProps<T>) {
|
|
47
|
+
const {
|
|
48
|
+
setNodeRef,
|
|
49
|
+
attributes,
|
|
50
|
+
listeners,
|
|
51
|
+
isDragging: isColumnBeingDragged,
|
|
52
|
+
transform,
|
|
53
|
+
transition
|
|
54
|
+
} = useSortable({
|
|
55
|
+
id,
|
|
56
|
+
data: { type: "COLUMN" },
|
|
57
|
+
disabled: !allowReorder
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
const combinedStyle = useMemo(() => ({
|
|
61
|
+
...style,
|
|
62
|
+
transform: CSS.Translate.toString(transform),
|
|
63
|
+
transition,
|
|
64
|
+
zIndex: isColumnBeingDragged ? 2 : 1
|
|
65
|
+
}), [style, transform, transition, isColumnBeingDragged]);
|
|
66
|
+
|
|
67
|
+
const dragListeners = allowReorder ? listeners : {};
|
|
68
|
+
|
|
69
|
+
const columnClassName = useMemo(() => cls(
|
|
70
|
+
"border h-full w-80 min-w-80 mx-2 flex flex-col rounded-md border-surface-200 dark:border-surface-800",
|
|
71
|
+
isColumnBeingDragged ? "ring-2 ring-primary" : ""
|
|
72
|
+
), [isColumnBeingDragged]);
|
|
73
|
+
|
|
74
|
+
const headerClassName = useMemo(() => cls(
|
|
75
|
+
"flex items-center justify-between px-2 rounded-t-md transition-colors duration-200 ease-in-out",
|
|
76
|
+
isColumnBeingDragged
|
|
77
|
+
? "bg-surface-100 dark:bg-surface-700"
|
|
78
|
+
: "bg-surface-50 hover:bg-surface-100 dark:bg-surface-800 dark:hover:bg-surface-700",
|
|
79
|
+
allowReorder ? "cursor-grab" : ""
|
|
80
|
+
), [isColumnBeingDragged, allowReorder]);
|
|
81
|
+
|
|
82
|
+
const itemIds = useMemo(() => items.map(i => i.id), [items]);
|
|
83
|
+
|
|
84
|
+
return (
|
|
85
|
+
<div
|
|
86
|
+
ref={setNodeRef}
|
|
87
|
+
style={combinedStyle}
|
|
88
|
+
{...attributes}
|
|
89
|
+
className={columnClassName}
|
|
90
|
+
>
|
|
91
|
+
<div
|
|
92
|
+
{...dragListeners}
|
|
93
|
+
className={headerClassName}
|
|
94
|
+
>
|
|
95
|
+
<div className="flex items-center gap-2">
|
|
96
|
+
<BoardColumnTitle aria-label={`${title} item list`} color={color}>
|
|
97
|
+
{title}
|
|
98
|
+
</BoardColumnTitle>
|
|
99
|
+
{totalCount !== undefined && (
|
|
100
|
+
<span className="text-xs text-surface-500 dark:text-surface-400">
|
|
101
|
+
{totalCount}
|
|
102
|
+
</span>
|
|
103
|
+
)}
|
|
104
|
+
</div>
|
|
105
|
+
{onAddItem && (
|
|
106
|
+
<IconButton
|
|
107
|
+
size="small"
|
|
108
|
+
onClick={(e: React.MouseEvent) => {
|
|
109
|
+
e.stopPropagation();
|
|
110
|
+
onAddItem();
|
|
111
|
+
}}
|
|
112
|
+
className="opacity-60 hover:opacity-100"
|
|
113
|
+
>
|
|
114
|
+
<PlusIcon size={iconSize.small}/>
|
|
115
|
+
</IconButton>
|
|
116
|
+
)}
|
|
117
|
+
</div>
|
|
118
|
+
<SortableContext
|
|
119
|
+
items={itemIds}
|
|
120
|
+
strategy={verticalListSortingStrategy}
|
|
121
|
+
>
|
|
122
|
+
<BoardSortableList
|
|
123
|
+
columnId={id}
|
|
124
|
+
items={items}
|
|
125
|
+
ItemComponent={ItemComponent}
|
|
126
|
+
isDragging={isDragging}
|
|
127
|
+
isDragOverColumn={isDragOverColumn}
|
|
128
|
+
loading={loading}
|
|
129
|
+
hasMore={hasMore}
|
|
130
|
+
onLoadMore={onLoadMore}
|
|
131
|
+
/>
|
|
132
|
+
</SortableContext>
|
|
133
|
+
</div>
|
|
134
|
+
);
|
|
135
|
+
}) as <T>(props: BoardColumnProps<T>) => React.ReactElement;
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import React, { useMemo } from "react";
|
|
2
|
+
import { Typography } from "../../components";
|
|
3
|
+
import { cls, getColorSchemeForKey } from "../../util";
|
|
4
|
+
|
|
5
|
+
export interface BoardColumnTitleProps {
|
|
6
|
+
children: React.ReactNode;
|
|
7
|
+
className?: string;
|
|
8
|
+
"aria-label"?: string;
|
|
9
|
+
color?: any;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function BoardColumnTitle({
|
|
13
|
+
children,
|
|
14
|
+
className,
|
|
15
|
+
color,
|
|
16
|
+
...props
|
|
17
|
+
}: BoardColumnTitleProps) {
|
|
18
|
+
const colorScheme = useMemo(() => {
|
|
19
|
+
if (!color) return undefined;
|
|
20
|
+
if (typeof color === "string") {
|
|
21
|
+
return getColorSchemeForKey(color);
|
|
22
|
+
}
|
|
23
|
+
return color;
|
|
24
|
+
}, [color]);
|
|
25
|
+
|
|
26
|
+
return (
|
|
27
|
+
<Typography
|
|
28
|
+
variant="subtitle2"
|
|
29
|
+
component="h4"
|
|
30
|
+
className={
|
|
31
|
+
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",
|
|
32
|
+
className)
|
|
33
|
+
}
|
|
34
|
+
{...props}
|
|
35
|
+
>
|
|
36
|
+
{colorScheme && (
|
|
37
|
+
<div
|
|
38
|
+
className="w-3 h-3 rounded-full flex-shrink-0"
|
|
39
|
+
style={{
|
|
40
|
+
backgroundColor: colorScheme.darkColor ?? colorScheme.color
|
|
41
|
+
}}
|
|
42
|
+
/>
|
|
43
|
+
)}
|
|
44
|
+
{children}
|
|
45
|
+
</Typography>
|
|
46
|
+
);
|
|
47
|
+
}
|