@protobase/ui 0.25.3 → 0.25.5

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.
Files changed (58) hide show
  1. package/dist/account/account-section.js +1 -1
  2. package/dist/account/password-dialog.js +1 -1
  3. package/dist/account/sign-in-policy-form.js +3 -3
  4. package/dist/account/two-factor-setup.js +1 -1
  5. package/dist/action-card/field-list.js +1 -1
  6. package/dist/app/displays/board-column.js +1 -1
  7. package/dist/app/displays/board-display.js +6 -7
  8. package/dist/app/displays/board-place.js +24 -11
  9. package/dist/app/displays/timeline-row.js +1 -1
  10. package/dist/app/displays/use-board-move.js +55 -19
  11. package/dist/app/error-banner.js +1 -1
  12. package/dist/app/export/export-dialog.js +3 -3
  13. package/dist/app/hand-order/hand-sorted-list.js +2 -1
  14. package/dist/app/hand-order/use-hand-order.js +19 -5
  15. package/dist/app/import/import-check-step.js +2 -2
  16. package/dist/app/named-icon.js +3 -2
  17. package/dist/app/pages/blocks/field.js +1 -1
  18. package/dist/app/pages/blocks/progress.js +1 -1
  19. package/dist/app/rank-after/dependency-rule.js +17 -0
  20. package/dist/app/rank-after/messages.js +8 -0
  21. package/dist/app/rank-after/rank-bounds.js +54 -0
  22. package/dist/app/rank-after/ranked-place.js +45 -0
  23. package/dist/app/staff-banner.js +1 -1
  24. package/dist/app-shell/sidebar-group.js +1 -1
  25. package/dist/app-shell/sidebar-item.js +1 -1
  26. package/dist/app-shell/sidebar.js +1 -1
  27. package/dist/assistant-dock/assistant-dock.js +1 -1
  28. package/dist/auth/code-form.js +2 -2
  29. package/dist/auth/forgot-password-form.js +1 -1
  30. package/dist/auth/reset-password-form.js +1 -1
  31. package/dist/auth/setup-required-form.js +1 -1
  32. package/dist/auth/sign-in-form.js +3 -3
  33. package/dist/auth/sign-up-details-form.js +1 -1
  34. package/dist/auth/sign-up-email-form.js +1 -1
  35. package/dist/auth/staff-sign-in-form.js +2 -2
  36. package/dist/auth/two-factor-form.js +2 -2
  37. package/dist/data-grid/grid-header.js +1 -1
  38. package/dist/filter-panel/date-presets.js +1 -1
  39. package/dist/forms/public-form-page.js +1 -1
  40. package/dist/organizations/invitation-page.js +2 -2
  41. package/dist/organizations/members-page.js +1 -1
  42. package/dist/package.js +1 -1
  43. package/dist/primitives/button.js +1 -1
  44. package/dist/primitives/segmented.js +1 -1
  45. package/dist/record-view/field.js +2 -2
  46. package/dist/shell/assets/app-pH0OC9QP.js +69 -0
  47. package/dist/shell/assets/{index-B7cIjCuz.js → index-BPi_cyLV.js} +1 -1
  48. package/dist/shell/assets/{index-BRkDA5KD.css → index-Bkc7w8I4.css} +1 -1
  49. package/dist/shell/assets/{maplibre-CVG5L9FD.js → maplibre-DUTS_eJM.js} +1 -1
  50. package/dist/shell/assets/{preload-helper-BX9CcZkY.js → preload-helper-BZ3hrQDD.js} +1 -1
  51. package/dist/shell/assets/{record-map-Cw3_NcUl.js → record-map-DFy7sJEU.js} +1 -1
  52. package/dist/shell/assets/{records-map-D4f3ZeDL.js → records-map-EQVZhkY9.js} +1 -1
  53. package/dist/shell/assets/{shared-protobase-ui-CB_i50NP.js → shared-protobase-ui-BrUtMzdS.js} +1 -1
  54. package/dist/shell/index.html +5 -5
  55. package/dist/shell/protobase.shell.json +1 -1
  56. package/dist/shell/protobase.sw.js +2 -2
  57. package/package.json +4 -4
  58. package/dist/shell/assets/app-CW3U56yM.js +0 -69
@@ -24,7 +24,7 @@ const AccountSection = ({ title, status, description, children }) => /* @__PURE_
24
24
  });
25
25
  const SectionError = ({ message }) => message ? /* @__PURE__ */ jsx("p", {
26
26
  role: "alert",
27
- className: "mb-3 text-xs font-medium text-danger-text",
27
+ className: "mb-3 text-body font-medium text-danger-text",
28
28
  children: message
29
29
  }) : null;
30
30
  //#endregion
@@ -50,7 +50,7 @@ const PasswordDialog = ({ title, description, confirmLabel, danger, needsPasswor
50
50
  })]
51
51
  }), error && /* @__PURE__ */ jsx("p", {
52
52
  role: "alert",
53
- className: "mt-2 text-xs font-medium text-danger-text",
53
+ className: "mt-2 text-body font-medium text-danger-text",
54
54
  children: error
55
55
  })]
56
56
  });
@@ -15,7 +15,7 @@ const RuleChoice = ({ row, value, onChange }) => /* @__PURE__ */ jsx("span", {
15
15
  role: "radio",
16
16
  "aria-checked": option.value === value,
17
17
  onClick: () => onChange(option.value),
18
- className: cn("rounded px-3 text-xs font-medium transition-colors", option.value === value ? "bg-background text-foreground shadow-sm" : "text-muted-foreground hover:text-foreground"),
18
+ className: cn("rounded px-3 text-body font-medium transition-colors", option.value === value ? "bg-background text-foreground shadow-sm" : "text-muted-foreground hover:text-foreground"),
19
19
  children: option.label
20
20
  }, option.value))
21
21
  });
@@ -85,12 +85,12 @@ const SignInPolicyForm = ({ state, onSave, busy, error, notice }) => {
85
85
  }),
86
86
  error && /* @__PURE__ */ jsx("p", {
87
87
  role: "alert",
88
- className: "rounded-md bg-danger-soft px-3 py-2 text-xs font-medium text-danger-text",
88
+ className: "rounded-md bg-danger-soft px-3 py-2 text-body font-medium text-danger-text",
89
89
  children: error
90
90
  }),
91
91
  notice && !changed && /* @__PURE__ */ jsx("p", {
92
92
  role: "status",
93
- className: "rounded-md bg-success-soft px-3 py-2 text-xs font-medium text-success-text",
93
+ className: "rounded-md bg-success-soft px-3 py-2 text-body font-medium text-success-text",
94
94
  children: notice
95
95
  }),
96
96
  /* @__PURE__ */ jsxs("div", {
@@ -59,7 +59,7 @@ const TwoFactorSetup = ({ offerEmailedCodes, needsPassword, onStartApp, onConfir
59
59
  });
60
60
  const errorLine = error && /* @__PURE__ */ jsx("p", {
61
61
  role: "alert",
62
- className: "text-xs font-medium text-danger-text",
62
+ className: "text-body font-medium text-danger-text",
63
63
  children: error
64
64
  });
65
65
  if (step.kind === "backup-codes") return /* @__PURE__ */ jsxs("div", {
@@ -2,7 +2,7 @@ import { jsx, jsxs } from "react/jsx-runtime";
2
2
  //#region src/action-card/field-list.tsx
3
3
  /** Labelled values, one per row, inside a card. */
4
4
  const FieldList = ({ fields }) => /* @__PURE__ */ jsx("dl", {
5
- className: "grid grid-cols-[auto_minmax(0,1fr)] gap-x-3 gap-y-1 text-xs",
5
+ className: "grid grid-cols-[auto_minmax(0,1fr)] gap-x-3 gap-y-1 text-body",
6
6
  children: fields.map((field, index) => /* @__PURE__ */ jsxs("div", {
7
7
  className: "contents",
8
8
  children: [/* @__PURE__ */ jsx("dt", {
@@ -61,7 +61,7 @@ const BoardColumnView = ({ model, view, display, column, fields, filter, orderBy
61
61
  /* @__PURE__ */ jsxs("header", {
62
62
  className: "flex items-center justify-between gap-2 px-1 pt-0.5",
63
63
  children: [/* @__PURE__ */ jsx("h2", {
64
- className: "min-w-0 truncate text-xs font-medium",
64
+ className: "min-w-0 truncate text-body font-medium",
65
65
  children: column.value === null ? /* @__PURE__ */ jsx("span", {
66
66
  className: "text-muted-foreground",
67
67
  children: column.label
@@ -11,7 +11,6 @@ import { cardCoordinates, collision, columnId } from "./board-dnd.js";
11
11
  import { BoardColumnView } from "./board-column.js";
12
12
  import { columnsAllowed } from "./board-allowed.js";
13
13
  import { dropWrite } from "./board-drop.js";
14
- import { placedRows } from "./board-place.js";
15
14
  import { useBoardMove } from "./use-board-move.js";
16
15
  import { useRef, useState } from "react";
17
16
  import { jsx, jsxs } from "react/jsx-runtime";
@@ -55,12 +54,15 @@ const BoardDisplay = ({ model, view, display, fields, filter, orderBy, limit })
55
54
  const placeable = drag?.rank === true && rank !== void 0 && listHandOrder(model, permissions) !== void 0;
56
55
  const columnOrder = rank ? `${rank} asc` : orderBy;
57
56
  const labelOf = (value) => every.find((column) => column.value === value)?.label ?? String(value);
57
+ const title = (row) => String(row.labels[card.title] ?? row.record[card.title] ?? row.key);
58
58
  const { moves, move } = useBoardMove({
59
59
  model,
60
+ view,
60
61
  by: display.by,
61
62
  rank,
62
63
  drop,
63
- labelOf
64
+ labelOf,
65
+ titleOf: title
64
66
  });
65
67
  const [active, setActive] = useState();
66
68
  const dragged = useRef(false);
@@ -76,7 +78,6 @@ const BoardDisplay = ({ model, view, display, fields, filter, orderBy, limit })
76
78
  coordinateGetter: cardCoordinates
77
79
  }));
78
80
  if (dropName !== void 0 && !drop) return /* @__PURE__ */ jsx(Problem, { children: slotted(text("noDrop"), dropName) });
79
- const title = (row) => String(row.labels[card.title] ?? row.record[card.title] ?? row.key);
80
81
  const columnOf = (id) => columns.find((column) => columnId(column.value) === id);
81
82
  const valueOf = (row) => row.record[display.by] ?? null;
82
83
  const crosses = (row, to) => takes(valueOf(row)) && takes(to.value);
@@ -124,11 +125,9 @@ const BoardDisplay = ({ model, view, display, fields, filter, orderBy, limit })
124
125
  if (from === -1 && !crosses(row, place.column)) return;
125
126
  if (from === -1 && drop) return move(row, place.column);
126
127
  if (from === place.index) return;
127
- const order = placedRows(place.rows, row, place.index);
128
128
  move(row, place.column, {
129
- order,
130
- first: from === -1 ? place.index : Math.min(from, place.index),
131
- last: from === -1 ? place.index : Math.max(from, place.index)
129
+ rows: place.rows,
130
+ index: place.index
132
131
  });
133
132
  };
134
133
  return /* @__PURE__ */ jsxs(DndContext, {
@@ -16,33 +16,46 @@ const placedRows = (rows, row, index) => {
16
16
  ...rows.slice(index)
17
17
  ];
18
18
  };
19
+ /** The column's cards once `row` is dropped at `index`, with the first and last place that changed. */
20
+ const placing = (rows, row, index) => {
21
+ const from = rows.findIndex((entry) => entry.key === row.key);
22
+ return {
23
+ order: placedRows(rows, row, index),
24
+ first: from === -1 ? index : Math.min(from, index),
25
+ last: from === -1 ? index : Math.max(from, index)
26
+ };
27
+ };
19
28
  const etagOf = (row) => typeof row.record.etag === "string" ? row.record.etag : "*";
20
29
  /**
21
30
  * The batch that saves a drop on a ranked board: in another column first the update of `by` with the card's ETag, then
22
31
  * 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.
32
+ * (see `reorderSpan`). The cards after it already rank higher, so they keep their place unwritten, loaded or not. With
33
+ * `bump`, the card, last in the column, takes that rank in a `reorder` of its own before the others are numbered, so
34
+ * it ranks above a record it depends on (see `rankedPlace`).
24
35
  */
25
- const placeOps = (model, by, rank, row, column, order, first, last) => {
36
+ const placeOps = (model, by, rank, row, column, order, first, last, bump) => {
26
37
  const moving = (row.record[by] ?? null) !== column.value;
27
38
  const span = reorderSpan(order.map((entry) => entry.record[rank]), first, last);
28
39
  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)]));
30
- const reorder = {
40
+ const etagFor = (entry) => moving && entry.key === row.key ? "*" : etagOf(entry);
41
+ const reorder = (entries, start) => ({
31
42
  op: "reorder",
32
43
  resource: model.name,
33
44
  field: rank,
34
- keys: sent.map((entry) => recordKey(model, entry.record)),
35
- start: span.start,
36
- etags
37
- };
38
- if (!moving) return [reorder];
45
+ keys: entries.map((entry) => recordKey(model, entry.record)),
46
+ start,
47
+ etags: Object.fromEntries(entries.map((entry) => [encodeKey(recordKey(model, entry.record)), etagFor(entry)]))
48
+ });
49
+ const others = sent.filter((entry) => entry.key !== row.key);
50
+ const reorders = bump === void 0 ? [reorder(sent, span.start)] : [reorder([row], bump), ...others.length > 0 ? [reorder(others, span.start)] : []];
51
+ if (!moving) return reorders;
39
52
  return [{
40
53
  op: "update",
41
54
  resource: model.name,
42
55
  key: recordKey(model, row.record),
43
56
  data: { [by]: column.value },
44
57
  etag: etagOf(row)
45
- }, reorder];
58
+ }, ...reorders];
46
59
  };
47
60
  /**
48
61
  * What the batch of a drop saved: each record it wrote, as it stored it last, with its values in `rows`, the column's
@@ -67,4 +80,4 @@ const boardWrites = (model, rows, result) => {
67
80
  });
68
81
  };
69
82
  //#endregion
70
- export { boardWrites, placeOps, placedRows };
83
+ export { boardWrites, placeOps, placedRows, placing };
@@ -14,7 +14,7 @@ const TimelineRow = ({ model, view, display, row, days, movable, dragged }) => {
14
14
  className: cn(timelineGrid, "relative border-b border-border last:border-b-0"),
15
15
  children: [/* @__PURE__ */ jsx("span", {
16
16
  "aria-hidden": true,
17
- className: "truncate px-2 pt-1 text-xs sm:py-1.5",
17
+ className: "truncate px-2 pt-1 text-body sm:py-1.5",
18
18
  children: eventTitle(model, view, display, row)
19
19
  }), /* @__PURE__ */ jsx("div", {
20
20
  className: "relative h-7",
@@ -5,8 +5,11 @@ import { text } from "./messages.js";
5
5
  import { refusalReason } from "./board-columns.js";
6
6
  import { useRefreshResource } from "../pages/use-layout-data.js";
7
7
  import { dropWrite } from "./board-drop.js";
8
- import { boardWrites, placeOps } from "./board-place.js";
8
+ import { text as text$1 } from "../hand-order/messages.js";
9
9
  import { undoOps } from "../hand-order/undo-ops.js";
10
+ import { rankBounds } from "../rank-after/rank-bounds.js";
11
+ import { boardWrites, placeOps, placing } from "./board-place.js";
12
+ import { rankedPlace } from "../rank-after/ranked-place.js";
10
13
  import { useRef, useState } from "react";
11
14
  import { useMutation } from "@tanstack/react-query";
12
15
  //#region src/app/displays/use-board-move.ts
@@ -17,9 +20,11 @@ import { useMutation } from "@tanstack/react-query";
17
20
  * With the board's `drop`, a card dropped on another column runs that column's write instead, and then reads the record
18
21
  * again to say which column it is in now. The card shows in its new place at once; a saved move says so with **Undo**,
19
22
  * which puts back the values it changed or runs the undo the drop's write gave, and a refusal puts it back and says why.
23
+ * With the position's `after`, a card dropped above a record it depends on lands at the first place below that keeps
24
+ * the order, or stays where it was, with a warning saying why (see `rankedPlace`).
20
25
  * Either way every list of the resource reloads, which puts the card where its record now is.
21
26
  */
22
- const useBoardMove = ({ model, by, rank, drop, labelOf }) => {
27
+ const useBoardMove = ({ model, view, by, rank, drop, labelOf, titleOf }) => {
23
28
  const client = useClient();
24
29
  const toast = useToast();
25
30
  const refresh = useRefreshResource();
@@ -58,12 +63,55 @@ const useBoardMove = ({ model, by, rank, drop, labelOf }) => {
58
63
  landed: to.value,
59
64
  undo: () => client.batchWrite(undoOps(model, writes, fields))
60
65
  });
66
+ const show = (row, to, order) => {
67
+ const moved = {
68
+ ...row,
69
+ record: {
70
+ ...row.record,
71
+ [by]: to.value
72
+ }
73
+ };
74
+ const keys = order?.map((entry) => entry.key);
75
+ setMoves((current) => [...current.filter((move) => move.row.key !== row.key), {
76
+ row: moved,
77
+ to: to.value,
78
+ ...keys && { order: keys }
79
+ }]);
80
+ };
81
+ const allowed = async (row, to, place) => {
82
+ if (!model.position?.after) return { index: place.index };
83
+ const bounds = await rankBounds(client, model, view, row.record);
84
+ const ranked = rankedPlace({
85
+ model,
86
+ rank,
87
+ rows: place.rows,
88
+ row,
89
+ index: place.index,
90
+ bounds,
91
+ title: titleOf(row)
92
+ });
93
+ const stays = !ranked.place || ranked.place.bump === void 0 && ranked.place.index === place.rows.findIndex((entry) => entry.key === row.key);
94
+ if (ranked.reason !== void 0) toast.show({
95
+ state: "warning",
96
+ title: stays ? text$1("notMoved") : text$1("movedToPlace", ranked.place.index + 1),
97
+ description: ranked.reason
98
+ });
99
+ if (stays) return void 0;
100
+ if (ranked.place.index !== place.index) show(row, to, placing(place.rows, row, ranked.place.index).order);
101
+ return ranked.place;
102
+ };
103
+ const placed = async (row, to, place) => {
104
+ const landing = await allowed(row, to, place);
105
+ if (!landing) return void 0;
106
+ const { order, first, last } = placing(place.rows, row, landing.index);
107
+ return saved(boardWrites(model, order, await client.batchWrite(placeOps(model, by, rank, row, to, order, first, last, landing.bump))), to);
108
+ };
61
109
  const mutation = useMutation({
62
110
  mutationFn: async ({ row, to, place }) => {
63
111
  if (place && !rank) throw new Error("A board places a card only by its rank");
64
112
  const from = row.record[by] ?? null;
65
113
  if (drop && to.value !== from) return dropped(row, to);
66
- if (place) return saved(boardWrites(model, place.order, await client.batchWrite(placeOps(model, by, rank, row, to, place.order, place.first, place.last))), to);
114
+ if (place) return placed(row, to, place);
67
115
  const etag = typeof row.record.etag === "string" ? row.record.etag : "*";
68
116
  const stored = await client.update(model.name, recordKey(model, row.record), { [by]: to.value }, etag);
69
117
  return saved([{
@@ -72,22 +120,10 @@ const useBoardMove = ({ model, by, rank, drop, labelOf }) => {
72
120
  etag: stored.etag
73
121
  }], to);
74
122
  },
75
- onMutate: ({ row, to, place }) => {
76
- const moved = {
77
- ...row,
78
- record: {
79
- ...row.record,
80
- [by]: to.value
81
- }
82
- };
83
- const order = place?.order.map((entry) => entry.key);
84
- setMoves((current) => [...current.filter((move) => move.row.key !== row.key), {
85
- row: moved,
86
- to: to.value,
87
- ...order && { order }
88
- }]);
89
- },
90
- onSuccess: ({ landed, undo: undoing }, { row, to }) => {
123
+ onMutate: ({ row, to, place }) => show(row, to, place && placing(place.rows, row, place.index).order),
124
+ onSuccess: (result, { row, to }) => {
125
+ if (!result) return;
126
+ const { landed, undo: undoing } = result;
91
127
  if (undoToast.current !== void 0) toast.dismiss(undoToast.current);
92
128
  undoToast.current = void 0;
93
129
  const from = row.record[by] ?? null;
@@ -18,7 +18,7 @@ const ErrorBanner = ({ error, title }) => /* @__PURE__ */ jsxs("div", {
18
18
  className: "font-medium",
19
19
  children: title
20
20
  }), describe(error).map((detail) => /* @__PURE__ */ jsxs("p", {
21
- className: "text-xs",
21
+ className: "text-body",
22
22
  children: [detail.message, detail.hint && /* @__PURE__ */ jsxs("span", {
23
23
  className: "text-muted-foreground",
24
24
  children: [" ", detail.hint]
@@ -41,7 +41,7 @@ const Setting = ({ label, help, children }) => /* @__PURE__ */ jsxs("div", {
41
41
  className: "flex flex-col items-start gap-1.5",
42
42
  children: [
43
43
  /* @__PURE__ */ jsx("span", {
44
- className: "text-[13px] font-medium",
44
+ className: "text-body font-medium",
45
45
  children: label
46
46
  }),
47
47
  children,
@@ -145,7 +145,7 @@ const ExportDialog = ({ plural, view, fields, initialColumns, count, selected, o
145
145
  children: text("export")
146
146
  })] }),
147
147
  children: /* @__PURE__ */ jsxs("div", {
148
- className: "flex flex-col gap-4 text-[13px]",
148
+ className: "flex flex-col gap-4 text-body",
149
149
  children: [
150
150
  /* @__PURE__ */ jsx(Setting, {
151
151
  label: text("format"),
@@ -208,7 +208,7 @@ const ExportProgressDialog = ({ plural, done, total, onCancel }) => {
208
208
  children: text("cancel")
209
209
  }),
210
210
  children: /* @__PURE__ */ jsxs("div", {
211
- className: "flex flex-col gap-2 text-[13px]",
211
+ className: "flex flex-col gap-2 text-body",
212
212
  children: [/* @__PURE__ */ jsx("div", {
213
213
  role: "progressbar",
214
214
  "aria-label": text("rowsExported"),
@@ -8,8 +8,8 @@ import { useAppAddress } from "../use-app-address.js";
8
8
  import { ErrorBanner } from "../error-banner.js";
9
9
  import { useProjectUi } from "../pages/project-ui.js";
10
10
  import { DataGrid } from "../../data-grid/data-grid.js";
11
- import { embeddedColumns } from "../embedded-list/embedded-columns.js";
12
11
  import { text } from "./messages.js";
12
+ import { embeddedColumns } from "../embedded-list/embedded-columns.js";
13
13
  import { useHandOrder } from "./use-hand-order.js";
14
14
  import { useMemo } from "react";
15
15
  import { jsx } from "react/jsx-runtime";
@@ -49,6 +49,7 @@ const HandSortedList = ({ model, view, field, filter, columns, sort }) => {
49
49
  const rule = ruleName === void 0 ? void 0 : dropRules?.[ruleName];
50
50
  const order = useHandOrder({
51
51
  model,
52
+ view,
52
53
  field,
53
54
  filter,
54
55
  fields,
@@ -1,16 +1,18 @@
1
1
  import { keys } from "../../data/query-keys.js";
2
2
  import { useClient } from "../../data/api-provider.js";
3
3
  import { useToast } from "../../toasts/toast-provider.js";
4
- import { recordId, recordKey } from "../../live/model-helpers.js";
4
+ import { recordId, recordKey, recordTitle } from "../../live/model-helpers.js";
5
5
  import { refusalReason } from "../displays/board-columns.js";
6
6
  import { displayRow } from "../displays/list-displays.js";
7
7
  import { useAdminMeta } from "../meta-gate.js";
8
8
  import { useRefreshResource } from "../pages/use-layout-data.js";
9
9
  import { listLabels, withLabels } from "../../live/relation-labels.js";
10
- import { reorderSpan } from "./reorder-span.js";
10
+ import { text } from "./messages.js";
11
11
  import { undoOps } from "./undo-ops.js";
12
+ import { rankBounds } from "../rank-after/rank-bounds.js";
13
+ import { reorderSpan } from "./reorder-span.js";
14
+ import { dependencyRule } from "../rank-after/dependency-rule.js";
12
15
  import { allowedDrop } from "./drop-rule.js";
13
- import { text } from "./messages.js";
14
16
  import { useRef, useState } from "react";
15
17
  import { encodeKey } from "@protobase/schema/internal";
16
18
  import { useQuery } from "@tanstack/react-query";
@@ -22,9 +24,10 @@ import { arrayMove } from "@dnd-kit/sortable";
22
24
  * `reorder` with their ETags; moves made while one saves are saved after it, with the values and ETags it gave back.
23
25
  * Each save says so with **Undo**, which puts back the values it changed. A refusal puts them back as the server has
24
26
  * them and says why; either way every list of the resource reloads. A drop `rule` refuses moves to the first place below
25
- * that it allows, with a warning giving its reason.
27
+ * that it allows, with a warning giving its reason; with the position's `after`, so do the records the moved one
28
+ * depends on, read first (see `rankBounds`).
26
29
  */
27
- const useHandOrder = ({ model, field, filter, fields, rule }) => {
30
+ const useHandOrder = ({ model, view, field, filter, fields, rule }) => {
28
31
  const client = useClient();
29
32
  const toast = useToast();
30
33
  const refresh = useRefreshResource();
@@ -122,6 +125,17 @@ const useHandOrder = ({ model, field, filter, fields, rule }) => {
122
125
  const move = (from, dropped) => {
123
126
  const rows = latest.current ?? loaded.data;
124
127
  if (!rows || from === dropped) return;
128
+ if (!model.position?.after) return place(rows, from, dropped, rule);
129
+ const record = displayRow(model, rows[from]).record;
130
+ const id = rows[from].id;
131
+ rankBounds(client, model, view, record).then((bounds) => {
132
+ const now = latest.current ?? loaded.data;
133
+ const dependencies = dependencyRule(model, bounds, recordTitle(model, view, record, id));
134
+ place(now, now.findIndex((row) => row.id === id), dropped, (drop) => dependencies(drop) ?? rule?.(drop));
135
+ }, refused(text("couldNotChange")));
136
+ };
137
+ const place = (rows, from, dropped, rule) => {
138
+ if (from === -1 || from === dropped) return;
125
139
  const to = rule ? placeAllowed(rows, from, dropped, rule) : dropped;
126
140
  if (from === to) return;
127
141
  latest.current = arrayMove(rows, from, to);
@@ -17,7 +17,7 @@ const downloadRows = (fileName, headers, rows) => saveFile(failedRowsCsv(headers
17
17
  /** The rows with what keeps each from being imported, at most `listedProblems` of them. */
18
18
  const ProblemList = ({ rows }) => /* @__PURE__ */ jsxs("ul", {
19
19
  "aria-label": text("rowsNotImported"),
20
- className: "flex flex-col gap-1 text-xs",
20
+ className: "flex flex-col gap-1 text-body",
21
21
  children: [rows.slice(0, listedProblems).map((row) => /* @__PURE__ */ jsxs("li", { children: [
22
22
  /* @__PURE__ */ jsx("span", {
23
23
  className: "font-medium",
@@ -58,7 +58,7 @@ const ImportCheckStep = ({ check, model, resources, views, columns, headers, fil
58
58
  className: "overflow-x-auto rounded-lg border",
59
59
  children: /* @__PURE__ */ jsxs("table", {
60
60
  "aria-label": text("firstRows"),
61
- className: "w-full text-left text-xs",
61
+ className: "w-full text-left text-body",
62
62
  children: [/* @__PURE__ */ jsx("thead", {
63
63
  className: "bg-muted text-muted-foreground",
64
64
  children: /* @__PURE__ */ jsxs("tr", { children: [/* @__PURE__ */ jsx("th", {
@@ -1,4 +1,4 @@
1
- import { Bath, Bed, Bell, Bookmark, Calendar, DoorOpen, ExternalLink, FileText, Heart, House, KeyRound, LandPlot, Leaf, RulerDimensionLine, Send, Tag, Wallet } from "lucide-react";
1
+ import { Ban, Bath, Bed, Bell, Bookmark, Calendar, DoorOpen, ExternalLink, FileText, Heart, House, KeyRound, LandPlot, Leaf, RulerDimensionLine, Send, Tag, Wallet } from "lucide-react";
2
2
  //#region src/app/named-icon.ts
3
3
  /** The icons an action's, a field's or a sign-in prompt's `icon` may name. */
4
4
  const byName = {
@@ -18,7 +18,8 @@ const byName = {
18
18
  heart: Heart,
19
19
  bell: Bell,
20
20
  bookmark: Bookmark,
21
- "external-link": ExternalLink
21
+ "external-link": ExternalLink,
22
+ ban: Ban
22
23
  };
23
24
  /** The icon a name stands for, or nothing for a name the app does not know. */
24
25
  const namedIcon = (name) => name !== void 0 && Object.hasOwn(byName, name) ? byName[name] : void 0;
@@ -30,7 +30,7 @@ const FieldBlock = ({ node }) => {
30
30
  return /* @__PURE__ */ jsxs("div", {
31
31
  className: "min-w-0",
32
32
  children: [/* @__PURE__ */ jsx("div", {
33
- className: "text-xs text-muted-foreground",
33
+ className: "text-body text-muted-foreground",
34
34
  children: text
35
35
  }), /* @__PURE__ */ jsx("div", {
36
36
  className: "mt-0.5 truncate",
@@ -25,7 +25,7 @@ const ProgressBlock = ({ node }) => {
25
25
  return /* @__PURE__ */ jsxs("div", {
26
26
  className: "min-w-0",
27
27
  children: [/* @__PURE__ */ jsxs("div", {
28
- className: "flex items-baseline justify-between gap-2 text-xs",
28
+ className: "flex items-baseline justify-between gap-2 text-body",
29
29
  children: [/* @__PURE__ */ jsx("span", {
30
30
  className: "text-muted-foreground",
31
31
  children: label
@@ -0,0 +1,17 @@
1
+ import { recordKey } from "../../live/model-helpers.js";
2
+ import { text } from "./messages.js";
3
+ import { encodeKey } from "@protobase/schema/internal";
4
+ //#region src/app/rank-after/dependency-rule.ts
5
+ /**
6
+ * The drop rule of a list that holds every record in rank order: the record titled `title` lands below each record it
7
+ * depends on and above each that depends on it, by `bounds`; records the list does not hold do not count.
8
+ */
9
+ const dependencyRule = (model, bounds, title) => ({ order, index }) => {
10
+ const at = (key) => order.findIndex((entry) => encodeKey(recordKey(model, entry)) === key);
11
+ const above = bounds.after.find((bound) => at(bound.key) > index);
12
+ if (above) return text("dependsOn", title, above.title);
13
+ const below = bounds.before.find((bound) => at(bound.key) !== -1 && at(bound.key) < index);
14
+ return below ? text("dependsOn", below.title, title) : void 0;
15
+ };
16
+ //#endregion
17
+ export { dependencyRule };
@@ -0,0 +1,8 @@
1
+ import { defineMessages } from "../../locale/messages.js";
2
+ //#region src/app/rank-after/messages.ts
3
+ const text = defineMessages({
4
+ en: { dependsOn: (record, other) => `${record} depends on ${other}.` },
5
+ nl: { dependsOn: (record, other) => `${record} hangt af van ${other}.` }
6
+ });
7
+ //#endregion
8
+ export { text };
@@ -0,0 +1,54 @@
1
+ import { recordKey, recordTitle } from "../../live/model-helpers.js";
2
+ import { encodeKey } from "@protobase/schema/internal";
3
+ import { printFilter, where } from "@protobase/schema";
4
+ //#region src/app/rank-after/rank-bounds.ts
5
+ const everyRecord = async (client, resource, filter) => {
6
+ const records = [];
7
+ let pageToken = "";
8
+ do {
9
+ const page = await client.search(resource, {
10
+ filter,
11
+ pageSize: 100,
12
+ ...pageToken && { pageToken }
13
+ });
14
+ records.push(...page.items);
15
+ pageToken = page.nextPageToken;
16
+ } while (pageToken);
17
+ return records;
18
+ };
19
+ const narrowed = (filter, extra) => extra === void 0 ? filter : `(${filter}) AND (${extra})`;
20
+ /**
21
+ * The records `record` must rank between, through the list API: the join records that hold its key on either side,
22
+ * then the records they name, those it depends on narrowed to the pending ones. Without `after` on the resource's
23
+ * position there are none.
24
+ */
25
+ const rankBounds = async (client, model, view, record) => {
26
+ const after = model.position?.after;
27
+ if (!after) return {
28
+ after: [],
29
+ before: []
30
+ };
31
+ const id = model.primaryKey[0];
32
+ const self = recordKey(model, record);
33
+ const encoded = (value) => encodeKey([value]);
34
+ const links = await everyRecord(client, after.resource, narrowed(printFilter(where.or(where.eq(after.field, self), where.eq(after.key, self))), after.filter));
35
+ const other = (from, to) => links.flatMap((link) => encoded(link[from]) === encoded(self) && encoded(link[to]) !== encoded(self) ? [link[to]] : []);
36
+ const dependsOn = other(after.field, after.key);
37
+ const dependents = other(after.key, after.field);
38
+ const [pending, waiting] = await Promise.all([everyRecord(client, model.name, narrowed(printFilter(where.in(id, [self, ...dependsOn])), after.while)), dependents.length === 0 ? [] : everyRecord(client, model.name, printFilter(where.in(id, dependents)))]);
39
+ const bound = (found) => {
40
+ const key = encodeKey(recordKey(model, found));
41
+ return {
42
+ key,
43
+ rank: found[model.position.field],
44
+ title: recordTitle(model, view, found, key)
45
+ };
46
+ };
47
+ const selfPending = pending.some((found) => encoded(found[id]) === encoded(self));
48
+ return {
49
+ after: pending.filter((found) => encoded(found[id]) !== encoded(self)).map(bound),
50
+ before: selfPending ? waiting.map(bound) : []
51
+ };
52
+ };
53
+ //#endregion
54
+ export { rankBounds };
@@ -0,0 +1,45 @@
1
+ import { recordKey } from "../../live/model-helpers.js";
2
+ import { reorderSpan } from "../hand-order/reorder-span.js";
3
+ import { placing } from "../displays/board-place.js";
4
+ import { text } from "./messages.js";
5
+ import { encodeKey } from "@protobase/schema/internal";
6
+ //#region src/app/rank-after/ranked-place.ts
7
+ const isRank = (value) => value !== null && value !== void 0 && value !== "";
8
+ /**
9
+ * Where a card of a ranked board dropped at `index` of a column, its cards `rows`, may land by `bounds`: at the first
10
+ * place from there down where the ranks its `reorder` gives keep it below each record it depends on and above each that
11
+ * depends on it, wherever those are. In the last place it may take a rank just above what it depends on, for a record
12
+ * of another column that ranks above the whole column. With the reason it refused `index`; with no place, none.
13
+ */
14
+ const rankedPlace = ({ model, rank, rows, row, index, bounds, title }) => {
15
+ const end = rows.some((entry) => entry.key === row.key) ? rows.length - 1 : rows.length;
16
+ let reason;
17
+ for (let at = index; at <= end; at++) {
18
+ const { order, first, last } = placing(rows, row, at);
19
+ const ranks = order.map((entry) => entry.record[rank]);
20
+ const span = reorderSpan(ranks, first, last);
21
+ const ranked = new Map(order.map((entry, i) => [encodeKey(recordKey(model, entry.record)), i >= span.first && i <= span.last ? span.start + i - span.first : ranks[i]]));
22
+ const rankOf = (bound) => ranked.has(bound.key) ? ranked.get(bound.key) : bound.rank;
23
+ const mine = Number(ranked.get(encodeKey(recordKey(model, row.record))));
24
+ const above = bounds.after.filter((bound) => isRank(rankOf(bound)) && Number(rankOf(bound)) >= mine);
25
+ const below = bounds.before.find((bound) => isRank(rankOf(bound)) && Number(rankOf(bound)) <= mine);
26
+ const refusal = below ? text("dependsOn", below.title, title) : above[0] && text("dependsOn", title, above[0].title);
27
+ if (!refusal) return {
28
+ place: { index: at },
29
+ ...reason !== void 0 && { reason }
30
+ };
31
+ reason ??= refusal;
32
+ if (at !== end || below) continue;
33
+ const bump = Math.max(...above.map((bound) => Number(rankOf(bound)))) + 1;
34
+ if (bounds.before.every((bound) => !isRank(rankOf(bound)) || Number(rankOf(bound)) > bump)) return {
35
+ place: {
36
+ index: at,
37
+ bump
38
+ },
39
+ reason
40
+ };
41
+ }
42
+ return { reason };
43
+ };
44
+ //#endregion
45
+ export { rankedPlace };
@@ -44,7 +44,7 @@ const StaffBanner = ({ signIn, onStop }) => {
44
44
  }),
45
45
  error && /* @__PURE__ */ jsx("p", {
46
46
  role: "alert",
47
- className: "text-xs font-medium text-danger-text",
47
+ className: "text-body font-medium text-danger-text",
48
48
  children: error
49
49
  }),
50
50
  /* @__PURE__ */ jsx(Button, {
@@ -9,7 +9,7 @@ import { jsx, jsxs } from "react/jsx-runtime";
9
9
  //#region src/app-shell/sidebar-group.tsx
10
10
  const row = {
11
11
  "text-large": "h-8 gap-2.5 px-2.5",
12
- "text-small": "h-7 gap-2 px-2 text-xs"
12
+ "text-small": "h-7 gap-2 px-2 text-body"
13
13
  };
14
14
  /** Lines the records up under the entry's icon. */
15
15
  const indent = {
@@ -6,7 +6,7 @@ import { jsx, jsxs } from "react/jsx-runtime";
6
6
  //#region src/app-shell/sidebar-item.tsx
7
7
  const layout = {
8
8
  "text-large": "h-9 gap-3 px-3 text-sm",
9
- "text-small": "h-7 gap-2.5 px-2.5 text-xs",
9
+ "text-small": "h-7 gap-2.5 px-2.5 text-body",
10
10
  icon: "size-9 justify-center",
11
11
  "icon-label": "w-full flex-col gap-0.5 py-1.5 text-[10px]",
12
12
  "icon-tooltip": "size-9 justify-center",
@@ -34,7 +34,7 @@ const Sidebar = ({ mode, activeItem, user, workspace, appName, forceExpanded, pr
34
34
  children: groups.map((group, index) => /* @__PURE__ */ jsxs("div", {
35
35
  className: cn("flex flex-col gap-0.5", index > 0 && "mt-3"),
36
36
  children: [inline ? /* @__PURE__ */ jsx("div", {
37
- className: cn("px-3 pb-1 pt-1 text-[11px] font-medium uppercase tracking-wide text-faint-foreground", expanding && "h-4 whitespace-nowrap py-0 opacity-0 transition-opacity group-hover/side:opacity-100 group-focus-within/side:opacity-100", expanded && "opacity-100"),
37
+ className: cn("px-3 pb-1 pt-1 text-xs font-medium uppercase tracking-wide text-faint-foreground", expanding && "h-4 whitespace-nowrap py-0 opacity-0 transition-opacity group-hover/side:opacity-100 group-focus-within/side:opacity-100", expanded && "opacity-100"),
38
38
  children: group.label
39
39
  }) : index > 0 && /* @__PURE__ */ jsx("div", { className: "mx-auto mb-1 h-px w-6 bg-border" }), group.items.map((item) => item.recent && isTextMode(mode) ? /* @__PURE__ */ jsx(SidebarGroup, {
40
40
  item,