@protobase/ui 0.21.6 → 0.21.8

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 (96) hide show
  1. package/dist/app/app.d.ts +3 -3
  2. package/dist/app/app.js +32 -13
  3. package/dist/app/comments/comment-body.js +13 -0
  4. package/dist/app/comments/comment-composer.js +149 -0
  5. package/dist/app/comments/comments-section.js +78 -0
  6. package/dist/app/comments/mention-text.js +45 -0
  7. package/dist/app/comments/use-mentionable.js +18 -0
  8. package/dist/app/comments/use-post-comment.js +15 -0
  9. package/dist/app/comments/use-record-comments.js +24 -0
  10. package/dist/app/create/create-page.js +4 -4
  11. package/dist/app/current-address.d.ts +4 -3
  12. package/dist/app/current-address.js +1 -0
  13. package/dist/app/custom-section.js +1 -1
  14. package/dist/app/displays/board-columns.js +1 -1
  15. package/dist/app/displays/board-display.js +1 -1
  16. package/dist/app/displays/calendar-controls.js +69 -0
  17. package/dist/app/displays/calendar-day.js +33 -0
  18. package/dist/app/displays/calendar-display.js +174 -0
  19. package/dist/app/displays/calendar-dnd.js +27 -0
  20. package/dist/app/displays/calendar-event.js +72 -0
  21. package/dist/app/displays/calendar-range.js +125 -0
  22. package/dist/app/displays/list-displays.js +8 -2
  23. package/dist/app/displays/map-display.js +1 -1
  24. package/dist/app/displays/record-card.js +2 -2
  25. package/dist/app/displays/use-board-column.js +1 -1
  26. package/dist/app/displays/use-board-move.js +1 -1
  27. package/dist/app/displays/use-calendar-move.js +68 -0
  28. package/dist/app/displays/use-calendar-rows.js +35 -0
  29. package/dist/app/displays/use-map-records.js +1 -1
  30. package/dist/app/embedded-list/embedded-list.js +26 -22
  31. package/dist/app/embedded-list/embedded-row-targets.js +65 -0
  32. package/dist/app/embedded-list/use-embedded-rows.js +1 -1
  33. package/dist/app/gallery/gallery-section.js +1 -1
  34. package/dist/app/gallery/lightbox.js +1 -1
  35. package/dist/app/hand-order/hand-sorted-list.js +3 -3
  36. package/dist/app/hand-order/use-hand-order.js +2 -2
  37. package/dist/app/import/import-dialog.js +1 -1
  38. package/dist/app/list-edit/use-list-edits.js +1 -1
  39. package/dist/app/list-page.js +20 -9
  40. package/dist/app/map/map-section.js +1 -1
  41. package/dist/app/pages/action-inputs/action-inputs-dialog.js +3 -3
  42. package/dist/app/pages/blocks/card-row.js +1 -1
  43. package/dist/app/pages/blocks/modal-form.js +3 -3
  44. package/dist/app/pages/blocks/record-card.js +1 -1
  45. package/dist/app/pages/field-value.js +1 -1
  46. package/dist/app/pages/use-run-action.js +9 -7
  47. package/dist/app/record-lines.js +1 -1
  48. package/dist/app/record-page.js +29 -15
  49. package/dist/app/record-row/record-row-body.js +1 -1
  50. package/dist/app/record-row/saved-record-row.js +1 -1
  51. package/dist/app/related-dots/related-dots.js +1 -1
  52. package/dist/app/related-opens.js +22 -0
  53. package/dist/app/related-section.js +5 -2
  54. package/dist/app/relation-picker.js +1 -1
  55. package/dist/app/search/search-model.js +1 -1
  56. package/dist/app/searches/saved-search-rows.js +2 -2
  57. package/dist/app/searches/saved-searches.js +2 -2
  58. package/dist/app/searches/update-search-dialog.js +1 -1
  59. package/dist/app/searches/use-save-search.js +1 -1
  60. package/dist/app/searches/use-saved-searches.js +1 -1
  61. package/dist/app/use-app-address.d.ts +3 -1
  62. package/dist/app/use-app-address.js +9 -10
  63. package/dist/assistant-dock/widget-view.js +1 -1
  64. package/dist/auth/auth-layout.d.ts +4 -2
  65. package/dist/auth/auth-layout.js +4 -3
  66. package/dist/auth/user-to-shell.js +3 -1
  67. package/dist/data/api-provider.d.ts +26 -0
  68. package/dist/data/query-keys.js +18 -1
  69. package/dist/data-grid/row-menu.js +8 -1
  70. package/dist/format/date.js +27 -1
  71. package/dist/forms/prove-human.js +24 -0
  72. package/dist/forms/public-form-page.js +142 -0
  73. package/dist/forms/use-form-address.js +11 -0
  74. package/dist/index.js +7 -7
  75. package/dist/live/columns-from-model.js +1 -1
  76. package/dist/live/use-filter-config.js +1 -1
  77. package/dist/package.js +1 -1
  78. package/dist/plugins/plugin-gate.js +1 -1
  79. package/dist/primitives/segmented.js +8 -3
  80. package/dist/primitives/textarea.d.ts +4 -3
  81. package/dist/primitives/textarea.js +9 -4
  82. package/dist/shell/assets/app-C01lazBK.js +69 -0
  83. package/dist/shell/assets/{export-worker-CVd5saLq.js → export-worker-BnOpS_4Q.js} +1 -1
  84. package/dist/shell/assets/index-CBGigFxb.css +2 -0
  85. package/dist/shell/assets/{index-BztYGbYc.js → index-xN6_oZU2.js} +1 -1
  86. package/dist/shell/assets/{maplibre-CVOEAJ62.js → maplibre-Ba9RapjY.js} +1 -1
  87. package/dist/shell/assets/{markdown-content-Bg8FC_aK.js → markdown-content-BUxrbovf.js} +1 -1
  88. package/dist/shell/assets/{record-map-ChUVZLR6.js → record-map-14zotZ2s.js} +1 -1
  89. package/dist/shell/assets/{records-map-BAI2D9VS.js → records-map-wi-0pXqF.js} +1 -1
  90. package/dist/shell/assets/shared-protobase-ui-BVwhKZk9.js +1 -0
  91. package/dist/shell/index.html +4 -4
  92. package/dist/shell/protobase.shell.json +1 -1
  93. package/package.json +5 -4
  94. package/dist/shell/assets/app-Dfqcc3td.js +0 -69
  95. package/dist/shell/assets/index-C1fzTv_X.css +0 -2
  96. package/dist/shell/assets/shared-protobase-ui-CzT3bJ35.js +0 -1
@@ -1,23 +1,25 @@
1
1
  import { useIsDesktop, useIsXs } from "../../lib/use-media-query.js";
2
2
  import { Skeleton } from "../../primitives/skeleton.js";
3
- import { recordKey } from "../../live/model-helpers.js";
4
3
  import { humanize } from "../../live/naming.js";
5
- import { useProjectUi } from "../pages/project-ui.js";
6
- import { useAdminMeta } from "../meta-gate.js";
7
4
  import { ErrorBanner } from "../error-banner.js";
8
5
  import { listColumnNames } from "../../live/columns-from-model.js";
9
6
  import { displayRow, loadedFields, shownDisplay } from "../displays/list-displays.js";
10
7
  import { defaultOrderBy, parseSort, sortToOrderBy } from "../list-order.js";
8
+ import { useAdminMeta, usePermissions } from "../meta-gate.js";
11
9
  import { AppLink, useAppAddress } from "../use-app-address.js";
10
+ import { useProjectUi } from "../pages/project-ui.js";
11
+ import { can } from "../permissions.js";
12
12
  import { DataGrid } from "../../data-grid/data-grid.js";
13
13
  import { viewAllAddress } from "./embedded-display.js";
14
14
  import { BoardDisplay } from "../displays/board-display.js";
15
+ import { CalendarDisplay } from "../displays/calendar-display.js";
15
16
  import { CardsDisplay } from "../displays/cards-display.js";
16
17
  import { CustomDisplay } from "../displays/custom-display.js";
17
18
  import { MapDisplay } from "../displays/map-display.js";
18
19
  import { DisplaySwitch } from "../displays/display-controls.js";
19
20
  import { embeddedColumns } from "./embedded-columns.js";
20
21
  import { cutOffNote } from "./cut-off-note.js";
22
+ import { embeddedRowTargets } from "./embedded-row-targets.js";
21
23
  import { useEmbeddedRows } from "./use-embedded-rows.js";
22
24
  import { useMemo, useState } from "react";
23
25
  import { ArrowRight } from "lucide-react";
@@ -39,8 +41,9 @@ const Loading = () => /* @__PURE__ */ jsx("div", {
39
41
  * to the whole list with the same filter, order and display, beside a note such as "The first 10 of 37." when there are
40
42
  * more. On a phone a cards display is one row that scrolls sideways.
41
43
  */
42
- const EmbeddedList = ({ model, view, filter, sort, columns, limit, display, empty, row = false, viewAll, link = true }) => {
44
+ const EmbeddedList = ({ model, view, filter, sort, columns, limit, display, empty, row = false, viewAll, link = true, opens }) => {
43
45
  const { resources, views } = useAdminMeta();
46
+ const canUpdate = can(usePermissions(model.name), "update");
44
47
  const { fields: fieldTypes } = useProjectUi();
45
48
  const { go, href } = useAppAddress();
46
49
  const xs = useIsXs();
@@ -122,20 +125,18 @@ const EmbeddedList = ({ model, view, filter, sort, columns, limit, display, empt
122
125
  more: false,
123
126
  loadMore: () => void 0
124
127
  };
125
- const openRecord = (row) => go({
126
- kind: "record",
127
- resource: model.name,
128
- key: recordKey(model, row)
128
+ const targets = embeddedRowTargets({
129
+ model,
130
+ view,
131
+ opens,
132
+ resources,
133
+ views,
134
+ canUpdate
129
135
  });
130
- const recordLink = (row) => ({
131
- href: href({
132
- kind: "record",
133
- resource: model.name,
134
- key: recordKey(model, row)
135
- }),
136
- onFollow: () => openRecord(row)
136
+ const linkTo = (to) => ({
137
+ href: href(to),
138
+ onFollow: () => go(to)
137
139
  });
138
- const singular = view?.names?.singular ?? humanize(model.name);
139
140
  const note = cutOffNote(source.total, rows.data.total, rows.data.estimated);
140
141
  return /* @__PURE__ */ jsxs("div", {
141
142
  className: "flex flex-col gap-3",
@@ -146,12 +147,9 @@ const EmbeddedList = ({ model, view, filter, sort, columns, limit, display, empt
146
147
  latencyMs: 0,
147
148
  sort: parseSort(orderBy),
148
149
  onSortChange: (next) => setOrderBy(sortToOrderBy(next)),
149
- onOpenRow: openRecord,
150
- rowLink: recordLink,
151
- rowActions: [{
152
- label: `Open ${singular.toLowerCase()}`,
153
- onSelect: openRecord
154
- }],
150
+ onOpenRow: (row) => go(targets.address(row)),
151
+ rowLink: (row) => linkTo(targets.address(row)),
152
+ rowActions: targets.actions(linkTo),
155
153
  rowNumbers: false,
156
154
  selectable: false
157
155
  }) : shown.kind === "cards" ? /* @__PURE__ */ jsx(CardsDisplay, {
@@ -177,6 +175,12 @@ const EmbeddedList = ({ model, view, filter, sort, columns, limit, display, empt
177
175
  filter,
178
176
  orderBy,
179
177
  limit
178
+ }) : shown.kind === "calendar" ? /* @__PURE__ */ jsx(CalendarDisplay, {
179
+ model,
180
+ view,
181
+ display: shown,
182
+ fields,
183
+ filter
180
184
  }) : /* @__PURE__ */ jsx(CustomDisplay, {
181
185
  display: shown,
182
186
  list
@@ -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 };
@@ -1,8 +1,8 @@
1
1
  import { keys } from "../../data/query-keys.js";
2
2
  import { useClient } from "../../data/api-provider.js";
3
3
  import { recordId } from "../../live/model-helpers.js";
4
- import { useAdminMeta } from "../meta-gate.js";
5
4
  import { listPhotos, withPhotos } from "../../live/row-photos.js";
5
+ import { useAdminMeta } from "../meta-gate.js";
6
6
  import { listLabels, withLabels } from "../../live/relation-labels.js";
7
7
  import { keepPreviousData, useQuery } from "@tanstack/react-query";
8
8
  //#region src/app/embedded-list/use-embedded-rows.ts
@@ -1,10 +1,10 @@
1
1
  import { cn } from "../../lib/cn.js";
2
2
  import { Skeleton } from "../../primitives/skeleton.js";
3
3
  import { humanize } from "../../live/naming.js";
4
- import { useAdminMeta, usePermissions } from "../meta-gate.js";
5
4
  import { Section } from "../../record-view/section.js";
6
5
  import { shownImage } from "../../live/file-values.js";
7
6
  import { ErrorBanner } from "../error-banner.js";
7
+ import { useAdminMeta, usePermissions } from "../meta-gate.js";
8
8
  import { FileImage } from "../../live/file-image.js";
9
9
  import { galleryTiles } from "./gallery-layout.js";
10
10
  import { Lightbox } from "./lightbox.js";
@@ -1,9 +1,9 @@
1
1
  import { cn } from "../../lib/cn.js";
2
2
  import { useIsDesktop } from "../../lib/use-media-query.js";
3
3
  import { useFocusTrap } from "../../lib/use-focus-trap.js";
4
- import { Sheet } from "../../primitives/sheet.js";
5
4
  import { shownImage } from "../../live/file-values.js";
6
5
  import { FileImage } from "../../live/file-image.js";
6
+ import { Sheet } from "../../primitives/sheet.js";
7
7
  import { keyStep, steppedPhoto, swipeStep, togglesDetails } from "./gallery-layout.js";
8
8
  import { readDetailsOpen, writeDetailsOpen } from "./details-open-storage.js";
9
9
  import { useEffect, useId, useRef, useState } from "react";
@@ -1,11 +1,11 @@
1
1
  import { Skeleton } from "../../primitives/skeleton.js";
2
- import { recordKey, recordTitle } from "../../live/model-helpers.js";
3
2
  import { humanize } from "../../live/naming.js";
4
- import { useProjectUi } from "../pages/project-ui.js";
5
- import { useAdminMeta } from "../meta-gate.js";
6
3
  import { ErrorBanner } from "../error-banner.js";
4
+ import { recordKey, recordTitle } from "../../live/model-helpers.js";
7
5
  import { loadedFields } from "../displays/list-displays.js";
6
+ import { useAdminMeta } from "../meta-gate.js";
8
7
  import { useAppAddress } from "../use-app-address.js";
8
+ import { useProjectUi } from "../pages/project-ui.js";
9
9
  import { DataGrid } from "../../data-grid/data-grid.js";
10
10
  import { embeddedColumns } from "../embedded-list/embedded-columns.js";
11
11
  import { useHandOrder } from "./use-hand-order.js";
@@ -1,9 +1,9 @@
1
1
  import { keys } from "../../data/query-keys.js";
2
2
  import { useClient } from "../../data/api-provider.js";
3
- import { recordId, recordKey } from "../../live/model-helpers.js";
4
3
  import { useToast } from "../../toasts/toast-provider.js";
5
- import { useAdminMeta } from "../meta-gate.js";
4
+ import { recordId, recordKey } from "../../live/model-helpers.js";
6
5
  import { refusalReason } from "../displays/board-columns.js";
6
+ import { useAdminMeta } from "../meta-gate.js";
7
7
  import { useRefreshResource } from "../pages/use-layout-data.js";
8
8
  import { listLabels, withLabels } from "../../live/relation-labels.js";
9
9
  import { useRef, useState } from "react";
@@ -1,7 +1,7 @@
1
1
  import { Button } from "../../primitives/button.js";
2
2
  import { humanize } from "../../live/naming.js";
3
- import { ResponsiveDialog } from "../../primitives/responsive-dialog.js";
4
3
  import { ErrorBanner } from "../error-banner.js";
4
+ import { ResponsiveDialog } from "../../primitives/responsive-dialog.js";
5
5
  import { unmappedRequired } from "./mapping.js";
6
6
  import { ImportCheckStep, ImportChecking } from "./import-check-step.js";
7
7
  import { ImportFileStep } from "./import-file-step.js";
@@ -1,7 +1,7 @@
1
1
  import { keys } from "../../data/query-keys.js";
2
2
  import { useClient } from "../../data/api-provider.js";
3
- import { recordKey } from "../../live/model-helpers.js";
4
3
  import { useToast } from "../../toasts/toast-provider.js";
4
+ import { recordKey } from "../../live/model-helpers.js";
5
5
  import { useRef, useState } from "react";
6
6
  import { isApiError } from "@protobase/client";
7
7
  import { useQueryClient } from "@tanstack/react-query";
@@ -3,17 +3,17 @@ import { usePageScroll } from "../app-shell/page-scroll.js";
3
3
  import { PageHeader } from "../app-shell/page-header.js";
4
4
  import { useAuth } from "../auth/auth-provider.js";
5
5
  import { Button, buttonClass } from "../primitives/button.js";
6
- import { recordKey, recordTitleField } from "../live/model-helpers.js";
7
6
  import { humanize } from "../live/naming.js";
8
- import { useProjectUi } from "./pages/project-ui.js";
9
- import { Notice, useAdminMeta, usePermissions } from "./meta-gate.js";
10
- import { Sheet } from "../primitives/sheet.js";
11
7
  import { ErrorBanner } from "./error-banner.js";
8
+ import { recordKey, recordTitleField } from "../live/model-helpers.js";
12
9
  import { columnsFromModel, listColumnNames } from "../live/columns-from-model.js";
13
10
  import { displayRow, listDisplayChoices, loadedFields, shownListDisplay } from "./displays/list-displays.js";
14
11
  import { parseSort, sortToOrderBy } from "./list-order.js";
15
12
  import { listedResource } from "./current-address.js";
13
+ import { Notice, useAdminMeta, usePermissions } from "./meta-gate.js";
16
14
  import { AppLink, useAppAddress } from "./use-app-address.js";
15
+ import { useProjectUi } from "./pages/project-ui.js";
16
+ import { Sheet } from "../primitives/sheet.js";
17
17
  import { can, rowAllows } from "./permissions.js";
18
18
  import { pinnedStorageKey } from "../data-grid/pinned-storage.js";
19
19
  import { DataGrid } from "../data-grid/data-grid.js";
@@ -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 sortMenu = display && /* @__PURE__ */ jsx(SortMenu, {
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 = display && /* @__PURE__ */ jsx(FilterSection, {
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
@@ -1,6 +1,6 @@
1
- import { useAdminMeta } from "../meta-gate.js";
2
1
  import { Section } from "../../record-view/section.js";
3
2
  import { SidebarCard } from "../../record-view/record-sidebar.js";
3
+ import { useAdminMeta } from "../meta-gate.js";
4
4
  import { recordPosition } from "./record-position.js";
5
5
  import { Suspense, lazy } from "react";
6
6
  import { jsx } from "react/jsx-runtime";
@@ -1,11 +1,11 @@
1
1
  import { Button } from "../../../primitives/button.js";
2
- import { useProjectUi } from "../project-ui.js";
3
- import { useAdminMeta } from "../../meta-gate.js";
4
- import { ResponsiveDialog } from "../../../primitives/responsive-dialog.js";
5
2
  import { Field } from "../../../record-view/field.js";
6
3
  import { isChosen } from "../../create/create-values.js";
7
4
  import { ErrorBanner } from "../../error-banner.js";
5
+ import { useAdminMeta } from "../../meta-gate.js";
6
+ import { useProjectUi } from "../project-ui.js";
8
7
  import { FieldEditor } from "../../field-editor.js";
8
+ import { ResponsiveDialog } from "../../../primitives/responsive-dialog.js";
9
9
  import { answeredValues, inputField, inputHints, refusedAnswers, startingAnswers } from "./input-answers.js";
10
10
  import { inputErrors } from "./input-errors.js";
11
11
  import { useId, useState } from "react";
@@ -1,6 +1,6 @@
1
1
  import { Skeleton } from "../../../primitives/skeleton.js";
2
- import { useAdminMeta } from "../../meta-gate.js";
3
2
  import { ErrorBanner } from "../../error-banner.js";
3
+ import { useAdminMeta } from "../../meta-gate.js";
4
4
  import { RecordScope } from "../record-scope.js";
5
5
  import { scopeOf, useListPage } from "../use-layout-data.js";
6
6
  import { propsOf } from "./block-props.js";
@@ -3,18 +3,18 @@ import { Dialog } from "../../../primitives/dialog.js";
3
3
  import { useClient } from "../../../data/api-provider.js";
4
4
  import { humanize } from "../../../live/naming.js";
5
5
  import { useToast } from "../../../toasts/toast-provider.js";
6
- import { useAdminMeta, usePermissions } from "../../meta-gate.js";
7
6
  import { Field } from "../../../record-view/field.js";
8
7
  import { differs, toDraft, toPatchValue } from "../../../live/field-values.js";
9
8
  import { createBody, isChosen } from "../../create/create-values.js";
10
9
  import { ErrorBanner } from "../../error-banner.js";
10
+ import { useAdminMeta, usePermissions } from "../../meta-gate.js";
11
11
  import { FieldEditor } from "../../field-editor.js";
12
+ import { fieldErrors, fieldMessage } from "../../field-errors.js";
13
+ import { useFocusFirstInvalid } from "../../use-focus-first-invalid.js";
12
14
  import { useLayoutRecord } from "../record-scope.js";
13
15
  import { can } from "../../permissions.js";
14
16
  import { useFirstRecord, useRefreshResource } from "../use-layout-data.js";
15
17
  import { propsOf } from "./block-props.js";
16
- import { fieldErrors, fieldMessage } from "../../field-errors.js";
17
- import { useFocusFirstInvalid } from "../../use-focus-first-invalid.js";
18
18
  import { useState } from "react";
19
19
  import { Fragment as Fragment$1, jsx, jsxs } from "react/jsx-runtime";
20
20
  //#region src/app/pages/blocks/modal-form.tsx
@@ -1,6 +1,6 @@
1
1
  import { Skeleton } from "../../../primitives/skeleton.js";
2
- import { useAdminMeta } from "../../meta-gate.js";
3
2
  import { ErrorBanner } from "../../error-banner.js";
3
+ import { useAdminMeta } from "../../meta-gate.js";
4
4
  import { RecordScope } from "../record-scope.js";
5
5
  import { useFirstRecord } from "../use-layout-data.js";
6
6
  import { propsOf } from "./block-props.js";
@@ -1,10 +1,10 @@
1
1
  import { cn } from "../../lib/cn.js";
2
2
  import { keys } from "../../data/query-keys.js";
3
3
  import { useClient } from "../../data/api-provider.js";
4
- import { useAdminMeta } from "../meta-gate.js";
5
4
  import { fileShown, shownImage } from "../../live/file-values.js";
6
5
  import { isMarkdown } from "../../markdown/markdown-hint.js";
7
6
  import { displayValue } from "../../live/display-value.js";
7
+ import { useAdminMeta } from "../meta-gate.js";
8
8
  import { AppLink } from "../use-app-address.js";
9
9
  import { FileImage } from "../../live/file-image.js";
10
10
  import { useShownWidget } from "../field-types/shown-widget.js";
@@ -1,8 +1,8 @@
1
1
  import { useClient, useResourceClient } from "../../data/api-provider.js";
2
2
  import { useToast } from "../../toasts/toast-provider.js";
3
- import { useProjectUi } from "./project-ui.js";
4
3
  import { useAdminMeta, usePermissions } from "../meta-gate.js";
5
4
  import { useAppAddress } from "../use-app-address.js";
5
+ import { useProjectUi } from "./project-ui.js";
6
6
  import { can } from "../permissions.js";
7
7
  import { actionFor, linkTarget, openLink } from "./action-href.js";
8
8
  import { useRefreshResource } from "./use-layout-data.js";
@@ -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 action's name. An update whose values
16
- * the record holds already is left out, and so is a link to a URL the record holds while it holds none. `blocked` says
17
- * why one cannot run; `afterRun` hears about every action that succeeded. `submit` runs one with the answers to its
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,
@@ -1,6 +1,6 @@
1
- import { usePermissions } from "./meta-gate.js";
2
1
  import { InlineChildTable } from "../inline-child-table/inline-child-table.js";
3
2
  import { Section } from "../record-view/section.js";
3
+ import { usePermissions } from "./meta-gate.js";
4
4
  import { can } from "./permissions.js";
5
5
  import { jsx } from "react/jsx-runtime";
6
6
  //#region src/app/record-lines.tsx
@@ -8,10 +8,7 @@ import { keys } from "../data/query-keys.js";
8
8
  import { useClient } from "../data/api-provider.js";
9
9
  import { useNavigationGuard, useRouter } from "./router.js";
10
10
  import { useOpenedRecord } from "../data/use-record.js";
11
- import { recordTitle } from "../live/model-helpers.js";
12
11
  import { humanize } from "../live/naming.js";
13
- import { useProjectUi } from "./pages/project-ui.js";
14
- import { Notice, useAdminMeta, usePermissions } from "./meta-gate.js";
15
12
  import { Field } from "../record-view/field.js";
16
13
  import { Section } from "../record-view/section.js";
17
14
  import { MetadataCard, RelatedRecords, SidebarCard } from "../record-view/record-sidebar.js";
@@ -23,19 +20,22 @@ import { fieldHelp, helpShown, recordShown, whenShown } from "./record-mode/reco
23
20
  import { isChosen, keyValue } from "./create/create-values.js";
24
21
  import { ErrorBanner } from "./error-banner.js";
25
22
  import { isMarkdown } from "../markdown/markdown-hint.js";
23
+ import { recordTitle } from "../live/model-helpers.js";
26
24
  import { toneFor } from "../live/columns-from-model.js";
27
25
  import { displayValue } from "../live/display-value.js";
28
26
  import { filledFields } from "./filled-fields.js";
27
+ import { Notice, useAdminMeta, usePermissions } from "./meta-gate.js";
29
28
  import { AppLink, useAppAddress } from "./use-app-address.js";
29
+ import { useProjectUi } from "./pages/project-ui.js";
30
30
  import { shownWidget } from "./field-types/shown-widget.js";
31
31
  import { WidgetPart } from "./field-types/widget-part.js";
32
32
  import { Markdown } from "../markdown/markdown.js";
33
33
  import { FieldEditor } from "./field-editor.js";
34
+ import { fieldMessage } from "./field-errors.js";
35
+ import { useFocusFirstInvalid } from "./use-focus-first-invalid.js";
34
36
  import { can, fieldAccess } from "./permissions.js";
35
37
  import { ValueBadge } from "./value-badge.js";
36
38
  import { FieldValue } from "./pages/field-value.js";
37
- import { fieldMessage } from "./field-errors.js";
38
- import { useFocusFirstInvalid } from "./use-focus-first-invalid.js";
39
39
  import { namedIcon } from "./named-icon.js";
40
40
  import { useDeleteFlow } from "./delete/use-delete-flow.js";
41
41
  import { useRestore } from "./delete/use-restore.js";
@@ -49,6 +49,7 @@ import { useLines } from "./use-lines.js";
49
49
  import { useLinesDraft } from "./use-lines-draft.js";
50
50
  import { GallerySection } from "./gallery/gallery-section.js";
51
51
  import { HistorySection } from "./history/history-section.js";
52
+ import { CommentsSection } from "./comments/comments-section.js";
52
53
  import { useRecordEditor } from "./use-record-editor.js";
53
54
  import { useRelatedRecords } from "./use-related-records.js";
54
55
  import { RelatedSection } from "./related-section.js";
@@ -62,21 +63,28 @@ import { enumLabel } from "@protobase/schema";
62
63
  const hidden = (model, name) => name === model.tenant || model.primaryKey.includes(name);
63
64
  /**
64
65
  * The view's layout, or one section with the editable fields when the view has none, then History when the resource
65
- * keeps it. The read view, which is all a user who may only read the resource gets, shows every field: the record rather
66
- * than what can be changed.
66
+ * keeps it and Comments when it takes them. The read view, which is all a user who may only read the resource gets,
67
+ * shows every field: the record rather than what can be changed.
67
68
  */
68
69
  const layoutFor = (model, view, read) => {
69
70
  if (view && view.layout.length > 0) return view.layout;
70
71
  const shown = Object.values(model.fields).filter((field) => !hidden(model, field.name));
71
72
  const editable = shown.filter((field) => !field.readOnly);
72
- return [{
73
- kind: "section",
74
- title: "Details",
75
- fields: (editable.length > 0 && !read ? editable : shown).map((field) => field.name)
76
- }, ...model.history ? [{
77
- kind: "history",
78
- title: "History"
79
- }] : []];
73
+ return [
74
+ {
75
+ kind: "section",
76
+ title: "Details",
77
+ fields: (editable.length > 0 && !read ? editable : shown).map((field) => field.name)
78
+ },
79
+ ...model.history ? [{
80
+ kind: "history",
81
+ title: "History"
82
+ }] : [],
83
+ ...model.comments ? [{
84
+ kind: "comments",
85
+ title: "Comments"
86
+ }] : []
87
+ ];
80
88
  };
81
89
  const RecordPage = ({ resource, recordKey }) => {
82
90
  const { resources } = useAdminMeta();
@@ -424,6 +432,12 @@ const LoadedRecord = ({ model, recordKey, stored, preview }) => {
424
432
  recordKey,
425
433
  etag: stored.etag
426
434
  }, `history:${index}`)];
435
+ if (item.kind === "comments") return [/* @__PURE__ */ jsx(CommentsSection, {
436
+ item,
437
+ resource: model.name,
438
+ recordKey,
439
+ canWrite: auth.kind !== "visitor"
440
+ }, `comments:${index}`)];
427
441
  if (item.kind !== "section") return [];
428
442
  const visibleFields = item.fields.filter((name) => model.fields[name] && visible(name));
429
443
  const facts = shown === "read" && item.presentation === "facts";
@@ -1,10 +1,10 @@
1
1
  import { useIsXs } from "../../lib/use-media-query.js";
2
2
  import { Skeleton } from "../../primitives/skeleton.js";
3
3
  import { humanize } from "../../live/naming.js";
4
- import { useAdminMeta } from "../meta-gate.js";
5
4
  import { ErrorBanner } from "../error-banner.js";
6
5
  import { displaysOf, loadedFields } from "../displays/list-displays.js";
7
6
  import { defaultOrderBy } from "../list-order.js";
7
+ import { useAdminMeta } from "../meta-gate.js";
8
8
  import { AppLink } from "../use-app-address.js";
9
9
  import { useLiveRows } from "../../live/use-live-rows.js";
10
10
  import { viewAllAddress } from "../embedded-list/embedded-display.js";
@@ -1,8 +1,8 @@
1
1
  import { Skeleton } from "../../primitives/skeleton.js";
2
2
  import { humanize } from "../../live/naming.js";
3
- import { useAdminMeta } from "../meta-gate.js";
4
3
  import { ErrorBanner } from "../error-banner.js";
5
4
  import { listedResource } from "../current-address.js";
5
+ import { useAdminMeta } from "../meta-gate.js";
6
6
  import { savedScope } from "../saved/saved-records.js";
7
7
  import { useSavedRecords } from "../saved/use-saved-records.js";
8
8
  import { RecordRowBody } from "./record-row-body.js";
@@ -1,7 +1,7 @@
1
1
  import { cn } from "../../lib/cn.js";
2
2
  import { toneDots } from "../../primitives/badge.js";
3
- import { recordTitleField } from "../../live/model-helpers.js";
4
3
  import { humanize } from "../../live/naming.js";
4
+ import { recordTitleField } from "../../live/model-helpers.js";
5
5
  import { toneFor } from "../../live/columns-from-model.js";
6
6
  import { displayValue } from "../../live/display-value.js";
7
7
  import { displayRow } from "../displays/list-displays.js";
@@ -0,0 +1,22 @@
1
+ //#region src/app/related-opens.ts
2
+ const opensTo = (model, name, resources) => {
3
+ const field = Object.hasOwn(model.fields, name) ? model.fields[name] : void 0;
4
+ return field?.type === "relation" && field.relation !== void 0 && resources[field.relation.resource]?.primaryKey.length === 1;
5
+ };
6
+ /**
7
+ * The relation field whose record a related section's row opens: `item.open`, else the one relation in the listed
8
+ * resource's primary key besides `item.field`, when there is exactly one; none when the rows are records in their own right.
9
+ */
10
+ const relatedOpens = (item, model, resources) => {
11
+ if (item.open !== void 0) return item.open !== item.field && opensTo(model, item.open, resources) ? item.open : void 0;
12
+ const candidates = model.primaryKey.filter((name) => name !== item.field && name !== model.tenant && opensTo(model, name, resources));
13
+ return candidates.length === 1 ? candidates[0] : void 0;
14
+ };
15
+ /** The key of the record a row's relation `name` points at; a list row holds a shown relation as `{ id, name }`. Undefined when empty. */
16
+ const relationTarget = (row, name) => {
17
+ const value = row[name];
18
+ const id = typeof value === "object" && value !== null && "id" in value ? value.id : value;
19
+ return typeof id === "string" || typeof id === "number" ? id : void 0;
20
+ };
21
+ //#endregion
22
+ export { relatedOpens, relationTarget };