@protobase/ui 0.23.12 → 0.23.14
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-display.js +2 -1
- package/dist/app/displays/board-place.js +9 -6
- package/dist/app/displays/use-board-move.js +1 -1
- package/dist/app/hand-order/hand-sorted-list.js +0 -7
- package/dist/app/hand-order/messages.js +2 -4
- package/dist/app/hand-order/{sort-button.js → reorder-button.js} +5 -5
- package/dist/app/hand-order/reorder-span.js +25 -0
- package/dist/app/hand-order/use-hand-order.js +41 -21
- package/dist/app/list-page.js +2 -2
- package/dist/app/related-section.js +3 -3
- package/dist/package.js +1 -1
- package/dist/shell/assets/{app-BRdywuCD.js → app-CXlnPi2R.js} +39 -39
- package/dist/shell/assets/{index-HdKQ9gvF.js → index-DL-zk94Z.js} +1 -1
- package/dist/shell/assets/{maplibre-BvFmZgXT.js → maplibre-BqnpnW9w.js} +1 -1
- package/dist/shell/assets/{markdown-content-BL8dxxr1.js → markdown-content-0PAPwJ3B.js} +1 -1
- package/dist/shell/assets/{record-map-DagdaaJK.js → record-map-e0DqjuCs.js} +1 -1
- package/dist/shell/assets/{records-map-eM1ob8eA.js → records-map-CA-lAL9M.js} +1 -1
- package/dist/shell/assets/{shared-protobase-ui-D8qQezaz.js → shared-protobase-ui-BZ2UBlKm.js} +1 -1
- package/dist/shell/index.html +3 -3
- package/dist/shell/protobase.shell.json +1 -1
- package/dist/shell/protobase.sw.js +2 -2
- package/package.json +4 -4
|
@@ -100,7 +100,8 @@ const BoardDisplay = ({ model, view, display, fields, filter, orderBy, limit })
|
|
|
100
100
|
const order = placedRows(place.rows, row, place.index);
|
|
101
101
|
move(row, place.column, {
|
|
102
102
|
order,
|
|
103
|
-
|
|
103
|
+
first: from === -1 ? place.index : Math.min(from, place.index),
|
|
104
|
+
last: from === -1 ? place.index : Math.max(from, place.index)
|
|
104
105
|
});
|
|
105
106
|
};
|
|
106
107
|
return /* @__PURE__ */ jsxs(DndContext, {
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { recordKey } from "../../live/model-helpers.js";
|
|
2
|
+
import { reorderSpan } from "../hand-order/reorder-span.js";
|
|
2
3
|
import { encodeKey } from "@protobase/schema/internal";
|
|
3
4
|
import { arrayMove } from "@dnd-kit/sortable";
|
|
4
5
|
//#region src/app/displays/board-place.ts
|
|
@@ -18,18 +19,20 @@ const placedRows = (rows, row, index) => {
|
|
|
18
19
|
const etagOf = (row) => typeof row.record.etag === "string" ? row.record.etag : "*";
|
|
19
20
|
/**
|
|
20
21
|
* 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`
|
|
22
|
-
*
|
|
22
|
+
* a `reorder` of the column's cards whose place changed, `first` to `last`, reaching only as far as their ranks need
|
|
23
|
+
* (see `reorderSpan`). The cards after it already rank higher, so they keep their place unwritten, loaded or not.
|
|
23
24
|
*/
|
|
24
|
-
const placeOps = (model, by, rank, row, column, order,
|
|
25
|
+
const placeOps = (model, by, rank, row, column, order, first, last) => {
|
|
25
26
|
const moving = (row.record[by] ?? null) !== column.value;
|
|
26
|
-
const
|
|
27
|
-
const
|
|
27
|
+
const span = reorderSpan(order.map((entry) => entry.record[rank]), first, last);
|
|
28
|
+
const sent = order.slice(span.first, span.last + 1);
|
|
29
|
+
const etags = Object.fromEntries(sent.map((entry) => [encodeKey(recordKey(model, entry.record)), moving && entry.key === row.key ? "*" : etagOf(entry)]));
|
|
28
30
|
const reorder = {
|
|
29
31
|
op: "reorder",
|
|
30
32
|
resource: model.name,
|
|
31
33
|
field: rank,
|
|
32
|
-
keys:
|
|
34
|
+
keys: sent.map((entry) => recordKey(model, entry.record)),
|
|
35
|
+
start: span.start,
|
|
33
36
|
etags
|
|
34
37
|
};
|
|
35
38
|
if (!moving) return [reorder];
|
|
@@ -23,7 +23,7 @@ const useBoardMove = (model, by, rank) => {
|
|
|
23
23
|
const mutation = useMutation({
|
|
24
24
|
mutationFn: async ({ row, to, place }) => {
|
|
25
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.
|
|
26
|
+
if (place) return client.batchWrite(placeOps(model, by, rank, row, to, place.order, place.first, place.last));
|
|
27
27
|
const etag = typeof row.record.etag === "string" ? row.record.etag : "*";
|
|
28
28
|
return client.update(model.name, recordKey(model, row.record), { [by]: to.value }, etag);
|
|
29
29
|
},
|
|
@@ -1,6 +1,4 @@
|
|
|
1
|
-
import { formatTag } from "../../locale/active-locale.js";
|
|
2
1
|
import { Skeleton } from "../../primitives/skeleton.js";
|
|
3
|
-
import { humanize } from "../../live/naming.js";
|
|
4
2
|
import { recordKey, recordTitle } from "../../live/model-helpers.js";
|
|
5
3
|
import { loadedFields } from "../displays/list-displays.js";
|
|
6
4
|
import { useAdminMeta } from "../meta-gate.js";
|
|
@@ -54,7 +52,6 @@ const HandSortedList = ({ model, view, field, filter, columns }) => {
|
|
|
54
52
|
total: rows.length,
|
|
55
53
|
rowAt: (index) => rows[index]
|
|
56
54
|
}, [rows]);
|
|
57
|
-
const plural = (view?.names?.plural ?? humanize(model.name)).toLowerCase();
|
|
58
55
|
if (order.loading) return /* @__PURE__ */ jsx("div", {
|
|
59
56
|
className: "flex flex-col gap-2",
|
|
60
57
|
children: Array.from({ length: 3 }, (_, index) => /* @__PURE__ */ jsx(Skeleton, { className: "h-4 w-full" }, index))
|
|
@@ -63,10 +60,6 @@ const HandSortedList = ({ model, view, field, filter, columns }) => {
|
|
|
63
60
|
title: text("couldNotLoad"),
|
|
64
61
|
error: order.error
|
|
65
62
|
});
|
|
66
|
-
if (order.more) return /* @__PURE__ */ jsx("p", {
|
|
67
|
-
className: "text-muted-foreground",
|
|
68
|
-
children: text("tooMany", 500 .toLocaleString(formatTag()), plural)
|
|
69
|
-
});
|
|
70
63
|
if (!source || source.total === 0) return /* @__PURE__ */ jsx("p", {
|
|
71
64
|
className: "text-muted-foreground",
|
|
72
65
|
children: text("nothingYet")
|
|
@@ -3,17 +3,15 @@ import { defineMessages } from "../../locale/messages.js";
|
|
|
3
3
|
const text = defineMessages({
|
|
4
4
|
en: {
|
|
5
5
|
done: "Done",
|
|
6
|
-
|
|
6
|
+
reorder: "Reorder",
|
|
7
7
|
couldNotLoad: "Could not load",
|
|
8
|
-
tooMany: (limit, plural) => `There are more than ${limit} ${plural}, too many to sort by hand.`,
|
|
9
8
|
nothingYet: "Nothing here yet.",
|
|
10
9
|
couldNotChange: "Could not change the order"
|
|
11
10
|
},
|
|
12
11
|
nl: {
|
|
13
12
|
done: "Klaar",
|
|
14
|
-
|
|
13
|
+
reorder: "Volgorde wijzigen",
|
|
15
14
|
couldNotLoad: "Kon niet laden",
|
|
16
|
-
tooMany: (limit, plural) => `Er zijn meer dan ${limit} ${plural}, te veel om met de hand te sorteren.`,
|
|
17
15
|
nothingYet: "Hier staat nog niets.",
|
|
18
16
|
couldNotChange: "Kon de volgorde niet wijzigen"
|
|
19
17
|
}
|
|
@@ -2,10 +2,10 @@ import { Button } from "../../primitives/button.js";
|
|
|
2
2
|
import { text } from "./messages.js";
|
|
3
3
|
import { ArrowUpDown, Check } from "lucide-react";
|
|
4
4
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
5
|
-
//#region src/app/hand-order/
|
|
6
|
-
/** The button that turns
|
|
7
|
-
const
|
|
8
|
-
const label = sorting ? text("done") : text("
|
|
5
|
+
//#region src/app/hand-order/reorder-button.tsx
|
|
6
|
+
/** The button that turns reordering by hand on, as **Reorder**, and off again, as **Done**; with `iconOnly` its icon alone. */
|
|
7
|
+
const ReorderButton = ({ sorting, onChange, size, iconOnly = false }) => {
|
|
8
|
+
const label = sorting ? text("done") : text("reorder");
|
|
9
9
|
return /* @__PURE__ */ jsxs(Button, {
|
|
10
10
|
size,
|
|
11
11
|
variant: sorting ? "primary" : "secondary",
|
|
@@ -18,4 +18,4 @@ const SortButton = ({ sorting, onChange, size, iconOnly = false }) => {
|
|
|
18
18
|
});
|
|
19
19
|
};
|
|
20
20
|
//#endregion
|
|
21
|
-
export {
|
|
21
|
+
export { ReorderButton };
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
//#region src/app/hand-order/reorder-span.ts
|
|
2
|
+
const isRank = (value) => typeof value === "number" && Number.isInteger(value);
|
|
3
|
+
/**
|
|
4
|
+
* The shortest stretch that saves a move: `ranks` are the records' values in their new order, and `first` to `last`
|
|
5
|
+
* the places that moved. The records before it keep their values, so they must already rise, and the stretch starts
|
|
6
|
+
* just above them, or at 1; it then reaches on until the next record already ranks above it. When the values were
|
|
7
|
+
* 1, 2, 3, ... that is the records that moved and nothing else, so a long list sends a few keys instead of all of them.
|
|
8
|
+
*/
|
|
9
|
+
const reorderSpan = (ranks, first, last) => {
|
|
10
|
+
let rising = 0;
|
|
11
|
+
while (rising < first && isRank(ranks[rising]) && (rising === 0 || ranks[rising] > ranks[rising - 1])) rising += 1;
|
|
12
|
+
let tail = ranks.length;
|
|
13
|
+
while (tail - 1 > last && isRank(ranks[tail - 1]) && (tail === ranks.length || ranks[tail - 1] < ranks[tail])) tail -= 1;
|
|
14
|
+
const from = Math.min(first, rising);
|
|
15
|
+
const start = from === 0 ? 1 : ranks[from - 1] + 1;
|
|
16
|
+
let to = Math.max(last, tail - 1);
|
|
17
|
+
while (to + 1 < ranks.length && ranks[to + 1] <= start + (to - from)) to += 1;
|
|
18
|
+
return {
|
|
19
|
+
first: from,
|
|
20
|
+
last: to,
|
|
21
|
+
start
|
|
22
|
+
};
|
|
23
|
+
};
|
|
24
|
+
//#endregion
|
|
25
|
+
export { reorderSpan };
|
|
@@ -6,6 +6,7 @@ import { refusalReason } from "../displays/board-columns.js";
|
|
|
6
6
|
import { useAdminMeta } from "../meta-gate.js";
|
|
7
7
|
import { useRefreshResource } from "../pages/use-layout-data.js";
|
|
8
8
|
import { listLabels, withLabels } from "../../live/relation-labels.js";
|
|
9
|
+
import { reorderSpan } from "./reorder-span.js";
|
|
9
10
|
import { text } from "./messages.js";
|
|
10
11
|
import { useRef, useState } from "react";
|
|
11
12
|
import { encodeKey } from "@protobase/schema/internal";
|
|
@@ -13,10 +14,10 @@ import { useQuery } from "@tanstack/react-query";
|
|
|
13
14
|
import { arrayMove } from "@dnd-kit/sortable";
|
|
14
15
|
//#region src/app/hand-order/use-hand-order.ts
|
|
15
16
|
/**
|
|
16
|
-
* The records of `filter` in the order of `field`, all of them
|
|
17
|
-
*
|
|
18
|
-
* `reorder` with their ETags; moves made while one saves are saved after it, with the ETags it gave back.
|
|
19
|
-
* puts them back as the server has them and says why; either way every list of the resource reloads.
|
|
17
|
+
* The records of `filter` in the order of `field`, all of them, a page of `handOrderPage` at a time, and `move` to put
|
|
18
|
+
* one elsewhere. A move shows at once and saves only the records whose place changed (see `reorderSpan`) in one batch
|
|
19
|
+
* `reorder` with their ETags; moves made while one saves are saved after it, with the values and ETags it gave back.
|
|
20
|
+
* A refusal puts them back as the server has them and says why; either way every list of the resource reloads.
|
|
20
21
|
*/
|
|
21
22
|
const useHandOrder = ({ model, field, filter, fields }) => {
|
|
22
23
|
const client = useClient();
|
|
@@ -28,34 +29,44 @@ const useHandOrder = ({ model, field, filter, fields }) => {
|
|
|
28
29
|
queryKey: [...keys.list(model.name, filter, orderBy, 500, "hand-order"), fields.join(",")],
|
|
29
30
|
queryFn: async () => {
|
|
30
31
|
const labels = listLabels(resources, views, model, fields);
|
|
31
|
-
const
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
32
|
+
const rows = [];
|
|
33
|
+
let pageToken = "";
|
|
34
|
+
do {
|
|
35
|
+
const page = await client.search(model.name, {
|
|
36
|
+
filter: filter || void 0,
|
|
37
|
+
orderBy,
|
|
38
|
+
pageSize: 500,
|
|
39
|
+
labels,
|
|
40
|
+
...pageToken && { pageToken }
|
|
41
|
+
});
|
|
42
|
+
rows.push(...withLabels(resources, model, fields, page.items, page.labels).map((row, index) => ({
|
|
39
43
|
...row,
|
|
40
44
|
id: recordId(model, page.items[index])
|
|
41
|
-
}))
|
|
42
|
-
|
|
43
|
-
};
|
|
45
|
+
})));
|
|
46
|
+
pageToken = page.nextPageToken;
|
|
47
|
+
} while (pageToken);
|
|
48
|
+
return rows;
|
|
44
49
|
}
|
|
45
50
|
});
|
|
46
51
|
const [moved, setMoved] = useState();
|
|
47
52
|
const latest = useRef(void 0);
|
|
53
|
+
const pending = useRef(void 0);
|
|
48
54
|
const saving = useRef(Promise.resolve());
|
|
49
55
|
const waiting = useRef(0);
|
|
50
56
|
const save = async () => {
|
|
51
57
|
const rows = latest.current;
|
|
52
|
-
|
|
53
|
-
|
|
58
|
+
const changed = pending.current;
|
|
59
|
+
pending.current = void 0;
|
|
60
|
+
if (!rows || !changed) return;
|
|
61
|
+
const span = reorderSpan(rows.map((row) => row[field]), changed.first, changed.last);
|
|
62
|
+
const sent = rows.slice(span.first, span.last + 1);
|
|
63
|
+
const etags = Object.fromEntries(sent.flatMap((row) => typeof row.etag === "string" ? [[encodeKey(recordKey(model, row)), row.etag]] : []));
|
|
54
64
|
const written = (await client.batchWrite([{
|
|
55
65
|
op: "reorder",
|
|
56
66
|
resource: model.name,
|
|
57
67
|
field,
|
|
58
|
-
keys:
|
|
68
|
+
keys: sent.map((row) => recordKey(model, row)),
|
|
69
|
+
start: span.start,
|
|
59
70
|
etags
|
|
60
71
|
}])).results[0];
|
|
61
72
|
if (written?.op !== "reorder" || !latest.current) return;
|
|
@@ -71,6 +82,7 @@ const useHandOrder = ({ model, field, filter, fields }) => {
|
|
|
71
82
|
};
|
|
72
83
|
const refused = (error) => {
|
|
73
84
|
latest.current = void 0;
|
|
85
|
+
pending.current = void 0;
|
|
74
86
|
setMoved(void 0);
|
|
75
87
|
toast.show({
|
|
76
88
|
state: "error",
|
|
@@ -87,16 +99,24 @@ const useHandOrder = ({ model, field, filter, fields }) => {
|
|
|
87
99
|
setMoved(void 0);
|
|
88
100
|
};
|
|
89
101
|
const move = (from, to) => {
|
|
90
|
-
const rows = latest.current ?? loaded.data
|
|
102
|
+
const rows = latest.current ?? loaded.data;
|
|
91
103
|
if (!rows || from === to) return;
|
|
92
104
|
latest.current = arrayMove(rows, from, to);
|
|
105
|
+
const first = Math.min(from, to);
|
|
106
|
+
const last = Math.max(from, to);
|
|
107
|
+
pending.current = pending.current ? {
|
|
108
|
+
first: Math.min(pending.current.first, first),
|
|
109
|
+
last: Math.max(pending.current.last, last)
|
|
110
|
+
} : {
|
|
111
|
+
first,
|
|
112
|
+
last
|
|
113
|
+
};
|
|
93
114
|
setMoved(latest.current);
|
|
94
115
|
waiting.current += 1;
|
|
95
116
|
saving.current = saving.current.then(save).catch(refused).then(settle);
|
|
96
117
|
};
|
|
97
118
|
return {
|
|
98
|
-
rows: moved ?? loaded.data
|
|
99
|
-
more: loaded.data?.more ?? false,
|
|
119
|
+
rows: moved ?? loaded.data,
|
|
100
120
|
loading: loaded.isPending,
|
|
101
121
|
error: loaded.error,
|
|
102
122
|
move
|
package/dist/app/list-page.js
CHANGED
|
@@ -62,7 +62,7 @@ import { SaveSearchButton } from "./searches/save-search-button.js";
|
|
|
62
62
|
import { SavedSearches } from "./searches/saved-searches.js";
|
|
63
63
|
import { useListCellEditor } from "./list-edit/use-list-cell-editor.js";
|
|
64
64
|
import { HandSortedList } from "./hand-order/hand-sorted-list.js";
|
|
65
|
-
import {
|
|
65
|
+
import { ReorderButton } from "./hand-order/reorder-button.js";
|
|
66
66
|
import { useMemo, useState } from "react";
|
|
67
67
|
import { Download, Plus, SlidersHorizontal } from "lucide-react";
|
|
68
68
|
import { isGroupChart, tableDisplay, viewOfList } from "@protobase/schema/internal";
|
|
@@ -316,7 +316,7 @@ const LoadedList = ({ model, address, scope, heading }) => {
|
|
|
316
316
|
},
|
|
317
317
|
children: [/* @__PURE__ */ jsx(Download, { className: "size-3.5" }), desktop && text$1("export")]
|
|
318
318
|
});
|
|
319
|
-
const sortButton = handOrder && /* @__PURE__ */ jsx(
|
|
319
|
+
const sortButton = handOrder && /* @__PURE__ */ jsx(ReorderButton, {
|
|
320
320
|
sorting: handSorting,
|
|
321
321
|
onChange: setSorting,
|
|
322
322
|
iconOnly: !desktop
|
|
@@ -13,7 +13,7 @@ import { EmbeddedDisplaySwitch, EmbeddedList } from "./embedded-list/embedded-li
|
|
|
13
13
|
import { useEmbeddedDisplay } from "./embedded-list/use-embedded-display.js";
|
|
14
14
|
import { text } from "./record-messages.js";
|
|
15
15
|
import { HandSortedList } from "./hand-order/hand-sorted-list.js";
|
|
16
|
-
import {
|
|
16
|
+
import { ReorderButton } from "./hand-order/reorder-button.js";
|
|
17
17
|
import { useState } from "react";
|
|
18
18
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
19
19
|
import { useQuery } from "@tanstack/react-query";
|
|
@@ -79,7 +79,7 @@ const Empty = ({ empty }) => /* @__PURE__ */ jsx("p", {
|
|
|
79
79
|
/**
|
|
80
80
|
* A record page section listing the first records of another resource that point at this record, directly or through
|
|
81
81
|
* a join resource, in the list page's table or display, with a link to them all in the list. When they are ordered by
|
|
82
|
-
* hand within this record, **
|
|
82
|
+
* hand within this record, **Reorder** shows them all in their order, to be moved by their drag handles.
|
|
83
83
|
*/
|
|
84
84
|
const RelatedSection = ({ item, value, owner }) => {
|
|
85
85
|
const { resources, views } = useAdminMeta();
|
|
@@ -108,7 +108,7 @@ const RelatedSection = ({ item, value, owner }) => {
|
|
|
108
108
|
});
|
|
109
109
|
const actions = handOrder ? /* @__PURE__ */ jsxs("div", {
|
|
110
110
|
className: "flex items-center gap-2",
|
|
111
|
-
children: [!sorting && displaySwitch, /* @__PURE__ */ jsx(
|
|
111
|
+
children: [!sorting && displaySwitch, /* @__PURE__ */ jsx(ReorderButton, {
|
|
112
112
|
size: "sm",
|
|
113
113
|
sorting,
|
|
114
114
|
onChange: setSorting
|
package/dist/package.js
CHANGED