@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.
- package/dist/account/account-section.js +1 -1
- package/dist/account/password-dialog.js +1 -1
- package/dist/account/sign-in-policy-form.js +3 -3
- package/dist/account/two-factor-setup.js +1 -1
- package/dist/action-card/field-list.js +1 -1
- package/dist/app/displays/board-column.js +1 -1
- package/dist/app/displays/board-display.js +6 -7
- package/dist/app/displays/board-place.js +24 -11
- package/dist/app/displays/timeline-row.js +1 -1
- package/dist/app/displays/use-board-move.js +55 -19
- package/dist/app/error-banner.js +1 -1
- package/dist/app/export/export-dialog.js +3 -3
- package/dist/app/hand-order/hand-sorted-list.js +2 -1
- package/dist/app/hand-order/use-hand-order.js +19 -5
- package/dist/app/import/import-check-step.js +2 -2
- package/dist/app/named-icon.js +3 -2
- package/dist/app/pages/blocks/field.js +1 -1
- package/dist/app/pages/blocks/progress.js +1 -1
- package/dist/app/rank-after/dependency-rule.js +17 -0
- package/dist/app/rank-after/messages.js +8 -0
- package/dist/app/rank-after/rank-bounds.js +54 -0
- package/dist/app/rank-after/ranked-place.js +45 -0
- package/dist/app/staff-banner.js +1 -1
- package/dist/app-shell/sidebar-group.js +1 -1
- package/dist/app-shell/sidebar-item.js +1 -1
- package/dist/app-shell/sidebar.js +1 -1
- package/dist/assistant-dock/assistant-dock.js +1 -1
- package/dist/auth/code-form.js +2 -2
- package/dist/auth/forgot-password-form.js +1 -1
- package/dist/auth/reset-password-form.js +1 -1
- package/dist/auth/setup-required-form.js +1 -1
- package/dist/auth/sign-in-form.js +3 -3
- package/dist/auth/sign-up-details-form.js +1 -1
- package/dist/auth/sign-up-email-form.js +1 -1
- package/dist/auth/staff-sign-in-form.js +2 -2
- package/dist/auth/two-factor-form.js +2 -2
- package/dist/data-grid/grid-header.js +1 -1
- package/dist/filter-panel/date-presets.js +1 -1
- package/dist/forms/public-form-page.js +1 -1
- package/dist/organizations/invitation-page.js +2 -2
- package/dist/organizations/members-page.js +1 -1
- package/dist/package.js +1 -1
- package/dist/primitives/button.js +1 -1
- package/dist/primitives/segmented.js +1 -1
- package/dist/record-view/field.js +2 -2
- package/dist/shell/assets/app-pH0OC9QP.js +69 -0
- package/dist/shell/assets/{index-B7cIjCuz.js → index-BPi_cyLV.js} +1 -1
- package/dist/shell/assets/{index-BRkDA5KD.css → index-Bkc7w8I4.css} +1 -1
- package/dist/shell/assets/{maplibre-CVG5L9FD.js → maplibre-DUTS_eJM.js} +1 -1
- package/dist/shell/assets/{preload-helper-BX9CcZkY.js → preload-helper-BZ3hrQDD.js} +1 -1
- package/dist/shell/assets/{record-map-Cw3_NcUl.js → record-map-DFy7sJEU.js} +1 -1
- package/dist/shell/assets/{records-map-D4f3ZeDL.js → records-map-EQVZhkY9.js} +1 -1
- package/dist/shell/assets/{shared-protobase-ui-CB_i50NP.js → shared-protobase-ui-BrUtMzdS.js} +1 -1
- package/dist/shell/index.html +5 -5
- package/dist/shell/protobase.shell.json +1 -1
- package/dist/shell/protobase.sw.js +2 -2
- package/package.json +4 -4
- 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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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
|
-
|
|
130
|
-
|
|
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
|
|
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:
|
|
35
|
-
start
|
|
36
|
-
etags
|
|
37
|
-
};
|
|
38
|
-
|
|
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
|
-
},
|
|
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-
|
|
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 {
|
|
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
|
|
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
|
-
|
|
77
|
-
|
|
78
|
-
|
|
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;
|
package/dist/app/error-banner.js
CHANGED
|
@@ -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-
|
|
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-
|
|
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-
|
|
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-
|
|
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 {
|
|
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-
|
|
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-
|
|
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", {
|
package/dist/app/named-icon.js
CHANGED
|
@@ -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-
|
|
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-
|
|
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 };
|
package/dist/app/staff-banner.js
CHANGED
|
@@ -44,7 +44,7 @@ const StaffBanner = ({ signIn, onStop }) => {
|
|
|
44
44
|
}),
|
|
45
45
|
error && /* @__PURE__ */ jsx("p", {
|
|
46
46
|
role: "alert",
|
|
47
|
-
className: "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-
|
|
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-
|
|
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-
|
|
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,
|