@monolith-forensics/monolith-ui 1.3.111 → 1.3.112-dev.1
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/Kanban/Kanban.d.ts +2 -0
- package/dist/Kanban/Kanban.js +234 -0
- package/dist/Kanban/KanbanColumn.d.ts +24 -0
- package/dist/Kanban/KanbanColumn.js +118 -0
- package/dist/Kanban/board.d.ts +12 -0
- package/dist/Kanban/board.js +84 -0
- package/dist/Kanban/constants.d.ts +4 -0
- package/dist/Kanban/constants.js +14 -0
- package/dist/Kanban/index.d.ts +2 -0
- package/dist/Kanban/index.js +2 -0
- package/dist/Kanban/styles.d.ts +17 -0
- package/dist/Kanban/styles.js +64 -0
- package/dist/Kanban/types.d.ts +67 -0
- package/dist/Kanban/types.js +1 -0
- package/dist/Kanban/useKanbanBoardData.d.ts +16 -0
- package/dist/Kanban/useKanbanBoardData.js +59 -0
- package/dist/Kanban/useKanbanRowHeights.d.ts +7 -0
- package/dist/Kanban/useKanbanRowHeights.js +36 -0
- package/dist/Kanban/utils.d.ts +13 -0
- package/dist/Kanban/utils.js +19 -0
- package/dist/Kanban/virtualRow.d.ts +14 -0
- package/dist/Kanban/virtualRow.js +56 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/package.json +5 -1
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
import { KanbanGroupKey, KanbanProps } from "./types";
|
|
2
|
+
export declare const Kanban: <TCard, TGroup extends KanbanGroupKey>({ cards, getCardId, getCardGroup, getCardOrder, renderCard, onCardsReorder, groupOrder, groupLabels, renderColumnHeader, columnWidth, columnGap, cardGap, columnHeight, estimatedCardHeight, overscanCount, orderStep, minOrderGap, emptyColumnPlaceholder, className, }: KanbanProps<TCard, TGroup>) => import("react/jsx-runtime").JSX.Element;
|
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { DndContext, DragOverlay, KeyboardSensor, MeasuringStrategy, PointerSensor, closestCenter, getFirstCollision, pointerWithin, rectIntersection, useSensor, useSensors, } from "@dnd-kit/core";
|
|
3
|
+
import { sortableKeyboardCoordinates } from "@dnd-kit/sortable";
|
|
4
|
+
import { useCallback, useEffect, useRef, useState } from "react";
|
|
5
|
+
import { KanbanColumn } from "./KanbanColumn";
|
|
6
|
+
import { findContainer, fromCardToken, isCardToken, isGroupToken, moveCard, moveCardToInsertIndex, } from "./board";
|
|
7
|
+
import { dropAnimation } from "./constants";
|
|
8
|
+
import { CardContainer, Board } from "./styles";
|
|
9
|
+
import { useKanbanBoardData } from "./useKanbanBoardData";
|
|
10
|
+
import { useKanbanRowHeights } from "./useKanbanRowHeights";
|
|
11
|
+
import { DEFAULT_MIN_ORDER_GAP, DEFAULT_ORDER_STEP, calculateOrderValue, } from "./utils";
|
|
12
|
+
export const Kanban = ({ cards, getCardId, getCardGroup, getCardOrder, renderCard, onCardsReorder, groupOrder, groupLabels, renderColumnHeader, columnWidth = 320, columnGap = 12, cardGap = 8, columnHeight, estimatedCardHeight = 100, overscanCount = 4, orderStep = DEFAULT_ORDER_STEP, minOrderGap = DEFAULT_MIN_ORDER_GAP, emptyColumnPlaceholder, className, }) => {
|
|
13
|
+
var _a, _b;
|
|
14
|
+
const [activeCardToken, setActiveCardToken] = useState(null);
|
|
15
|
+
const [activeOverlayWidth, setActiveOverlayWidth] = useState(null);
|
|
16
|
+
const [projectedBoard, setProjectedBoard] = useState(null);
|
|
17
|
+
const activeOriginContainerRef = useRef(null);
|
|
18
|
+
const lastOverId = useRef(null);
|
|
19
|
+
const recentlyMovedToNewContainer = useRef(false);
|
|
20
|
+
const { cardByToken, cardIdByToken, groupValueByToken, baseBoard } = useKanbanBoardData({
|
|
21
|
+
cards,
|
|
22
|
+
getCardId,
|
|
23
|
+
getCardGroup,
|
|
24
|
+
getCardOrder,
|
|
25
|
+
groupOrder,
|
|
26
|
+
});
|
|
27
|
+
const { columnHeights, listRefMap, onRowMeasured } = useKanbanRowHeights();
|
|
28
|
+
const board = projectedBoard !== null && projectedBoard !== void 0 ? projectedBoard : baseBoard;
|
|
29
|
+
const sensors = useSensors(useSensor(PointerSensor, {
|
|
30
|
+
activationConstraint: { distance: 6 },
|
|
31
|
+
}), useSensor(KeyboardSensor, {
|
|
32
|
+
coordinateGetter: sortableKeyboardCoordinates,
|
|
33
|
+
}));
|
|
34
|
+
const collisionDetectionStrategy = useCallback((args) => {
|
|
35
|
+
var _a, _b;
|
|
36
|
+
if (activeCardToken) {
|
|
37
|
+
const pointerIntersections = pointerWithin(args);
|
|
38
|
+
const intersections = pointerIntersections.length > 0
|
|
39
|
+
? pointerIntersections
|
|
40
|
+
: rectIntersection(args);
|
|
41
|
+
let overId = getFirstCollision(intersections, "id");
|
|
42
|
+
if (overId != null) {
|
|
43
|
+
const overToken = String(overId);
|
|
44
|
+
if (isGroupToken(overToken) && ((_a = board[overToken]) === null || _a === void 0 ? void 0 : _a.length) > 0) {
|
|
45
|
+
const closestCard = (_b = closestCenter(Object.assign(Object.assign({}, args), { droppableContainers: args.droppableContainers.filter((container) => board[overToken].includes(String(container.id))) }))[0]) === null || _b === void 0 ? void 0 : _b.id;
|
|
46
|
+
if (closestCard) {
|
|
47
|
+
overId = closestCard;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
lastOverId.current = overId;
|
|
51
|
+
return [{ id: overId }];
|
|
52
|
+
}
|
|
53
|
+
if (recentlyMovedToNewContainer.current) {
|
|
54
|
+
lastOverId.current = activeCardToken;
|
|
55
|
+
}
|
|
56
|
+
return lastOverId.current ? [{ id: lastOverId.current }] : [];
|
|
57
|
+
}
|
|
58
|
+
return closestCenter(args);
|
|
59
|
+
}, [activeCardToken, board]);
|
|
60
|
+
useEffect(() => {
|
|
61
|
+
requestAnimationFrame(() => {
|
|
62
|
+
recentlyMovedToNewContainer.current = false;
|
|
63
|
+
});
|
|
64
|
+
}, [board]);
|
|
65
|
+
const handleDragStart = useCallback(({ active }) => {
|
|
66
|
+
var _a, _b;
|
|
67
|
+
const activeToken = String(active.id);
|
|
68
|
+
if (!isCardToken(activeToken)) {
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
setActiveCardToken(activeToken);
|
|
72
|
+
setActiveOverlayWidth((_b = (_a = active.rect.current.initial) === null || _a === void 0 ? void 0 : _a.width) !== null && _b !== void 0 ? _b : null);
|
|
73
|
+
setProjectedBoard(baseBoard);
|
|
74
|
+
activeOriginContainerRef.current = findContainer(baseBoard, activeToken);
|
|
75
|
+
}, [baseBoard]);
|
|
76
|
+
const handleDragOver = useCallback(({ active, over }) => {
|
|
77
|
+
if (!over) {
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
const activeToken = String(active.id);
|
|
81
|
+
if (!isCardToken(activeToken)) {
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
setProjectedBoard((currentBoard) => {
|
|
85
|
+
var _a;
|
|
86
|
+
const source = currentBoard !== null && currentBoard !== void 0 ? currentBoard : baseBoard;
|
|
87
|
+
const sourceContainer = findContainer(source, activeToken);
|
|
88
|
+
const overToken = String(over.id);
|
|
89
|
+
const overContainer = isGroupToken(overToken) && source[overToken]
|
|
90
|
+
? overToken
|
|
91
|
+
: findContainer(source, overToken);
|
|
92
|
+
if (!sourceContainer || !overContainer) {
|
|
93
|
+
return source;
|
|
94
|
+
}
|
|
95
|
+
if (sourceContainer !== overContainer) {
|
|
96
|
+
recentlyMovedToNewContainer.current = true;
|
|
97
|
+
const overItems = (_a = source[overContainer]) !== null && _a !== void 0 ? _a : [];
|
|
98
|
+
const overIndex = overItems.indexOf(overToken);
|
|
99
|
+
const isPointerBelowOverCard = overIndex >= 0 &&
|
|
100
|
+
active.rect.current.translated != null &&
|
|
101
|
+
over.rect != null &&
|
|
102
|
+
active.rect.current.translated.top >
|
|
103
|
+
over.rect.top + over.rect.height;
|
|
104
|
+
const insertIndex = isGroupToken(overToken)
|
|
105
|
+
? overItems.length
|
|
106
|
+
: overIndex >= 0
|
|
107
|
+
? overIndex + (isPointerBelowOverCard ? 1 : 0)
|
|
108
|
+
: overItems.length;
|
|
109
|
+
return moveCardToInsertIndex(source, activeToken, sourceContainer, overContainer, insertIndex);
|
|
110
|
+
}
|
|
111
|
+
return moveCard(source, activeToken, over.id);
|
|
112
|
+
});
|
|
113
|
+
}, [baseBoard]);
|
|
114
|
+
const handleDragCancel = useCallback(() => {
|
|
115
|
+
setActiveCardToken(null);
|
|
116
|
+
setActiveOverlayWidth(null);
|
|
117
|
+
setProjectedBoard(null);
|
|
118
|
+
activeOriginContainerRef.current = null;
|
|
119
|
+
}, []);
|
|
120
|
+
const resetDragState = useCallback(() => {
|
|
121
|
+
setActiveCardToken(null);
|
|
122
|
+
setActiveOverlayWidth(null);
|
|
123
|
+
setProjectedBoard(null);
|
|
124
|
+
activeOriginContainerRef.current = null;
|
|
125
|
+
}, []);
|
|
126
|
+
const handleDragEnd = useCallback(({ active, over }) => {
|
|
127
|
+
var _a, _b, _c;
|
|
128
|
+
const activeToken = String(active.id);
|
|
129
|
+
if (!isCardToken(activeToken) || !over) {
|
|
130
|
+
resetDragState();
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
const activeCard = cardByToken.get(activeToken);
|
|
134
|
+
if (!activeCard) {
|
|
135
|
+
resetDragState();
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
const movedBoard = projectedBoard !== null && projectedBoard !== void 0 ? projectedBoard : moveCard(baseBoard, activeToken, over.id);
|
|
139
|
+
const fromContainer = (_a = activeOriginContainerRef.current) !== null && _a !== void 0 ? _a : findContainer(baseBoard, activeToken);
|
|
140
|
+
const toContainer = findContainer(movedBoard, activeToken);
|
|
141
|
+
if (!fromContainer || !toContainer) {
|
|
142
|
+
resetDragState();
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
const fromItems = (_b = baseBoard[fromContainer]) !== null && _b !== void 0 ? _b : [];
|
|
146
|
+
const toItems = (_c = movedBoard[toContainer]) !== null && _c !== void 0 ? _c : [];
|
|
147
|
+
const fromIndex = fromItems.indexOf(activeToken);
|
|
148
|
+
const toIndex = toItems.indexOf(activeToken);
|
|
149
|
+
if (fromContainer === toContainer && fromIndex === toIndex) {
|
|
150
|
+
resetDragState();
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
const previousToken = toIndex > 0 ? toItems[toIndex - 1] : null;
|
|
154
|
+
const nextToken = toIndex < toItems.length - 1 ? toItems[toIndex + 1] : null;
|
|
155
|
+
const previousCard = previousToken
|
|
156
|
+
? cardByToken.get(previousToken)
|
|
157
|
+
: null;
|
|
158
|
+
const nextCard = nextToken ? cardByToken.get(nextToken) : null;
|
|
159
|
+
const previousOrder = previousCard ? getCardOrder(previousCard) : null;
|
|
160
|
+
const nextOrder = nextCard ? getCardOrder(nextCard) : null;
|
|
161
|
+
const fromGroup = groupValueByToken.get(fromContainer);
|
|
162
|
+
const toGroup = groupValueByToken.get(toContainer);
|
|
163
|
+
if (fromGroup == null || toGroup == null) {
|
|
164
|
+
resetDragState();
|
|
165
|
+
return;
|
|
166
|
+
}
|
|
167
|
+
const { order, needsRebalance } = calculateOrderValue({
|
|
168
|
+
previousOrder,
|
|
169
|
+
nextOrder,
|
|
170
|
+
step: orderStep,
|
|
171
|
+
minGap: minOrderGap,
|
|
172
|
+
});
|
|
173
|
+
const change = {
|
|
174
|
+
movedCard: activeCard,
|
|
175
|
+
movedCardId: fromCardToken(activeToken),
|
|
176
|
+
fromGroup,
|
|
177
|
+
toGroup,
|
|
178
|
+
newOrder: order,
|
|
179
|
+
beforeCardId: previousToken ? fromCardToken(previousToken) : null,
|
|
180
|
+
afterCardId: nextToken ? fromCardToken(nextToken) : null,
|
|
181
|
+
needsRebalance,
|
|
182
|
+
updatedCards: [
|
|
183
|
+
{
|
|
184
|
+
id: fromCardToken(activeToken),
|
|
185
|
+
group: toGroup,
|
|
186
|
+
order,
|
|
187
|
+
},
|
|
188
|
+
],
|
|
189
|
+
};
|
|
190
|
+
onCardsReorder(change);
|
|
191
|
+
resetDragState();
|
|
192
|
+
}, [
|
|
193
|
+
baseBoard,
|
|
194
|
+
cardByToken,
|
|
195
|
+
getCardOrder,
|
|
196
|
+
groupValueByToken,
|
|
197
|
+
minOrderGap,
|
|
198
|
+
onCardsReorder,
|
|
199
|
+
orderStep,
|
|
200
|
+
projectedBoard,
|
|
201
|
+
resetDragState,
|
|
202
|
+
]);
|
|
203
|
+
const activeCard = activeCardToken ? cardByToken.get(activeCardToken) : null;
|
|
204
|
+
const activeContainer = activeCardToken
|
|
205
|
+
? findContainer(board, activeCardToken)
|
|
206
|
+
: null;
|
|
207
|
+
const activeGroup = activeContainer
|
|
208
|
+
? ((_a = groupValueByToken.get(activeContainer)) !== null && _a !== void 0 ? _a : null)
|
|
209
|
+
: null;
|
|
210
|
+
const activeIndex = activeCardToken && activeContainer
|
|
211
|
+
? ((_b = board[activeContainer]) !== null && _b !== void 0 ? _b : []).indexOf(activeCardToken)
|
|
212
|
+
: -1;
|
|
213
|
+
return (_jsxs(DndContext, { sensors: sensors, collisionDetection: collisionDetectionStrategy, measuring: {
|
|
214
|
+
droppable: {
|
|
215
|
+
strategy: MeasuringStrategy.Always,
|
|
216
|
+
},
|
|
217
|
+
}, onDragStart: handleDragStart, onDragOver: handleDragOver, onDragEnd: handleDragEnd, onDragCancel: handleDragCancel, children: [_jsx(Board, { className: className, "$columnGap": columnGap, children: Object.keys(board).map((columnToken) => {
|
|
218
|
+
var _a, _b;
|
|
219
|
+
const columnItems = (_a = board[columnToken]) !== null && _a !== void 0 ? _a : [];
|
|
220
|
+
const groupValue = groupValueByToken.get(columnToken);
|
|
221
|
+
if (groupValue == null) {
|
|
222
|
+
return null;
|
|
223
|
+
}
|
|
224
|
+
return (_jsx(KanbanColumn, { columnToken: columnToken, groupValue: groupValue, groupLabel: groupLabels === null || groupLabels === void 0 ? void 0 : groupLabels[String(groupValue)], renderColumnHeader: renderColumnHeader, itemTokens: columnItems, cardByToken: cardByToken, cardIdByToken: cardIdByToken, groupByToken: groupValueByToken, renderCard: renderCard, onMeasure: onRowMeasured, rowHeights: (_b = columnHeights[columnToken]) !== null && _b !== void 0 ? _b : {}, listRefMap: listRefMap, columnWidth: columnWidth, columnHeight: columnHeight, cardGap: cardGap, estimatedCardHeight: estimatedCardHeight, overscanCount: overscanCount, emptyColumnPlaceholder: emptyColumnPlaceholder }, columnToken));
|
|
225
|
+
}) }), _jsx(DragOverlay, { adjustScale: false, dropAnimation: dropAnimation, children: activeCard && activeGroup != null ? (_jsx("div", { style: { width: activeOverlayWidth !== null && activeOverlayWidth !== void 0 ? activeOverlayWidth : undefined }, children: _jsx(CardContainer, { "$isDragging": true, "$isDragOverlay": true, children: renderCard({
|
|
226
|
+
card: activeCard,
|
|
227
|
+
cardId: getCardId(activeCard),
|
|
228
|
+
group: activeGroup,
|
|
229
|
+
index: activeIndex,
|
|
230
|
+
isDragging: true,
|
|
231
|
+
isSorting: true,
|
|
232
|
+
isDragOverlay: true,
|
|
233
|
+
}) }) })) : null })] }));
|
|
234
|
+
};
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { MutableRefObject, ReactNode } from "react";
|
|
2
|
+
import { VariableSizeList } from "react-window";
|
|
3
|
+
import { KanbanGroupKey, RenderCardArgs } from "./types";
|
|
4
|
+
export interface KanbanColumnProps<TCard, TGroup extends KanbanGroupKey> {
|
|
5
|
+
columnToken: string;
|
|
6
|
+
groupValue: TGroup;
|
|
7
|
+
groupLabel?: ReactNode;
|
|
8
|
+
renderColumnHeader?: (group: TGroup, cardCount: number) => ReactNode;
|
|
9
|
+
itemTokens: string[];
|
|
10
|
+
cardByToken: Map<string, TCard>;
|
|
11
|
+
cardIdByToken: Map<string, string>;
|
|
12
|
+
groupByToken: Map<string, TGroup>;
|
|
13
|
+
renderCard: (args: RenderCardArgs<TCard, TGroup>) => ReactNode;
|
|
14
|
+
onMeasure: (columnToken: string, cardToken: string, index: number, height: number) => void;
|
|
15
|
+
rowHeights: Record<string, number>;
|
|
16
|
+
listRefMap: MutableRefObject<Record<string, VariableSizeList | null>>;
|
|
17
|
+
columnWidth: number;
|
|
18
|
+
columnHeight?: number;
|
|
19
|
+
cardGap: number;
|
|
20
|
+
estimatedCardHeight: number;
|
|
21
|
+
overscanCount: number;
|
|
22
|
+
emptyColumnPlaceholder?: ReactNode;
|
|
23
|
+
}
|
|
24
|
+
export declare const KanbanColumn: <TCard, TGroup extends KanbanGroupKey>({ columnToken, groupValue, groupLabel, renderColumnHeader, itemTokens, cardByToken, cardIdByToken, groupByToken, renderCard, onMeasure, rowHeights, listRefMap, columnWidth, columnHeight, cardGap, estimatedCardHeight, overscanCount, emptyColumnPlaceholder, }: KanbanColumnProps<TCard, TGroup>) => import("react/jsx-runtime").JSX.Element;
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useDroppable } from "@dnd-kit/core";
|
|
3
|
+
import { SortableContext, verticalListSortingStrategy } from "@dnd-kit/sortable";
|
|
4
|
+
import { useCallback, useEffect, useLayoutEffect, useMemo, useRef, } from "react";
|
|
5
|
+
import { useOverlayScrollbars } from "overlayscrollbars-react";
|
|
6
|
+
import AutoSizer from "react-virtualized-auto-sizer";
|
|
7
|
+
import { VariableSizeList } from "react-window";
|
|
8
|
+
import { Column, ColumnBody, ColumnCount, ColumnHeader, EmptyColumn, } from "./styles";
|
|
9
|
+
import { MemoizedSortableVirtualRow } from "./virtualRow";
|
|
10
|
+
export const KanbanColumn = ({ columnToken, groupValue, groupLabel, renderColumnHeader, itemTokens, cardByToken, cardIdByToken, groupByToken, renderCard, onMeasure, rowHeights, listRefMap, columnWidth, columnHeight, cardGap, estimatedCardHeight, overscanCount, emptyColumnPlaceholder, }) => {
|
|
11
|
+
const { setNodeRef, isOver } = useDroppable({
|
|
12
|
+
id: columnToken,
|
|
13
|
+
data: {
|
|
14
|
+
type: "column",
|
|
15
|
+
},
|
|
16
|
+
});
|
|
17
|
+
const cardCount = itemTokens.length;
|
|
18
|
+
const rowData = useMemo(() => ({
|
|
19
|
+
itemTokens,
|
|
20
|
+
columnToken,
|
|
21
|
+
cardByToken,
|
|
22
|
+
cardIdByToken,
|
|
23
|
+
groupByToken,
|
|
24
|
+
renderCard,
|
|
25
|
+
onMeasure,
|
|
26
|
+
cardGap,
|
|
27
|
+
}), [
|
|
28
|
+
cardByToken,
|
|
29
|
+
cardIdByToken,
|
|
30
|
+
columnToken,
|
|
31
|
+
groupByToken,
|
|
32
|
+
itemTokens,
|
|
33
|
+
onMeasure,
|
|
34
|
+
renderCard,
|
|
35
|
+
cardGap,
|
|
36
|
+
]);
|
|
37
|
+
const previousOrderTokensRef = useRef([]);
|
|
38
|
+
const scrollTargetRef = useRef(null);
|
|
39
|
+
const scrollViewportRef = useRef(null);
|
|
40
|
+
const [initializeScrollbars, getScrollbarsInstance] = useOverlayScrollbars({
|
|
41
|
+
options: {
|
|
42
|
+
overflow: {
|
|
43
|
+
x: "hidden",
|
|
44
|
+
y: "scroll",
|
|
45
|
+
},
|
|
46
|
+
scrollbars: {
|
|
47
|
+
autoHide: "scroll",
|
|
48
|
+
autoHideDelay: 500,
|
|
49
|
+
},
|
|
50
|
+
},
|
|
51
|
+
});
|
|
52
|
+
const initializeColumnScrollbars = useCallback(() => {
|
|
53
|
+
var _a;
|
|
54
|
+
const target = scrollTargetRef.current;
|
|
55
|
+
const viewport = scrollViewportRef.current;
|
|
56
|
+
if (!target || !viewport || cardCount === 0) {
|
|
57
|
+
(_a = getScrollbarsInstance()) === null || _a === void 0 ? void 0 : _a.destroy();
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
initializeScrollbars({
|
|
61
|
+
target,
|
|
62
|
+
elements: {
|
|
63
|
+
viewport,
|
|
64
|
+
},
|
|
65
|
+
});
|
|
66
|
+
}, [cardCount, getScrollbarsInstance, initializeScrollbars]);
|
|
67
|
+
const setScrollTargetRef = useCallback((node) => {
|
|
68
|
+
scrollTargetRef.current = node;
|
|
69
|
+
if (node) {
|
|
70
|
+
node.style.overflowX = "hidden";
|
|
71
|
+
}
|
|
72
|
+
initializeColumnScrollbars();
|
|
73
|
+
}, [initializeColumnScrollbars]);
|
|
74
|
+
const setScrollViewportRef = useCallback((node) => {
|
|
75
|
+
scrollViewportRef.current = node;
|
|
76
|
+
if (node) {
|
|
77
|
+
node.style.overflowX = "hidden";
|
|
78
|
+
}
|
|
79
|
+
initializeColumnScrollbars();
|
|
80
|
+
}, [initializeColumnScrollbars]);
|
|
81
|
+
useLayoutEffect(() => {
|
|
82
|
+
var _a;
|
|
83
|
+
const previousTokens = previousOrderTokensRef.current;
|
|
84
|
+
const currentTokens = rowData.itemTokens;
|
|
85
|
+
if (previousTokens.length === currentTokens.length &&
|
|
86
|
+
previousTokens.every((token, index) => token === currentTokens[index])) {
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
if (previousTokens.length === 0) {
|
|
90
|
+
previousOrderTokensRef.current = [...currentTokens];
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
const maxLength = Math.max(previousTokens.length, currentTokens.length);
|
|
94
|
+
let firstChangedIndex = 0;
|
|
95
|
+
for (let index = 0; index < maxLength; index += 1) {
|
|
96
|
+
if (previousTokens[index] !== currentTokens[index]) {
|
|
97
|
+
firstChangedIndex = index;
|
|
98
|
+
break;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
previousOrderTokensRef.current = [...currentTokens];
|
|
102
|
+
(_a = listRefMap.current[columnToken]) === null || _a === void 0 ? void 0 : _a.resetAfterIndex(firstChangedIndex);
|
|
103
|
+
}, [columnToken, listRefMap, rowData.itemTokens]);
|
|
104
|
+
useEffect(() => {
|
|
105
|
+
initializeColumnScrollbars();
|
|
106
|
+
return () => {
|
|
107
|
+
var _a;
|
|
108
|
+
(_a = getScrollbarsInstance()) === null || _a === void 0 ? void 0 : _a.destroy();
|
|
109
|
+
};
|
|
110
|
+
}, [getScrollbarsInstance, initializeColumnScrollbars]);
|
|
111
|
+
return (_jsxs(Column, { ref: setNodeRef, "$width": columnWidth, "$isOver": isOver, children: [_jsx(ColumnHeader, { children: renderColumnHeader ? (renderColumnHeader(groupValue, cardCount)) : (_jsxs(_Fragment, { children: [_jsx("span", { children: groupLabel !== null && groupLabel !== void 0 ? groupLabel : String(groupValue) }), _jsx(ColumnCount, { children: cardCount })] })) }), _jsx(SortableContext, { items: rowData.itemTokens, strategy: verticalListSortingStrategy, children: _jsx(ColumnBody, { ref: setScrollTargetRef, "$height": columnHeight, children: cardCount === 0 ? (_jsx(EmptyColumn, { children: emptyColumnPlaceholder !== null && emptyColumnPlaceholder !== void 0 ? emptyColumnPlaceholder : "Drop cards here" })) : (_jsx(AutoSizer, { children: ({ width, height }) => (_jsx(VariableSizeList, { ref: (node) => {
|
|
112
|
+
listRefMap.current[columnToken] = node;
|
|
113
|
+
}, outerRef: setScrollViewportRef, width: width, height: height, itemCount: cardCount, overscanCount: overscanCount, itemData: rowData, itemKey: (index, data) => data.itemTokens[index], itemSize: (index) => {
|
|
114
|
+
var _a;
|
|
115
|
+
const token = rowData.itemTokens[index];
|
|
116
|
+
return (_a = rowHeights[token]) !== null && _a !== void 0 ? _a : estimatedCardHeight + cardGap;
|
|
117
|
+
}, children: MemoizedSortableVirtualRow })) })) }) })] }));
|
|
118
|
+
};
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { UniqueIdentifier } from "@dnd-kit/core";
|
|
2
|
+
import { KanbanGroupKey } from "./types";
|
|
3
|
+
export type BoardState = Record<string, string[]>;
|
|
4
|
+
export type HeightMap = Record<string, Record<string, number>>;
|
|
5
|
+
export declare const isCardToken: (value: string) => boolean;
|
|
6
|
+
export declare const isGroupToken: (value: string) => boolean;
|
|
7
|
+
export declare const toCardToken: (cardId: string) => string;
|
|
8
|
+
export declare const toGroupToken: (group: KanbanGroupKey) => string;
|
|
9
|
+
export declare const fromCardToken: (cardToken: string) => string;
|
|
10
|
+
export declare function findContainer(board: BoardState, id: UniqueIdentifier | null): string | null;
|
|
11
|
+
export declare function moveCard(board: BoardState, activeCardToken: string, overId: UniqueIdentifier | null): BoardState;
|
|
12
|
+
export declare function moveCardToInsertIndex(board: BoardState, activeCardToken: string, activeContainer: string, overContainer: string, insertIndex: number): BoardState;
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { arrayMove } from "@dnd-kit/sortable";
|
|
2
|
+
import { CARD_PREFIX, GROUP_PREFIX } from "./constants";
|
|
3
|
+
export const isCardToken = (value) => value.startsWith(CARD_PREFIX);
|
|
4
|
+
export const isGroupToken = (value) => value.startsWith(GROUP_PREFIX);
|
|
5
|
+
export const toCardToken = (cardId) => `${CARD_PREFIX}${cardId}`;
|
|
6
|
+
export const toGroupToken = (group) => `${GROUP_PREFIX}${String(group)}`;
|
|
7
|
+
export const fromCardToken = (cardToken) => cardToken.replace(CARD_PREFIX, "");
|
|
8
|
+
export function findContainer(board, id) {
|
|
9
|
+
if (id == null) {
|
|
10
|
+
return null;
|
|
11
|
+
}
|
|
12
|
+
const targetId = String(id);
|
|
13
|
+
if (isGroupToken(targetId) && board[targetId]) {
|
|
14
|
+
return targetId;
|
|
15
|
+
}
|
|
16
|
+
const groupTokens = Object.keys(board);
|
|
17
|
+
for (const groupToken of groupTokens) {
|
|
18
|
+
if (board[groupToken].includes(targetId)) {
|
|
19
|
+
return groupToken;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
return null;
|
|
23
|
+
}
|
|
24
|
+
export function moveCard(board, activeCardToken, overId) {
|
|
25
|
+
if (overId == null) {
|
|
26
|
+
return board;
|
|
27
|
+
}
|
|
28
|
+
const overToken = String(overId);
|
|
29
|
+
const activeContainer = findContainer(board, activeCardToken);
|
|
30
|
+
const overContainer = isGroupToken(overToken) && board[overToken]
|
|
31
|
+
? overToken
|
|
32
|
+
: findContainer(board, overToken);
|
|
33
|
+
if (!activeContainer || !overContainer) {
|
|
34
|
+
return board;
|
|
35
|
+
}
|
|
36
|
+
const activeItems = board[activeContainer];
|
|
37
|
+
const overItems = board[overContainer];
|
|
38
|
+
const activeIndex = activeItems.indexOf(activeCardToken);
|
|
39
|
+
if (activeIndex === -1) {
|
|
40
|
+
return board;
|
|
41
|
+
}
|
|
42
|
+
if (activeContainer === overContainer) {
|
|
43
|
+
if (isGroupToken(overToken)) {
|
|
44
|
+
const targetIndex = Math.max(activeItems.length - 1, 0);
|
|
45
|
+
if (activeIndex === targetIndex) {
|
|
46
|
+
return board;
|
|
47
|
+
}
|
|
48
|
+
return Object.assign(Object.assign({}, board), { [activeContainer]: arrayMove(activeItems, activeIndex, targetIndex) });
|
|
49
|
+
}
|
|
50
|
+
const overIndex = overItems.indexOf(overToken);
|
|
51
|
+
if (overIndex === -1 || overIndex === activeIndex) {
|
|
52
|
+
return board;
|
|
53
|
+
}
|
|
54
|
+
return Object.assign(Object.assign({}, board), { [activeContainer]: arrayMove(activeItems, activeIndex, overIndex) });
|
|
55
|
+
}
|
|
56
|
+
const nextActiveItems = activeItems.filter((item) => item !== activeCardToken);
|
|
57
|
+
const overIndex = isGroupToken(overToken)
|
|
58
|
+
? overItems.length
|
|
59
|
+
: overItems.indexOf(overToken);
|
|
60
|
+
const insertIndex = overIndex < 0 ? overItems.length : overIndex;
|
|
61
|
+
const nextOverItems = [
|
|
62
|
+
...overItems.slice(0, insertIndex),
|
|
63
|
+
activeCardToken,
|
|
64
|
+
...overItems.slice(insertIndex),
|
|
65
|
+
];
|
|
66
|
+
return Object.assign(Object.assign({}, board), { [activeContainer]: nextActiveItems, [overContainer]: nextOverItems });
|
|
67
|
+
}
|
|
68
|
+
export function moveCardToInsertIndex(board, activeCardToken, activeContainer, overContainer, insertIndex) {
|
|
69
|
+
var _a, _b;
|
|
70
|
+
const activeItems = (_a = board[activeContainer]) !== null && _a !== void 0 ? _a : [];
|
|
71
|
+
const overItems = (_b = board[overContainer]) !== null && _b !== void 0 ? _b : [];
|
|
72
|
+
const activeIndex = activeItems.indexOf(activeCardToken);
|
|
73
|
+
if (activeIndex === -1 || activeContainer === overContainer) {
|
|
74
|
+
return board;
|
|
75
|
+
}
|
|
76
|
+
const clampedInsertIndex = Math.max(0, Math.min(insertIndex, overItems.length));
|
|
77
|
+
const nextActiveItems = activeItems.filter((item) => item !== activeCardToken);
|
|
78
|
+
const nextOverItems = [
|
|
79
|
+
...overItems.slice(0, clampedInsertIndex),
|
|
80
|
+
activeCardToken,
|
|
81
|
+
...overItems.slice(clampedInsertIndex),
|
|
82
|
+
];
|
|
83
|
+
return Object.assign(Object.assign({}, board), { [activeContainer]: nextActiveItems, [overContainer]: nextOverItems });
|
|
84
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { defaultDropAnimationSideEffects } from "@dnd-kit/core";
|
|
2
|
+
export const GROUP_PREFIX = "group::";
|
|
3
|
+
export const CARD_PREFIX = "card::";
|
|
4
|
+
export const dropAnimation = {
|
|
5
|
+
duration: 220,
|
|
6
|
+
easing: "cubic-bezier(0.2, 0, 0, 1)",
|
|
7
|
+
sideEffects: defaultDropAnimationSideEffects({
|
|
8
|
+
styles: {
|
|
9
|
+
active: {
|
|
10
|
+
opacity: "0.35",
|
|
11
|
+
},
|
|
12
|
+
},
|
|
13
|
+
}),
|
|
14
|
+
};
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export declare const Board: import("styled-components/dist/types").IStyledComponentBase<"web", import("styled-components/dist/types").Substitute<import("react").DetailedHTMLProps<import("react").HTMLAttributes<HTMLDivElement>, HTMLDivElement>, {
|
|
2
|
+
$columnGap?: number;
|
|
3
|
+
}>> & string;
|
|
4
|
+
export declare const Column: import("styled-components/dist/types").IStyledComponentBase<"web", import("styled-components/dist/types").Substitute<import("react").DetailedHTMLProps<import("react").HTMLAttributes<HTMLDivElement>, HTMLDivElement>, {
|
|
5
|
+
$width: number;
|
|
6
|
+
$isOver: boolean;
|
|
7
|
+
}>> & string;
|
|
8
|
+
export declare const ColumnHeader: import("styled-components/dist/types").IStyledComponentBase<"web", import("styled-components").FastOmit<import("react").DetailedHTMLProps<import("react").HTMLAttributes<HTMLDivElement>, HTMLDivElement>, never>> & string;
|
|
9
|
+
export declare const ColumnCount: import("styled-components/dist/types").IStyledComponentBase<"web", import("styled-components").FastOmit<import("react").DetailedHTMLProps<import("react").HTMLAttributes<HTMLSpanElement>, HTMLSpanElement>, never>> & string;
|
|
10
|
+
export declare const ColumnBody: import("styled-components/dist/types").IStyledComponentBase<"web", import("styled-components/dist/types").Substitute<import("react").DetailedHTMLProps<import("react").HTMLAttributes<HTMLDivElement>, HTMLDivElement>, {
|
|
11
|
+
$height?: number;
|
|
12
|
+
}>> & string;
|
|
13
|
+
export declare const EmptyColumn: import("styled-components/dist/types").IStyledComponentBase<"web", import("styled-components").FastOmit<import("react").DetailedHTMLProps<import("react").HTMLAttributes<HTMLDivElement>, HTMLDivElement>, never>> & string;
|
|
14
|
+
export declare const CardContainer: import("styled-components/dist/types").IStyledComponentBase<"web", import("styled-components/dist/types").Substitute<import("react").DetailedHTMLProps<import("react").HTMLAttributes<HTMLDivElement>, HTMLDivElement>, {
|
|
15
|
+
$isDragging: boolean;
|
|
16
|
+
$isDragOverlay: boolean;
|
|
17
|
+
}>> & string;
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import styled from "styled-components";
|
|
2
|
+
export const Board = styled.div `
|
|
3
|
+
display: flex;
|
|
4
|
+
align-items: stretch;
|
|
5
|
+
gap: ${({ $columnGap }) => $columnGap !== null && $columnGap !== void 0 ? $columnGap : 12}px;
|
|
6
|
+
width: 100%;
|
|
7
|
+
height: 100%;
|
|
8
|
+
min-height: 0;
|
|
9
|
+
overflow-x: auto;
|
|
10
|
+
overflow-y: hidden;
|
|
11
|
+
padding-bottom: 8px;
|
|
12
|
+
`;
|
|
13
|
+
export const Column = styled.div `
|
|
14
|
+
width: ${({ $width }) => $width}px;
|
|
15
|
+
min-width: ${({ $width }) => $width}px;
|
|
16
|
+
max-width: ${({ $width }) => $width}px;
|
|
17
|
+
display: flex;
|
|
18
|
+
flex-direction: column;
|
|
19
|
+
height: 100%;
|
|
20
|
+
min-height: 0;
|
|
21
|
+
transition: border-color 120ms ease;
|
|
22
|
+
`;
|
|
23
|
+
export const ColumnHeader = styled.div `
|
|
24
|
+
display: flex;
|
|
25
|
+
justify-content: space-between;
|
|
26
|
+
align-items: center;
|
|
27
|
+
padding: 10px 5px;
|
|
28
|
+
color: ${({ theme }) => theme.palette.text.primary};
|
|
29
|
+
font-weight: 600;
|
|
30
|
+
font-size: 13px;
|
|
31
|
+
`;
|
|
32
|
+
export const ColumnCount = styled.span `
|
|
33
|
+
color: ${({ theme }) => theme.palette.text.secondary};
|
|
34
|
+
font-size: 12px;
|
|
35
|
+
`;
|
|
36
|
+
export const ColumnBody = styled.div `
|
|
37
|
+
${({ $height }) => typeof $height === "number"
|
|
38
|
+
? `height: ${$height}px;`
|
|
39
|
+
: `
|
|
40
|
+
flex: 1 1 auto;
|
|
41
|
+
min-height: 0;
|
|
42
|
+
`}
|
|
43
|
+
width: 100%;
|
|
44
|
+
padding: 8px;
|
|
45
|
+
`;
|
|
46
|
+
export const EmptyColumn = styled.div `
|
|
47
|
+
height: 100%;
|
|
48
|
+
display: flex;
|
|
49
|
+
align-items: center;
|
|
50
|
+
justify-content: center;
|
|
51
|
+
border: 1px dashed ${({ theme }) => theme.palette.divider};
|
|
52
|
+
border-radius: 6px;
|
|
53
|
+
color: ${({ theme }) => theme.palette.text.secondary};
|
|
54
|
+
font-size: 12px;
|
|
55
|
+
text-align: center;
|
|
56
|
+
padding: 8px;
|
|
57
|
+
`;
|
|
58
|
+
export const CardContainer = styled.div `
|
|
59
|
+
box-sizing: border-box;
|
|
60
|
+
padding: 4px 2px;
|
|
61
|
+
transition: opacity 120ms ease;
|
|
62
|
+
opacity: ${({ $isDragging, $isDragOverlay }) => $isDragging && !$isDragOverlay ? 0.35 : 1};
|
|
63
|
+
pointer-events: ${({ $isDragOverlay }) => ($isDragOverlay ? "none" : "auto")};
|
|
64
|
+
`;
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { ReactNode } from "react";
|
|
2
|
+
export type KanbanGroupKey = string | number;
|
|
3
|
+
export interface KanbanUpdatedCard<TGroup extends KanbanGroupKey> {
|
|
4
|
+
id: string;
|
|
5
|
+
group: TGroup;
|
|
6
|
+
order: number;
|
|
7
|
+
}
|
|
8
|
+
export interface KanbanReorderChange<TCard, TGroup extends KanbanGroupKey> {
|
|
9
|
+
movedCard: TCard;
|
|
10
|
+
movedCardId: string;
|
|
11
|
+
fromGroup: TGroup;
|
|
12
|
+
toGroup: TGroup;
|
|
13
|
+
newOrder: number;
|
|
14
|
+
beforeCardId: string | null;
|
|
15
|
+
afterCardId: string | null;
|
|
16
|
+
needsRebalance: boolean;
|
|
17
|
+
updatedCards: KanbanUpdatedCard<TGroup>[];
|
|
18
|
+
}
|
|
19
|
+
export interface RenderCardArgs<TCard, TGroup extends KanbanGroupKey> {
|
|
20
|
+
card: TCard;
|
|
21
|
+
cardId: string;
|
|
22
|
+
group: TGroup;
|
|
23
|
+
index: number;
|
|
24
|
+
isDragging: boolean;
|
|
25
|
+
isSorting: boolean;
|
|
26
|
+
isDragOverlay: boolean;
|
|
27
|
+
}
|
|
28
|
+
export interface KanbanProps<TCard, TGroup extends KanbanGroupKey> {
|
|
29
|
+
/** Full collection of cards rendered by the board. */
|
|
30
|
+
cards: TCard[];
|
|
31
|
+
/** Returns a stable unique id for a card. Tells the kanban how to get the id of a card. */
|
|
32
|
+
getCardId: (card: TCard) => string;
|
|
33
|
+
/** Returns the current group/column key for a card. Tells the kanban how to get the group of a card. */
|
|
34
|
+
getCardGroup: (card: TCard) => TGroup;
|
|
35
|
+
/** Returns the numeric sort order used inside a column. Tells the kanban how to get the order of a card. */
|
|
36
|
+
getCardOrder: (card: TCard) => number;
|
|
37
|
+
/** Renders the visual content for a single card. */
|
|
38
|
+
renderCard: (args: RenderCardArgs<TCard, TGroup>) => ReactNode;
|
|
39
|
+
/** Called after drag-and-drop with the computed reorder payload. */
|
|
40
|
+
onCardsReorder: (change: KanbanReorderChange<TCard, TGroup>) => void;
|
|
41
|
+
/** Explicit column order; if omitted, columns are derived from card groups. */
|
|
42
|
+
groupOrder?: TGroup[];
|
|
43
|
+
/** Optional display labels keyed by group value (`String(group)`). */
|
|
44
|
+
groupLabels?: Partial<Record<string, ReactNode>>;
|
|
45
|
+
/** Custom header renderer for each column. */
|
|
46
|
+
renderColumnHeader?: (group: TGroup, cardCount: number) => ReactNode;
|
|
47
|
+
/** Width of each column in pixels. Defaults to `320`. */
|
|
48
|
+
columnWidth?: number;
|
|
49
|
+
/** Gap between columns in pixels. Defaults to `8`. */
|
|
50
|
+
columnGap?: number;
|
|
51
|
+
/** Fixed column body height in pixels. If omitted, it grows with container layout. */
|
|
52
|
+
columnHeight?: number;
|
|
53
|
+
/** Fallback card height estimate for virtualization. Defaults to `100`. */
|
|
54
|
+
estimatedCardHeight?: number;
|
|
55
|
+
/** Number of extra rows rendered outside the viewport. Defaults to `4`. */
|
|
56
|
+
overscanCount?: number;
|
|
57
|
+
/** Preferred spacing for generated order values. */
|
|
58
|
+
orderStep?: number;
|
|
59
|
+
/** Minimum allowed gap between adjacent order values before rebalance is suggested. */
|
|
60
|
+
minOrderGap?: number;
|
|
61
|
+
/** Content shown when a column has no cards. */
|
|
62
|
+
emptyColumnPlaceholder?: ReactNode;
|
|
63
|
+
/** Optional class name applied to the board root element. */
|
|
64
|
+
className?: string;
|
|
65
|
+
/** Gap between cards in pixels. Defaults to `8`. */
|
|
66
|
+
cardGap?: number;
|
|
67
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { BoardState } from "./board";
|
|
2
|
+
import { KanbanGroupKey } from "./types";
|
|
3
|
+
interface UseKanbanBoardDataArgs<TCard, TGroup extends KanbanGroupKey> {
|
|
4
|
+
cards: TCard[];
|
|
5
|
+
getCardId: (card: TCard) => string;
|
|
6
|
+
getCardGroup: (card: TCard) => TGroup;
|
|
7
|
+
getCardOrder: (card: TCard) => number;
|
|
8
|
+
groupOrder?: TGroup[];
|
|
9
|
+
}
|
|
10
|
+
export declare function useKanbanBoardData<TCard, TGroup extends KanbanGroupKey>({ cards, getCardId, getCardGroup, getCardOrder, groupOrder, }: UseKanbanBoardDataArgs<TCard, TGroup>): {
|
|
11
|
+
cardByToken: Map<string, TCard>;
|
|
12
|
+
cardIdByToken: Map<string, string>;
|
|
13
|
+
groupValueByToken: Map<string, TGroup>;
|
|
14
|
+
baseBoard: BoardState;
|
|
15
|
+
};
|
|
16
|
+
export {};
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { useMemo } from "react";
|
|
2
|
+
import { toCardToken, toGroupToken } from "./board";
|
|
3
|
+
export function useKanbanBoardData({ cards, getCardId, getCardGroup, getCardOrder, groupOrder, }) {
|
|
4
|
+
const cardByToken = useMemo(() => {
|
|
5
|
+
const map = new Map();
|
|
6
|
+
for (const card of cards) {
|
|
7
|
+
map.set(toCardToken(getCardId(card)), card);
|
|
8
|
+
}
|
|
9
|
+
return map;
|
|
10
|
+
}, [cards, getCardId]);
|
|
11
|
+
const cardIdByToken = useMemo(() => {
|
|
12
|
+
const map = new Map();
|
|
13
|
+
for (const card of cards) {
|
|
14
|
+
const cardId = getCardId(card);
|
|
15
|
+
map.set(toCardToken(cardId), cardId);
|
|
16
|
+
}
|
|
17
|
+
return map;
|
|
18
|
+
}, [cards, getCardId]);
|
|
19
|
+
const groupValueByToken = useMemo(() => {
|
|
20
|
+
const map = new Map();
|
|
21
|
+
if (groupOrder) {
|
|
22
|
+
for (const group of groupOrder) {
|
|
23
|
+
map.set(toGroupToken(group), group);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
for (const card of cards) {
|
|
27
|
+
const group = getCardGroup(card);
|
|
28
|
+
map.set(toGroupToken(group), group);
|
|
29
|
+
}
|
|
30
|
+
return map;
|
|
31
|
+
}, [cards, getCardGroup, groupOrder]);
|
|
32
|
+
const baseBoard = useMemo(() => {
|
|
33
|
+
const board = {};
|
|
34
|
+
for (const groupToken of groupValueByToken.keys()) {
|
|
35
|
+
board[groupToken] = [];
|
|
36
|
+
}
|
|
37
|
+
const sortedCards = [...cards].sort((cardA, cardB) => {
|
|
38
|
+
const orderDiff = getCardOrder(cardA) - getCardOrder(cardB);
|
|
39
|
+
if (orderDiff !== 0) {
|
|
40
|
+
return orderDiff;
|
|
41
|
+
}
|
|
42
|
+
return getCardId(cardA).localeCompare(getCardId(cardB));
|
|
43
|
+
});
|
|
44
|
+
for (const card of sortedCards) {
|
|
45
|
+
const groupToken = toGroupToken(getCardGroup(card));
|
|
46
|
+
if (!board[groupToken]) {
|
|
47
|
+
board[groupToken] = [];
|
|
48
|
+
}
|
|
49
|
+
board[groupToken].push(toCardToken(getCardId(card)));
|
|
50
|
+
}
|
|
51
|
+
return board;
|
|
52
|
+
}, [cards, getCardGroup, getCardId, getCardOrder, groupValueByToken]);
|
|
53
|
+
return {
|
|
54
|
+
cardByToken,
|
|
55
|
+
cardIdByToken,
|
|
56
|
+
groupValueByToken,
|
|
57
|
+
baseBoard,
|
|
58
|
+
};
|
|
59
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { VariableSizeList } from "react-window";
|
|
2
|
+
import { HeightMap } from "./board";
|
|
3
|
+
export declare function useKanbanRowHeights(): {
|
|
4
|
+
columnHeights: HeightMap;
|
|
5
|
+
listRefMap: import("react").RefObject<Record<string, VariableSizeList<any> | null>>;
|
|
6
|
+
onRowMeasured: (columnToken: string, cardToken: string, index: number, height: number) => void;
|
|
7
|
+
};
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { useCallback, useEffect, useRef, useState } from "react";
|
|
2
|
+
export function useKanbanRowHeights() {
|
|
3
|
+
const [columnHeights, setColumnHeights] = useState({});
|
|
4
|
+
const listRefMap = useRef({});
|
|
5
|
+
const pendingListResetsRef = useRef({});
|
|
6
|
+
const onRowMeasured = useCallback((columnToken, cardToken, index, height) => {
|
|
7
|
+
setColumnHeights((prev) => {
|
|
8
|
+
var _a;
|
|
9
|
+
const columnMap = (_a = prev[columnToken]) !== null && _a !== void 0 ? _a : {};
|
|
10
|
+
if (columnMap[cardToken] === height) {
|
|
11
|
+
return prev;
|
|
12
|
+
}
|
|
13
|
+
const next = Object.assign(Object.assign({}, prev), { [columnToken]: Object.assign(Object.assign({}, columnMap), { [cardToken]: height }) });
|
|
14
|
+
const pendingIndex = pendingListResetsRef.current[columnToken];
|
|
15
|
+
pendingListResetsRef.current[columnToken] =
|
|
16
|
+
pendingIndex == null ? index : Math.min(pendingIndex, index);
|
|
17
|
+
return next;
|
|
18
|
+
});
|
|
19
|
+
}, []);
|
|
20
|
+
useEffect(() => {
|
|
21
|
+
var _a;
|
|
22
|
+
const entries = Object.entries(pendingListResetsRef.current);
|
|
23
|
+
if (!entries.length) {
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
for (const [columnToken, index] of entries) {
|
|
27
|
+
(_a = listRefMap.current[columnToken]) === null || _a === void 0 ? void 0 : _a.resetAfterIndex(index);
|
|
28
|
+
}
|
|
29
|
+
pendingListResetsRef.current = {};
|
|
30
|
+
}, [columnHeights]);
|
|
31
|
+
return {
|
|
32
|
+
columnHeights,
|
|
33
|
+
listRefMap,
|
|
34
|
+
onRowMeasured,
|
|
35
|
+
};
|
|
36
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export interface CalculateOrderInput {
|
|
2
|
+
previousOrder: number | null;
|
|
3
|
+
nextOrder: number | null;
|
|
4
|
+
step?: number;
|
|
5
|
+
minGap?: number;
|
|
6
|
+
}
|
|
7
|
+
export interface CalculateOrderOutput {
|
|
8
|
+
order: number;
|
|
9
|
+
needsRebalance: boolean;
|
|
10
|
+
}
|
|
11
|
+
export declare const DEFAULT_ORDER_STEP = 1024;
|
|
12
|
+
export declare const DEFAULT_MIN_ORDER_GAP = 0.000001;
|
|
13
|
+
export declare function calculateOrderValue({ previousOrder, nextOrder, step, minGap, }: CalculateOrderInput): CalculateOrderOutput;
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
export const DEFAULT_ORDER_STEP = 1024;
|
|
2
|
+
export const DEFAULT_MIN_ORDER_GAP = 0.000001;
|
|
3
|
+
export function calculateOrderValue({ previousOrder, nextOrder, step = DEFAULT_ORDER_STEP, minGap = DEFAULT_MIN_ORDER_GAP, }) {
|
|
4
|
+
if (previousOrder == null && nextOrder == null) {
|
|
5
|
+
return { order: 0, needsRebalance: false };
|
|
6
|
+
}
|
|
7
|
+
if (previousOrder == null && nextOrder != null) {
|
|
8
|
+
return { order: nextOrder - step, needsRebalance: false };
|
|
9
|
+
}
|
|
10
|
+
if (previousOrder != null && nextOrder == null) {
|
|
11
|
+
return { order: previousOrder + step, needsRebalance: false };
|
|
12
|
+
}
|
|
13
|
+
const gap = Math.abs(nextOrder - previousOrder);
|
|
14
|
+
const order = previousOrder + (nextOrder - previousOrder) / 2;
|
|
15
|
+
return {
|
|
16
|
+
order,
|
|
17
|
+
needsRebalance: gap <= minGap,
|
|
18
|
+
};
|
|
19
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { ListChildComponentProps } from "react-window";
|
|
2
|
+
import { ReactNode } from "react";
|
|
3
|
+
import { KanbanGroupKey, RenderCardArgs } from "./types";
|
|
4
|
+
export interface ColumnRowData<TCard, TGroup extends KanbanGroupKey> {
|
|
5
|
+
itemTokens: string[];
|
|
6
|
+
columnToken: string;
|
|
7
|
+
cardByToken: Map<string, TCard>;
|
|
8
|
+
cardIdByToken: Map<string, string>;
|
|
9
|
+
groupByToken: Map<string, TGroup>;
|
|
10
|
+
renderCard: (args: RenderCardArgs<TCard, TGroup>) => ReactNode;
|
|
11
|
+
cardGap: number;
|
|
12
|
+
onMeasure: (columnToken: string, cardToken: string, index: number, height: number) => void;
|
|
13
|
+
}
|
|
14
|
+
export declare const MemoizedSortableVirtualRow: import("react").MemoExoticComponent<({ data, index, style, }: ListChildComponentProps<ColumnRowData<any, any>>) => import("react/jsx-runtime").JSX.Element | null>;
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { defaultAnimateLayoutChanges, useSortable } from "@dnd-kit/sortable";
|
|
3
|
+
import { CSS } from "@dnd-kit/utilities";
|
|
4
|
+
import { areEqual, } from "react-window";
|
|
5
|
+
import { memo, useCallback, useEffect, useState } from "react";
|
|
6
|
+
import { CardContainer } from "./styles";
|
|
7
|
+
const SortableVirtualRow = ({ data, index, style, }) => {
|
|
8
|
+
const cardToken = data.itemTokens[index];
|
|
9
|
+
const card = data.cardByToken.get(cardToken);
|
|
10
|
+
const cardId = data.cardIdByToken.get(cardToken);
|
|
11
|
+
const group = data.groupByToken.get(data.columnToken);
|
|
12
|
+
const [contentNode, setContentNode] = useState(null);
|
|
13
|
+
const { attributes, listeners, setNodeRef, transform, transition, isDragging, isSorting, } = useSortable({
|
|
14
|
+
id: cardToken,
|
|
15
|
+
data: {
|
|
16
|
+
type: "card",
|
|
17
|
+
columnToken: data.columnToken,
|
|
18
|
+
},
|
|
19
|
+
transition: {
|
|
20
|
+
duration: 180,
|
|
21
|
+
easing: "cubic-bezier(0.2, 0, 0, 1)",
|
|
22
|
+
},
|
|
23
|
+
animateLayoutChanges: (args) => defaultAnimateLayoutChanges(Object.assign(Object.assign({}, args), { wasDragging: true })),
|
|
24
|
+
});
|
|
25
|
+
const setCombinedRef = useCallback((node) => {
|
|
26
|
+
setNodeRef(node);
|
|
27
|
+
}, [setNodeRef]);
|
|
28
|
+
useEffect(() => {
|
|
29
|
+
if (!contentNode || typeof ResizeObserver === "undefined") {
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
const observer = new ResizeObserver((entries) => {
|
|
33
|
+
var _a, _b;
|
|
34
|
+
const nextHeight = (_b = (_a = entries[0]) === null || _a === void 0 ? void 0 : _a.contentRect) === null || _b === void 0 ? void 0 : _b.height;
|
|
35
|
+
if (!nextHeight || nextHeight <= 0) {
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
data.onMeasure(data.columnToken, cardToken, index, Math.ceil(nextHeight) + data.cardGap);
|
|
39
|
+
});
|
|
40
|
+
observer.observe(contentNode);
|
|
41
|
+
return () => observer.disconnect();
|
|
42
|
+
}, [cardToken, contentNode, data, index]);
|
|
43
|
+
if (!card || !cardId || group == null) {
|
|
44
|
+
return null;
|
|
45
|
+
}
|
|
46
|
+
return (_jsx("div", { ref: setCombinedRef, style: Object.assign(Object.assign({}, style), { transform: CSS.Transform.toString(transform), transition, willChange: isDragging || isSorting ? "transform" : undefined, zIndex: isDragging ? 2 : 1 }), children: _jsx(CardContainer, { ref: setContentNode, style: { marginBottom: data.cardGap }, "$isDragging": isDragging, "$isDragOverlay": false, children: _jsx("div", Object.assign({}, attributes, listeners, { children: data.renderCard({
|
|
47
|
+
card,
|
|
48
|
+
cardId,
|
|
49
|
+
group,
|
|
50
|
+
index,
|
|
51
|
+
isDragging,
|
|
52
|
+
isSorting,
|
|
53
|
+
isDragOverlay: false,
|
|
54
|
+
}) })) }) }));
|
|
55
|
+
};
|
|
56
|
+
export const MemoizedSortableVirtualRow = memo(SortableVirtualRow, areEqual);
|
package/dist/index.d.ts
CHANGED
|
@@ -11,6 +11,7 @@ export * from "./Button";
|
|
|
11
11
|
export * from "./DropDownMenu";
|
|
12
12
|
export * from "./Switch";
|
|
13
13
|
export * from "./Alert";
|
|
14
|
+
export * from "./Kanban";
|
|
14
15
|
export { default as IconButton } from "./IconButton";
|
|
15
16
|
export { default as DateInput } from "./DateInput";
|
|
16
17
|
export { default as TextArea } from "./TextArea";
|
package/dist/index.js
CHANGED
|
@@ -6,6 +6,7 @@ export * from "./Button";
|
|
|
6
6
|
export * from "./DropDownMenu";
|
|
7
7
|
export * from "./Switch";
|
|
8
8
|
export * from "./Alert";
|
|
9
|
+
export * from "./Kanban";
|
|
9
10
|
export { default as IconButton } from "./IconButton";
|
|
10
11
|
export { default as DateInput } from "./DateInput";
|
|
11
12
|
export { default as TextArea } from "./TextArea";
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@monolith-forensics/monolith-ui",
|
|
3
|
-
"version": "1.3.
|
|
3
|
+
"version": "1.3.112-dev.1",
|
|
4
4
|
"main": "./dist/index.js",
|
|
5
5
|
"types": "./dist/index.d.ts",
|
|
6
6
|
"author": "Matt Danner (Monolith Forensics LLC)",
|
|
@@ -25,12 +25,16 @@
|
|
|
25
25
|
"listFiles": "tsc --listFiles",
|
|
26
26
|
"dist-alpha": "yarn build && npm publish --tag alpha-$npm_package_version && yarn clean",
|
|
27
27
|
"dist-beta": "yarn build && npm publish --tag beta-$npm_package_version && yarn clean",
|
|
28
|
+
"dist-dev": "yarn build && npm version prerelease --preid=dev && npm publish --tag dev && yarn clean",
|
|
28
29
|
"release-patch": "yarn version --patch --deferred && yarn build && npm publish && yarn clean",
|
|
29
30
|
"release-minor": "yarn version --minor --deferred && yarn build && npm publish && yarn clean",
|
|
30
31
|
"release-major": "yarn version --major --deferred && yarn build && npm publish && yarn clean"
|
|
31
32
|
},
|
|
32
33
|
"dependencies": {
|
|
33
34
|
"@codemirror/language-data": "^6.5.1",
|
|
35
|
+
"@dnd-kit/core": "^6.3.1",
|
|
36
|
+
"@dnd-kit/sortable": "^10.0.0",
|
|
37
|
+
"@dnd-kit/utilities": "^3.2.2",
|
|
34
38
|
"@floating-ui/react": "^0.26.16",
|
|
35
39
|
"@mantine/hooks": "^7.13.0",
|
|
36
40
|
"@radix-ui/react-switch": "^1.0.7",
|