@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.
Files changed (29) hide show
  1. package/dist/app/export/cells.js +4 -1
  2. package/dist/app/field-editor.js +1 -1
  3. package/dist/app/hand-order/hand-order.js +1 -1
  4. package/dist/app/incoming-relations.js +1 -1
  5. package/dist/app/pages/field-value.js +1 -1
  6. package/dist/app/record-page.js +11 -7
  7. package/dist/app/related-section.js +11 -6
  8. package/dist/app/related-value.js +14 -0
  9. package/dist/data-grid/cell-text.js +2 -2
  10. package/dist/data-grid/cells.js +2 -1
  11. package/dist/filter-panel/filter-config.js +2 -0
  12. package/dist/format/date.js +5 -4
  13. package/dist/live/columns-from-model.js +1 -0
  14. package/dist/live/display-value.js +11 -6
  15. package/dist/live/filter-string.js +4 -0
  16. package/dist/package.js +1 -1
  17. package/dist/shell/assets/app-3MBLkJAB.js +69 -0
  18. package/dist/shell/assets/export-worker-C1djGgjD.js +2 -0
  19. package/dist/shell/assets/{index-BvrIX29L.js → index-K4WBo5hS.js} +1 -1
  20. package/dist/shell/assets/{maplibre-DEGWdV6B.js → maplibre-CPwgBgXC.js} +1 -1
  21. package/dist/shell/assets/{markdown-content-DdB7hUqJ.js → markdown-content-kdNPOEJy.js} +1 -1
  22. package/dist/shell/assets/{record-map-C_dz1z7c.js → record-map-C57pIfol.js} +1 -1
  23. package/dist/shell/assets/{records-map-ByiKJWnW.js → records-map-B8_M8kjM.js} +1 -1
  24. package/dist/shell/assets/{shared-protobase-ui-Di_9bo0v.js → shared-protobase-ui-BRE7ZUBU.js} +1 -1
  25. package/dist/shell/index.html +3 -3
  26. package/dist/shell/protobase.shell.json +1 -1
  27. package/package.json +4 -4
  28. package/dist/shell/assets/app-xO_xPXCq.js +0 -69
  29. package/dist/shell/assets/export-worker-BoAr3vuF.js +0 -2
@@ -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, hints);
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
  };
@@ -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
  });
@@ -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") return [/* @__PURE__ */ jsx(RelatedSection, {
365
- item,
366
- recordKey,
367
- owner: model.name
368
- }, `related:${item.title}`)];
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, recordKey) => {
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, recordKey)), through.filter) : "";
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, recordKey)), item.filter),
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, recordKey, owner }) => {
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, recordKey);
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, recordKey)),
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));
@@ -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",
@@ -79,11 +79,12 @@ const units = [
79
79
  ["hour", 3600],
80
80
  ["minute", 60]
81
81
  ];
82
- /** "5 minutes ago", "yesterday", "3 years ago"; `seconds` is how long ago it was. */
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 [unit, size] = units.find(([, size]) => seconds >= size) ?? ["second", 1];
85
- if (unit === "second" && seconds < 45) return "just now";
86
- return relative.format(-Math.floor(seconds / size), unit);
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": return formatDateTime(Date.parse(String(value)), zone);
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
- return Number.isNaN(ms) ? void 0 : `Shown in ${zoneLabel(ms, zone)}`;
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 };
@@ -7,6 +7,10 @@ const span = {
7
7
  end: 0
8
8
  };
9
9
  const relative = {
10
+ "24h": {
11
+ amount: 24,
12
+ unit: "h"
13
+ },
10
14
  "7d": {
11
15
  amount: 7,
12
16
  unit: "d"
package/dist/package.js CHANGED
@@ -1,5 +1,5 @@
1
1
  //#region package.json
2
- var version = "0.22.1";
2
+ var version = "0.22.3";
3
3
  var protobase = { "pluginApi": 1 };
4
4
  //#endregion
5
5
  export { protobase, version };