@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.
@@ -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
- changed: from === -1 ? place.index : Math.max(from, place.index)
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` 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.
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, changed) => {
25
+ const placeOps = (model, by, rank, row, column, order, first, last) => {
25
26
  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)]));
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: prefix.map((entry) => recordKey(model, entry.record)),
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.changed));
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
- sort: "Sort",
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
- sort: "Sorteren",
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/sort-button.tsx
6
- /** The button that turns sorting by hand on, as **Sort**, and off again, as **Done**; with `iconOnly` its icon alone. */
7
- const SortButton = ({ sorting, onChange, size, iconOnly = false }) => {
8
- const label = sorting ? text("done") : text("sort");
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 { SortButton };
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 up to `handOrderLimit`, with `more` when there are
17
- * others, and `move` to put one elsewhere. A move shows at once and renumbers them all 1, 2, 3, ... in one batch
18
- * `reorder` with their ETags; moves made while one saves are saved after it, with the ETags it gave back. A refusal
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 page = await client.search(model.name, {
32
- filter: filter || void 0,
33
- orderBy,
34
- pageSize: 500,
35
- labels
36
- });
37
- return {
38
- rows: withLabels(resources, model, fields, page.items, page.labels).map((row, index) => ({
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
- more: Boolean(page.nextPageToken)
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
- if (!rows) return;
53
- const etags = Object.fromEntries(rows.flatMap((row) => typeof row.etag === "string" ? [[encodeKey(recordKey(model, row)), row.etag]] : []));
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: rows.map((row) => recordKey(model, row)),
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?.rows;
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?.rows,
99
- more: loaded.data?.more ?? false,
119
+ rows: moved ?? loaded.data,
100
120
  loading: loaded.isPending,
101
121
  error: loaded.error,
102
122
  move
@@ -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 { SortButton } from "./hand-order/sort-button.js";
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(SortButton, {
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 { SortButton } from "./hand-order/sort-button.js";
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, **Sort** shows them all in their order, to be moved by their drag handles.
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(SortButton, {
111
+ children: [!sorting && displaySwitch, /* @__PURE__ */ jsx(ReorderButton, {
112
112
  size: "sm",
113
113
  sorting,
114
114
  onChange: setSorting
package/dist/package.js CHANGED
@@ -1,5 +1,5 @@
1
1
  //#region package.json
2
- var version = "0.23.12";
2
+ var version = "0.23.14";
3
3
  var protobase = { "pluginApi": 1 };
4
4
  //#endregion
5
5
  export { protobase, version };