@protobase/ui 0.21.7 → 0.21.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/app/app.js +1 -0
- package/dist/app/charts/chart-view.js +3 -1
- package/dist/app/comments/comment-body.js +13 -0
- package/dist/app/comments/comment-composer.js +149 -0
- package/dist/app/comments/comments-section.js +78 -0
- package/dist/app/comments/mention-text.js +45 -0
- package/dist/app/comments/use-mentionable.js +18 -0
- package/dist/app/comments/use-post-comment.js +15 -0
- package/dist/app/comments/use-record-comments.js +24 -0
- package/dist/app/current-address.d.ts +4 -3
- package/dist/app/current-address.js +1 -0
- package/dist/app/displays/calendar-controls.js +70 -0
- package/dist/app/displays/calendar-day.js +33 -0
- package/dist/app/displays/calendar-display.js +175 -0
- package/dist/app/displays/calendar-dnd.js +27 -0
- package/dist/app/displays/calendar-event.js +73 -0
- package/dist/app/displays/calendar-range.js +129 -0
- package/dist/app/displays/list-displays.js +7 -1
- package/dist/app/displays/use-calendar-move.js +68 -0
- package/dist/app/displays/use-calendar-rows.js +35 -0
- package/dist/app/embedded-list/embedded-list.js +25 -21
- package/dist/app/embedded-list/embedded-row-targets.js +65 -0
- package/dist/app/export/cells.js +6 -4
- package/dist/app/field-editor.js +13 -1
- package/dist/app/import/convert.js +2 -12
- package/dist/app/import/locale-formats.js +2 -1
- package/dist/app/incoming-card.js +35 -0
- package/dist/app/incoming-relations.js +36 -0
- package/dist/app/list-chart.js +3 -1
- package/dist/app/list-page.js +16 -5
- package/dist/app/pages/field-value.js +9 -1
- package/dist/app/pages/record-actions.js +34 -16
- package/dist/app/pages/use-run-action.js +8 -6
- package/dist/app/record-page.js +50 -28
- package/dist/app/related-opens.js +22 -0
- package/dist/app/related-section.js +4 -1
- package/dist/app/runtime/runtime-status.js +5 -4
- package/dist/app/search/navigation-hits.js +45 -0
- package/dist/app/search/use-global-search.js +13 -4
- package/dist/app/use-app-address.d.ts +3 -1
- package/dist/app/use-app-address.js +8 -9
- package/dist/app/use-incoming-counts.js +38 -0
- package/dist/app/use-related-records.js +3 -10
- package/dist/app-shell/app-shell.js +9 -2
- package/dist/app-shell/index.d.ts +1 -0
- package/dist/app-shell/logo.d.ts +5 -1
- package/dist/app-shell/logo.js +10 -3
- package/dist/app-shell/sidebar.js +3 -2
- package/dist/app-shell/top-bar-slot.js +11 -0
- package/dist/app-shell/top-bar.d.ts +3 -0
- package/dist/app-shell/top-bar.js +27 -19
- package/dist/auth/user-to-shell.js +3 -1
- package/dist/data/api-provider.d.ts +20 -0
- package/dist/data/query-keys.js +15 -0
- package/dist/data-grid/cells.js +8 -1
- package/dist/data-grid/grid-header.js +17 -16
- package/dist/data-grid/header-menu.js +16 -6
- package/dist/data-grid/row-menu.js +10 -40
- package/dist/format/date.js +52 -6
- package/dist/format/number-text.js +16 -0
- package/dist/format/zone.js +62 -0
- package/dist/live/columns-from-model.js +2 -0
- package/dist/live/contact-link.js +25 -0
- package/dist/live/contact-links.js +42 -0
- package/dist/live/display-value.js +14 -4
- package/dist/live/field-values.js +20 -9
- package/dist/live/filter-string.js +30 -30
- package/dist/package.js +1 -1
- package/dist/primitives/popover-menu.js +56 -0
- package/dist/primitives/segmented.js +8 -3
- package/dist/primitives/textarea.d.ts +4 -3
- package/dist/primitives/textarea.js +9 -4
- package/dist/record-view/action-menu.js +59 -0
- package/dist/record-view/record-header.js +2 -6
- package/dist/record-view/record-sidebar.js +2 -30
- package/dist/shell/assets/app-B_L-OYQJ.js +69 -0
- package/dist/shell/assets/export-worker-BoAr3vuF.js +2 -0
- package/dist/shell/assets/{index-CQc-60lQ.js → index-CRNWPM01.js} +1 -1
- package/dist/shell/assets/index-CzqIlZPP.css +2 -0
- package/dist/shell/assets/{maplibre-BvtgTvfx.js → maplibre-DZlOrjmh.js} +1 -1
- package/dist/shell/assets/{markdown-content-CJ8g5iKO.js → markdown-content-DT_18T89.js} +1 -1
- package/dist/shell/assets/{record-map-DSXYVoqB.js → record-map-Bmns3gJf.js} +1 -1
- package/dist/shell/assets/{records-map-cU_FAK7w.js → records-map-Ci3qFCDs.js} +1 -1
- package/dist/shell/assets/{shared-protobase-ui-Bc0CoTXD.js → shared-protobase-ui-DPgVYrS4.js} +1 -1
- package/dist/shell/index.html +4 -4
- package/dist/shell/protobase.shell.json +1 -1
- package/package.json +4 -4
- package/dist/shell/assets/app-BbVdHzEY.js +0 -69
- package/dist/shell/assets/export-worker-CVd5saLq.js +0 -2
- package/dist/shell/assets/index-C1fzTv_X.css +0 -2
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { humanize } from "../../live/naming.js";
|
|
2
|
+
import { recordKey } from "../../live/model-helpers.js";
|
|
3
|
+
import { rowAllows } from "../permissions.js";
|
|
4
|
+
import { relationTarget } from "../related-opens.js";
|
|
5
|
+
//#region src/app/embedded-list/embedded-row-targets.ts
|
|
6
|
+
const singularOf = (model, view) => (view?.names?.singular ?? humanize(model.name)).toLowerCase();
|
|
7
|
+
/**
|
|
8
|
+
* Where a table row of an embedded list leads, and its menu's items. A row opens its own record, or with `opens` the
|
|
9
|
+
* record that relation points at when it holds one; its menu then opens that record and edits, or for a user who may
|
|
10
|
+
* not, opens the row's own.
|
|
11
|
+
*/
|
|
12
|
+
const embeddedRowTargets = ({ model, view, opens, resources, views, canUpdate }) => {
|
|
13
|
+
const own = (row) => ({
|
|
14
|
+
kind: "record",
|
|
15
|
+
resource: model.name,
|
|
16
|
+
key: recordKey(model, row)
|
|
17
|
+
});
|
|
18
|
+
const farName = opens && model.fields[opens]?.relation?.resource;
|
|
19
|
+
const far = farName ? resources[farName] : void 0;
|
|
20
|
+
const other = (row) => {
|
|
21
|
+
const key = opens && far ? relationTarget(row, opens) : void 0;
|
|
22
|
+
return far && key !== void 0 ? {
|
|
23
|
+
kind: "record",
|
|
24
|
+
resource: far.name,
|
|
25
|
+
key
|
|
26
|
+
} : void 0;
|
|
27
|
+
};
|
|
28
|
+
const address = (row) => other(row) ?? own(row);
|
|
29
|
+
const singular = singularOf(model, view);
|
|
30
|
+
const actions = (linkTo) => {
|
|
31
|
+
const openOwn = {
|
|
32
|
+
label: `Open ${singular}`,
|
|
33
|
+
link: (row) => linkTo(own(row))
|
|
34
|
+
};
|
|
35
|
+
if (!far) return [openOwn];
|
|
36
|
+
const editable = (row) => canUpdate && rowAllows(row, "update");
|
|
37
|
+
return [
|
|
38
|
+
{
|
|
39
|
+
label: `Open ${singularOf(far, views[far.name])}`,
|
|
40
|
+
available: (row) => other(row) !== void 0,
|
|
41
|
+
link: (row) => linkTo(other(row))
|
|
42
|
+
},
|
|
43
|
+
{
|
|
44
|
+
label: `Edit ${singular}`,
|
|
45
|
+
available: editable,
|
|
46
|
+
link: (row) => linkTo({
|
|
47
|
+
kind: "record",
|
|
48
|
+
resource: model.name,
|
|
49
|
+
key: recordKey(model, row),
|
|
50
|
+
edit: true
|
|
51
|
+
})
|
|
52
|
+
},
|
|
53
|
+
{
|
|
54
|
+
...openOwn,
|
|
55
|
+
available: (row) => !editable(row)
|
|
56
|
+
}
|
|
57
|
+
];
|
|
58
|
+
};
|
|
59
|
+
return {
|
|
60
|
+
address,
|
|
61
|
+
actions
|
|
62
|
+
};
|
|
63
|
+
};
|
|
64
|
+
//#endregion
|
|
65
|
+
export { embeddedRowTargets };
|
package/dist/app/export/cells.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { viewerZone, zoneOffset } from "../../format/zone.js";
|
|
1
2
|
import { fileShown, isStoredFile } from "../../live/file-values.js";
|
|
2
3
|
import { displayValue } from "../../live/display-value.js";
|
|
3
4
|
//#region src/app/export/cells.ts
|
|
@@ -57,17 +58,18 @@ const numberCell = (column, value, mode) => {
|
|
|
57
58
|
value: number
|
|
58
59
|
};
|
|
59
60
|
};
|
|
60
|
-
const dateCell = (type, value, mode) => {
|
|
61
|
+
const dateCell = (type, value, mode, zone) => {
|
|
61
62
|
const ms = Date.parse(String(value));
|
|
62
63
|
if (Number.isNaN(ms)) return text(String(value));
|
|
64
|
+
const clock = type === "timestamp" && mode === "shown" ? ms + zoneOffset(ms, zone) : ms;
|
|
63
65
|
return {
|
|
64
66
|
kind: "number",
|
|
65
|
-
value: excelSerial(
|
|
67
|
+
value: excelSerial(clock),
|
|
66
68
|
format: dateFormats[mode][type]
|
|
67
69
|
};
|
|
68
70
|
};
|
|
69
71
|
/** A value as one typed Excel cell: numbers, dates and booleans as such, labels or stored values as text. */
|
|
70
|
-
const xlsxCell = (column, value, labels, mode) => {
|
|
72
|
+
const xlsxCell = (column, value, labels, mode, zone = viewerZone()) => {
|
|
71
73
|
if (isEmpty(value)) return empty;
|
|
72
74
|
const { field } = column;
|
|
73
75
|
switch (field.type) {
|
|
@@ -79,7 +81,7 @@ const xlsxCell = (column, value, labels, mode) => {
|
|
|
79
81
|
case "bigint":
|
|
80
82
|
case "decimal": return numberCell(column, value, mode);
|
|
81
83
|
case "date":
|
|
82
|
-
case "timestamp": return dateCell(field.type, value, mode);
|
|
84
|
+
case "timestamp": return dateCell(field.type, value, mode, zone);
|
|
83
85
|
case "relation":
|
|
84
86
|
if (mode === "shown") return text(relationLabel(column, value, labels));
|
|
85
87
|
return typeof value === "number" ? {
|
package/dist/app/field-editor.js
CHANGED
|
@@ -7,7 +7,8 @@ import { fromDraft, isEditable, toFieldDraft } from "../live/field-values.js";
|
|
|
7
7
|
import { Switch } from "../primitives/switch.js";
|
|
8
8
|
import { Textarea } from "../primitives/textarea.js";
|
|
9
9
|
import { isMarkdown } from "../markdown/markdown-hint.js";
|
|
10
|
-
import {
|
|
10
|
+
import { contactFormat, contactInputProps } from "../live/contact-links.js";
|
|
11
|
+
import { displayValue, valueTitle } from "../live/display-value.js";
|
|
11
12
|
import { AppLink } from "./use-app-address.js";
|
|
12
13
|
import { RelationPicker } from "./relation-picker.js";
|
|
13
14
|
import { SensitiveField } from "./sensitive-field.js";
|
|
@@ -17,6 +18,7 @@ import { WidgetPart } from "./field-types/widget-part.js";
|
|
|
17
18
|
import { Markdown } from "../markdown/markdown.js";
|
|
18
19
|
import { MarkdownEditor } from "../markdown/markdown-editor.js";
|
|
19
20
|
import { numeric, numericInputProps } from "./numeric-input.js";
|
|
21
|
+
import { ContactLink } from "../live/contact-link.js";
|
|
20
22
|
import { ArrowUpRight } from "lucide-react";
|
|
21
23
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
22
24
|
import { enumLabel } from "@protobase/schema";
|
|
@@ -95,7 +97,16 @@ const BuiltInEditor = ({ field, hints, value, stored, relationLabel, relationTo,
|
|
|
95
97
|
source: stored,
|
|
96
98
|
className: "py-1.5"
|
|
97
99
|
});
|
|
100
|
+
const contact = field.type === "text" ? contactFormat(hints) : void 0;
|
|
101
|
+
if ((!isEditable(field) || locked) && contact && typeof stored === "string" && stored !== "") return /* @__PURE__ */ jsx("div", {
|
|
102
|
+
className: "min-w-0 break-words py-1.5",
|
|
103
|
+
children: /* @__PURE__ */ jsx(ContactLink, {
|
|
104
|
+
format: contact,
|
|
105
|
+
value: stored
|
|
106
|
+
})
|
|
107
|
+
});
|
|
98
108
|
if (!isEditable(field) || locked) return /* @__PURE__ */ jsx("div", {
|
|
109
|
+
title: valueTitle(field, stored),
|
|
99
110
|
className: cn("py-1.5 tabular-nums", field.type === "decimal" || field.type === "integer" ? "font-medium" : "text-muted-foreground", multiline && "whitespace-pre-line break-words"),
|
|
100
111
|
children: displayValue(field, stored, hints)
|
|
101
112
|
});
|
|
@@ -133,6 +144,7 @@ const BuiltInEditor = ({ field, hints, value, stored, relationLabel, relationTo,
|
|
|
133
144
|
placeholder,
|
|
134
145
|
className: cn(numeric.has(field.type) && "text-right tabular-nums"),
|
|
135
146
|
...numericInputProps(field.type),
|
|
147
|
+
...contactInputProps(contact),
|
|
136
148
|
value: String(value),
|
|
137
149
|
onChange: (event) => onChange(event.target.value)
|
|
138
150
|
});
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { parseNumberText } from "../../format/number-text.js";
|
|
1
2
|
import { cellText, isEmptyCell, isSheetDate } from "./sheet-cell.js";
|
|
2
3
|
import { enumLabel } from "@protobase/schema";
|
|
3
4
|
//#region src/app/import/convert.ts
|
|
@@ -10,17 +11,6 @@ const problem = (text) => ({
|
|
|
10
11
|
problem: text
|
|
11
12
|
});
|
|
12
13
|
const quoted = (cell) => `“${cellText(cell)}”`;
|
|
13
|
-
const escapeRegExp = (text) => text.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
14
|
-
/**
|
|
15
|
-
* A number written as text, as a plain decimal (`-1234.5`): digits grouped by thousands with the other separator or
|
|
16
|
-
* spaces, and the `decimal` separator before the fraction. Undefined when the text is no such number.
|
|
17
|
-
*/
|
|
18
|
-
const parseNumberText = (text, decimal) => {
|
|
19
|
-
const group = `[${escapeRegExp(decimal === "." ? "," : ".")}\\s\\u00a0\\u202f']`;
|
|
20
|
-
const match = new RegExp(`^([-+]?)(\\d{1,3}(?:${group}\\d{3})+|\\d+)(?:${escapeRegExp(decimal)}(\\d+))?$`).exec(text.trim());
|
|
21
|
-
if (match) return `${match[1] === "-" ? "-" : ""}${match[2].replace(/\D/g, "")}${match[3] ? `.${match[3]}` : ""}`;
|
|
22
|
-
if (decimal === "." && /^[-+]?\d+(\.\d+)?e[-+]?\d+$/i.test(text.trim())) return String(Number(text.trim()));
|
|
23
|
-
};
|
|
24
14
|
const toNumberText = (cell, options) => {
|
|
25
15
|
if (typeof cell === "number") return String(cell);
|
|
26
16
|
if (typeof cell === "string") return parseNumberText(cell, options.decimal);
|
|
@@ -148,4 +138,4 @@ const convertCell = (field, hints, cell, options) => {
|
|
|
148
138
|
}
|
|
149
139
|
};
|
|
150
140
|
//#endregion
|
|
151
|
-
export { convertCell
|
|
141
|
+
export { convertCell };
|
|
@@ -1,10 +1,11 @@
|
|
|
1
|
+
import { localeDecimal } from "../../format/number-text.js";
|
|
1
2
|
//#region src/app/import/locale-formats.ts
|
|
2
3
|
/**
|
|
3
4
|
* How the browser's locale writes numbers and dates, as the import's starting choice: its decimal separator, and whether
|
|
4
5
|
* its dates put the day or the month first. The user sees both and can change them.
|
|
5
6
|
*/
|
|
6
7
|
const localeFormats = (locale) => {
|
|
7
|
-
const decimal =
|
|
8
|
+
const decimal = localeDecimal(locale);
|
|
8
9
|
const parts = new Intl.DateTimeFormat(locale).formatToParts(new Date(2026, 3, 3)).map((part) => part.type);
|
|
9
10
|
return {
|
|
10
11
|
decimal,
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { Skeleton } from "../primitives/skeleton.js";
|
|
2
|
+
import { formatInt } from "../format/number.js";
|
|
3
|
+
import { SidebarCard } from "../record-view/record-sidebar.js";
|
|
4
|
+
import { useAdminMeta } from "./meta-gate.js";
|
|
5
|
+
import { AppLink } from "./use-app-address.js";
|
|
6
|
+
import { incomingAddress, incomingRelations } from "./incoming-relations.js";
|
|
7
|
+
import { useIncomingCounts } from "./use-incoming-counts.js";
|
|
8
|
+
import { ChevronRight } from "lucide-react";
|
|
9
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
10
|
+
//#region src/app/incoming-card.tsx
|
|
11
|
+
/** The rail's card of the records pointing at this one: per resource and field, how many, linking to them in their list. */
|
|
12
|
+
const IncomingCard = ({ model, recordKey, layout, lines }) => {
|
|
13
|
+
const { resources, views } = useAdminMeta();
|
|
14
|
+
const counts = useIncomingCounts(incomingRelations(model, resources, views, layout, lines), resources, recordKey);
|
|
15
|
+
if (counts.length === 0) return null;
|
|
16
|
+
return /* @__PURE__ */ jsx(SidebarCard, {
|
|
17
|
+
title: "Related records",
|
|
18
|
+
children: /* @__PURE__ */ jsx("ul", {
|
|
19
|
+
className: "-mx-2",
|
|
20
|
+
children: counts.map(({ relation, count }) => /* @__PURE__ */ jsx("li", { children: /* @__PURE__ */ jsxs(AppLink, {
|
|
21
|
+
to: incomingAddress(relation, recordKey),
|
|
22
|
+
className: "group flex items-center justify-between gap-3 rounded-md px-2 py-2 hover:bg-muted",
|
|
23
|
+
children: [/* @__PURE__ */ jsx("span", {
|
|
24
|
+
className: "min-w-0 truncate font-medium text-primary-text",
|
|
25
|
+
children: relation.label
|
|
26
|
+
}), /* @__PURE__ */ jsxs("span", {
|
|
27
|
+
className: "flex shrink-0 items-center gap-1 tabular-nums text-muted-foreground",
|
|
28
|
+
children: [count === void 0 ? /* @__PURE__ */ jsx(Skeleton, { className: "h-4 w-6" }) : `${formatInt(count.count)}${count.exact ? "" : "+"}`, /* @__PURE__ */ jsx(ChevronRight, { className: "size-4 text-faint-foreground group-hover:text-foreground" })]
|
|
29
|
+
})]
|
|
30
|
+
}) }, `${relation.resource}.${relation.field}`))
|
|
31
|
+
})
|
|
32
|
+
});
|
|
33
|
+
};
|
|
34
|
+
//#endregion
|
|
35
|
+
export { IncomingCard };
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { humanize } from "../live/naming.js";
|
|
2
|
+
import { createWhere, printFilter } from "@protobase/schema";
|
|
3
|
+
//#region src/app/incoming-relations.ts
|
|
4
|
+
const where$1 = createWhere();
|
|
5
|
+
/**
|
|
6
|
+
* The relations that point at records of `model`, as the rail lists them by default: each filterable relation of
|
|
7
|
+
* another resource the user can see (a hidden resource or field is not in the meta), to a record of one key column.
|
|
8
|
+
* Left out are a resource's tenant, which every record holds, the lines drawn on the page, and what an `l.related`
|
|
9
|
+
* section of the page already lists. Two fields of one resource are told apart by their labels.
|
|
10
|
+
*/
|
|
11
|
+
const incomingRelations = (model, resources, views, layout, lines) => {
|
|
12
|
+
if (model.primaryKey.length !== 1) return [];
|
|
13
|
+
const listed = new Set(layout.flatMap((item) => item.kind === "related" && !item.through ? [`${item.resource}.${item.field}`] : []));
|
|
14
|
+
const found = Object.values(resources).flatMap((source) => Object.values(source.fields).filter((field) => field.type === "relation" && field.relation?.resource === model.name && field.relation.columns.length === 1 && field.filterable).filter((field) => field.name !== source.tenant && source.name !== lines?.name && !listed.has(`${source.name}.${field.name}`)).map((field) => ({
|
|
15
|
+
source,
|
|
16
|
+
field: field.name
|
|
17
|
+
})));
|
|
18
|
+
return found.map(({ source, field }) => {
|
|
19
|
+
const view = views[source.name];
|
|
20
|
+
const plural = view?.names?.plural ?? humanize(source.name);
|
|
21
|
+
const several = found.filter((other) => other.source.name === source.name).length > 1;
|
|
22
|
+
return {
|
|
23
|
+
resource: source.name,
|
|
24
|
+
field,
|
|
25
|
+
label: several ? `${plural} (${view?.fields[field]?.label ?? humanize(field)})` : plural
|
|
26
|
+
};
|
|
27
|
+
});
|
|
28
|
+
};
|
|
29
|
+
/** The list of the records that point at `key` through `relation`, filtered as its list's address reads. */
|
|
30
|
+
const incomingAddress = (relation, key) => ({
|
|
31
|
+
kind: "list",
|
|
32
|
+
resource: relation.resource,
|
|
33
|
+
filter: printFilter(where$1.eq(relation.field, key))
|
|
34
|
+
});
|
|
35
|
+
//#endregion
|
|
36
|
+
export { incomingAddress, incomingRelations };
|
package/dist/app/list-chart.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { viewerZone } from "../format/zone.js";
|
|
1
2
|
import { useSeries } from "../data/use-aggregates.js";
|
|
2
3
|
import { ActivityChart } from "../activity-chart/activity-chart.js";
|
|
3
4
|
import { useState } from "react";
|
|
@@ -18,7 +19,8 @@ const ListChart = ({ resource, chart, title, noun, filter }) => {
|
|
|
18
19
|
field: chart.field,
|
|
19
20
|
range,
|
|
20
21
|
granularity,
|
|
21
|
-
filter
|
|
22
|
+
filter,
|
|
23
|
+
timeZone: viewerZone()
|
|
22
24
|
}).data?.points.map((point) => ({
|
|
23
25
|
at: Date.parse(`${point.bucket}Z`),
|
|
24
26
|
count: point.count
|
package/dist/app/list-page.js
CHANGED
|
@@ -31,6 +31,7 @@ import { SignInPrompt } from "../auth/sign-in-prompt.js";
|
|
|
31
31
|
import { relatedColumnLoads } from "./related-dots/dot-card.js";
|
|
32
32
|
import { useDisplayRows } from "./displays/use-display-rows.js";
|
|
33
33
|
import { BoardDisplay } from "./displays/board-display.js";
|
|
34
|
+
import { CalendarDisplay } from "./displays/calendar-display.js";
|
|
34
35
|
import { CustomDisplay } from "./displays/custom-display.js";
|
|
35
36
|
import { MapDisplay } from "./displays/map-display.js";
|
|
36
37
|
import { DisplaySwitch, SortMenu } from "./displays/display-controls.js";
|
|
@@ -115,7 +116,7 @@ const ListPage = ({ address }) => {
|
|
|
115
116
|
};
|
|
116
117
|
const LoadedList = ({ model, address, scope, heading }) => {
|
|
117
118
|
const meta = useAdminMeta();
|
|
118
|
-
const { set, go, href } = useAppAddress();
|
|
119
|
+
const { set, listTo, go, href } = useAppAddress();
|
|
119
120
|
const resource = model.name;
|
|
120
121
|
const view = meta.views[resource];
|
|
121
122
|
const fields = useMemo(() => widgetFields(view, model), [view, model]);
|
|
@@ -202,10 +203,11 @@ const LoadedList = ({ model, address, scope, heading }) => {
|
|
|
202
203
|
setFilter(state, text ? [...others, where$1.search(text)] : others);
|
|
203
204
|
};
|
|
204
205
|
const plural = view?.names?.plural ?? humanize(resource);
|
|
205
|
-
const
|
|
206
|
+
const sorted = display?.kind === "calendar" ? void 0 : display;
|
|
207
|
+
const sortMenu = sorted && /* @__PURE__ */ jsx(SortMenu, {
|
|
206
208
|
model,
|
|
207
209
|
view,
|
|
208
|
-
display,
|
|
210
|
+
display: sorted,
|
|
209
211
|
orderBy,
|
|
210
212
|
onChange: (next) => set({ orderBy: next })
|
|
211
213
|
});
|
|
@@ -234,12 +236,12 @@ const LoadedList = ({ model, address, scope, heading }) => {
|
|
|
234
236
|
onRemove: (key) => setFilter(removeChip(state, key)),
|
|
235
237
|
onClear: () => setFilter(emptyFilterState)
|
|
236
238
|
});
|
|
237
|
-
const phoneSort =
|
|
239
|
+
const phoneSort = sorted && /* @__PURE__ */ jsx(FilterSection, {
|
|
238
240
|
title: "Sort",
|
|
239
241
|
children: /* @__PURE__ */ jsx(SortMenu, {
|
|
240
242
|
model,
|
|
241
243
|
view,
|
|
242
|
-
display,
|
|
244
|
+
display: sorted,
|
|
243
245
|
orderBy,
|
|
244
246
|
onChange: (next) => set({ orderBy: next }),
|
|
245
247
|
className: "min-h-11 w-full"
|
|
@@ -425,6 +427,15 @@ const LoadedList = ({ model, address, scope, heading }) => {
|
|
|
425
427
|
filter: loadFilter,
|
|
426
428
|
orderBy
|
|
427
429
|
});
|
|
430
|
+
if (display.kind === "calendar") return /* @__PURE__ */ jsx(CalendarDisplay, {
|
|
431
|
+
model,
|
|
432
|
+
view,
|
|
433
|
+
display,
|
|
434
|
+
fields: loaded,
|
|
435
|
+
filter: loadFilter,
|
|
436
|
+
at: address.calendar,
|
|
437
|
+
linkTo: (calendar) => listTo({ calendar })
|
|
438
|
+
});
|
|
428
439
|
return /* @__PURE__ */ jsx(CustomDisplay, {
|
|
429
440
|
display,
|
|
430
441
|
list: displayRows
|
|
@@ -3,13 +3,15 @@ import { keys } from "../../data/query-keys.js";
|
|
|
3
3
|
import { useClient } from "../../data/api-provider.js";
|
|
4
4
|
import { fileShown, shownImage } from "../../live/file-values.js";
|
|
5
5
|
import { isMarkdown } from "../../markdown/markdown-hint.js";
|
|
6
|
-
import {
|
|
6
|
+
import { contactFormat } from "../../live/contact-links.js";
|
|
7
|
+
import { displayValue, valueTitle } from "../../live/display-value.js";
|
|
7
8
|
import { useAdminMeta } from "../meta-gate.js";
|
|
8
9
|
import { AppLink } from "../use-app-address.js";
|
|
9
10
|
import { FileImage } from "../../live/file-image.js";
|
|
10
11
|
import { useShownWidget } from "../field-types/shown-widget.js";
|
|
11
12
|
import { WidgetPart } from "../field-types/widget-part.js";
|
|
12
13
|
import { Markdown } from "../../markdown/markdown.js";
|
|
14
|
+
import { ContactLink } from "../../live/contact-link.js";
|
|
13
15
|
import { fetchLabels } from "../../live/relation-labels.js";
|
|
14
16
|
import { ValueBadge } from "../value-badge.js";
|
|
15
17
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
@@ -74,12 +76,18 @@ const BuiltInValue = ({ model, field, value, format, relationLabel }) => {
|
|
|
74
76
|
}
|
|
75
77
|
if (isMarkdown(field, hints)) return /* @__PURE__ */ jsx(Markdown, { source: String(value) });
|
|
76
78
|
const text = displayValue(field, value, hints);
|
|
79
|
+
const contact = field.type === "text" ? contactFormat(hints) : void 0;
|
|
80
|
+
if (contact) return /* @__PURE__ */ jsx(ContactLink, {
|
|
81
|
+
format: contact,
|
|
82
|
+
value: text
|
|
83
|
+
});
|
|
77
84
|
if (field.type === "enum" || hints.format === "badge") return /* @__PURE__ */ jsx(ValueBadge, {
|
|
78
85
|
value,
|
|
79
86
|
hints,
|
|
80
87
|
children: text
|
|
81
88
|
});
|
|
82
89
|
return /* @__PURE__ */ jsx("span", {
|
|
90
|
+
title: valueTitle(field, value),
|
|
83
91
|
className: cn(hints.format === "code" && "font-mono text-xs", (field.type === "decimal" || field.type === "integer") && "tabular-nums", hints.multiline && "whitespace-pre-line break-words"),
|
|
84
92
|
children: text
|
|
85
93
|
});
|
|
@@ -1,4 +1,7 @@
|
|
|
1
|
+
import { useTopBarSlot } from "../../app-shell/top-bar-slot.js";
|
|
2
|
+
import "../../app-shell/top-bar.js";
|
|
1
3
|
import { ActionRail } from "../../record-view/action-rail.js";
|
|
4
|
+
import { ActionMenu } from "../../record-view/action-menu.js";
|
|
2
5
|
import { AppLink, useAppAddress } from "../use-app-address.js";
|
|
3
6
|
import { ActionPrompt, asksFirst } from "./action-prompt.js";
|
|
4
7
|
import { useActionRunner } from "./use-run-action.js";
|
|
@@ -6,14 +9,17 @@ import { actionIcon } from "../action-icon.js";
|
|
|
6
9
|
import { useState } from "react";
|
|
7
10
|
import { Pencil, Trash2 } from "lucide-react";
|
|
8
11
|
import { Fragment as Fragment$1, jsx, jsxs } from "react/jsx-runtime";
|
|
12
|
+
import { createPortal } from "react-dom";
|
|
9
13
|
//#region src/app/pages/record-actions.tsx
|
|
10
14
|
/**
|
|
11
15
|
* The record page's actions in one rail: Edit, a link to the form at `edit`, when the page offers it; the view's named
|
|
12
16
|
* actions, run like the Action block runs them, a delete among them returning to the list; and the record's own Delete,
|
|
13
|
-
* `onDelete`, unless a named action deletes already.
|
|
17
|
+
* `onDelete`, unless a named action deletes already. On a mobile screen they sit in the top bar's menu instead, at hand
|
|
18
|
+
* however long the record is, when the shell has a top bar.
|
|
14
19
|
*/
|
|
15
20
|
const RecordActions = ({ model, stored, recordKey, edit, onDelete }) => {
|
|
16
21
|
const { go } = useAppAddress();
|
|
22
|
+
const slot = useTopBarSlot();
|
|
17
23
|
const scope = {
|
|
18
24
|
model,
|
|
19
25
|
record: stored.record,
|
|
@@ -39,8 +45,9 @@ const RecordActions = ({ model, stored, recordKey, edit, onDelete }) => {
|
|
|
39
45
|
...edit ? [{
|
|
40
46
|
name: "Edit",
|
|
41
47
|
icon: Pencil,
|
|
42
|
-
render: ({ className, children }) => /* @__PURE__ */ jsx(AppLink, {
|
|
48
|
+
render: ({ className, children, role }) => /* @__PURE__ */ jsx(AppLink, {
|
|
43
49
|
to: edit,
|
|
50
|
+
role,
|
|
44
51
|
className,
|
|
45
52
|
children
|
|
46
53
|
})
|
|
@@ -54,20 +61,31 @@ const RecordActions = ({ model, stored, recordKey, edit, onDelete }) => {
|
|
|
54
61
|
}] : []
|
|
55
62
|
];
|
|
56
63
|
if (actions.length === 0) return null;
|
|
57
|
-
return /* @__PURE__ */ jsxs(Fragment$1, { children: [
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
64
|
+
return /* @__PURE__ */ jsxs(Fragment$1, { children: [
|
|
65
|
+
/* @__PURE__ */ jsx("div", {
|
|
66
|
+
className: slot ? "max-md:hidden" : void 0,
|
|
67
|
+
children: /* @__PURE__ */ jsx(ActionRail, { actions })
|
|
68
|
+
}),
|
|
69
|
+
slot && createPortal(/* @__PURE__ */ jsx(ActionMenu, {
|
|
70
|
+
actions,
|
|
71
|
+
label: "Record actions",
|
|
72
|
+
className: "inline-flex size-11 shrink-0 items-center justify-center rounded-md text-muted-foreground hover:bg-muted md:hidden"
|
|
73
|
+
}), slot),
|
|
74
|
+
asking && /* @__PURE__ */ jsx(ActionPrompt, {
|
|
75
|
+
action: asking,
|
|
76
|
+
label: asking.label,
|
|
77
|
+
danger: asking.run?.kind === "delete",
|
|
78
|
+
onCancel: () => setAsking(void 0),
|
|
79
|
+
onConfirm: () => {
|
|
80
|
+
setAsking(void 0);
|
|
81
|
+
runner.run(asking);
|
|
82
|
+
},
|
|
83
|
+
onSubmit: async (values) => {
|
|
84
|
+
await runner.submit(asking, values);
|
|
85
|
+
setAsking(void 0);
|
|
86
|
+
}
|
|
87
|
+
})
|
|
88
|
+
] });
|
|
71
89
|
};
|
|
72
90
|
//#endregion
|
|
73
91
|
export { RecordActions };
|
|
@@ -12,10 +12,10 @@ import { fillAddress } from "@protobase/schema/internal";
|
|
|
12
12
|
const message = (error) => error instanceof Error ? error.message : "Something went wrong";
|
|
13
13
|
/**
|
|
14
14
|
* The named actions of `resource`'s view, ready to run on `scope` (the record they act on): built-in behaviour first
|
|
15
|
-
* (`update`, `delete`, `link`), else the handler the project registered under the
|
|
16
|
-
* the record holds already is left out, and so is a link to a URL the record
|
|
17
|
-
* why one cannot run; `afterRun` hears about every action that succeeded.
|
|
18
|
-
* inputs and throws its failure, for the dialog to show.
|
|
15
|
+
* (`update`, `delete`, `link`, and `ai`, which the server runs), else the handler the project registered under the
|
|
16
|
+
* action's name. An update whose values the record holds already is left out, and so is a link to a URL the record
|
|
17
|
+
* holds while it holds none. `blocked` says why one cannot run; `afterRun` hears about every action that succeeded.
|
|
18
|
+
* `submit` runs one with the answers to its inputs and throws its failure, for the dialog to show.
|
|
19
19
|
*/
|
|
20
20
|
const useActionRunner = (resource, scope, afterRun) => {
|
|
21
21
|
const { views } = useAdminMeta();
|
|
@@ -30,8 +30,9 @@ const useActionRunner = (resource, scope, afterRun) => {
|
|
|
30
30
|
const actions = (views[resource]?.actions ?? []).flatMap((action) => actionFor(action, scope?.record) ?? []);
|
|
31
31
|
const blocked = (action) => {
|
|
32
32
|
const kind = action.run?.kind;
|
|
33
|
-
if ((kind === "update" || kind === "delete") && !scope) return "Needs a record";
|
|
34
|
-
if (kind === "update" && (!can(permissions, "update") || scope?.permissions?.update === false)) return "You cannot change this record";
|
|
33
|
+
if ((kind === "update" || kind === "delete" || kind === "ai") && !scope) return "Needs a record";
|
|
34
|
+
if ((kind === "update" || kind === "ai") && (!can(permissions, "update") || scope?.permissions?.update === false)) return "You cannot change this record";
|
|
35
|
+
if (action.run?.kind === "ai" && scope?.permissions?.fields?.[action.run.writes] === "read") return "You cannot change this record";
|
|
35
36
|
if (kind === "delete" && (!can(permissions, "delete") || scope?.permissions?.delete === false)) return "You cannot delete this record";
|
|
36
37
|
if (!action.run && !handlers?.[action.name]) return `The app has no handler for "${action.name}"`;
|
|
37
38
|
const custom = action.inputs?.find((input) => input.kind === "custom" && !components?.[input.component ?? ""]);
|
|
@@ -46,6 +47,7 @@ const useActionRunner = (resource, scope, afterRun) => {
|
|
|
46
47
|
}
|
|
47
48
|
if (run?.kind === "update") await client.update(resource, scope.key, run.values, scope.etag ?? "*");
|
|
48
49
|
else if (run?.kind === "delete") await client.remove(resource, scope.key, { etag: scope.etag });
|
|
50
|
+
else if (run?.kind === "ai") await client.generate(resource, scope.key, action.name);
|
|
49
51
|
else await handlers[action.name]({
|
|
50
52
|
resource,
|
|
51
53
|
record: scope?.record,
|