@questpie/admin 3.5.2 → 3.5.4

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 (195) hide show
  1. package/README.md +8 -0
  2. package/dist/client/blocks/block-renderer.d.mts +2 -2
  3. package/dist/client/builder/index.d.mts +1 -1
  4. package/dist/client/builder/types/collection-types.d.mts +89 -5
  5. package/dist/client/builder/types/common.d.mts +5 -0
  6. package/dist/client/builder/types/field-types.d.mts +41 -1
  7. package/dist/client/builder/view/view.d.mts +3 -2
  8. package/dist/client/components/actions/action-dialog.mjs +5 -0
  9. package/dist/client/components/admin-link.d.mts +2 -2
  10. package/dist/client/components/fields/boolean-field.mjs +2 -1
  11. package/dist/client/components/fields/date-field.mjs +2 -1
  12. package/dist/client/components/fields/datetime-field.mjs +2 -1
  13. package/dist/client/components/fields/email-field.mjs +2 -1
  14. package/dist/client/components/fields/field-utils.d.mts +11 -0
  15. package/dist/client/components/fields/field-utils.mjs +3 -1
  16. package/dist/client/components/fields/field-wrapper.mjs +3 -3
  17. package/dist/client/components/fields/number-field.mjs +2 -1
  18. package/dist/client/components/fields/object-field.mjs +2 -1
  19. package/dist/client/components/fields/relation/displays/types.mjs +3 -3
  20. package/dist/client/components/fields/rich-text-editor/bubble-menu.mjs +7 -0
  21. package/dist/client/components/fields/rich-text-editor/extensions.mjs +19 -2
  22. package/dist/client/components/fields/rich-text-editor/image-popover.mjs +6 -2
  23. package/dist/client/components/fields/rich-text-editor/image-upload.mjs +2 -1
  24. package/dist/client/components/fields/rich-text-editor/index.d.mts +5 -3
  25. package/dist/client/components/fields/rich-text-editor/index.mjs +38 -76
  26. package/dist/client/components/fields/rich-text-editor/slash-commands.mjs +30 -7
  27. package/dist/client/components/fields/rich-text-editor/toolbar.mjs +1 -312
  28. package/dist/client/components/fields/rich-text-editor/types.d.mts +4 -0
  29. package/dist/client/components/fields/rich-text-editor/types.mjs +1 -1
  30. package/dist/client/components/fields/rich-text-editor/utils.mjs +6 -12
  31. package/dist/client/components/fields/select-field.mjs +2 -1
  32. package/dist/client/components/fields/text-field.mjs +2 -1
  33. package/dist/client/components/fields/textarea-field.mjs +2 -1
  34. package/dist/client/components/fields/time-field.mjs +2 -1
  35. package/dist/client/components/filter-builder/filter-builder-sheet.mjs +75 -22
  36. package/dist/client/components/layout/field-layout-renderer.mjs +4 -4
  37. package/dist/client/components/media/media-grid.mjs +2 -1
  38. package/dist/client/components/primitives/asset-preview.mjs +4 -2
  39. package/dist/client/components/primitives/dropzone.d.mts +100 -0
  40. package/dist/client/components/primitives/field-select-control.mjs +2 -1
  41. package/dist/client/components/ui/button.d.mts +23 -0
  42. package/dist/client/components/ui/button.mjs +2 -2
  43. package/dist/client/components/ui/dropdown-menu.d.mts +49 -0
  44. package/dist/client/components/ui/dropdown-menu.mjs +7 -19
  45. package/dist/client/components/ui/popover.mjs +1 -1
  46. package/dist/client/components/ui/search-input.d.mts +56 -0
  47. package/dist/client/components/ui/select.mjs +2 -2
  48. package/dist/client/components/ui/sheet.d.mts +40 -0
  49. package/dist/client/components/ui/table.d.mts +49 -0
  50. package/dist/client/components/ui/table.mjs +15 -1
  51. package/dist/client/components/ui/tooltip.d.mts +21 -0
  52. package/dist/client/contexts/focus-context.d.mts +2 -2
  53. package/dist/client/hooks/query-access.d.mts +9 -0
  54. package/dist/client/hooks/query-access.mjs +20 -0
  55. package/dist/client/hooks/typed-hooks.d.mts +4 -2
  56. package/dist/client/hooks/typed-hooks.mjs +30 -29
  57. package/dist/client/hooks/use-admin-config.mjs +20 -1
  58. package/dist/client/hooks/use-autosave.mjs +91 -0
  59. package/dist/client/hooks/use-collection.mjs +65 -23
  60. package/dist/client/hooks/use-reactive-fields.d.mts +1 -0
  61. package/dist/client/hooks/use-reactive-fields.mjs +16 -1
  62. package/dist/client/hooks/use-server-actions.mjs +12 -1
  63. package/dist/client/hooks/use-upload.d.mts +40 -0
  64. package/dist/client/hooks/use-upload.mjs +4 -2
  65. package/dist/client/hooks/use-view-state.mjs +15 -7
  66. package/dist/client/i18n/hooks.d.mts +20 -0
  67. package/dist/client/lib/utils.d.mts +6 -0
  68. package/dist/client/lib/view-filter-utils.mjs +30 -0
  69. package/dist/client/preview/block-scope-context.d.mts +2 -2
  70. package/dist/client/preview/preview-banner.d.mts +2 -2
  71. package/dist/client/preview/preview-field.d.mts +4 -4
  72. package/dist/client/runtime/provider.mjs +22 -3
  73. package/dist/client/scope/picker.d.mts +2 -2
  74. package/dist/client/scope/provider.d.mts +2 -2
  75. package/dist/client/styles/base.css +75 -79
  76. package/dist/client/utils/asset-url.mjs +27 -0
  77. package/dist/client/utils/build-field-definitions-from-schema.mjs +1 -0
  78. package/dist/client/views/auth/accept-invite-form.d.mts +2 -2
  79. package/dist/client/views/auth/auth-layout.d.mts +3 -3
  80. package/dist/client/views/auth/forgot-password-form.d.mts +2 -2
  81. package/dist/client/views/auth/login-form.d.mts +2 -2
  82. package/dist/client/views/auth/reset-password-form.d.mts +2 -2
  83. package/dist/client/views/auth/setup-form.d.mts +2 -2
  84. package/dist/client/views/collection/auto-form-fields.mjs +7 -6
  85. package/dist/client/views/collection/cells/primitive-cells.mjs +9 -6
  86. package/dist/client/views/collection/cells/shared/asset-thumbnail.d.mts +7 -0
  87. package/dist/client/views/collection/cells/shared/asset-thumbnail.mjs +3 -2
  88. package/dist/client/views/collection/cells/shared/cell-helpers.mjs +3 -2
  89. package/dist/client/views/collection/cells/upload-cells.mjs +2 -1
  90. package/dist/client/views/collection/columns/build-columns.mjs +3 -1
  91. package/dist/client/views/collection/document-view.d.mts +30 -0
  92. package/dist/client/views/collection/document-view.mjs +377 -0
  93. package/dist/client/views/collection/field-context.mjs +3 -2
  94. package/dist/client/views/collection/field-renderer.mjs +13 -5
  95. package/dist/client/views/collection/form-view.mjs +221 -282
  96. package/dist/client/views/collection/list-view.mjs +592 -190
  97. package/dist/client/views/collection/outline.mjs +44 -19
  98. package/dist/client/views/collection/quick-filter-bar.mjs +45 -0
  99. package/dist/client/views/collection/table-view.mjs +61 -17
  100. package/dist/client/views/globals/global-form-view.mjs +12 -9
  101. package/dist/client/views/layout/admin-layout-provider.mjs +4 -3
  102. package/dist/client/views/layout/admin-layout.mjs +108 -21
  103. package/dist/client/views/layout/admin-router.mjs +19 -3
  104. package/dist/client/views/layout/admin-sidebar.mjs +70 -20
  105. package/dist/client/views/layout/admin-theme.mjs +5 -4
  106. package/dist/client/views/layout/admin-view-layout.d.mts +36 -0
  107. package/dist/client/views/pages/accept-invite-page.d.mts +2 -2
  108. package/dist/client/views/pages/dashboard-page.d.mts +2 -2
  109. package/dist/client/views/pages/forgot-password-page.d.mts +2 -2
  110. package/dist/client/views/pages/invite-page.d.mts +2 -2
  111. package/dist/client/views/pages/login-page.d.mts +2 -2
  112. package/dist/client/views/pages/reset-password-page.d.mts +2 -2
  113. package/dist/client/views/pages/setup-page.d.mts +2 -2
  114. package/dist/client.d.mts +17 -2
  115. package/dist/client.mjs +17 -2
  116. package/dist/components/rich-text/rich-text-renderer.d.mts +5 -5
  117. package/dist/components/rich-text/rich-text-renderer.mjs +5 -2
  118. package/dist/factories.d.mts +4 -2
  119. package/dist/factories.mjs +2 -2
  120. package/dist/index.d.mts +17 -3
  121. package/dist/index.mjs +17 -2
  122. package/dist/modules/admin.d.mts +1 -1
  123. package/dist/server/adapters/index.d.mts +2 -0
  124. package/dist/server/adapters/nextjs.d.mts +1 -0
  125. package/dist/server/augmentation/actions.d.mts +9 -3
  126. package/dist/server/augmentation/dashboard.d.mts +11 -11
  127. package/dist/server/augmentation/form-layout.d.mts +16 -6
  128. package/dist/server/augmentation/index.d.mts +7 -0
  129. package/dist/server/augmentation/sidebar.d.mts +8 -8
  130. package/dist/server/augmentation/views.d.mts +4 -1
  131. package/dist/server/auth-helpers.d.mts +1 -0
  132. package/dist/server/codegen/admin-client-template.mjs +7 -6
  133. package/dist/server/fields/blocks.mjs +4 -1
  134. package/dist/server/fields/index.d.mts +1 -1
  135. package/dist/server/fields/reactive-runtime.mjs +3 -0
  136. package/dist/server/fields/rich-text.d.mts +16 -17
  137. package/dist/server/fields/rich-text.mjs +18 -7
  138. package/dist/server/i18n/messages/cs.mjs +2 -0
  139. package/dist/server/i18n/messages/de.mjs +2 -0
  140. package/dist/server/i18n/messages/en.mjs +4 -0
  141. package/dist/server/i18n/messages/es.mjs +2 -0
  142. package/dist/server/i18n/messages/fr.mjs +2 -0
  143. package/dist/server/i18n/messages/pl.mjs +2 -0
  144. package/dist/server/i18n/messages/pt.mjs +2 -0
  145. package/dist/server/i18n/messages/sk.mjs +2 -0
  146. package/dist/server/modules/admin/.generated/module.d.mts +1 -1
  147. package/dist/server/modules/admin/auth-helpers.mjs +7 -1
  148. package/dist/server/modules/admin/block/block-builder.d.mts +0 -8
  149. package/dist/server/modules/admin/block/introspection.d.mts +2 -2
  150. package/dist/server/modules/admin/block/introspection.mjs +28 -4
  151. package/dist/server/modules/admin/block/prefetch.d.mts +11 -0
  152. package/dist/server/modules/admin/block/prefetch.mjs +108 -27
  153. package/dist/server/modules/admin/client/.generated/module.d.mts +68 -67
  154. package/dist/server/modules/admin/client/.generated/module.mjs +2 -0
  155. package/dist/server/modules/admin/client/views/collection-document.d.mts +6 -0
  156. package/dist/server/modules/admin/client/views/collection-document.mjs +10 -0
  157. package/dist/server/modules/admin/collections/account.d.mts +53 -52
  158. package/dist/server/modules/admin/collections/admin-locks.d.mts +57 -56
  159. package/dist/server/modules/admin/collections/admin-preferences.d.mts +38 -37
  160. package/dist/server/modules/admin/collections/admin-saved-views.d.mts +50 -49
  161. package/dist/server/modules/admin/collections/apikey.d.mts +76 -67
  162. package/dist/server/modules/admin/collections/assets.d.mts +37 -36
  163. package/dist/server/modules/admin/collections/session.d.mts +42 -41
  164. package/dist/server/modules/admin/collections/user.d.mts +57 -56
  165. package/dist/server/modules/admin/collections/verification.d.mts +34 -33
  166. package/dist/server/modules/admin/dto/admin-config.dto.mjs +34 -4
  167. package/dist/server/modules/admin/factories.mjs +4 -34
  168. package/dist/server/modules/admin/index.d.mts +3 -3
  169. package/dist/server/modules/admin/routes/admin-config.d.mts +4 -2
  170. package/dist/server/modules/admin/routes/admin-config.mjs +56 -24
  171. package/dist/server/modules/admin/routes/execute-action.d.mts +9 -9
  172. package/dist/server/modules/admin/routes/execute-action.mjs +35 -9
  173. package/dist/server/modules/admin/routes/locales.mjs +1 -1
  174. package/dist/server/modules/admin/routes/preview.d.mts +11 -11
  175. package/dist/server/modules/admin/routes/preview.mjs +6 -5
  176. package/dist/server/modules/admin/routes/reactive.d.mts +9 -9
  177. package/dist/server/modules/admin/routes/reactive.mjs +2 -2
  178. package/dist/server/modules/admin/routes/route-helpers.d.mts +11 -7
  179. package/dist/server/modules/admin/routes/route-helpers.mjs +1 -1
  180. package/dist/server/modules/admin/routes/setup.d.mts +7 -7
  181. package/dist/server/modules/admin/routes/translations.d.mts +4 -4
  182. package/dist/server/modules/admin/routes/widget-data.d.mts +5 -5
  183. package/dist/server/modules/admin/routes/widget-data.mjs +12 -4
  184. package/dist/server/modules/admin-preferences/collections/saved-views.d.mts +45 -45
  185. package/dist/server/modules/audit/.generated/module.d.mts +6 -6
  186. package/dist/server/modules/audit/collections/audit-log.d.mts +81 -80
  187. package/dist/server/modules/audit/jobs/audit-cleanup.d.mts +2 -2
  188. package/dist/server/plugin.mjs +10 -5
  189. package/dist/server/proxy-factories.d.mts +8 -1
  190. package/dist/server/proxy-factories.mjs +33 -1
  191. package/dist/server.d.mts +3 -1
  192. package/dist/shared/types/index.d.mts +1 -0
  193. package/dist/shared/types/saved-views.types.d.mts +14 -7
  194. package/dist/shared.d.mts +3 -2
  195. package/package.json +5 -4
@@ -0,0 +1,377 @@
1
+ import { useResolveText, useTranslation } from "../../i18n/hooks.mjs";
2
+ import { resolveIconElement } from "../../components/component-renderer.mjs";
3
+ import { Button } from "../../components/ui/button.mjs";
4
+ import { Separator } from "../../components/ui/separator.mjs";
5
+ import { Badge } from "../../components/ui/badge.mjs";
6
+ import { adminCollectionKey } from "../../hooks/query-access.mjs";
7
+ import { useCollectionFields } from "../../hooks/use-collection-fields.mjs";
8
+ import { FieldRenderer } from "./field-renderer.mjs";
9
+ import { EmptyState } from "../../components/ui/empty-state.mjs";
10
+ import { useCollectionValidation } from "../../hooks/use-collection-validation.mjs";
11
+ import { usePreferServerValidation } from "../../hooks/use-server-validation.mjs";
12
+ import { useAutosave } from "../../hooks/use-autosave.mjs";
13
+ import { useCollectionItem, useCollectionUpdate } from "../../hooks/use-collection.mjs";
14
+ import { AdminViewHeader, AdminViewLayout } from "../layout/admin-view-layout.mjs";
15
+ import { FormViewSkeleton } from "./view-skeletons.mjs";
16
+ import { Icon } from "@iconify/react";
17
+ import * as React from "react";
18
+ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
19
+ import { FormProvider, useForm, useFormContext, useFormState } from "react-hook-form";
20
+ import { toast } from "sonner";
21
+
22
+ //#region src/client/views/collection/document-view.tsx
23
+ /**
24
+ * Document View — Notion-style document page
25
+ *
26
+ * Renders a collection record as a centered page: an optional title, a compact
27
+ * block of inline-editable property rows, a divider, and a dominant long-form
28
+ * body field rendered with the rich-text editor.
29
+ *
30
+ * Drop-in with the form view: takes the same props (`CollectionFormViewProps`).
31
+ * Layout and which fields are body/title/properties are declared via
32
+ * `viewConfig.document` (the `DocumentViewConfig` contract) — no hardcoded field
33
+ * names. Save is configurable: `"autosave"` (debounced, shared `useAutosave`
34
+ * pipeline) or `"manual"` (a Save affordance with dirty tracking).
35
+ */
36
+ /**
37
+ * Presentation-only default icons per field type, used for property rows when a
38
+ * field carries no explicit icon. Falls back to a generic tag icon.
39
+ */
40
+ const FIELD_TYPE_ICONS = {
41
+ text: "ph:text-aa",
42
+ textarea: "ph:text-align-left",
43
+ richText: "ph:article",
44
+ number: "ph:hash",
45
+ boolean: "ph:toggle-left",
46
+ select: "ph:list-checks",
47
+ relation: "ph:link-simple",
48
+ date: "ph:calendar-blank",
49
+ datetime: "ph:calendar-blank",
50
+ upload: "ph:paperclip",
51
+ json: "ph:brackets-curly",
52
+ color: "ph:palette",
53
+ email: "ph:envelope-simple",
54
+ url: "ph:globe-simple"
55
+ };
56
+ const DEFAULT_PROPERTY_ICON = "ph:tag";
57
+ /**
58
+ * A single Notion-style property row: icon + label + the field's inline editor.
59
+ */
60
+ const DocumentPropertyRow = React.memo(function DocumentPropertyRow$1({ collection, fieldName, fieldDef, icon, label, registry, allCollectionsConfig }) {
61
+ return /* @__PURE__ */ jsxs("div", {
62
+ className: "qa-document-view__property hover:bg-muted/40 flex items-start gap-3 rounded-md px-2 py-1.5 transition-colors duration-150",
63
+ children: [/* @__PURE__ */ jsxs("div", {
64
+ className: "text-muted-foreground flex w-32 shrink-0 items-center gap-2 pt-2",
65
+ children: [/* @__PURE__ */ jsx("span", {
66
+ className: "flex size-3.5 shrink-0 items-center justify-center",
67
+ children: icon
68
+ }), /* @__PURE__ */ jsx("span", {
69
+ className: "truncate text-xs",
70
+ children: label
71
+ })]
72
+ }), /* @__PURE__ */ jsx("div", {
73
+ className: "qa-document-view__property-control min-w-0 flex-1",
74
+ children: /* @__PURE__ */ jsx(FieldRenderer, {
75
+ fieldName,
76
+ fieldDef,
77
+ collection,
78
+ registry,
79
+ allCollectionsConfig,
80
+ hideLabel: true
81
+ })
82
+ })]
83
+ });
84
+ });
85
+ const DocumentSaveButton = React.memo(function DocumentSaveButton$1({ isPending }) {
86
+ const { t } = useTranslation();
87
+ const { isDirty, isSubmitting } = useFormState();
88
+ const busy = isPending || isSubmitting;
89
+ return /* @__PURE__ */ jsx(Button, {
90
+ type: "submit",
91
+ size: "sm",
92
+ disabled: busy || !isDirty,
93
+ className: "gap-2",
94
+ children: busy ? /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx(Icon, {
95
+ icon: "ph:spinner-gap",
96
+ className: "size-4 animate-spin"
97
+ }), t("common.loading")] }) : /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx(Icon, {
98
+ icon: "ph:check",
99
+ width: 16,
100
+ height: 16
101
+ }), t("common.save")] })
102
+ });
103
+ });
104
+ const DocumentAutosaveIndicator = React.memo(function DocumentAutosaveIndicator$1({ isSaving, lastSaved }) {
105
+ const { t } = useTranslation();
106
+ const { isDirty } = useFormState();
107
+ const [, forceUpdate] = React.useReducer((x) => x + 1, 0);
108
+ React.useEffect(() => {
109
+ if (!lastSaved) return;
110
+ const interval = setInterval(forceUpdate, 1e4);
111
+ return () => clearInterval(interval);
112
+ }, [lastSaved]);
113
+ const formatTimeAgo = (date) => {
114
+ const seconds = Math.floor((Date.now() - date.getTime()) / 1e3);
115
+ if (seconds < 10) return t("autosave.justNow");
116
+ if (seconds < 60) return t("autosave.secondsAgo", { count: seconds });
117
+ const minutes = Math.floor(seconds / 60);
118
+ if (minutes < 60) return t("autosave.minutesAgo", { count: minutes });
119
+ return t("autosave.hoursAgo", { count: Math.floor(minutes / 60) });
120
+ };
121
+ if (isSaving) return /* @__PURE__ */ jsxs(Badge, {
122
+ variant: "secondary",
123
+ className: "gap-1.5",
124
+ children: [/* @__PURE__ */ jsx(Icon, {
125
+ icon: "ph:spinner-gap",
126
+ className: "size-3 animate-spin"
127
+ }), t("autosave.saving")]
128
+ });
129
+ if (isDirty) return /* @__PURE__ */ jsxs(Badge, {
130
+ variant: "outline",
131
+ className: "gap-1.5",
132
+ children: [/* @__PURE__ */ jsx(Icon, {
133
+ icon: "ph:clock-counter-clockwise",
134
+ className: "size-3"
135
+ }), t("autosave.unsavedChanges")]
136
+ });
137
+ if (lastSaved) return /* @__PURE__ */ jsxs(Badge, {
138
+ variant: "secondary",
139
+ className: "text-muted-foreground gap-1.5",
140
+ children: [
141
+ /* @__PURE__ */ jsx(Icon, {
142
+ icon: "ph:check",
143
+ className: "size-3"
144
+ }),
145
+ t("autosave.saved"),
146
+ " ",
147
+ formatTimeAgo(lastSaved)
148
+ ]
149
+ });
150
+ return null;
151
+ });
152
+ /**
153
+ * Resolve the ordered list of property field names.
154
+ * - `"auto"` → every schema field except the body and title, in schema order.
155
+ * - `string[]` → the explicit list (filtered to fields that exist).
156
+ */
157
+ function resolvePropertyNames(properties, resolvedFields, body, title) {
158
+ const allNames = Object.keys(resolvedFields);
159
+ if (Array.isArray(properties)) return properties.filter((name) => allNames.includes(name));
160
+ return allNames.filter((name) => name !== body && name !== title);
161
+ }
162
+ const DocumentForm = React.memo(function DocumentForm$1({ collection, documentConfig, propertyNames, resolvedFields, schema, registry, allCollectionsConfig, title, saveMode, isSaving, lastSaved, isMutationPending }) {
163
+ const resolveText = useResolveText();
164
+ const form = useFormContext();
165
+ const propertyRows = React.useMemo(() => {
166
+ const schemaFields = schema?.fields ?? {};
167
+ return propertyNames.map((name) => {
168
+ const fieldDef = resolvedFields[name];
169
+ if (!fieldDef) return null;
170
+ const meta = schemaFields[name]?.metadata;
171
+ const fieldType = fieldDef.name;
172
+ return {
173
+ name,
174
+ fieldDef,
175
+ label: resolveText(meta?.label ?? name, name, form.getValues()),
176
+ icon: (meta?.icon ? resolveIconElement(meta.icon, { className: "size-3.5" }) : null) ?? /* @__PURE__ */ jsx(Icon, {
177
+ icon: FIELD_TYPE_ICONS[fieldType] ?? DEFAULT_PROPERTY_ICON,
178
+ className: "size-3.5"
179
+ })
180
+ };
181
+ }).filter(Boolean);
182
+ }, [
183
+ propertyNames,
184
+ resolvedFields,
185
+ schema?.fields,
186
+ resolveText,
187
+ form
188
+ ]);
189
+ const bodyFieldDef = resolvedFields[documentConfig.body];
190
+ return /* @__PURE__ */ jsxs("div", {
191
+ className: "qa-document-view mx-auto max-w-3xl px-6 py-8",
192
+ children: [
193
+ /* @__PURE__ */ jsx(AdminViewHeader, {
194
+ className: "mb-6",
195
+ title,
196
+ titleAccessory: saveMode === "autosave" ? /* @__PURE__ */ jsx(DocumentAutosaveIndicator, {
197
+ isSaving,
198
+ lastSaved
199
+ }) : /* @__PURE__ */ jsx(DocumentSaveButton, { isPending: isMutationPending })
200
+ }),
201
+ propertyRows.length > 0 && /* @__PURE__ */ jsx("div", {
202
+ className: "qa-document-view__properties mb-6 space-y-px",
203
+ children: propertyRows.map((row) => /* @__PURE__ */ jsx(DocumentPropertyRow, {
204
+ collection,
205
+ fieldName: row.name,
206
+ fieldDef: row.fieldDef,
207
+ icon: row.icon,
208
+ label: row.label,
209
+ registry,
210
+ allCollectionsConfig
211
+ }, row.name))
212
+ }),
213
+ /* @__PURE__ */ jsx(Separator, { className: "mb-8" }),
214
+ /* @__PURE__ */ jsx("div", {
215
+ className: "qa-document-view__body min-w-0",
216
+ children: bodyFieldDef ? /* @__PURE__ */ jsx(FieldRenderer, {
217
+ fieldName: documentConfig.body,
218
+ fieldDef: bodyFieldDef,
219
+ collection,
220
+ registry,
221
+ allCollectionsConfig,
222
+ hideLabel: true
223
+ }) : /* @__PURE__ */ jsx(EmptyState, {
224
+ iconName: "ph:warning",
225
+ title: `Document body field "${documentConfig.body}" not found.`
226
+ })
227
+ })
228
+ ]
229
+ });
230
+ });
231
+ /**
232
+ * The form-bearing inner component. The outer `DocumentView` gates on loading
233
+ * and mounts this with `initialValues` already populated, so `useForm` — and
234
+ * therefore the TipTap body editor (`content: value`) — initializes WITH the
235
+ * stored content on its very first render. This is the fix for the empty-body /
236
+ * spurious "unsaved changes" bug: we never mount the editor empty and then try
237
+ * to re-sync a value-prop change into ProseMirror.
238
+ *
239
+ * Remounted (via `key`) by the outer when the record identity changes, so each
240
+ * record gets a fresh form seeded with its own values.
241
+ */
242
+ function DocumentEditor({ collection, id, documentConfig, propertyNames, resolvedFields, schema, registry, allCollectionsConfig, resolver, updateMutation, initialValues, title, autosaveDebounce }) {
243
+ const { t } = useTranslation();
244
+ const saveMode = documentConfig.save === "manual" ? "manual" : "autosave";
245
+ const form = useForm({
246
+ defaultValues: initialValues,
247
+ resolver,
248
+ mode: "onBlur"
249
+ });
250
+ const formIsDirtyRef = React.useRef(false);
251
+ const formIsSubmittingRef = React.useRef(false);
252
+ const { isDirty: formIsDirty, isSubmitting: formIsSubmitting } = useFormState({ control: form.control });
253
+ React.useEffect(() => {
254
+ formIsDirtyRef.current = formIsDirty;
255
+ }, [formIsDirty]);
256
+ React.useEffect(() => {
257
+ formIsSubmittingRef.current = formIsSubmitting;
258
+ }, [formIsSubmitting]);
259
+ const [isSaving, setIsSaving] = React.useState(false);
260
+ const [lastSaved, setLastSaved] = React.useState(null);
261
+ useAutosave({
262
+ form,
263
+ id,
264
+ enabled: saveMode === "autosave",
265
+ debounce: autosaveDebounce,
266
+ isDirtyRef: formIsDirtyRef,
267
+ isSubmittingRef: formIsSubmittingRef,
268
+ updateMutation,
269
+ onSavingChange: setIsSaving,
270
+ onSaved: setLastSaved
271
+ });
272
+ const onManualSubmit = form.handleSubmit(async (data) => {
273
+ if (!id) return;
274
+ try {
275
+ const result = await updateMutation.mutateAsync({
276
+ id,
277
+ data
278
+ });
279
+ form.reset(result, { keepTouched: true });
280
+ setLastSaved(/* @__PURE__ */ new Date());
281
+ toast.success(t("toast.saveSuccess"));
282
+ } catch (error) {
283
+ toast.error(t("toast.saveFailed"), { description: error instanceof Error ? error.message : void 0 });
284
+ }
285
+ });
286
+ return /* @__PURE__ */ jsx(FormProvider, {
287
+ ...form,
288
+ children: /* @__PURE__ */ jsx("form", {
289
+ onSubmit: (e) => {
290
+ e.stopPropagation();
291
+ if (saveMode === "manual") onManualSubmit(e);
292
+ else e.preventDefault();
293
+ },
294
+ children: /* @__PURE__ */ jsx(DocumentForm, {
295
+ collection,
296
+ documentConfig,
297
+ propertyNames,
298
+ resolvedFields,
299
+ schema,
300
+ registry,
301
+ allCollectionsConfig,
302
+ title,
303
+ saveMode,
304
+ isSaving,
305
+ lastSaved,
306
+ isMutationPending: updateMutation.isPending
307
+ })
308
+ })
309
+ });
310
+ }
311
+ function DocumentView({ collection, id, config, viewConfig, defaultValues: defaultValuesProp, registry, allCollectionsConfig, title: titleProp }) {
312
+ const { t } = useTranslation();
313
+ const isEditMode = !!id;
314
+ const collectionKey = adminCollectionKey(collection);
315
+ const { fields: resolvedFields, schema, isLoading: isFieldsLoading } = useCollectionFields(collection, { fallbackFields: config?.fields });
316
+ const documentConfig = React.useMemo(() => viewConfig?.document ?? (schema?.admin?.form)?.document ?? config?.form?.document, [
317
+ viewConfig,
318
+ schema?.admin?.form,
319
+ config
320
+ ]);
321
+ const propertyNames = React.useMemo(() => documentConfig ? resolvePropertyNames(documentConfig.properties, resolvedFields, documentConfig.body, documentConfig.title) : [], [documentConfig, resolvedFields]);
322
+ const { data: item, isLoading, error: itemError } = useCollectionItem(collectionKey, id ?? "", { localeFallback: false }, { enabled: isEditMode });
323
+ const updateMutation = useCollectionUpdate(collectionKey);
324
+ const hasServerValidationSchema = !!schema?.validation?.update;
325
+ const clientResolver = useCollectionValidation(collection, { enabled: !isFieldsLoading && !hasServerValidationSchema });
326
+ const resolver = usePreferServerValidation(collection, {
327
+ mode: "update",
328
+ schema
329
+ }, clientResolver);
330
+ const autosaveDebounce = config?.autoSave?.debounce ?? 300;
331
+ if (!documentConfig) return /* @__PURE__ */ jsx(AdminViewLayout, {
332
+ contentClassName: "overflow-y-auto",
333
+ children: /* @__PURE__ */ jsx("div", {
334
+ className: "mx-auto max-w-3xl px-6 py-8",
335
+ children: /* @__PURE__ */ jsx(EmptyState, {
336
+ iconName: "ph:warning",
337
+ title: "No document configuration",
338
+ description: "This view has kind 'document' but no document config (body/properties). Configure it via view(..., { kind: 'document', document: { body, properties } })."
339
+ })
340
+ })
341
+ });
342
+ if (isEditMode && itemError) return /* @__PURE__ */ jsx(AdminViewLayout, {
343
+ contentClassName: "overflow-y-auto",
344
+ children: /* @__PURE__ */ jsx("div", {
345
+ className: "mx-auto max-w-3xl px-6 py-8",
346
+ children: /* @__PURE__ */ jsx(EmptyState, {
347
+ iconName: "ph:warning",
348
+ title: t("error.failedToLoad"),
349
+ description: itemError instanceof Error ? itemError.message : void 0
350
+ })
351
+ })
352
+ });
353
+ if (isEditMode && (isLoading || !item) || isFieldsLoading) return /* @__PURE__ */ jsx(FormViewSkeleton, {});
354
+ const initialValues = isEditMode ? item : defaultValuesProp ?? {};
355
+ const titleFieldValue = documentConfig.title ? initialValues[documentConfig.title] : void 0;
356
+ return /* @__PURE__ */ jsx(AdminViewLayout, {
357
+ contentClassName: "overflow-y-auto",
358
+ children: /* @__PURE__ */ jsx(DocumentEditor, {
359
+ collection,
360
+ id,
361
+ documentConfig,
362
+ propertyNames,
363
+ resolvedFields,
364
+ schema,
365
+ registry,
366
+ allCollectionsConfig,
367
+ resolver,
368
+ updateMutation,
369
+ initialValues,
370
+ title: (titleFieldValue && titleFieldValue.trim() ? titleFieldValue : void 0) ?? titleProp ?? initialValues.id ?? collection,
371
+ autosaveDebounce
372
+ }, id ?? "new")
373
+ });
374
+ }
375
+
376
+ //#endregion
377
+ export { DocumentView as default };
@@ -105,7 +105,7 @@ function getFieldContext({ fieldName, fieldDef, collection, form, fieldPrefix, l
105
105
  * Build props for a FieldInstance component (field.component).
106
106
  * Returns raw props with I18nText - resolve before passing to component.
107
107
  */
108
- function buildComponentProps(context) {
108
+ function buildComponentProps(context, overrides) {
109
109
  return {
110
110
  name: context.fullFieldName,
111
111
  value: context.fieldValue,
@@ -118,7 +118,8 @@ function buildComponentProps(context) {
118
118
  readOnly: context.isReadOnly,
119
119
  error: context.fieldError,
120
120
  localized: context.isLocalized,
121
- locale: context.locale
121
+ locale: context.locale,
122
+ hideLabel: overrides?.hideLabel
122
123
  };
123
124
  }
124
125
 
@@ -6,6 +6,7 @@ import { Skeleton } from "../../components/ui/skeleton.mjs";
6
6
  import { scopeDependencies, trackDependencies } from "../../utils/dependency-tracker.mjs";
7
7
  import { buildComponentProps, getFieldContext, getFieldOptions, getFullFieldName } from "./field-context.mjs";
8
8
  import { useFieldHooks } from "../../hooks/use-field-hooks.mjs";
9
+ import { mergeReactiveFieldState, useReactiveFieldState } from "../../hooks/use-reactive-fields.mjs";
9
10
  import { useReactiveProps } from "../../hooks/use-reactive-prop.mjs";
10
11
  import * as React from "react";
11
12
  import { Fragment, jsx, jsxs } from "react/jsx-runtime";
@@ -137,7 +138,7 @@ function renderEmbeddedField({ context, registry, allCollectionsConfig, componen
137
138
  * 2. FieldDefinition.field.component (registry-first approach)
138
139
  * 3. Error message if no component registered
139
140
  */
140
- function FieldRenderer({ fieldName, fieldDef, collection, mode = "collection", registry, fieldPrefix, allCollectionsConfig, renderEmbeddedFields, className, entityMeta: entityMetaProp, extraProps }) {
141
+ function FieldRenderer({ fieldName, fieldDef, collection, mode = "collection", registry, fieldPrefix, allCollectionsConfig, renderEmbeddedFields, className, entityMeta: entityMetaProp, extraProps, hideLabel }) {
141
142
  const form = useFormContext();
142
143
  const { locale } = useScopedLocale();
143
144
  const resolveText = useResolveText();
@@ -178,6 +179,12 @@ function FieldRenderer({ fieldName, fieldDef, collection, mode = "collection", r
178
179
  entityMeta: entityMetaProp,
179
180
  formValues
180
181
  });
182
+ const reactiveFieldState = useReactiveFieldState(fullFieldName);
183
+ const effectiveFieldState = mergeReactiveFieldState({
184
+ hidden: context.isHidden,
185
+ readOnly: context.isReadOnly,
186
+ disabled: context.isDisabled
187
+ }, reactiveFieldState);
181
188
  const { Component: resolvedComponent, loading: componentLoading } = useLazyComponent(context.component);
182
189
  const clientSideCompute = typeof fieldOptions.compute === "function" ? fieldOptions.compute : void 0;
183
190
  const { handleChange, computedValue, isComputed, options: hookOptions, optionsLoading } = useFieldHooks({
@@ -191,7 +198,7 @@ function FieldRenderer({ fieldName, fieldDef, collection, mode = "collection", r
191
198
  staticOptions: context.options
192
199
  });
193
200
  const resolvedOptions = hookOptions ?? context.options;
194
- const rawComponentProps = buildComponentProps(context);
201
+ const rawComponentProps = buildComponentProps(context, { hideLabel });
195
202
  const { props: resolvedFieldProps } = useReactiveProps({
196
203
  entity: collection,
197
204
  entityType: mode,
@@ -200,9 +207,9 @@ function FieldRenderer({ fieldName, fieldDef, collection, mode = "collection", r
200
207
  ...stripFieldUiOptions(getFieldOptions(fieldDef)),
201
208
  ...extraProps ?? {}
202
209
  }), [fieldDef, extraProps]),
203
- enabled: !context.isHidden && !!fieldDef
210
+ enabled: !effectiveFieldState.hidden && !!fieldDef
204
211
  });
205
- if (context.isHidden) return null;
212
+ if (effectiveFieldState.hidden) return null;
206
213
  if (!fieldDef) return renderConfigError(`Field "${fieldName}" not found in ${mode === "global" ? "global" : "collection"} "${collection}" config.`);
207
214
  const fieldValue = isComputed ? computedValue : watchedFieldValue === void 0 ? rawComponentProps.value : watchedFieldValue;
208
215
  const componentProps = {
@@ -212,7 +219,8 @@ function FieldRenderer({ fieldName, fieldDef, collection, mode = "collection", r
212
219
  onChange: handleChange,
213
220
  options: resolvedOptions,
214
221
  optionsLoading,
215
- readOnly: rawComponentProps.readOnly || isComputed,
222
+ readOnly: effectiveFieldState.readOnly || isComputed,
223
+ disabled: effectiveFieldState.disabled === true,
216
224
  label: resolveText(rawComponentProps.label, "", formValues),
217
225
  description: resolveText(rawComponentProps.description, "", formValues),
218
226
  placeholder: resolveText(rawComponentProps.placeholder, "", formValues)