@protobase/ui 0.22.1 → 0.22.3
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/export/cells.js +4 -1
- package/dist/app/field-editor.js +1 -1
- package/dist/app/hand-order/hand-order.js +1 -1
- package/dist/app/incoming-relations.js +1 -1
- package/dist/app/pages/field-value.js +1 -1
- package/dist/app/record-page.js +11 -7
- package/dist/app/related-section.js +11 -6
- package/dist/app/related-value.js +14 -0
- package/dist/data-grid/cell-text.js +2 -2
- package/dist/data-grid/cells.js +2 -1
- package/dist/filter-panel/filter-config.js +2 -0
- package/dist/format/date.js +5 -4
- package/dist/live/columns-from-model.js +1 -0
- package/dist/live/display-value.js +11 -6
- package/dist/live/filter-string.js +4 -0
- package/dist/package.js +1 -1
- package/dist/shell/assets/app-3MBLkJAB.js +69 -0
- package/dist/shell/assets/export-worker-C1djGgjD.js +2 -0
- package/dist/shell/assets/{index-BvrIX29L.js → index-K4WBo5hS.js} +1 -1
- package/dist/shell/assets/{maplibre-DEGWdV6B.js → maplibre-CPwgBgXC.js} +1 -1
- package/dist/shell/assets/{markdown-content-DdB7hUqJ.js → markdown-content-kdNPOEJy.js} +1 -1
- package/dist/shell/assets/{record-map-C_dz1z7c.js → record-map-C57pIfol.js} +1 -1
- package/dist/shell/assets/{records-map-ByiKJWnW.js → records-map-B8_M8kjM.js} +1 -1
- package/dist/shell/assets/{shared-protobase-ui-Di_9bo0v.js → shared-protobase-ui-BRE7ZUBU.js} +1 -1
- package/dist/shell/index.html +3 -3
- package/dist/shell/protobase.shell.json +1 -1
- package/package.json +4 -4
- package/dist/shell/assets/app-xO_xPXCq.js +0 -69
- package/dist/shell/assets/export-worker-BoAr3vuF.js +0 -2
package/dist/app/export/cells.js
CHANGED
|
@@ -19,7 +19,10 @@ const csvText = (column, value, labels, mode) => {
|
|
|
19
19
|
const { field, hints } = column;
|
|
20
20
|
if (field.type === "file") return fileText(value, mode);
|
|
21
21
|
if (field.type === "relation") return mode === "shown" ? relationLabel(column, value, labels) : String(value);
|
|
22
|
-
if (mode === "shown") return displayValue(field, value,
|
|
22
|
+
if (mode === "shown") return displayValue(field, value, {
|
|
23
|
+
...hints,
|
|
24
|
+
format: void 0
|
|
25
|
+
});
|
|
23
26
|
if (typeof value === "object") return JSON.stringify(value);
|
|
24
27
|
return String(value);
|
|
25
28
|
};
|
package/dist/app/field-editor.js
CHANGED
|
@@ -106,7 +106,7 @@ const BuiltInEditor = ({ field, hints, value, stored, relationLabel, relationTo,
|
|
|
106
106
|
})
|
|
107
107
|
});
|
|
108
108
|
if (!isEditable(field) || locked) return /* @__PURE__ */ jsx("div", {
|
|
109
|
-
title: valueTitle(field, stored),
|
|
109
|
+
title: valueTitle(field, stored, void 0, hints),
|
|
110
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"),
|
|
111
111
|
children: displayValue(field, stored, hints)
|
|
112
112
|
});
|
|
@@ -16,6 +16,6 @@ const listHandOrder = (model, permissions) => model.position?.within === void 0
|
|
|
16
16
|
* The field a related section sorts its records by hand in: those of one parent, when the section lists every record
|
|
17
17
|
* whose `within` relation points at the page's record, no filter or join resource leaving some out.
|
|
18
18
|
*/
|
|
19
|
-
const relatedHandOrder = (item, model, permissions) => model.position?.within !== void 0 && item.field === model.position.within && !item.through && !item.filter ? writablePosition(model, permissions) : void 0;
|
|
19
|
+
const relatedHandOrder = (item, model, permissions) => model.position?.within !== void 0 && item.field === model.position.within && !item.through && !item.from && !item.filter ? writablePosition(model, permissions) : void 0;
|
|
20
20
|
//#endregion
|
|
21
21
|
export { listHandOrder, relatedHandOrder };
|
|
@@ -11,7 +11,7 @@ const where$1 = createWhere();
|
|
|
11
11
|
*/
|
|
12
12
|
const incomingRelations = (model, resources, views, layout, lines) => {
|
|
13
13
|
if (model.primaryKey.length !== 1) return [];
|
|
14
|
-
const listed = new Set(layoutItems(layout).flatMap((item) => item.kind === "related" && !item.through ? [`${item.resource}.${item.field}`] : []));
|
|
14
|
+
const listed = new Set(layoutItems(layout).flatMap((item) => item.kind === "related" && !item.through && !item.from ? [`${item.resource}.${item.field}`] : []));
|
|
15
15
|
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) => ({
|
|
16
16
|
source,
|
|
17
17
|
field: field.name
|
|
@@ -87,7 +87,7 @@ const BuiltInValue = ({ model, field, value, format, relationLabel }) => {
|
|
|
87
87
|
children: text
|
|
88
88
|
});
|
|
89
89
|
return /* @__PURE__ */ jsx("span", {
|
|
90
|
-
title: valueTitle(field, value),
|
|
90
|
+
title: valueTitle(field, value, void 0, hints),
|
|
91
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"),
|
|
92
92
|
children: text
|
|
93
93
|
});
|
package/dist/app/record-page.js
CHANGED
|
@@ -59,6 +59,7 @@ import { useRecordEditor } from "./use-record-editor.js";
|
|
|
59
59
|
import { useRelatedRecords } from "./use-related-records.js";
|
|
60
60
|
import { IncomingCard } from "./incoming-card.js";
|
|
61
61
|
import { RelatedSection } from "./related-section.js";
|
|
62
|
+
import { relatedValue } from "./related-value.js";
|
|
62
63
|
import { Fragment, useMemo } from "react";
|
|
63
64
|
import { Check } from "lucide-react";
|
|
64
65
|
import { isApiError } from "@protobase/client";
|
|
@@ -302,7 +303,7 @@ const LoadedRecord = ({ model, recordKey, stored, preview }) => {
|
|
|
302
303
|
format: contact,
|
|
303
304
|
value: text
|
|
304
305
|
}) : /* @__PURE__ */ jsx("strong", {
|
|
305
|
-
title: valueTitle(field, value),
|
|
306
|
+
title: valueTitle(field, value, void 0, hint(name)),
|
|
306
307
|
className: hint(name)?.multiline ? "whitespace-pre-line break-words" : void 0,
|
|
307
308
|
children: text
|
|
308
309
|
});
|
|
@@ -361,11 +362,14 @@ const LoadedRecord = ({ model, recordKey, stored, preview }) => {
|
|
|
361
362
|
children
|
|
362
363
|
}, `group:${item.title}`)];
|
|
363
364
|
}
|
|
364
|
-
if (item.kind === "related")
|
|
365
|
-
item
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
365
|
+
if (item.kind === "related") {
|
|
366
|
+
if (item.from !== void 0 && !known(item.from)) return [];
|
|
367
|
+
return [/* @__PURE__ */ jsx(RelatedSection, {
|
|
368
|
+
item,
|
|
369
|
+
value: relatedValue(item, stored.record, recordKey),
|
|
370
|
+
owner: model.name
|
|
371
|
+
}, `related:${item.title}`)];
|
|
372
|
+
}
|
|
369
373
|
if (preview && (item.kind === "custom" || item.kind === "map" || item.kind === "gallery")) return [];
|
|
370
374
|
if (item.kind === "custom") return item.sidebar ? [] : [/* @__PURE__ */ jsx(CustomSection, {
|
|
371
375
|
item,
|
|
@@ -520,7 +524,7 @@ const LoadedRecord = ({ model, recordKey, stored, preview }) => {
|
|
|
520
524
|
return [{
|
|
521
525
|
label: hint(name)?.label ?? humanize(name),
|
|
522
526
|
value: known(name) ? /* @__PURE__ */ jsx("span", {
|
|
523
|
-
title: valueTitle(field, stored.record[name]),
|
|
527
|
+
title: valueTitle(field, stored.record[name], void 0, hint(name)),
|
|
524
528
|
children: displayValue(field, stored.record[name], hint(name))
|
|
525
529
|
}) : /* @__PURE__ */ jsx(Skeleton, { className: "h-4 w-24" })
|
|
526
530
|
}];
|
|
@@ -28,13 +28,13 @@ const and = (...parts) => {
|
|
|
28
28
|
* all the join records that point at this record hold, read page by page. `null` when no join record does, so nothing
|
|
29
29
|
* can match.
|
|
30
30
|
*/
|
|
31
|
-
const useMatch = (item,
|
|
31
|
+
const useMatch = (item, value) => {
|
|
32
32
|
const client = useClient();
|
|
33
33
|
const { resources } = useAdminMeta();
|
|
34
34
|
const through = item.through;
|
|
35
35
|
const via = through ? resources[through.resource] : void 0;
|
|
36
36
|
const key = through?.key ?? via?.primaryKey[0];
|
|
37
|
-
const viaFilter = through ? and(printFilter(where$1.eq(through.field,
|
|
37
|
+
const viaFilter = through ? and(printFilter(where$1.eq(through.field, value)), through.filter) : "";
|
|
38
38
|
const collected = useQuery({
|
|
39
39
|
queryKey: keys.list(via?.name ?? "", viaFilter, "", 0, `related:${key}`),
|
|
40
40
|
enabled: Boolean(via && key),
|
|
@@ -46,8 +46,13 @@ const useMatch = (item, recordKey) => {
|
|
|
46
46
|
return [...new Set(joined.items.flatMap((row) => row[key] === null || row[key] === void 0 ? [] : [String(row[key])]))];
|
|
47
47
|
}
|
|
48
48
|
});
|
|
49
|
+
if (value === null) return {
|
|
50
|
+
filter: null,
|
|
51
|
+
isPending: false,
|
|
52
|
+
error: null
|
|
53
|
+
};
|
|
49
54
|
if (!through) return {
|
|
50
|
-
filter: and(printFilter(where$1.eq(item.field,
|
|
55
|
+
filter: and(printFilter(where$1.eq(item.field, value)), item.filter),
|
|
51
56
|
isPending: false,
|
|
52
57
|
error: null
|
|
53
58
|
};
|
|
@@ -75,11 +80,11 @@ const Empty = ({ text }) => /* @__PURE__ */ jsx("p", {
|
|
|
75
80
|
* a join resource, in the list page's table or display, with a link to them all in the list. When they are ordered by
|
|
76
81
|
* hand within this record, **Sort** shows them all in their order, to be moved by their drag handles.
|
|
77
82
|
*/
|
|
78
|
-
const RelatedSection = ({ item,
|
|
83
|
+
const RelatedSection = ({ item, value, owner }) => {
|
|
79
84
|
const { resources, views } = useAdminMeta();
|
|
80
85
|
const model = resources[item.resource];
|
|
81
86
|
const view = model && views[model.name];
|
|
82
|
-
const match = useMatch(item,
|
|
87
|
+
const match = useMatch(item, value);
|
|
83
88
|
const [display, pick] = useEmbeddedDisplay(view, `related:${owner}:${item.title}`, item.display);
|
|
84
89
|
const permissions = usePermissions(item.resource);
|
|
85
90
|
const [sorting, setSorting] = useState(false);
|
|
@@ -116,7 +121,7 @@ const RelatedSection = ({ item, recordKey, owner }) => {
|
|
|
116
121
|
model,
|
|
117
122
|
view,
|
|
118
123
|
field: handOrder,
|
|
119
|
-
filter: printFilter(where$1.eq(item.field,
|
|
124
|
+
filter: printFilter(where$1.eq(item.field, value)),
|
|
120
125
|
columns
|
|
121
126
|
})
|
|
122
127
|
});
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { relationTarget } from "./related-opens.js";
|
|
2
|
+
//#region src/app/related-value.ts
|
|
3
|
+
/**
|
|
4
|
+
* The value a related section's `field` is matched against: the record's key, or with `from` that field's value as it
|
|
5
|
+
* is (a relation by its key). Null when the record holds none, so the section lists nothing.
|
|
6
|
+
*/
|
|
7
|
+
const relatedValue = (item, record, recordKey) => {
|
|
8
|
+
if (item.from === void 0) return recordKey;
|
|
9
|
+
const value = record[item.from];
|
|
10
|
+
if (typeof value === "boolean") return value;
|
|
11
|
+
return relationTarget(record, item.from) ?? null;
|
|
12
|
+
};
|
|
13
|
+
//#endregion
|
|
14
|
+
export { relatedValue };
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { formatMoney } from "../format/money.js";
|
|
2
2
|
import { formatInt, formatPercent, formatSigned } from "../format/number.js";
|
|
3
|
-
import { formatDate, formatDateTime } from "../format/date.js";
|
|
3
|
+
import { formatDate, formatDateTime, formatRelativeTime } from "../format/date.js";
|
|
4
4
|
import { userById } from "../mocks/users.js";
|
|
5
5
|
import { fileShown } from "../live/file-values.js";
|
|
6
6
|
import { enumLabel } from "@protobase/schema";
|
|
@@ -15,7 +15,7 @@ const cellText = (spec, value) => {
|
|
|
15
15
|
case "id": return String(value);
|
|
16
16
|
case "money": return `${formatMoney(Number(value), spec.currency, spec.decimals)}${spec.suffix ?? ""}`;
|
|
17
17
|
case "date": return formatDate(Number(value));
|
|
18
|
-
case "datetime": return formatDateTime(Number(value));
|
|
18
|
+
case "datetime": return spec.relative ? formatRelativeTime(Math.round((Date.now() - Number(value)) / 1e3)) : formatDateTime(Number(value));
|
|
19
19
|
case "number": return `${formatInt(Number(value))}${spec.suffix ?? ""}`;
|
|
20
20
|
case "signed": return `${formatSigned(Number(value))}${spec.suffix ?? ""}`;
|
|
21
21
|
case "percent": return Number(value) === 0 ? "—" : formatPercent(Number(value));
|
package/dist/data-grid/cells.js
CHANGED
|
@@ -2,6 +2,7 @@ import { cn } from "../lib/cn.js";
|
|
|
2
2
|
import { Avatar } from "../primitives/avatar.js";
|
|
3
3
|
import { Badge } from "../primitives/badge.js";
|
|
4
4
|
import { viewerZone, zoneLabel } from "../format/zone.js";
|
|
5
|
+
import { formatDateTime } from "../format/date.js";
|
|
5
6
|
import { userById } from "../mocks/users.js";
|
|
6
7
|
import { fileShown, shownImage } from "../live/file-values.js";
|
|
7
8
|
import { cellText } from "./cell-text.js";
|
|
@@ -39,7 +40,7 @@ const renderCell = (spec, value) => {
|
|
|
39
40
|
});
|
|
40
41
|
case "datetime": return /* @__PURE__ */ jsx("span", {
|
|
41
42
|
className: "tabular-nums text-muted-foreground",
|
|
42
|
-
title: `Shown in ${zoneLabel(Number(value), viewerZone())}`,
|
|
43
|
+
title: spec.relative ? formatDateTime(Number(value)) : `Shown in ${zoneLabel(Number(value), viewerZone())}`,
|
|
43
44
|
children: text
|
|
44
45
|
});
|
|
45
46
|
case "number": return /* @__PURE__ */ jsx("span", {
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
//#region src/filter-panel/filter-config.ts
|
|
2
2
|
const defaultDatePresets = [
|
|
3
|
+
"today",
|
|
3
4
|
"7d",
|
|
4
5
|
"30d",
|
|
5
6
|
"year"
|
|
@@ -7,6 +8,7 @@ const defaultDatePresets = [
|
|
|
7
8
|
const datePresetLabels = {
|
|
8
9
|
today: "Today",
|
|
9
10
|
yesterday: "Yesterday",
|
|
11
|
+
"24h": "Last 24 hours",
|
|
10
12
|
"7d": "Last 7 days",
|
|
11
13
|
"30d": "Last 30 days",
|
|
12
14
|
"90d": "Last 90 days",
|
package/dist/format/date.js
CHANGED
|
@@ -79,11 +79,12 @@ const units = [
|
|
|
79
79
|
["hour", 3600],
|
|
80
80
|
["minute", 60]
|
|
81
81
|
];
|
|
82
|
-
/** "5 minutes ago", "yesterday", "3
|
|
82
|
+
/** "5 minutes ago", "yesterday", "in 3 hours"; `seconds` is how long ago it was, negative for a time to come. */
|
|
83
83
|
const formatRelativeTime = (seconds) => {
|
|
84
|
-
const
|
|
85
|
-
|
|
86
|
-
|
|
84
|
+
const away = Math.abs(seconds);
|
|
85
|
+
const [unit, size] = units.find(([, size]) => away >= size) ?? ["second", 1];
|
|
86
|
+
if (unit === "second" && away < 45) return "just now";
|
|
87
|
+
return relative.format(-Math.sign(seconds) * Math.floor(away / size), unit);
|
|
87
88
|
};
|
|
88
89
|
//#endregion
|
|
89
90
|
export { formatAgo, formatDate, formatDateTime, formatDayRange, formatDayShort, formatMonth, formatRelativeTime, formatTime, formatWeekday, formatWeekdayDate };
|
|
@@ -92,6 +92,7 @@ const columnsFromModel = (resource, view, highlight = [], names = listColumnName
|
|
|
92
92
|
...field.type === "enum" && viewField?.valueColors && { colors: viewField.valueColors },
|
|
93
93
|
...field.type === "enum" && viewField?.badgeShape && { shape: viewField.badgeShape },
|
|
94
94
|
...viewField?.suffix !== void 0 && { suffix: viewField.suffix },
|
|
95
|
+
...kind === "datetime" && viewField?.format === "relative" && { relative: true },
|
|
95
96
|
...kind === "money" && viewField?.decimals !== void 0 && { decimals: viewField.decimals },
|
|
96
97
|
...kind === "money" && {
|
|
97
98
|
currency: "EUR",
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { viewerZone, zoneLabel } from "../format/zone.js";
|
|
2
|
-
import { formatDate, formatDateTime } from "../format/date.js";
|
|
2
|
+
import { formatDate, formatDateTime, formatRelativeTime } from "../format/date.js";
|
|
3
3
|
import { fileShown } from "./file-values.js";
|
|
4
4
|
import { enumLabel } from "@protobase/schema";
|
|
5
5
|
//#region src/live/display-value.ts
|
|
@@ -8,13 +8,16 @@ const affixed = (text, hints) => `${hints.prefix ?? ""}${text}${hints.suffix ??
|
|
|
8
8
|
* A read-only rendering of a stored value, following the view's prefix, suffix, decimals, format and value label hints; a
|
|
9
9
|
* timestamp on the clock of `zone`, the viewer's unless given.
|
|
10
10
|
*/
|
|
11
|
-
const displayValue = (field, value, hints = {}, zone = viewerZone()) => {
|
|
11
|
+
const displayValue = (field, value, hints = {}, zone = viewerZone(), now = Date.now()) => {
|
|
12
12
|
if (value === null || value === void 0 || value === "") return "—";
|
|
13
13
|
switch (field.type) {
|
|
14
14
|
case "boolean": return value ? "Yes" : "No";
|
|
15
15
|
case "file": return fileShown(value)?.name ?? "—";
|
|
16
16
|
case "enum": return enumLabel(String(value), hints.valueLabels);
|
|
17
|
-
case "timestamp":
|
|
17
|
+
case "timestamp": {
|
|
18
|
+
const ms = Date.parse(String(value));
|
|
19
|
+
return hints.format === "relative" ? formatRelativeTime(Math.round((now - ms) / 1e3)) : formatDateTime(ms, zone);
|
|
20
|
+
}
|
|
18
21
|
case "date": return formatDate(Date.parse(String(value)));
|
|
19
22
|
case "decimal": {
|
|
20
23
|
const text = hints.decimals === void 0 ? String(value) : Number(value).toFixed(hints.decimals);
|
|
@@ -29,11 +32,13 @@ const displayValue = (field, value, hints = {}, zone = viewerZone()) => {
|
|
|
29
32
|
default: return typeof value === "object" ? JSON.stringify(value) : String(value);
|
|
30
33
|
}
|
|
31
34
|
};
|
|
32
|
-
/** The hover label of a shown value: the time zone a timestamp is shown in, nothing for other values. */
|
|
33
|
-
const valueTitle = (field, value, zone = viewerZone()) => {
|
|
35
|
+
/** The hover label of a shown value: the time zone a timestamp is shown in, or its exact time when shown relative; nothing for other values. */
|
|
36
|
+
const valueTitle = (field, value, zone = viewerZone(), hints = {}) => {
|
|
34
37
|
if (field.type !== "timestamp" || typeof value !== "string") return void 0;
|
|
35
38
|
const ms = Date.parse(value);
|
|
36
|
-
|
|
39
|
+
if (Number.isNaN(ms)) return void 0;
|
|
40
|
+
if (hints.format === "relative") return formatDateTime(ms, zone);
|
|
41
|
+
return `Shown in ${zoneLabel(ms, zone)}`;
|
|
37
42
|
};
|
|
38
43
|
//#endregion
|
|
39
44
|
export { displayValue, valueTitle };
|
package/dist/package.js
CHANGED