@protobase/ui 0.19.13 → 0.19.15

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 (64) hide show
  1. package/dist/app/create/create-page.js +3 -3
  2. package/dist/app/create/create-values.js +8 -3
  3. package/dist/app/custom-section.js +1 -1
  4. package/dist/app/displays/list-cards.js +1 -1
  5. package/dist/app/displays/map-display.js +1 -1
  6. package/dist/app/displays/record-card.js +2 -2
  7. package/dist/app/displays/record-link.js +1 -1
  8. package/dist/app/embedded-list/embedded-list.js +1 -1
  9. package/dist/app/field-editor.js +6 -6
  10. package/dist/app/gallery/detail-fields.js +4 -3
  11. package/dist/app/gallery/gallery-section.js +1 -1
  12. package/dist/app/gallery/lightbox.js +1 -1
  13. package/dist/app/gallery/photo-details.js +3 -3
  14. package/dist/app/import/import-dialog.js +1 -1
  15. package/dist/app/list-page.js +3 -3
  16. package/dist/app/pages/action-inputs/action-inputs-dialog.d.ts +13 -0
  17. package/dist/app/pages/action-inputs/action-inputs-dialog.js +111 -0
  18. package/dist/app/pages/action-inputs/input-answers.js +88 -0
  19. package/dist/app/pages/action-inputs/input-errors.d.ts +11 -0
  20. package/dist/app/pages/action-inputs/input-errors.js +25 -0
  21. package/dist/app/pages/action-prompt.js +29 -0
  22. package/dist/app/pages/blocks/action.js +8 -4
  23. package/dist/app/pages/blocks/card-row.js +1 -1
  24. package/dist/app/pages/blocks/link.js +1 -1
  25. package/dist/app/pages/blocks/modal-form.js +5 -5
  26. package/dist/app/pages/blocks/record-card.js +1 -1
  27. package/dist/app/pages/blocks/stat.js +1 -1
  28. package/dist/app/pages/field-value.js +2 -2
  29. package/dist/app/pages/project-ui.d.ts +5 -0
  30. package/dist/app/pages/record-actions.js +8 -4
  31. package/dist/app/pages/use-run-action.js +17 -7
  32. package/dist/app/record-lines.js +1 -1
  33. package/dist/app/record-mode/record-mode.js +7 -1
  34. package/dist/app/record-page.js +13 -13
  35. package/dist/app/record-row/record-row-body.js +1 -1
  36. package/dist/app/record-row/saved-record-row.js +1 -1
  37. package/dist/app/related-section.js +1 -1
  38. package/dist/app/searches/saved-search-rows.js +1 -1
  39. package/dist/app/searches/saved-searches.js +2 -2
  40. package/dist/app/searches/search-controls.js +1 -1
  41. package/dist/app/searches/search-name-dialog.js +1 -1
  42. package/dist/app/searches/update-search-dialog.js +1 -1
  43. package/dist/app/searches/use-save-search.js +1 -1
  44. package/dist/app/sensitive-field.js +2 -2
  45. package/dist/app/use-record-editor.js +1 -1
  46. package/dist/auth/sign-in-prompt.js +1 -1
  47. package/dist/data-grid/cells.js +1 -1
  48. package/dist/index.d.ts +3 -1
  49. package/dist/index.js +5 -4
  50. package/dist/live/columns-from-model.js +1 -1
  51. package/dist/markdown/markdown-editor.js +1 -1
  52. package/dist/package.js +1 -1
  53. package/dist/shell/assets/app-WromtADN.js +69 -0
  54. package/dist/shell/assets/{index-ztKNmFFb.js → index-CzV9U_NJ.js} +1 -1
  55. package/dist/shell/assets/{maplibre-CsiuejGw.js → maplibre-G35v6TYU.js} +2 -2
  56. package/dist/shell/assets/{markdown-content-4BktGTHi.js → markdown-content-DGFnggSX.js} +1 -1
  57. package/dist/shell/assets/{record-map-De6U-Z0I.js → record-map-ZAC_U52Y.js} +1 -1
  58. package/dist/shell/assets/{records-map-DaIZwI2y.js → records-map-DLXOQVpM.js} +1 -1
  59. package/dist/shell/assets/shared-protobase-ui-xByMDSPg.js +1 -0
  60. package/dist/shell/index.html +3 -3
  61. package/dist/shell/protobase.shell.json +1 -1
  62. package/package.json +4 -4
  63. package/dist/shell/assets/app-D65L8GHE.js +0 -69
  64. package/dist/shell/assets/shared-protobase-ui-DdZuFJz6.js +0 -1
@@ -2,11 +2,11 @@ import { useClient } from "../../data/api-provider.js";
2
2
  import { recordKey } from "../../live/model-helpers.js";
3
3
  import { humanize } from "../../live/naming.js";
4
4
  import { Notice, useAdminMeta, usePermissions } from "../meta-gate.js";
5
- import { can } from "../permissions.js";
6
- import { useAppAddress } from "../use-app-address.js";
7
- import { fetchLabels } from "../../live/relation-labels.js";
8
5
  import { createBody, createSections, hasDatabaseDefault, initialDraft, isChosen, isRequired } from "./create-values.js";
6
+ import { useAppAddress } from "../use-app-address.js";
9
7
  import { FieldEditor } from "../field-editor.js";
8
+ import { can } from "../permissions.js";
9
+ import { fetchLabels } from "../../live/relation-labels.js";
10
10
  import { useLiveSave } from "../use-live-save.js";
11
11
  import { CreateRecordForm } from "./create-record-form.js";
12
12
  import { useEffect, useState } from "react";
@@ -1,15 +1,20 @@
1
1
  import { isEditable, toPatchValue } from "../../live/field-values.js";
2
+ import { whenShown } from "../record-mode/record-mode.js";
2
3
  //#region src/app/create/create-values.ts
3
4
  const isChosen = (value) => typeof value === "object" && value !== null && "key" in value;
4
5
  /** Fields the user can set on a new record: not read-only, not the tenant, and not structured data. */
5
6
  const isCreatable = (model, field) => field.name !== model.tenant && !field.readOnly && (field.type === "relation" || isEditable(field));
6
- /** The view's sections limited to creatable fields, plus a section for any creatable field the layout leaves out. */
7
+ /**
8
+ * The view's sections limited to creatable fields, plus a section for any creatable field the layout leaves out. The
9
+ * create form is a form, so sections and fields meant only for reading the record stay out of their sections, and a
10
+ * creatable field among them lands in that last section.
11
+ */
7
12
  const createSections = (model, view) => {
8
13
  const creatable = Object.values(model.fields).filter((field) => isCreatable(model, field)).map((field) => field.name);
9
14
  const placed = /* @__PURE__ */ new Set();
10
15
  const sections = (view?.layout ?? []).flatMap((item) => {
11
- if (item.kind !== "section") return [];
12
- const fields = item.fields.filter((name) => creatable.includes(name) && !placed.has(name));
16
+ if (item.kind !== "section" || !whenShown(item.when, true)) return [];
17
+ const fields = item.fields.filter((name) => creatable.includes(name) && !placed.has(name) && whenShown(view?.fields[name]?.when, true));
13
18
  fields.forEach((name) => placed.add(name));
14
19
  return fields.length > 0 ? [{
15
20
  title: item.title,
@@ -1,9 +1,9 @@
1
1
  import { Problem } from "./pages/problem.js";
2
2
  import { CustomBoundary } from "./pages/custom-boundary.js";
3
3
  import { useProjectUi } from "./pages/project-ui.js";
4
- import { RecordScope } from "./pages/record-scope.js";
5
4
  import { Section } from "../record-view/section.js";
6
5
  import { SidebarCard } from "../record-view/record-sidebar.js";
6
+ import { RecordScope } from "./pages/record-scope.js";
7
7
  import { jsx, jsxs } from "react/jsx-runtime";
8
8
  import { unmarkFieldRefs } from "@protobase/schema/internal";
9
9
  //#region src/app/custom-section.tsx
@@ -1,7 +1,7 @@
1
1
  import { Skeleton } from "../../primitives/skeleton.js";
2
2
  import { humanize } from "../../live/naming.js";
3
- import { displayRow } from "./list-displays.js";
4
3
  import { ErrorBanner } from "../error-banner.js";
4
+ import { displayRow } from "./list-displays.js";
5
5
  import { RecordCard } from "./record-card.js";
6
6
  import { VirtualCards } from "./virtual-cards.js";
7
7
  import { jsx, jsxs } from "react/jsx-runtime";
@@ -3,8 +3,8 @@ import { Button } from "../../primitives/button.js";
3
3
  import { Problem } from "../pages/problem.js";
4
4
  import { humanize } from "../../live/naming.js";
5
5
  import { useAdminMeta } from "../meta-gate.js";
6
- import { mapCards } from "./list-displays.js";
7
6
  import { ErrorBanner } from "../error-banner.js";
7
+ import { mapCards } from "./list-displays.js";
8
8
  import { useDebounced } from "../../lib/use-debounced.js";
9
9
  import { RecordCard } from "./record-card.js";
10
10
  import { boundsAround, worldArea } from "../map/map-area.js";
@@ -8,10 +8,10 @@ import { amountParts, cardFields, filledAmount, filledMeta } from "./list-displa
8
8
  import { AppLink } from "../use-app-address.js";
9
9
  import { shownWidget } from "../field-types/shown-widget.js";
10
10
  import { WidgetPart } from "../field-types/widget-part.js";
11
- import { markdownExcerpt } from "../../markdown/markdown-excerpt.js";
11
+ import { Markdown } from "../../markdown/markdown.js";
12
12
  import { isMarkdown } from "../../markdown/markdown-hint.js";
13
+ import { markdownExcerpt } from "../../markdown/markdown-excerpt.js";
13
14
  import { ValueBadge } from "../value-badge.js";
14
- import { Markdown } from "../../markdown/markdown.js";
15
15
  import { namedIcon } from "../named-icon.js";
16
16
  import { FavoriteButton } from "../saved/favorite-button.js";
17
17
  import { CardImage } from "./card-image.js";
@@ -1,5 +1,5 @@
1
- import { useLayoutRecord } from "../pages/record-scope.js";
2
1
  import { AppLink } from "../use-app-address.js";
2
+ import { useLayoutRecord } from "../pages/record-scope.js";
3
3
  import { useOptionalListDisplay } from "./display-context.js";
4
4
  import { jsx } from "react/jsx-runtime";
5
5
  //#region src/app/displays/record-link.tsx
@@ -4,10 +4,10 @@ import { recordKey } from "../../live/model-helpers.js";
4
4
  import { humanize } from "../../live/naming.js";
5
5
  import { useProjectUi } from "../pages/project-ui.js";
6
6
  import { useAdminMeta } from "../meta-gate.js";
7
+ import { ErrorBanner } from "../error-banner.js";
7
8
  import { displayRow, loadedFields, shownDisplay } from "../displays/list-displays.js";
8
9
  import { defaultOrderBy, parseSort, sortToOrderBy } from "../list-order.js";
9
10
  import { AppLink, useAppAddress } from "../use-app-address.js";
10
- import { ErrorBanner } from "../error-banner.js";
11
11
  import { listColumnNames } from "../../live/columns-from-model.js";
12
12
  import { DataGrid } from "../../data-grid/data-grid.js";
13
13
  import { viewAllAddress } from "./embedded-display.js";
@@ -1,19 +1,19 @@
1
1
  import { cn } from "../lib/cn.js";
2
2
  import { Input } from "../primitives/input.js";
3
3
  import { humanize } from "../live/naming.js";
4
- import { displayValue } from "../live/display-value.js";
5
- import { AppLink } from "./use-app-address.js";
6
- import { useShownWidget } from "./field-types/shown-widget.js";
7
- import { WidgetPart } from "./field-types/widget-part.js";
8
- import { isMarkdown } from "../markdown/markdown-hint.js";
9
- import { Markdown } from "../markdown/markdown.js";
10
4
  import { fromDraft, isEditable, toFieldDraft } from "../live/field-values.js";
11
5
  import { Switch } from "../primitives/switch.js";
12
6
  import { Textarea } from "../primitives/textarea.js";
7
+ import { displayValue } from "../live/display-value.js";
8
+ import { AppLink } from "./use-app-address.js";
13
9
  import { RelationPicker } from "./relation-picker.js";
14
10
  import { SensitiveField } from "./sensitive-field.js";
15
11
  import { FileField } from "./file-field.js";
12
+ import { useShownWidget } from "./field-types/shown-widget.js";
13
+ import { WidgetPart } from "./field-types/widget-part.js";
14
+ import { Markdown } from "../markdown/markdown.js";
16
15
  import { MarkdownEditor } from "../markdown/markdown-editor.js";
16
+ import { isMarkdown } from "../markdown/markdown-hint.js";
17
17
  import { jsx, jsxs } from "react/jsx-runtime";
18
18
  import { enumLabel } from "@protobase/schema";
19
19
  //#region src/app/field-editor.tsx
@@ -1,12 +1,13 @@
1
+ import { whenShown } from "../record-mode/record-mode.js";
1
2
  //#region src/app/gallery/detail-fields.ts
2
3
  /**
3
4
  * The fields of a photo's details: those its record page shows in its sections, or without a layout every field but its
4
- * key and tenant, less sensitive ones and those the gallery already shows: `omit`, such as the photo's file and the
5
+ * key and tenant, less sensitive ones, those meant only for its form and those the gallery already shows: `omit`, such as the photo's file and the
5
6
  * field pointing at the gallery's record.
6
7
  */
7
8
  const detailFields = (model, view, omit) => {
8
- const sections = (view?.layout ?? []).flatMap((item) => item.kind === "section" ? item.fields : []);
9
- return (sections.length > 0 ? [...new Set(sections)] : Object.keys(model.fields).filter((name) => name !== model.tenant && !model.primaryKey.includes(name))).filter((name) => model.fields[name] && !model.fields[name].sensitive && !omit.includes(name));
9
+ const sections = (view?.layout ?? []).flatMap((item) => item.kind === "section" && whenShown(item.when, false) ? item.fields : []);
10
+ return (sections.length > 0 ? [...new Set(sections)] : Object.keys(model.fields).filter((name) => name !== model.tenant && !model.primaryKey.includes(name))).filter((name) => model.fields[name] && !model.fields[name].sensitive && !omit.includes(name) && whenShown(view?.fields[name]?.when, false));
10
11
  };
11
12
  //#endregion
12
13
  export { detailFields };
@@ -2,10 +2,10 @@ import { cn } from "../../lib/cn.js";
2
2
  import { Skeleton } from "../../primitives/skeleton.js";
3
3
  import { humanize } from "../../live/naming.js";
4
4
  import { useAdminMeta, usePermissions } from "../meta-gate.js";
5
+ import { Section } from "../../record-view/section.js";
5
6
  import { shownImage } from "../../live/file-values.js";
6
7
  import { ErrorBanner } from "../error-banner.js";
7
8
  import { FileImage } from "../../live/file-image.js";
8
- import { Section } from "../../record-view/section.js";
9
9
  import { galleryTiles } from "./gallery-layout.js";
10
10
  import { Lightbox } from "./lightbox.js";
11
11
  import { detailFields } from "./detail-fields.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";
4
5
  import { shownImage } from "../../live/file-values.js";
5
6
  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
2
  import { useRecord } from "../../data/use-record.js";
3
3
  import { humanize } from "../../live/naming.js";
4
- import { fieldAccess } from "../permissions.js";
5
- import { AppLink } from "../use-app-address.js";
4
+ import { SpecTable } from "../../record-view/spec-table.js";
6
5
  import { ErrorBanner } from "../error-banner.js";
6
+ import { AppLink } from "../use-app-address.js";
7
+ import { fieldAccess } from "../permissions.js";
7
8
  import { FieldValue } from "../pages/field-value.js";
8
- import { SpecTable } from "../../record-view/spec-table.js";
9
9
  import { Fragment, jsx, jsxs } from "react/jsx-runtime";
10
10
  //#region src/app/gallery/photo-details.tsx
11
11
  /**
@@ -1,7 +1,7 @@
1
1
  import { Button } from "../../primitives/button.js";
2
2
  import { humanize } from "../../live/naming.js";
3
- import { ErrorBanner } from "../error-banner.js";
4
3
  import { ResponsiveDialog } from "../../primitives/responsive-dialog.js";
4
+ import { ErrorBanner } from "../error-banner.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";
@@ -7,12 +7,13 @@ import { recordKey, recordTitleField } from "../live/model-helpers.js";
7
7
  import { humanize } from "../live/naming.js";
8
8
  import { useProjectUi } from "./pages/project-ui.js";
9
9
  import { Notice, useAdminMeta, usePermissions } from "./meta-gate.js";
10
- import { can, rowAllows } from "./permissions.js";
10
+ import { Sheet } from "../primitives/sheet.js";
11
+ import { ErrorBanner } from "./error-banner.js";
11
12
  import { loadedFields, shownDisplay } from "./displays/list-displays.js";
12
13
  import { parseSort, sortToOrderBy } from "./list-order.js";
13
14
  import { listedResource } from "./current-address.js";
14
15
  import { AppLink, useAppAddress } from "./use-app-address.js";
15
- import { ErrorBanner } from "./error-banner.js";
16
+ import { can, rowAllows } from "./permissions.js";
16
17
  import { columnsFromModel } from "../live/columns-from-model.js";
17
18
  import { pinnedStorageKey } from "../data-grid/pinned-storage.js";
18
19
  import { DataGrid } from "../data-grid/data-grid.js";
@@ -25,7 +26,6 @@ import { FilterPanel } from "../filter-panel/filter-panel.js";
25
26
  import { stateToText, textToState } from "../live/filter-string.js";
26
27
  import { widgetFields } from "../live/view-fields.js";
27
28
  import { facetNames, filterChips } from "./searches/saved-search.js";
28
- import { Sheet } from "../primitives/sheet.js";
29
29
  import { SignInPrompt } from "../auth/sign-in-prompt.js";
30
30
  import { CustomDisplay } from "./displays/custom-display.js";
31
31
  import { MapDisplay } from "./displays/map-display.js";
@@ -0,0 +1,13 @@
1
+ import { ActionInputModel, JsonValue } from "@protobase/schema";
2
+ //#region src/app/pages/action-inputs/action-inputs-dialog.d.ts
3
+ /** What a custom input's component gets, as a field type's editor does: the answer, a way to set it, whether it is refused, and `props`. */
4
+ export type ActionInputProps<V = unknown, P = Record<string, JsonValue>> = {
5
+ /** The input, as the action declares it. */
6
+ input: ActionInputModel;
7
+ value: V | null;
8
+ onChange: (value: V | null) => void;
9
+ invalid: boolean;
10
+ /** What the input gives in `props`; `{}` without. */
11
+ props: P;
12
+ };
13
+ //#endregion
@@ -0,0 +1,111 @@
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
+ import { Field } from "../../../record-view/field.js";
6
+ import { isChosen } from "../../create/create-values.js";
7
+ import { ErrorBanner } from "../../error-banner.js";
8
+ import { FieldEditor } from "../../field-editor.js";
9
+ import { answeredValues, inputField, inputHints, refusedAnswers, startingAnswers } from "./input-answers.js";
10
+ import { inputErrors } from "./input-errors.js";
11
+ import { useId, useState } from "react";
12
+ import { Fragment as Fragment$1, jsx, jsxs } from "react/jsx-runtime";
13
+ //#region src/app/pages/action-inputs/action-inputs-dialog.tsx
14
+ const InputEditor = ({ input, held, invalid, onChange }) => {
15
+ const { resources, views } = useAdminMeta();
16
+ const { components } = useProjectUi();
17
+ if (input.kind === "custom") {
18
+ const Custom = components?.[input.component ?? ""];
19
+ if (!Custom) throw new Error(`Input "${input.name}": the app has no component "${input.component}"`);
20
+ return /* @__PURE__ */ jsx(Custom, {
21
+ input,
22
+ value: held ?? null,
23
+ onChange,
24
+ invalid,
25
+ props: input.props ?? {}
26
+ });
27
+ }
28
+ const target = input.resource === void 0 ? void 0 : resources[input.resource];
29
+ return /* @__PURE__ */ jsx(FieldEditor, {
30
+ field: inputField(input),
31
+ hints: inputHints(input),
32
+ value: isChosen(held) ? held.key : held,
33
+ stored: void 0,
34
+ relationLabel: isChosen(held) ? held.label : void 0,
35
+ relationPicker: target ? {
36
+ target,
37
+ view: views[target.name]
38
+ } : void 0,
39
+ invalid,
40
+ onChange
41
+ });
42
+ };
43
+ /** The dialog an action with inputs opens: the inputs that show, edited as on the create form, and the action's button. */
44
+ const ActionInputsDialog = ({ action, label, danger, onCancel, onSubmit }) => {
45
+ const [draft, setDraft] = useState(() => startingAnswers(action.inputs));
46
+ const [errors, setErrors] = useState({});
47
+ const [failure, setFailure] = useState();
48
+ const [running, setRunning] = useState(false);
49
+ const formId = useId();
50
+ const { shown, values } = answeredValues(action.inputs, draft);
51
+ const submit = async () => {
52
+ const refused = refusedAnswers(action.inputs, values);
53
+ setErrors(refused);
54
+ setFailure(void 0);
55
+ if (Object.keys(refused).length > 0) return;
56
+ setRunning(true);
57
+ await onSubmit(values).catch((error) => {
58
+ setErrors(inputErrors(error, action.inputs));
59
+ setFailure(error);
60
+ });
61
+ setRunning(false);
62
+ };
63
+ const change = (name, value) => {
64
+ setErrors(({ [name]: _, ...rest }) => rest);
65
+ setDraft((current) => ({
66
+ ...current,
67
+ [name]: value
68
+ }));
69
+ };
70
+ return /* @__PURE__ */ jsx(ResponsiveDialog, {
71
+ title: label,
72
+ description: action.confirm,
73
+ open: true,
74
+ onClose: onCancel,
75
+ actions: /* @__PURE__ */ jsxs(Fragment$1, { children: [/* @__PURE__ */ jsx(Button, {
76
+ onClick: onCancel,
77
+ children: "Cancel"
78
+ }), /* @__PURE__ */ jsx(Button, {
79
+ variant: danger ? "danger" : "primary",
80
+ type: "submit",
81
+ form: formId,
82
+ loading: running,
83
+ children: label
84
+ })] }),
85
+ children: /* @__PURE__ */ jsxs("form", {
86
+ id: formId,
87
+ className: "flex flex-col gap-4",
88
+ onSubmit: (event) => {
89
+ event.preventDefault();
90
+ submit();
91
+ },
92
+ children: [failure !== void 0 && Object.keys(errors).length === 0 && /* @__PURE__ */ jsx(ErrorBanner, {
93
+ title: `${label} failed`,
94
+ error: failure
95
+ }), action.inputs.filter((input) => shown.has(input.name)).map((input) => /* @__PURE__ */ jsx(Field, {
96
+ label: input.label,
97
+ required: input.required,
98
+ help: input.help,
99
+ error: errors[input.name],
100
+ children: /* @__PURE__ */ jsx(InputEditor, {
101
+ input,
102
+ held: draft[input.name],
103
+ invalid: Boolean(errors[input.name]),
104
+ onChange: (next) => change(input.name, next)
105
+ })
106
+ }, input.name))]
107
+ })
108
+ });
109
+ };
110
+ //#endregion
111
+ export { ActionInputsDialog };
@@ -0,0 +1,88 @@
1
+ import { toDraft, toPatchValue } from "../../../live/field-values.js";
2
+ import { isChosen } from "../../create/create-values.js";
3
+ import { shownInputs } from "@protobase/schema/internal";
4
+ //#region src/app/pages/action-inputs/input-answers.ts
5
+ const fieldTypes = {
6
+ text: "text",
7
+ integer: "integer",
8
+ decimal: "decimal",
9
+ boolean: "boolean",
10
+ date: "date"
11
+ };
12
+ /** The field an input is edited as, so the create form's editors edit it: a choice of options is an enum, of records a relation. */
13
+ const inputField = (input) => {
14
+ const base = {
15
+ name: input.name,
16
+ column: input.name,
17
+ nullable: true,
18
+ readOnly: false,
19
+ filterable: false,
20
+ sortable: false,
21
+ aliases: []
22
+ };
23
+ if (input.kind === "custom") return {
24
+ ...base,
25
+ type: "json"
26
+ };
27
+ if (input.kind !== "choice") return {
28
+ ...base,
29
+ type: fieldTypes[input.kind]
30
+ };
31
+ if (input.resource !== void 0) return {
32
+ ...base,
33
+ type: "relation",
34
+ relation: {
35
+ resource: input.resource,
36
+ columns: [input.name]
37
+ }
38
+ };
39
+ return {
40
+ ...base,
41
+ type: "enum",
42
+ enumValues: (input.options ?? []).map((option) => option.value)
43
+ };
44
+ };
45
+ /** The view hints an input's editor reads: its label, a box that grows for a multiline text, a choice's labels. */
46
+ const inputHints = (input) => ({
47
+ label: input.label,
48
+ ...input.multiline && { multiline: true },
49
+ ...input.options && { valueLabels: Object.fromEntries(input.options.map((option) => [option.value, option.label])) }
50
+ });
51
+ /** What each input holds when the dialog opens: its default, as its editor holds it. */
52
+ const startingAnswers = (inputs) => Object.fromEntries(inputs.map((input) => {
53
+ if (input.kind === "custom") return [input.name, input.default ?? null];
54
+ if (input.kind === "boolean") return [input.name, Boolean(input.default)];
55
+ return [input.name, toDraft(input.default)];
56
+ }));
57
+ const answerOf = (input, held) => {
58
+ if (input.kind === "custom") return held ?? null;
59
+ if (isChosen(held)) return held.key;
60
+ if (input.resource !== void 0) return String(held ?? "") === "" ? null : String(held);
61
+ return toPatchValue(inputField(input), held);
62
+ };
63
+ /** The answers of the inputs that show, as the handler gets them in `values`, and the names of those that show. */
64
+ const answeredValues = (inputs, draft) => {
65
+ const all = Object.fromEntries(inputs.map((input) => [input.name, answerOf(input, draft[input.name])]));
66
+ const shown = shownInputs(inputs, all);
67
+ return {
68
+ shown,
69
+ values: Object.fromEntries(Object.entries(all).filter(([name]) => shown.has(name)))
70
+ };
71
+ };
72
+ const decimal = /^-?\d+(\.\d+)?$/;
73
+ const date = /^\d{4}-\d{2}-\d{2}$/;
74
+ const refusal = (input, value) => {
75
+ if (value === null || value === "") return input.required ? "Required" : void 0;
76
+ if (input.kind === "integer" && !Number.isInteger(value)) return "A whole number";
77
+ if (input.kind === "decimal" && !decimal.test(String(value))) return "A number";
78
+ if (input.kind === "date" && !date.test(String(value))) return "A date as YYYY-MM-DD";
79
+ if (input.max !== void 0 && String(value).length > input.max) return `At most ${input.max} characters`;
80
+ };
81
+ /** What is wrong with the answers that show, by input name: one left empty that is required, a number that is none, a text too long. */
82
+ const refusedAnswers = (inputs, values) => Object.fromEntries(inputs.flatMap((input) => {
83
+ if (!Object.hasOwn(values, input.name)) return [];
84
+ const reason = refusal(input, values[input.name]);
85
+ return reason ? [[input.name, reason]] : [];
86
+ }));
87
+ //#endregion
88
+ export { answeredValues, inputField, inputHints, refusedAnswers, startingAnswers };
@@ -0,0 +1,11 @@
1
+ import { ActionInputModel } from "@protobase/schema";
2
+ //#region src/app/pages/action-inputs/input-errors.d.ts
3
+ /**
4
+ * What a handler throws to refuse answers, each message under the input it is about, such as
5
+ * `throw new ActionInputError({ email: 'No user has this address' })`: the dialog stays open and shows each under its input.
6
+ */
7
+ export declare class ActionInputError extends Error {
8
+ readonly errors: Readonly<Record<string, string>>;
9
+ constructor(errors: Readonly<Record<string, string>>);
10
+ }
11
+ //#endregion
@@ -0,0 +1,25 @@
1
+ import { isApiError } from "@protobase/client";
2
+ //#region src/app/pages/action-inputs/input-errors.ts
3
+ /**
4
+ * What a handler throws to refuse answers, each message under the input it is about, such as
5
+ * `throw new ActionInputError({ email: 'No user has this address' })`: the dialog stays open and shows each under its input.
6
+ */
7
+ var ActionInputError = class extends Error {
8
+ errors;
9
+ constructor(errors) {
10
+ super(Object.values(errors).join("; "));
11
+ this.errors = errors;
12
+ this.name = "ActionInputError";
13
+ }
14
+ };
15
+ /**
16
+ * The messages of a failed action about its inputs, by input name: an `ActionInputError`'s, or those of an API error
17
+ * whose field is an input's name, as a function the handler invokes reports them. Empty when none is about an input.
18
+ */
19
+ const inputErrors = (error, inputs) => {
20
+ const names = new Set(inputs.map((input) => input.name));
21
+ const entries = error instanceof ActionInputError ? Object.entries(error.errors) : isApiError(error) ? error.errors.flatMap((entry) => entry.field ? [[entry.field, entry.message]] : []) : [];
22
+ return Object.fromEntries(entries.filter(([name]) => names.has(name)));
23
+ };
24
+ //#endregion
25
+ export { ActionInputError, inputErrors };
@@ -0,0 +1,29 @@
1
+ import { ActionInputsDialog } from "./action-inputs/action-inputs-dialog.js";
2
+ import { ConfirmAction } from "./confirm-action.js";
3
+ import { jsx } from "react/jsx-runtime";
4
+ //#region src/app/pages/action-prompt.tsx
5
+ /** Whether pressing the action's button opens a dialog first: to ask its question, or for its inputs. */
6
+ const asksFirst = (action) => Boolean(action.confirm || action.inputs?.length);
7
+ /** The dialog an action that asks first opens: its inputs, or else its question. */
8
+ const ActionPrompt = ({ action, label, danger, onCancel, onConfirm, onSubmit }) => {
9
+ const { inputs } = action;
10
+ if (inputs?.length) return /* @__PURE__ */ jsx(ActionInputsDialog, {
11
+ action: {
12
+ ...action,
13
+ inputs
14
+ },
15
+ label,
16
+ danger,
17
+ onCancel,
18
+ onSubmit
19
+ });
20
+ return /* @__PURE__ */ jsx(ConfirmAction, {
21
+ action,
22
+ label,
23
+ danger,
24
+ onCancel,
25
+ onConfirm
26
+ });
27
+ };
28
+ //#endregion
29
+ export { ActionPrompt, asksFirst };
@@ -1,12 +1,12 @@
1
1
  import { Button } from "../../../primitives/button.js";
2
- import { ConfirmAction } from "../confirm-action.js";
2
+ import { ActionPrompt, asksFirst } from "../action-prompt.js";
3
3
  import { useLayoutRecord } from "../record-scope.js";
4
4
  import { useRunAction } from "../use-run-action.js";
5
5
  import { propsOf } from "./block-props.js";
6
6
  import { useState } from "react";
7
7
  import { Fragment as Fragment$1, jsx, jsxs } from "react/jsx-runtime";
8
8
  //#region src/app/pages/blocks/action.tsx
9
- /** A button for a named action of the resource's view, asking first when the action has `confirm`. */
9
+ /** A button for a named action of the resource's view, asking first when the action has `confirm` or `inputs`. */
10
10
  const ActionBlock = ({ node }) => {
11
11
  const { name, resource, label, variant } = propsOf(node);
12
12
  const scope = useLayoutRecord();
@@ -14,7 +14,7 @@ const ActionBlock = ({ node }) => {
14
14
  const [asking, setAsking] = useState(false);
15
15
  if (!action.action) return null;
16
16
  const text = label ?? action.action.label;
17
- const start = () => action.action.confirm ? setAsking(true) : void action.run();
17
+ const start = () => asksFirst(action.action) ? setAsking(true) : void action.run();
18
18
  return /* @__PURE__ */ jsxs(Fragment$1, { children: [/* @__PURE__ */ jsx(Button, {
19
19
  size: "sm",
20
20
  variant: variant ?? "secondary",
@@ -23,7 +23,7 @@ const ActionBlock = ({ node }) => {
23
23
  title: action.blocked,
24
24
  onClick: start,
25
25
  children: text
26
- }), asking && /* @__PURE__ */ jsx(ConfirmAction, {
26
+ }), asking && /* @__PURE__ */ jsx(ActionPrompt, {
27
27
  action: action.action,
28
28
  label: text,
29
29
  danger: variant === "danger",
@@ -31,6 +31,10 @@ const ActionBlock = ({ node }) => {
31
31
  onConfirm: () => {
32
32
  setAsking(false);
33
33
  action.run();
34
+ },
35
+ onSubmit: async (values) => {
36
+ await action.submit(values);
37
+ setAsking(false);
34
38
  }
35
39
  })] });
36
40
  };
@@ -1,10 +1,10 @@
1
1
  import { Skeleton } from "../../../primitives/skeleton.js";
2
2
  import { useAdminMeta } from "../../meta-gate.js";
3
+ import { ErrorBanner } from "../../error-banner.js";
3
4
  import { RecordScope } from "../record-scope.js";
4
5
  import { scopeOf, useListPage } from "../use-layout-data.js";
5
6
  import { propsOf } from "./block-props.js";
6
7
  import { CardFrame, Quiet } from "./card-frame.js";
7
- import { ErrorBanner } from "../../error-banner.js";
8
8
  import { jsx } from "react/jsx-runtime";
9
9
  //#region src/app/pages/blocks/card-row.tsx
10
10
  const LoadedCardRow = ({ model, filter, sort, limit = 12, title, description, empty, actions, children }) => {
@@ -1,5 +1,5 @@
1
- import { useLayoutRecord } from "../record-scope.js";
2
1
  import { AppLink } from "../../use-app-address.js";
2
+ import { useLayoutRecord } from "../record-scope.js";
3
3
  import { propsOf } from "./block-props.js";
4
4
  import { jsx } from "react/jsx-runtime";
5
5
  import { appPath } from "@protobase/schema";
@@ -4,15 +4,15 @@ 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
6
  import { useAdminMeta, usePermissions } from "../../meta-gate.js";
7
+ import { Field } from "../../../record-view/field.js";
8
+ import { differs, toDraft, toPatchValue } from "../../../live/field-values.js";
9
+ import { createBody, isChosen } from "../../create/create-values.js";
10
+ import { ErrorBanner } from "../../error-banner.js";
11
+ import { FieldEditor } from "../../field-editor.js";
7
12
  import { useLayoutRecord } from "../record-scope.js";
8
13
  import { can } from "../../permissions.js";
9
14
  import { useFirstRecord, useRefreshResource } from "../use-layout-data.js";
10
15
  import { propsOf } from "./block-props.js";
11
- import { ErrorBanner } from "../../error-banner.js";
12
- import { differs, toDraft, toPatchValue } from "../../../live/field-values.js";
13
- import { Field } from "../../../record-view/field.js";
14
- import { createBody, isChosen } from "../../create/create-values.js";
15
- import { FieldEditor } from "../../field-editor.js";
16
16
  import { useState } from "react";
17
17
  import { isApiError } from "@protobase/client";
18
18
  import { Fragment as Fragment$1, jsx, jsxs } from "react/jsx-runtime";
@@ -1,10 +1,10 @@
1
1
  import { Skeleton } from "../../../primitives/skeleton.js";
2
2
  import { useAdminMeta } from "../../meta-gate.js";
3
+ import { ErrorBanner } from "../../error-banner.js";
3
4
  import { RecordScope } from "../record-scope.js";
4
5
  import { useFirstRecord } from "../use-layout-data.js";
5
6
  import { propsOf } from "./block-props.js";
6
7
  import { CardFrame, Quiet } from "./card-frame.js";
7
- import { ErrorBanner } from "../../error-banner.js";
8
8
  import { jsx, jsxs } from "react/jsx-runtime";
9
9
  //#region src/app/pages/blocks/record-card.tsx
10
10
  const LoadedRecordCard = ({ model, filter, sort, recordKey, title, description, empty, actions, children }) => {
@@ -1,7 +1,7 @@
1
1
  import { Skeleton } from "../../../primitives/skeleton.js";
2
2
  import { useAdminMeta } from "../../meta-gate.js";
3
- import { useLayoutRecord } from "../record-scope.js";
4
3
  import { AppLink } from "../../use-app-address.js";
4
+ import { useLayoutRecord } from "../record-scope.js";
5
5
  import { useCount } from "../use-layout-data.js";
6
6
  import { propsOf } from "./block-props.js";
7
7
  import { CardFrame } from "./card-frame.js";