@protobase/ui 0.22.8 → 0.22.9
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/app/displays/board-column.js +63 -21
- package/dist/app/displays/board-columns.js +3 -1
- package/dist/app/displays/board-display.js +62 -11
- package/dist/app/displays/board-dnd.js +41 -5
- package/dist/app/displays/board-place.js +45 -0
- package/dist/app/displays/use-board-move.js +18 -9
- package/dist/app/list-page.js +3 -2
- package/dist/app/related-section.js +1 -1
- package/dist/package.js +1 -1
- package/dist/shell/assets/app-B6FpXlpX.js +69 -0
- package/dist/shell/assets/index-BflGBHJH.css +2 -0
- package/dist/shell/assets/{index-BU2imrOR.js → index-DMNlGow_.js} +1 -1
- package/dist/shell/assets/{maplibre-CpkT3k-d.js → maplibre-Cm6vGs92.js} +1 -1
- package/dist/shell/assets/{markdown-content-Cu8Q0N0V.js → markdown-content-DuCUZXHs.js} +1 -1
- package/dist/shell/assets/{record-map-DMFFj00J.js → record-map-Cj9snUMw.js} +1 -1
- package/dist/shell/assets/{records-map-C1VrhxtO.js → records-map-ByEdl0IP.js} +1 -1
- package/dist/shell/assets/{shared-protobase-ui-DyjCtT2d.js → shared-protobase-ui-DaruuG7b.js} +1 -1
- package/dist/shell/index.html +4 -4
- package/dist/shell/protobase.shell.json +1 -1
- package/package.json +4 -4
- package/dist/shell/assets/app-DivHWgft.js +0 -69
- package/dist/shell/assets/index-BtU8c1DC.css +0 -2
|
@@ -10,10 +10,20 @@ import { RecordCard } from "./record-card.js";
|
|
|
10
10
|
import { columnId } from "./board-dnd.js";
|
|
11
11
|
import { useBoardColumn } from "./use-board-column.js";
|
|
12
12
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
13
|
-
import {
|
|
13
|
+
import { useDroppable } from "@dnd-kit/core";
|
|
14
|
+
import { SortableContext, useSortable, verticalListSortingStrategy } from "@dnd-kit/sortable";
|
|
14
15
|
//#region src/app/displays/board-column.tsx
|
|
15
|
-
|
|
16
|
-
|
|
16
|
+
const inOrder = (rows, order) => {
|
|
17
|
+
if (!order) return rows;
|
|
18
|
+
const byKey = new Map(rows.map((row) => [row.key, row]));
|
|
19
|
+
const named = new Set(order);
|
|
20
|
+
return [...order.flatMap((key) => byKey.get(key) ?? []), ...rows.filter((row) => !named.has(row.key))];
|
|
21
|
+
};
|
|
22
|
+
/**
|
|
23
|
+
* One column of a board: its loaded records, those on their way out shown in their new column, and those on their way
|
|
24
|
+
* in at its top, or on a ranked board at the place they were dropped.
|
|
25
|
+
*/
|
|
26
|
+
const BoardColumnView = ({ model, view, display, column, fields, filter, orderBy, limit, moves, movable, placeable, dragged }) => {
|
|
17
27
|
const list = useBoardColumn({
|
|
18
28
|
model,
|
|
19
29
|
by: display.by,
|
|
@@ -22,14 +32,21 @@ const BoardColumnView = ({ model, view, display, column, fields, filter, orderBy
|
|
|
22
32
|
filter,
|
|
23
33
|
orderBy
|
|
24
34
|
});
|
|
25
|
-
const { setNodeRef, isOver } = useDroppable({ id: columnId(column.value) });
|
|
26
35
|
const card = boardCard(model, view, display);
|
|
27
36
|
const loaded = new Set(list.rows.map((row) => row.key));
|
|
28
37
|
const moving = new Map(moves.map((entry) => [entry.row.key, entry]));
|
|
29
38
|
const leaving = list.rows.filter((row) => moving.has(row.key) && moving.get(row.key).to !== column.value);
|
|
30
39
|
const arriving = moves.filter((entry) => entry.to === column.value && !loaded.has(entry.row.key)).map((entry) => entry.row);
|
|
31
40
|
const staying = list.rows.filter((row) => !leaving.includes(row)).map((row) => moving.has(row.key) ? moving.get(row.key).row : row);
|
|
32
|
-
const
|
|
41
|
+
const placing = moves.findLast((entry) => entry.to === column.value && entry.order);
|
|
42
|
+
const rows = inOrder([...arriving, ...staying], placing?.order).slice(0, limit);
|
|
43
|
+
const { setNodeRef, isOver } = useDroppable({
|
|
44
|
+
id: columnId(column.value),
|
|
45
|
+
data: {
|
|
46
|
+
column,
|
|
47
|
+
rows
|
|
48
|
+
}
|
|
49
|
+
});
|
|
33
50
|
const count = list.total - leaving.length + arriving.length;
|
|
34
51
|
if (column.value === null && !list.loading && count === 0 && !isOver) return null;
|
|
35
52
|
const plural = (view?.names?.plural ?? humanize(model.name)).toLowerCase();
|
|
@@ -62,16 +79,24 @@ const BoardColumnView = ({ model, view, display, column, fields, filter, orderBy
|
|
|
62
79
|
}) : rows.length === 0 ? /* @__PURE__ */ jsxs("p", {
|
|
63
80
|
className: "px-1 py-4 text-center text-xs text-muted-foreground",
|
|
64
81
|
children: ["No ", plural]
|
|
65
|
-
}) : /* @__PURE__ */ jsx(
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
row,
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
82
|
+
}) : /* @__PURE__ */ jsx(SortableContext, {
|
|
83
|
+
id: columnId(column.value),
|
|
84
|
+
items: rows.map((row) => row.key),
|
|
85
|
+
strategy: verticalListSortingStrategy,
|
|
86
|
+
children: /* @__PURE__ */ jsx("ul", {
|
|
87
|
+
className: "-mb-2 flex flex-col",
|
|
88
|
+
children: rows.map((row) => /* @__PURE__ */ jsx(BoardCard, {
|
|
89
|
+
model,
|
|
90
|
+
view,
|
|
91
|
+
card,
|
|
92
|
+
row,
|
|
93
|
+
column,
|
|
94
|
+
rows,
|
|
95
|
+
draggable: (movable || placeable) && rowAllows(row.record, "update"),
|
|
96
|
+
placeable,
|
|
97
|
+
dragged
|
|
98
|
+
}, row.key))
|
|
99
|
+
})
|
|
75
100
|
}),
|
|
76
101
|
limit === void 0 && list.more && /* @__PURE__ */ jsx(Button, {
|
|
77
102
|
size: "sm",
|
|
@@ -82,13 +107,26 @@ const BoardColumnView = ({ model, view, display, column, fields, filter, orderBy
|
|
|
82
107
|
]
|
|
83
108
|
});
|
|
84
109
|
};
|
|
85
|
-
|
|
86
|
-
|
|
110
|
+
/**
|
|
111
|
+
* A record's card, picked up by dragging it or by Space on its link; one the user may not update stays put. Where the
|
|
112
|
+
* user may change the order of a ranked board, a card is dropped on too: in its own column the cards make room as it
|
|
113
|
+
* moves, in another a bar marks its place.
|
|
114
|
+
*/
|
|
115
|
+
const BoardCard = ({ model, view, card, row, column, rows, draggable, placeable, dragged }) => {
|
|
116
|
+
const { setNodeRef, setActivatorNodeRef, listeners, isDragging, isOver, active, transform, transition } = useSortable({
|
|
87
117
|
id: row.key,
|
|
88
|
-
data: {
|
|
89
|
-
|
|
118
|
+
data: {
|
|
119
|
+
row,
|
|
120
|
+
column,
|
|
121
|
+
rows
|
|
122
|
+
},
|
|
123
|
+
disabled: {
|
|
124
|
+
draggable: !draggable,
|
|
125
|
+
droppable: !placeable
|
|
126
|
+
}
|
|
90
127
|
});
|
|
91
|
-
|
|
128
|
+
const arriving = isOver && (active?.data.current?.column)?.value !== column.value;
|
|
129
|
+
return /* @__PURE__ */ jsx("li", {
|
|
92
130
|
ref: (element) => {
|
|
93
131
|
setNodeRef(element);
|
|
94
132
|
setActivatorNodeRef(element?.querySelector("a") ?? element);
|
|
@@ -100,7 +138,11 @@ const BoardCard = ({ model, view, card, row, movable, dragged }) => {
|
|
|
100
138
|
event.preventDefault();
|
|
101
139
|
event.stopPropagation();
|
|
102
140
|
},
|
|
103
|
-
|
|
141
|
+
style: {
|
|
142
|
+
transform: transform ? `translate3d(${transform.x}px, ${transform.y}px, 0)` : void 0,
|
|
143
|
+
transition
|
|
144
|
+
},
|
|
145
|
+
className: cn("relative pb-2", draggable && "cursor-grab touch-manipulation", isDragging && "opacity-40", arriving && "before:absolute before:inset-x-0 before:-top-1 before:h-0.5 before:rounded-full before:bg-primary"),
|
|
104
146
|
children: /* @__PURE__ */ jsx(RecordCard, {
|
|
105
147
|
model,
|
|
106
148
|
view,
|
|
@@ -21,6 +21,8 @@ const boardColumns = (model, view, display) => {
|
|
|
21
21
|
label: `No ${(hints?.label ?? humanize(display.by)).toLowerCase()}`
|
|
22
22
|
}];
|
|
23
23
|
};
|
|
24
|
+
/** The field a board orders each column by: the resource's `.position()`, when its records are ordered as one rather than within a parent. */
|
|
25
|
+
const boardRank = (model) => model.position && model.position.within === void 0 ? model.position.field : void 0;
|
|
24
26
|
/** The list's filter, its search included, narrowed to the records of one column. */
|
|
25
27
|
const columnFilter = (filter, by, value) => {
|
|
26
28
|
const column = printFilter(value === null ? where.isNull(by) : where.eq(by, value));
|
|
@@ -42,4 +44,4 @@ const refusalReason = (error) => {
|
|
|
42
44
|
return reasons.length > 0 ? reasons.join("; ") : error.message;
|
|
43
45
|
};
|
|
44
46
|
//#endregion
|
|
45
|
-
export { boardCard, boardColumns, columnFilter, refusalReason };
|
|
47
|
+
export { boardCard, boardColumns, boardRank, columnFilter, refusalReason };
|
|
@@ -1,25 +1,47 @@
|
|
|
1
|
-
import { boardCard, boardColumns } from "./board-columns.js";
|
|
1
|
+
import { boardCard, boardColumns, boardRank } from "./board-columns.js";
|
|
2
2
|
import { usePermissions } from "../meta-gate.js";
|
|
3
3
|
import { can } from "../permissions.js";
|
|
4
4
|
import { RecordCard } from "./record-card.js";
|
|
5
|
-
import {
|
|
5
|
+
import { listHandOrder } from "../hand-order/hand-order.js";
|
|
6
|
+
import { cardCoordinates, collision, columnId } from "./board-dnd.js";
|
|
6
7
|
import { BoardColumnView } from "./board-column.js";
|
|
7
8
|
import { columnsAllowed } from "./board-allowed.js";
|
|
9
|
+
import { placedRows } from "./board-place.js";
|
|
8
10
|
import { useBoardMove } from "./use-board-move.js";
|
|
9
11
|
import { useRef, useState } from "react";
|
|
10
12
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
11
13
|
import { DndContext, DragOverlay, KeyboardSensor, MouseSensor, TouchSensor, useSensor, useSensors } from "@dnd-kit/core";
|
|
12
14
|
//#region src/app/displays/board-display.tsx
|
|
15
|
+
const placeOver = (over, row) => {
|
|
16
|
+
const data = over.data.current;
|
|
17
|
+
if (!data?.column || !data.rows) return void 0;
|
|
18
|
+
if (data.sortable) return {
|
|
19
|
+
column: data.column,
|
|
20
|
+
rows: data.rows,
|
|
21
|
+
index: data.sortable.index
|
|
22
|
+
};
|
|
23
|
+
const own = data.rows.some((entry) => entry.key === row.key);
|
|
24
|
+
return {
|
|
25
|
+
column: data.column,
|
|
26
|
+
rows: data.rows,
|
|
27
|
+
index: own ? data.rows.length - 1 : data.rows.length
|
|
28
|
+
};
|
|
29
|
+
};
|
|
13
30
|
/**
|
|
14
31
|
* A column per value of the board's enum field, each loading its own records through the list API, with their count;
|
|
15
|
-
* a card dragged, or moved with the keyboard, to another column saves the record with that column's value.
|
|
32
|
+
* a card dragged, or moved with the keyboard, to another column saves the record with that column's value. When the
|
|
33
|
+
* resource has a position, its records ordered as one, the columns are in its order and a card is dropped at a place:
|
|
34
|
+
* the up and down arrow keys move it within its column, and the drop renumbers the cards before it.
|
|
16
35
|
*/
|
|
17
36
|
const BoardDisplay = ({ model, view, display, fields, filter, orderBy, limit }) => {
|
|
18
37
|
const columns = columnsAllowed(filter, display.by, boardColumns(model, view, display));
|
|
19
38
|
const card = boardCard(model, view, display);
|
|
20
39
|
const permissions = usePermissions(model.name);
|
|
21
40
|
const movable = can(permissions, "update") && !model.fields[display.by]?.readOnly;
|
|
22
|
-
const
|
|
41
|
+
const rank = boardRank(model);
|
|
42
|
+
const placeable = rank !== void 0 && listHandOrder(model, permissions) !== void 0;
|
|
43
|
+
const columnOrder = rank ? `${rank} asc` : orderBy;
|
|
44
|
+
const { moves, move } = useBoardMove(model, display.by, rank);
|
|
23
45
|
const [active, setActive] = useState();
|
|
24
46
|
const dragged = useRef(false);
|
|
25
47
|
const sensors = useSensors(useSensor(MouseSensor, { activationConstraint: { distance: 4 } }), useSensor(TouchSensor, { activationConstraint: {
|
|
@@ -31,15 +53,30 @@ const BoardDisplay = ({ model, view, display, fields, filter, orderBy, limit })
|
|
|
31
53
|
cancel: ["Escape"],
|
|
32
54
|
end: ["Space", "Enter"]
|
|
33
55
|
},
|
|
34
|
-
coordinateGetter:
|
|
56
|
+
coordinateGetter: cardCoordinates
|
|
35
57
|
}));
|
|
36
58
|
const title = (row) => String(row.labels[card.title] ?? row.record[card.title] ?? row.key);
|
|
37
59
|
const columnOf = (id) => columns.find((column) => columnId(column.value) === id);
|
|
38
60
|
const valueOf = (row) => row.record[display.by] ?? null;
|
|
61
|
+
const announced = (row, over) => {
|
|
62
|
+
if (!over) return { over: "no column" };
|
|
63
|
+
const place = placeable ? placeOver(over, row) : void 0;
|
|
64
|
+
if (!place) return { over: columnOf(over.id)?.label ?? "no column" };
|
|
65
|
+
const count = place.rows.some((entry) => entry.key === row.key) ? place.rows.length : place.rows.length + 1;
|
|
66
|
+
return {
|
|
67
|
+
over: `${place.column.label}, place ${place.index + 1} of ${count}`,
|
|
68
|
+
place: `${place.column.label}, place ${place.index + 1}`
|
|
69
|
+
};
|
|
70
|
+
};
|
|
39
71
|
const announcements = {
|
|
40
72
|
onDragStart: ({ active: picked }) => `Picked up ${title(picked.data.current.row)}.`,
|
|
41
|
-
onDragOver: ({ active: picked, over }) => `${title(picked.data.current.row)} is over ${
|
|
42
|
-
onDragEnd: ({ active: picked, over }) =>
|
|
73
|
+
onDragOver: ({ active: picked, over }) => `${title(picked.data.current.row)} is over ${announced(picked.data.current.row, over).over}.`,
|
|
74
|
+
onDragEnd: ({ active: picked, over }) => {
|
|
75
|
+
const row = picked.data.current.row;
|
|
76
|
+
if (!over) return `${title(row)} was put back.`;
|
|
77
|
+
const said = announced(row, over);
|
|
78
|
+
return `${title(row)} was moved to ${said.place ?? said.over}.`;
|
|
79
|
+
},
|
|
43
80
|
onDragCancel: ({ active: picked }) => `Moving ${title(picked.data.current.row)} was cancelled.`
|
|
44
81
|
};
|
|
45
82
|
const start = ({ active: picked }) => {
|
|
@@ -50,8 +87,21 @@ const BoardDisplay = ({ model, view, display, fields, filter, orderBy, limit })
|
|
|
50
87
|
setActive(void 0);
|
|
51
88
|
setTimeout(() => dragged.current = false);
|
|
52
89
|
const row = picked.data.current.row;
|
|
53
|
-
|
|
54
|
-
|
|
90
|
+
if (!over) return;
|
|
91
|
+
const place = placeable ? placeOver(over, row) : void 0;
|
|
92
|
+
if (!place) {
|
|
93
|
+
const target = columnOf(over.id);
|
|
94
|
+
if (target && target.value !== valueOf(row) && movable) move(row, target);
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
const from = place.rows.findIndex((entry) => entry.key === row.key);
|
|
98
|
+
if (from === -1 && !movable) return;
|
|
99
|
+
if (from === place.index) return;
|
|
100
|
+
const order = placedRows(place.rows, row, place.index);
|
|
101
|
+
move(row, place.column, {
|
|
102
|
+
order,
|
|
103
|
+
changed: from === -1 ? place.index : Math.max(from, place.index)
|
|
104
|
+
});
|
|
55
105
|
};
|
|
56
106
|
return /* @__PURE__ */ jsxs(DndContext, {
|
|
57
107
|
sensors,
|
|
@@ -64,7 +114,7 @@ const BoardDisplay = ({ model, view, display, fields, filter, orderBy, limit })
|
|
|
64
114
|
},
|
|
65
115
|
accessibility: {
|
|
66
116
|
announcements,
|
|
67
|
-
screenReaderInstructions: { draggable: "To move a card to another column, press Space, move it with the left and right arrow keys, and press Space again to drop it there. Escape puts it back." }
|
|
117
|
+
screenReaderInstructions: { draggable: placeable ? "To move a card, press Space, move it with the up and down arrow keys within its column and the left and right arrow keys to another column, and press Space again to drop it there. Escape puts it back." : "To move a card to another column, press Space, move it with the left and right arrow keys, and press Space again to drop it there. Escape puts it back." }
|
|
68
118
|
},
|
|
69
119
|
children: [/* @__PURE__ */ jsx("div", {
|
|
70
120
|
className: "relative min-w-0 flex-1 overflow-x-auto",
|
|
@@ -77,10 +127,11 @@ const BoardDisplay = ({ model, view, display, fields, filter, orderBy, limit })
|
|
|
77
127
|
column,
|
|
78
128
|
fields,
|
|
79
129
|
filter,
|
|
80
|
-
orderBy,
|
|
130
|
+
orderBy: columnOrder,
|
|
81
131
|
limit,
|
|
82
132
|
moves,
|
|
83
133
|
movable,
|
|
134
|
+
placeable,
|
|
84
135
|
dragged: () => dragged.current
|
|
85
136
|
}, columnId(column.value)))
|
|
86
137
|
})
|
|
@@ -2,17 +2,24 @@ import { pointerWithin, rectIntersection } from "@dnd-kit/core";
|
|
|
2
2
|
//#region src/app/displays/board-dnd.ts
|
|
3
3
|
/** The id a board's column is dropped on by. */
|
|
4
4
|
const columnId = (value) => value === null ? "none" : `value:${value}`;
|
|
5
|
-
|
|
5
|
+
const isCard = (container) => container.data.current?.sortable !== void 0;
|
|
6
|
+
/**
|
|
7
|
+
* A pointer drops a card on the card under it, on a ranked board, else on the column under it; the keyboard, which has
|
|
8
|
+
* no pointer, on what the card overlaps.
|
|
9
|
+
*/
|
|
6
10
|
const collision = (args) => {
|
|
7
11
|
const under = pointerWithin(args);
|
|
12
|
+
const card = under.find((entry) => entry.data?.droppableContainer && isCard(entry.data.droppableContainer));
|
|
13
|
+
if (card) return [card];
|
|
8
14
|
return under.length > 0 ? under : rectIntersection(args);
|
|
9
15
|
};
|
|
10
|
-
/** The arrow keys move a picked-up card to the top of the next column
|
|
16
|
+
/** The left and right arrow keys move a picked-up card to the top of the next column, onto its first card when it has one. */
|
|
11
17
|
const columnCoordinates = (event, { context: { collisionRect, droppableRects, droppableContainers, over } }) => {
|
|
12
18
|
const step = event.code === "ArrowRight" ? 1 : event.code === "ArrowLeft" ? -1 : 0;
|
|
13
19
|
if (step === 0 || !collisionRect) return void 0;
|
|
14
20
|
event.preventDefault();
|
|
15
|
-
const
|
|
21
|
+
const enabled = droppableContainers.getEnabled();
|
|
22
|
+
const columns = enabled.filter((container) => !isCard(container)).flatMap((container) => {
|
|
16
23
|
const rect = droppableRects.get(container.id);
|
|
17
24
|
return rect ? [{
|
|
18
25
|
id: container.id,
|
|
@@ -20,13 +27,42 @@ const columnCoordinates = (event, { context: { collisionRect, droppableRects, dr
|
|
|
20
27
|
}] : [];
|
|
21
28
|
}).sort((a, b) => a.rect.left - b.rect.left);
|
|
22
29
|
const center = collisionRect.left + collisionRect.width / 2;
|
|
23
|
-
const
|
|
30
|
+
const current = over?.data.current?.sortable?.containerId ?? over?.id;
|
|
31
|
+
const at = current !== void 0 ? columns.findIndex((column) => column.id === current) : columns.findIndex((column) => column.rect.left <= center && center <= column.rect.right);
|
|
24
32
|
const target = columns[at + step];
|
|
25
33
|
if (at === -1 || !target) return void 0;
|
|
34
|
+
const first = cardRects(enabled, droppableRects, target.id)[0];
|
|
26
35
|
return {
|
|
27
36
|
x: target.rect.left + (target.rect.width - collisionRect.width) / 2,
|
|
37
|
+
y: first?.rect.top ?? target.rect.top
|
|
38
|
+
};
|
|
39
|
+
};
|
|
40
|
+
const cardRects = (enabled, rects, column) => enabled.filter((container) => container.data.current?.sortable?.containerId === column).flatMap((container) => {
|
|
41
|
+
const rect = rects.get(container.id);
|
|
42
|
+
return rect ? [{
|
|
43
|
+
id: container.id,
|
|
44
|
+
rect
|
|
45
|
+
}] : [];
|
|
46
|
+
}).sort((a, b) => a.rect.top - b.rect.top);
|
|
47
|
+
/**
|
|
48
|
+
* On a ranked board the up and down arrow keys move a picked-up card to the place of the card above or below it in its
|
|
49
|
+
* column; the left and right ones as `columnCoordinates` does. An unranked board has no cards to drop on, so up and down
|
|
50
|
+
* do nothing there.
|
|
51
|
+
*/
|
|
52
|
+
const cardCoordinates = (event, args) => {
|
|
53
|
+
const step = event.code === "ArrowDown" ? 1 : event.code === "ArrowUp" ? -1 : 0;
|
|
54
|
+
if (step === 0) return columnCoordinates(event, args);
|
|
55
|
+
const { droppableRects, droppableContainers, over } = args.context;
|
|
56
|
+
const column = over?.data.current?.sortable?.containerId ?? over?.id;
|
|
57
|
+
if (column === void 0) return void 0;
|
|
58
|
+
const cards = cardRects(droppableContainers.getEnabled(), droppableRects, column);
|
|
59
|
+
const target = cards[(over?.id === column ? cards.length : cards.findIndex((card) => card.id === over?.id)) + step];
|
|
60
|
+
if (!target) return void 0;
|
|
61
|
+
event.preventDefault();
|
|
62
|
+
return {
|
|
63
|
+
x: target.rect.left,
|
|
28
64
|
y: target.rect.top
|
|
29
65
|
};
|
|
30
66
|
};
|
|
31
67
|
//#endregion
|
|
32
|
-
export { collision, columnCoordinates, columnId };
|
|
68
|
+
export { cardCoordinates, collision, columnCoordinates, columnId };
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { recordKey } from "../../live/model-helpers.js";
|
|
2
|
+
import { encodeKey } from "@protobase/schema/internal";
|
|
3
|
+
import { arrayMove } from "@dnd-kit/sortable";
|
|
4
|
+
//#region src/app/displays/board-place.ts
|
|
5
|
+
/**
|
|
6
|
+
* The column's cards once `row` is dropped at `index`. In its own column it moves there, as a sortable list moves an
|
|
7
|
+
* item; in another it goes in before the card at `index`, or last.
|
|
8
|
+
*/
|
|
9
|
+
const placedRows = (rows, row, index) => {
|
|
10
|
+
const from = rows.findIndex((entry) => entry.key === row.key);
|
|
11
|
+
if (from !== -1) return arrayMove(rows, from, index);
|
|
12
|
+
return [
|
|
13
|
+
...rows.slice(0, index),
|
|
14
|
+
row,
|
|
15
|
+
...rows.slice(index)
|
|
16
|
+
];
|
|
17
|
+
};
|
|
18
|
+
const etagOf = (row) => typeof row.record.etag === "string" ? row.record.etag : "*";
|
|
19
|
+
/**
|
|
20
|
+
* The batch that saves a drop on a ranked board: in another column first the update of `by` with the card's ETag, then
|
|
21
|
+
* a `reorder` numbering the column's cards 1, 2, 3, … up to and including the one at `changed`, the last whose place
|
|
22
|
+
* changed. The cards after it already rank higher, so they keep their place unwritten, loaded or not.
|
|
23
|
+
*/
|
|
24
|
+
const placeOps = (model, by, rank, row, column, order, changed) => {
|
|
25
|
+
const moving = (row.record[by] ?? null) !== column.value;
|
|
26
|
+
const prefix = order.slice(0, changed + 1);
|
|
27
|
+
const etags = Object.fromEntries(prefix.map((entry) => [encodeKey(recordKey(model, entry.record)), moving && entry.key === row.key ? "*" : etagOf(entry)]));
|
|
28
|
+
const reorder = {
|
|
29
|
+
op: "reorder",
|
|
30
|
+
resource: model.name,
|
|
31
|
+
field: rank,
|
|
32
|
+
keys: prefix.map((entry) => recordKey(model, entry.record)),
|
|
33
|
+
etags
|
|
34
|
+
};
|
|
35
|
+
if (!moving) return [reorder];
|
|
36
|
+
return [{
|
|
37
|
+
op: "update",
|
|
38
|
+
resource: model.name,
|
|
39
|
+
key: recordKey(model, row.record),
|
|
40
|
+
data: { [by]: column.value },
|
|
41
|
+
etag: etagOf(row)
|
|
42
|
+
}, reorder];
|
|
43
|
+
};
|
|
44
|
+
//#endregion
|
|
45
|
+
export { placeOps, placedRows };
|
|
@@ -3,26 +3,31 @@ import { useToast } from "../../toasts/toast-provider.js";
|
|
|
3
3
|
import { recordKey } from "../../live/model-helpers.js";
|
|
4
4
|
import { refusalReason } from "./board-columns.js";
|
|
5
5
|
import { useRefreshResource } from "../pages/use-layout-data.js";
|
|
6
|
+
import { placeOps } from "./board-place.js";
|
|
6
7
|
import { useState } from "react";
|
|
7
8
|
import { useMutation } from "@tanstack/react-query";
|
|
8
9
|
//#region src/app/displays/use-board-move.ts
|
|
9
10
|
/**
|
|
10
11
|
* Moving a board's card to another column: one update of the record with the column's value and the ETag it was listed
|
|
11
|
-
* with, as a named update action sends, so the resource's access rules and `validate` decide.
|
|
12
|
-
*
|
|
12
|
+
* with, as a named update action sends, so the resource's access rules and `validate` decide. On a ranked board a drop
|
|
13
|
+
* at a place is one batch instead, that update when the column changes and a `reorder` by `rank` of the column's cards.
|
|
14
|
+
* The card shows in its new place at once; a refusal puts it back and says why, and either way every list of the
|
|
15
|
+
* resource reloads.
|
|
13
16
|
*/
|
|
14
|
-
const useBoardMove = (model, by) => {
|
|
17
|
+
const useBoardMove = (model, by, rank) => {
|
|
15
18
|
const client = useClient();
|
|
16
19
|
const toast = useToast();
|
|
17
20
|
const refresh = useRefreshResource();
|
|
18
21
|
const [moves, setMoves] = useState([]);
|
|
19
22
|
const settle = (key) => setMoves((current) => current.filter((move) => move.row.key !== key));
|
|
20
23
|
const mutation = useMutation({
|
|
21
|
-
mutationFn: ({ row, to }) => {
|
|
24
|
+
mutationFn: async ({ row, to, place }) => {
|
|
25
|
+
if (place && !rank) throw new Error("A board places a card only by its rank");
|
|
26
|
+
if (place) return client.batchWrite(placeOps(model, by, rank, row, to, place.order, place.changed));
|
|
22
27
|
const etag = typeof row.record.etag === "string" ? row.record.etag : "*";
|
|
23
28
|
return client.update(model.name, recordKey(model, row.record), { [by]: to.value }, etag);
|
|
24
29
|
},
|
|
25
|
-
onMutate: ({ row, to }) => {
|
|
30
|
+
onMutate: ({ row, to, place }) => {
|
|
26
31
|
const moved = {
|
|
27
32
|
...row,
|
|
28
33
|
record: {
|
|
@@ -30,16 +35,19 @@ const useBoardMove = (model, by) => {
|
|
|
30
35
|
[by]: to.value
|
|
31
36
|
}
|
|
32
37
|
};
|
|
38
|
+
const order = place?.order.map((entry) => entry.key);
|
|
33
39
|
setMoves((current) => [...current.filter((move) => move.row.key !== row.key), {
|
|
34
40
|
row: moved,
|
|
35
|
-
to: to.value
|
|
41
|
+
to: to.value,
|
|
42
|
+
...order && { order }
|
|
36
43
|
}]);
|
|
37
44
|
},
|
|
38
45
|
onError: (error, { row, to }) => {
|
|
39
46
|
settle(row.key);
|
|
47
|
+
const title = (row.record[by] ?? null) === to.value ? "Could not change the order" : `Could not move it to ${to.label}`;
|
|
40
48
|
toast.show({
|
|
41
49
|
state: "error",
|
|
42
|
-
title
|
|
50
|
+
title,
|
|
43
51
|
description: refusalReason(error)
|
|
44
52
|
});
|
|
45
53
|
},
|
|
@@ -50,9 +58,10 @@ const useBoardMove = (model, by) => {
|
|
|
50
58
|
});
|
|
51
59
|
return {
|
|
52
60
|
moves,
|
|
53
|
-
move: (row, to) => mutation.mutate({
|
|
61
|
+
move: (row, to, place) => mutation.mutate({
|
|
54
62
|
row,
|
|
55
|
-
to
|
|
63
|
+
to,
|
|
64
|
+
...place && { place }
|
|
56
65
|
})
|
|
57
66
|
};
|
|
58
67
|
};
|
package/dist/app/list-page.js
CHANGED
|
@@ -6,6 +6,7 @@ import { Button, buttonClass } from "../primitives/button.js";
|
|
|
6
6
|
import { humanize } from "../live/naming.js";
|
|
7
7
|
import { recordKey, recordTitleField } from "../live/model-helpers.js";
|
|
8
8
|
import { columnsFromModel, listColumnNames } from "../live/columns-from-model.js";
|
|
9
|
+
import { boardRank } from "./displays/board-columns.js";
|
|
9
10
|
import { displayRow, listDisplayChoices, loadedFields, shownListDisplay } from "./displays/list-displays.js";
|
|
10
11
|
import { parseSort, sortToOrderBy } from "./list-order.js";
|
|
11
12
|
import { listedResource } from "./current-address.js";
|
|
@@ -29,6 +30,7 @@ import { facetNames, filterChips } from "./searches/saved-search.js";
|
|
|
29
30
|
import { useSeedRecord } from "./record-preview.js";
|
|
30
31
|
import { SignInPrompt } from "../auth/sign-in-prompt.js";
|
|
31
32
|
import { relatedColumnLoads } from "./related-dots/dot-card.js";
|
|
33
|
+
import { listHandOrder } from "./hand-order/hand-order.js";
|
|
32
34
|
import { useDisplayRows } from "./displays/use-display-rows.js";
|
|
33
35
|
import { BoardDisplay } from "./displays/board-display.js";
|
|
34
36
|
import { CalendarDisplay } from "./displays/calendar-display.js";
|
|
@@ -56,7 +58,6 @@ import { SavedList } from "./saved/saved-list.js";
|
|
|
56
58
|
import { SaveSearchButton } from "./searches/save-search-button.js";
|
|
57
59
|
import { SavedSearches } from "./searches/saved-searches.js";
|
|
58
60
|
import { useListCellEditor } from "./list-edit/use-list-cell-editor.js";
|
|
59
|
-
import { listHandOrder } from "./hand-order/hand-order.js";
|
|
60
61
|
import { HandSortedList } from "./hand-order/hand-sorted-list.js";
|
|
61
62
|
import { SortButton } from "./hand-order/sort-button.js";
|
|
62
63
|
import { useMemo, useState } from "react";
|
|
@@ -212,7 +213,7 @@ const LoadedList = ({ model, address, scope, heading }) => {
|
|
|
212
213
|
setFilter(state, text ? [...others, where$1.search(text)] : others);
|
|
213
214
|
};
|
|
214
215
|
const plural = view?.names?.plural ?? humanize(resource);
|
|
215
|
-
const sorted = display?.kind === "calendar" ? void 0 : display;
|
|
216
|
+
const sorted = display?.kind === "calendar" || display?.kind === "board" && boardRank(model) ? void 0 : display;
|
|
216
217
|
const sortMenu = sorted && /* @__PURE__ */ jsx(SortMenu, {
|
|
217
218
|
model,
|
|
218
219
|
view,
|
|
@@ -7,10 +7,10 @@ import { useAdminMeta, usePermissions } from "./meta-gate.js";
|
|
|
7
7
|
import { ErrorBanner } from "./error-banner.js";
|
|
8
8
|
import { countQuery } from "./pages/use-layout-data.js";
|
|
9
9
|
import { searchAll } from "../live/search-all.js";
|
|
10
|
+
import { relatedHandOrder } from "./hand-order/hand-order.js";
|
|
10
11
|
import { relatedOpens } from "./related-opens.js";
|
|
11
12
|
import { EmbeddedDisplaySwitch, EmbeddedList } from "./embedded-list/embedded-list.js";
|
|
12
13
|
import { useEmbeddedDisplay } from "./embedded-list/use-embedded-display.js";
|
|
13
|
-
import { relatedHandOrder } from "./hand-order/hand-order.js";
|
|
14
14
|
import { HandSortedList } from "./hand-order/hand-sorted-list.js";
|
|
15
15
|
import { SortButton } from "./hand-order/sort-button.js";
|
|
16
16
|
import { useState } from "react";
|
package/dist/package.js
CHANGED