@questpie/admin 3.5.3 → 3.5.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +8 -0
- package/dist/client/blocks/block-renderer.d.mts +2 -2
- package/dist/client/builder/index.d.mts +1 -1
- package/dist/client/builder/types/collection-types.d.mts +80 -5
- package/dist/client/builder/types/common.d.mts +5 -0
- package/dist/client/builder/types/field-types.d.mts +41 -1
- package/dist/client/builder/view/view.d.mts +3 -2
- package/dist/client/components/admin-link.d.mts +2 -2
- package/dist/client/components/fields/boolean-field.mjs +2 -1
- package/dist/client/components/fields/date-field.mjs +2 -1
- package/dist/client/components/fields/datetime-field.mjs +2 -1
- package/dist/client/components/fields/email-field.mjs +2 -1
- package/dist/client/components/fields/field-utils.d.mts +11 -0
- package/dist/client/components/fields/field-utils.mjs +3 -1
- package/dist/client/components/fields/field-wrapper.mjs +3 -3
- package/dist/client/components/fields/number-field.mjs +2 -1
- package/dist/client/components/fields/object-field.mjs +2 -1
- package/dist/client/components/fields/relation/displays/types.mjs +3 -3
- package/dist/client/components/fields/rich-text-editor/extensions.mjs +2 -1
- package/dist/client/components/fields/rich-text-editor/image-popover.mjs +6 -2
- package/dist/client/components/fields/rich-text-editor/image-upload.mjs +2 -1
- package/dist/client/components/fields/rich-text-editor/index.d.mts +3 -2
- package/dist/client/components/fields/rich-text-editor/index.mjs +4 -3
- package/dist/client/components/fields/select-field.mjs +2 -1
- package/dist/client/components/fields/text-field.mjs +2 -1
- package/dist/client/components/fields/textarea-field.mjs +2 -1
- package/dist/client/components/fields/time-field.mjs +2 -1
- package/dist/client/components/layout/field-layout-renderer.mjs +4 -4
- package/dist/client/components/media/media-grid.mjs +2 -1
- package/dist/client/components/primitives/asset-preview.mjs +4 -2
- package/dist/client/components/primitives/dropzone.d.mts +100 -0
- package/dist/client/components/primitives/field-select-control.mjs +2 -1
- package/dist/client/components/ui/button.d.mts +23 -0
- package/dist/client/components/ui/button.mjs +2 -2
- package/dist/client/components/ui/dropdown-menu.d.mts +49 -0
- package/dist/client/components/ui/dropdown-menu.mjs +22 -1
- package/dist/client/components/ui/popover.mjs +1 -1
- package/dist/client/components/ui/search-input.d.mts +56 -0
- package/dist/client/components/ui/select.mjs +2 -2
- package/dist/client/components/ui/sheet.d.mts +40 -0
- package/dist/client/components/ui/table.d.mts +49 -0
- package/dist/client/components/ui/table.mjs +15 -1
- package/dist/client/components/ui/tooltip.d.mts +21 -0
- package/dist/client/contexts/focus-context.d.mts +2 -2
- package/dist/client/hooks/use-admin-config.mjs +20 -1
- package/dist/client/hooks/use-autosave.mjs +91 -0
- package/dist/client/hooks/use-collection.mjs +65 -23
- package/dist/client/hooks/use-upload.d.mts +40 -0
- package/dist/client/hooks/use-upload.mjs +4 -2
- package/dist/client/i18n/hooks.d.mts +20 -0
- package/dist/client/lib/utils.d.mts +6 -0
- package/dist/client/preview/block-scope-context.d.mts +2 -2
- package/dist/client/preview/preview-banner.d.mts +2 -2
- package/dist/client/preview/preview-field.d.mts +4 -4
- package/dist/client/runtime/provider.mjs +22 -3
- package/dist/client/scope/picker.d.mts +2 -2
- package/dist/client/scope/provider.d.mts +2 -2
- package/dist/client/styles/base.css +22 -18
- package/dist/client/utils/asset-url.mjs +27 -0
- package/dist/client/views/auth/accept-invite-form.d.mts +2 -2
- package/dist/client/views/auth/auth-layout.d.mts +3 -3
- package/dist/client/views/auth/forgot-password-form.d.mts +2 -2
- package/dist/client/views/auth/login-form.d.mts +2 -2
- package/dist/client/views/auth/reset-password-form.d.mts +2 -2
- package/dist/client/views/auth/setup-form.d.mts +2 -2
- package/dist/client/views/collection/auto-form-fields.mjs +4 -4
- package/dist/client/views/collection/cells/shared/asset-thumbnail.d.mts +7 -0
- package/dist/client/views/collection/cells/shared/asset-thumbnail.mjs +3 -2
- package/dist/client/views/collection/cells/shared/cell-helpers.mjs +3 -2
- package/dist/client/views/collection/cells/upload-cells.mjs +2 -1
- package/dist/client/views/collection/document-view.d.mts +30 -0
- package/dist/client/views/collection/document-view.mjs +377 -0
- package/dist/client/views/collection/field-context.mjs +3 -2
- package/dist/client/views/collection/field-renderer.mjs +2 -2
- package/dist/client/views/collection/form-view.mjs +14 -80
- package/dist/client/views/collection/list-view.mjs +19 -15
- package/dist/client/views/collection/table-view.mjs +1 -1
- package/dist/client/views/layout/admin-layout-provider.mjs +4 -3
- package/dist/client/views/layout/admin-layout.mjs +107 -20
- package/dist/client/views/layout/admin-router.mjs +19 -3
- package/dist/client/views/layout/admin-sidebar.mjs +50 -6
- package/dist/client/views/layout/admin-view-layout.d.mts +36 -0
- package/dist/client/views/pages/accept-invite-page.d.mts +2 -2
- package/dist/client/views/pages/dashboard-page.d.mts +2 -2
- package/dist/client/views/pages/forgot-password-page.d.mts +2 -2
- package/dist/client/views/pages/invite-page.d.mts +2 -2
- package/dist/client/views/pages/login-page.d.mts +2 -2
- package/dist/client/views/pages/reset-password-page.d.mts +2 -2
- package/dist/client/views/pages/setup-page.d.mts +2 -2
- package/dist/client.d.mts +17 -2
- package/dist/client.mjs +16 -1
- package/dist/components/rich-text/rich-text-renderer.d.mts +2 -2
- package/dist/factories.d.mts +2 -2
- package/dist/factories.mjs +2 -2
- package/dist/index.d.mts +17 -3
- package/dist/index.mjs +16 -1
- package/dist/server/augmentation/actions.d.mts +5 -0
- package/dist/server/augmentation/form-layout.d.mts +5 -0
- package/dist/server/augmentation/views.d.mts +4 -1
- package/dist/server/fields/blocks.mjs +4 -1
- package/dist/server/fields/reactive-runtime.mjs +3 -0
- package/dist/server/modules/admin/.generated/module.d.mts +1 -1
- package/dist/server/modules/admin/auth-helpers.mjs +7 -1
- package/dist/server/modules/admin/block/introspection.mjs +28 -4
- package/dist/server/modules/admin/block/prefetch.d.mts +11 -0
- package/dist/server/modules/admin/block/prefetch.mjs +108 -27
- package/dist/server/modules/admin/client/.generated/module.d.mts +68 -67
- package/dist/server/modules/admin/client/.generated/module.mjs +2 -0
- package/dist/server/modules/admin/client/views/collection-document.d.mts +6 -0
- package/dist/server/modules/admin/client/views/collection-document.mjs +10 -0
- package/dist/server/modules/admin/collections/account.d.mts +46 -46
- package/dist/server/modules/admin/collections/admin-locks.d.mts +57 -57
- package/dist/server/modules/admin/collections/admin-preferences.d.mts +42 -42
- package/dist/server/modules/admin/collections/admin-saved-views.d.mts +50 -50
- package/dist/server/modules/admin/collections/apikey.d.mts +79 -71
- package/dist/server/modules/admin/collections/assets.d.mts +42 -42
- package/dist/server/modules/admin/collections/session.d.mts +45 -45
- package/dist/server/modules/admin/collections/user.d.mts +66 -66
- package/dist/server/modules/admin/collections/verification.d.mts +39 -39
- package/dist/server/modules/admin/dto/admin-config.dto.mjs +34 -4
- package/dist/server/modules/admin/factories.mjs +4 -34
- package/dist/server/modules/admin/routes/admin-config.d.mts +3 -2
- package/dist/server/modules/admin/routes/admin-config.mjs +18 -2
- package/dist/server/modules/admin/routes/execute-action.d.mts +9 -9
- package/dist/server/modules/admin/routes/execute-action.mjs +10 -4
- package/dist/server/modules/admin/routes/locales.d.mts +2 -2
- package/dist/server/modules/admin/routes/locales.mjs +1 -1
- package/dist/server/modules/admin/routes/preview.d.mts +11 -11
- package/dist/server/modules/admin/routes/preview.mjs +6 -5
- package/dist/server/modules/admin/routes/reactive.d.mts +9 -9
- package/dist/server/modules/admin/routes/reactive.mjs +2 -2
- package/dist/server/modules/admin/routes/route-helpers.mjs +1 -1
- package/dist/server/modules/admin/routes/setup.d.mts +7 -7
- package/dist/server/modules/admin/routes/translations.d.mts +4 -4
- package/dist/server/modules/admin/routes/widget-data.d.mts +5 -5
- package/dist/server/modules/admin/routes/widget-data.mjs +1 -1
- package/dist/server/modules/admin-preferences/collections/saved-views.d.mts +27 -27
- package/dist/server/plugin.mjs +8 -3
- package/dist/server/proxy-factories.d.mts +8 -1
- package/dist/server/proxy-factories.mjs +33 -1
- package/package.json +4 -4
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { useTranslation } from "../../../i18n/hooks.mjs";
|
|
2
2
|
import { Badge } from "../../../components/ui/badge.mjs";
|
|
3
|
+
import { resolveAssetUrl } from "../../../utils/asset-url.mjs";
|
|
3
4
|
import { AssetThumbnail } from "./shared/asset-thumbnail.mjs";
|
|
4
5
|
import { useCollectionItem } from "../../../hooks/use-collection.mjs";
|
|
5
6
|
import "react";
|
|
@@ -81,7 +82,7 @@ function UploadManyCell({ value }) {
|
|
|
81
82
|
children: [/* @__PURE__ */ jsx("div", {
|
|
82
83
|
className: "flex",
|
|
83
84
|
children: imageAssets.map((asset, index) => /* @__PURE__ */ jsx("img", {
|
|
84
|
-
src: asset.url,
|
|
85
|
+
src: resolveAssetUrl(asset.url) ?? "",
|
|
85
86
|
alt: asset.filename || "Asset",
|
|
86
87
|
className: `image-outline bg-background size-6 rounded object-cover${index > 0 ? " -ms-2" : ""}`
|
|
87
88
|
}, asset.id || index))
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { ComponentRegistry, DocumentViewConfig } from "../../builder/types/field-types.mjs";
|
|
2
|
+
import { CollectionBuilderState } from "../../builder/types/collection-types.mjs";
|
|
3
|
+
import * as React from "react";
|
|
4
|
+
|
|
5
|
+
//#region src/client/views/collection/document-view.d.ts
|
|
6
|
+
|
|
7
|
+
interface DocumentViewProps {
|
|
8
|
+
collection: string;
|
|
9
|
+
id?: string;
|
|
10
|
+
config?: Partial<CollectionBuilderState> | Record<string, any>;
|
|
11
|
+
viewConfig?: DocumentViewConfig & Record<string, any>;
|
|
12
|
+
navigate: (path: string) => void;
|
|
13
|
+
basePath?: string;
|
|
14
|
+
defaultValues?: Record<string, any>;
|
|
15
|
+
registry?: ComponentRegistry;
|
|
16
|
+
allCollectionsConfig?: Record<string, any>;
|
|
17
|
+
title?: string;
|
|
18
|
+
}
|
|
19
|
+
declare function DocumentView({
|
|
20
|
+
collection,
|
|
21
|
+
id,
|
|
22
|
+
config,
|
|
23
|
+
viewConfig,
|
|
24
|
+
defaultValues: defaultValuesProp,
|
|
25
|
+
registry,
|
|
26
|
+
allCollectionsConfig,
|
|
27
|
+
title: titleProp
|
|
28
|
+
}: DocumentViewProps): React.ReactElement;
|
|
29
|
+
//#endregion
|
|
30
|
+
export { DocumentView };
|
|
@@ -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
|
|
|
@@ -138,7 +138,7 @@ function renderEmbeddedField({ context, registry, allCollectionsConfig, componen
|
|
|
138
138
|
* 2. FieldDefinition.field.component (registry-first approach)
|
|
139
139
|
* 3. Error message if no component registered
|
|
140
140
|
*/
|
|
141
|
-
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 }) {
|
|
142
142
|
const form = useFormContext();
|
|
143
143
|
const { locale } = useScopedLocale();
|
|
144
144
|
const resolveText = useResolveText();
|
|
@@ -198,7 +198,7 @@ function FieldRenderer({ fieldName, fieldDef, collection, mode = "collection", r
|
|
|
198
198
|
staticOptions: context.options
|
|
199
199
|
});
|
|
200
200
|
const resolvedOptions = hookOptions ?? context.options;
|
|
201
|
-
const rawComponentProps = buildComponentProps(context);
|
|
201
|
+
const rawComponentProps = buildComponentProps(context, { hideLabel });
|
|
202
202
|
const { props: resolvedFieldProps } = useReactiveProps({
|
|
203
203
|
entity: collection,
|
|
204
204
|
entityType: mode,
|
|
@@ -29,6 +29,7 @@ import { useCollectionValidation } from "../../hooks/use-collection-validation.m
|
|
|
29
29
|
import { useSearchParamToggle } from "../../hooks/use-search-param-toggle.mjs";
|
|
30
30
|
import { usePreferServerValidation } from "../../hooks/use-server-validation.mjs";
|
|
31
31
|
import { useSidebarSearchParam } from "../../hooks/use-sidebar-search-param.mjs";
|
|
32
|
+
import { useAutosave } from "../../hooks/use-autosave.mjs";
|
|
32
33
|
import { useCollectionCreate, useCollectionDelete, useCollectionItem, useCollectionRestore, useCollectionRevertVersion, useCollectionUpdate, useCollectionVersions } from "../../hooks/use-collection.mjs";
|
|
33
34
|
import { getLockUser, useLock } from "../../hooks/use-locks.mjs";
|
|
34
35
|
import { useServerActions } from "../../hooks/use-server-actions.mjs";
|
|
@@ -248,71 +249,6 @@ const PreviewPatchBridge = React.memo(function PreviewPatchBridge$1({ form, prev
|
|
|
248
249
|
]);
|
|
249
250
|
return null;
|
|
250
251
|
});
|
|
251
|
-
const AutosaveManager = React.memo(function AutosaveManager$1({ form, formElementRef, isEditMode, id, enabled, debounce, isDirtyRef, isSubmittingRef, updateMutation, onPreviewRefresh, onPreviewCommit, onSavingChange, onSaved }) {
|
|
252
|
-
const { t } = useTranslation();
|
|
253
|
-
const timerRef = React.useRef(null);
|
|
254
|
-
const runAutosave = React.useCallback(async () => {
|
|
255
|
-
if (!id || !isDirtyRef.current || isSubmittingRef.current) return;
|
|
256
|
-
try {
|
|
257
|
-
onSavingChange(true);
|
|
258
|
-
await form.handleSubmit(async (data) => {
|
|
259
|
-
const result = await updateMutation.mutateAsync({
|
|
260
|
-
id,
|
|
261
|
-
data
|
|
262
|
-
});
|
|
263
|
-
form.reset(result, { keepTouched: true });
|
|
264
|
-
onPreviewCommit?.(result);
|
|
265
|
-
onPreviewRefresh?.();
|
|
266
|
-
onSaved(/* @__PURE__ */ new Date());
|
|
267
|
-
onSavingChange(false);
|
|
268
|
-
}, () => {
|
|
269
|
-
onSavingChange(false);
|
|
270
|
-
})();
|
|
271
|
-
} catch (error) {
|
|
272
|
-
onSavingChange(false);
|
|
273
|
-
console.error("Autosave failed:", error);
|
|
274
|
-
toast.error(t("error.autosaveFailed"), { description: error instanceof Error ? error.message : void 0 });
|
|
275
|
-
}
|
|
276
|
-
}, [
|
|
277
|
-
form,
|
|
278
|
-
id,
|
|
279
|
-
isDirtyRef,
|
|
280
|
-
isSubmittingRef,
|
|
281
|
-
onSaved,
|
|
282
|
-
onSavingChange,
|
|
283
|
-
onPreviewCommit,
|
|
284
|
-
onPreviewRefresh,
|
|
285
|
-
t,
|
|
286
|
-
updateMutation
|
|
287
|
-
]);
|
|
288
|
-
React.useEffect(() => {
|
|
289
|
-
if (timerRef.current) clearTimeout(timerRef.current);
|
|
290
|
-
if (!enabled || !isEditMode || !id) return;
|
|
291
|
-
const target = formElementRef.current;
|
|
292
|
-
if (!target) return;
|
|
293
|
-
const scheduleAutosave = () => {
|
|
294
|
-
if (timerRef.current) clearTimeout(timerRef.current);
|
|
295
|
-
timerRef.current = setTimeout(() => {
|
|
296
|
-
runAutosave();
|
|
297
|
-
}, debounce);
|
|
298
|
-
};
|
|
299
|
-
target.addEventListener("input", scheduleAutosave, { capture: true });
|
|
300
|
-
target.addEventListener("change", scheduleAutosave, { capture: true });
|
|
301
|
-
return () => {
|
|
302
|
-
target.removeEventListener("input", scheduleAutosave, { capture: true });
|
|
303
|
-
target.removeEventListener("change", scheduleAutosave, { capture: true });
|
|
304
|
-
if (timerRef.current) clearTimeout(timerRef.current);
|
|
305
|
-
};
|
|
306
|
-
}, [
|
|
307
|
-
debounce,
|
|
308
|
-
enabled,
|
|
309
|
-
formElementRef,
|
|
310
|
-
id,
|
|
311
|
-
isEditMode,
|
|
312
|
-
runAutosave
|
|
313
|
-
]);
|
|
314
|
-
return null;
|
|
315
|
-
});
|
|
316
252
|
const AutosaveIndicator = React.memo(function AutosaveIndicator$1({ control, enabled, indicator, isEditMode, isSaving, lastSaved, formatTimeAgo, t }) {
|
|
317
253
|
const { isDirty } = useFormState({ control });
|
|
318
254
|
const [, forceUpdate] = React.useReducer((x) => x + 1, 0);
|
|
@@ -603,6 +539,19 @@ function FormView({ collection, id, config, viewConfig, navigate, basePath = "/a
|
|
|
603
539
|
preventNavigation: cfg.preventNavigation !== false
|
|
604
540
|
};
|
|
605
541
|
}, [config]);
|
|
542
|
+
useAutosave({
|
|
543
|
+
form,
|
|
544
|
+
id,
|
|
545
|
+
enabled: autoSaveConfig.enabled,
|
|
546
|
+
debounce: autoSaveConfig.debounce,
|
|
547
|
+
isDirtyRef: formIsDirtyRef,
|
|
548
|
+
isSubmittingRef: formIsSubmittingRef,
|
|
549
|
+
updateMutation,
|
|
550
|
+
onPreviewCommit: commitPreviewSnapshot,
|
|
551
|
+
onPreviewRefresh: triggerPreviewRefresh,
|
|
552
|
+
onSavingChange: setIsSaving,
|
|
553
|
+
onSaved: setLastSaved
|
|
554
|
+
});
|
|
606
555
|
const { locale: contentLocale, setLocale: setContentLocale } = useScopedLocale();
|
|
607
556
|
const localeOptions = useSafeContentLocales()?.locales ?? [];
|
|
608
557
|
const prevLocaleRef = React.useRef(contentLocale);
|
|
@@ -1218,21 +1167,6 @@ function FormView({ collection, id, config, viewConfig, navigate, basePath = "/a
|
|
|
1218
1167
|
onDirtyChange: handleFormDirtyChange,
|
|
1219
1168
|
onSubmittingChange: handleFormSubmittingChange
|
|
1220
1169
|
}),
|
|
1221
|
-
/* @__PURE__ */ jsx(AutosaveManager, {
|
|
1222
|
-
form,
|
|
1223
|
-
formElementRef,
|
|
1224
|
-
isEditMode,
|
|
1225
|
-
id,
|
|
1226
|
-
enabled: autoSaveConfig.enabled,
|
|
1227
|
-
debounce: autoSaveConfig.debounce,
|
|
1228
|
-
isDirtyRef: formIsDirtyRef,
|
|
1229
|
-
isSubmittingRef: formIsSubmittingRef,
|
|
1230
|
-
updateMutation,
|
|
1231
|
-
onPreviewCommit: commitPreviewSnapshot,
|
|
1232
|
-
onPreviewRefresh: triggerPreviewRefresh,
|
|
1233
|
-
onSavingChange: setIsSaving,
|
|
1234
|
-
onSaved: setLastSaved
|
|
1235
|
-
}),
|
|
1236
1170
|
/* @__PURE__ */ jsx(PreviewPatchBridge, {
|
|
1237
1171
|
form,
|
|
1238
1172
|
previewRef,
|