@aphexcms/cms-core 2.0.10 → 2.1.0
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/dist/api/api-keys.d.ts +4 -0
- package/dist/api/api-keys.d.ts.map +1 -1
- package/dist/api/assets.d.ts.map +1 -1
- package/dist/api/index.d.ts +1 -0
- package/dist/api/index.d.ts.map +1 -1
- package/dist/api/index.js +1 -0
- package/dist/api/roles.d.ts +22 -0
- package/dist/api/roles.d.ts.map +1 -0
- package/dist/api/roles.js +26 -0
- package/dist/api/schemas/api-keys.d.ts +50 -4
- package/dist/api/schemas/api-keys.d.ts.map +1 -1
- package/dist/api/schemas/api-keys.js +22 -3
- package/dist/api/schemas/organizations.d.ts +4 -22
- package/dist/api/schemas/organizations.d.ts.map +1 -1
- package/dist/api/schemas/organizations.js +13 -2
- package/dist/api/schemas/roles.d.ts +77 -0
- package/dist/api/schemas/roles.d.ts.map +1 -0
- package/dist/api/schemas/roles.js +42 -0
- package/dist/app-augment.d.ts +32 -0
- package/dist/app-augment.d.ts.map +1 -0
- package/dist/app-augment.js +18 -0
- package/dist/auth/auth-hooks.d.ts +2 -1
- package/dist/auth/auth-hooks.d.ts.map +1 -1
- package/dist/auth/auth-hooks.js +22 -2
- package/dist/client/index.d.ts +4 -2
- package/dist/client/index.d.ts.map +1 -1
- package/dist/client/index.js +6 -2
- package/dist/components/AdminApp.svelte +300 -140
- package/dist/components/AdminApp.svelte.d.ts +8 -0
- package/dist/components/AdminApp.svelte.d.ts.map +1 -1
- package/dist/components/admin/DocumentEditor.svelte +633 -350
- package/dist/components/admin/DocumentEditor.svelte.d.ts +2 -0
- package/dist/components/admin/DocumentEditor.svelte.d.ts.map +1 -1
- package/dist/components/admin/DocumentVersionPanel.svelte +46 -20
- package/dist/components/admin/DocumentVersionPanel.svelte.d.ts.map +1 -1
- package/dist/components/admin/MediaBrowser.svelte +70 -30
- package/dist/components/admin/MediaBrowser.svelte.d.ts.map +1 -1
- package/dist/components/admin/ObjectModal.svelte +70 -95
- package/dist/components/admin/ObjectModal.svelte.d.ts +1 -2
- package/dist/components/admin/ObjectModal.svelte.d.ts.map +1 -1
- package/dist/components/admin/PermissionsDebug.svelte +119 -0
- package/dist/components/admin/PermissionsDebug.svelte.d.ts +4 -0
- package/dist/components/admin/PermissionsDebug.svelte.d.ts.map +1 -0
- package/dist/components/admin/SchemaField.svelte +3 -1
- package/dist/components/admin/SchemaField.svelte.d.ts.map +1 -1
- package/dist/components/admin/confirm-dialog/ConfirmDialogHost.svelte +8 -10
- package/dist/components/admin/confirm-dialog/ConfirmDialogHost.svelte.d.ts.map +1 -1
- package/dist/components/admin/fields/ArrayField.svelte +104 -108
- package/dist/components/admin/fields/ArrayField.svelte.d.ts.map +1 -1
- package/dist/components/admin/fields/FileField.svelte +22 -17
- package/dist/components/admin/fields/FileField.svelte.d.ts.map +1 -1
- package/dist/components/admin/fields/ImageField.svelte +21 -15
- package/dist/components/admin/fields/ImageField.svelte.d.ts.map +1 -1
- package/dist/components/admin/fields/ReferenceField.svelte +142 -112
- package/dist/components/admin/fields/ReferenceField.svelte.d.ts.map +1 -1
- package/dist/components/admin/fields/TextareaField.svelte +4 -3
- package/dist/components/admin/fields/TextareaField.svelte.d.ts +2 -2
- package/dist/components/admin/fields/TextareaField.svelte.d.ts.map +1 -1
- package/dist/components/layout/Sidebar.svelte +21 -11
- package/dist/components/layout/Sidebar.svelte.d.ts.map +1 -1
- package/dist/components/layout/sidebar/NavMain.svelte +1 -0
- package/dist/db/interfaces/document.d.ts.map +1 -1
- package/dist/db/interfaces/index.d.ts +3 -1
- package/dist/db/interfaces/index.d.ts.map +1 -1
- package/dist/db/interfaces/organization.d.ts +1 -1
- package/dist/db/interfaces/organization.d.ts.map +1 -1
- package/dist/db/interfaces/role.d.ts +25 -0
- package/dist/db/interfaces/role.d.ts.map +1 -0
- package/dist/db/interfaces/role.js +1 -0
- package/dist/engine.d.ts.map +1 -1
- package/dist/engine.js +5 -0
- package/dist/field-access.d.ts +30 -0
- package/dist/field-access.d.ts.map +1 -0
- package/dist/field-access.js +79 -0
- package/dist/hooks.d.ts +2 -0
- package/dist/hooks.d.ts.map +1 -1
- package/dist/hooks.js +19 -13
- package/dist/lib/api/api-keys.d.ts +4 -0
- package/dist/lib/api/api-keys.d.ts.map +1 -1
- package/dist/lib/api/api-keys.js.map +1 -1
- package/dist/lib/api/assets.d.ts.map +1 -1
- package/dist/lib/api/assets.js.map +1 -1
- package/dist/lib/api/index.d.ts +1 -0
- package/dist/lib/api/index.d.ts.map +1 -1
- package/dist/lib/api/index.js +1 -0
- package/dist/lib/api/index.js.map +1 -1
- package/dist/lib/api/roles.d.ts +22 -0
- package/dist/lib/api/roles.d.ts.map +1 -0
- package/dist/lib/api/roles.js +27 -0
- package/dist/lib/api/roles.js.map +1 -0
- package/dist/lib/api/schemas/api-keys.d.ts +50 -4
- package/dist/lib/api/schemas/api-keys.d.ts.map +1 -1
- package/dist/lib/api/schemas/api-keys.js +22 -3
- package/dist/lib/api/schemas/api-keys.js.map +1 -1
- package/dist/lib/api/schemas/organizations.d.ts +4 -22
- package/dist/lib/api/schemas/organizations.d.ts.map +1 -1
- package/dist/lib/api/schemas/organizations.js +13 -2
- package/dist/lib/api/schemas/organizations.js.map +1 -1
- package/dist/lib/api/schemas/roles.d.ts +77 -0
- package/dist/lib/api/schemas/roles.d.ts.map +1 -0
- package/dist/lib/api/schemas/roles.js +43 -0
- package/dist/lib/api/schemas/roles.js.map +1 -0
- package/dist/lib/app-augment.d.ts +32 -0
- package/dist/lib/app-augment.d.ts.map +1 -0
- package/dist/lib/app-augment.js +19 -0
- package/dist/lib/app-augment.js.map +1 -0
- package/dist/lib/auth/auth-hooks.d.ts +2 -1
- package/dist/lib/auth/auth-hooks.d.ts.map +1 -1
- package/dist/lib/auth/auth-hooks.js +22 -2
- package/dist/lib/auth/auth-hooks.js.map +1 -1
- package/dist/lib/client/index.d.ts +4 -2
- package/dist/lib/client/index.d.ts.map +1 -1
- package/dist/lib/client/index.js +6 -2
- package/dist/lib/client/index.js.map +1 -1
- package/dist/lib/db/interfaces/document.d.ts.map +1 -1
- package/dist/lib/db/interfaces/index.d.ts +3 -1
- package/dist/lib/db/interfaces/index.d.ts.map +1 -1
- package/dist/lib/db/interfaces/organization.d.ts +1 -1
- package/dist/lib/db/interfaces/organization.d.ts.map +1 -1
- package/dist/lib/db/interfaces/role.d.ts +25 -0
- package/dist/lib/db/interfaces/role.d.ts.map +1 -0
- package/dist/lib/db/interfaces/role.js +2 -0
- package/dist/lib/db/interfaces/role.js.map +1 -0
- package/dist/lib/engine.d.ts.map +1 -1
- package/dist/lib/engine.js +5 -0
- package/dist/lib/engine.js.map +1 -1
- package/dist/lib/field-access.d.ts +30 -0
- package/dist/lib/field-access.d.ts.map +1 -0
- package/dist/lib/field-access.js +80 -0
- package/dist/lib/field-access.js.map +1 -0
- package/dist/lib/hooks.d.ts +2 -0
- package/dist/lib/hooks.d.ts.map +1 -1
- package/dist/lib/hooks.js +19 -13
- package/dist/lib/hooks.js.map +1 -1
- package/dist/lib/local-api/collection-api.d.ts +2 -0
- package/dist/lib/local-api/collection-api.d.ts.map +1 -1
- package/dist/lib/local-api/collection-api.js +98 -30
- package/dist/lib/local-api/collection-api.js.map +1 -1
- package/dist/lib/local-api/index.d.ts.map +1 -1
- package/dist/lib/local-api/index.js.map +1 -1
- package/dist/lib/local-api/permissions.d.ts +26 -26
- package/dist/lib/local-api/permissions.d.ts.map +1 -1
- package/dist/lib/local-api/permissions.js +122 -80
- package/dist/lib/local-api/permissions.js.map +1 -1
- package/dist/lib/permissions-context.svelte.d.ts +44 -0
- package/dist/lib/permissions-context.svelte.d.ts.map +1 -0
- package/dist/lib/permissions-context.svelte.js +80 -0
- package/dist/lib/permissions-context.svelte.js.map +1 -0
- package/dist/lib/routes/assets.d.ts.map +1 -1
- package/dist/lib/routes/assets.js.map +1 -1
- package/dist/lib/routes/document-versions.d.ts.map +1 -1
- package/dist/lib/routes/document-versions.js.map +1 -1
- package/dist/lib/routes/organizations-by-id.d.ts.map +1 -1
- package/dist/lib/routes/organizations-by-id.js +6 -3
- package/dist/lib/routes/organizations-by-id.js.map +1 -1
- package/dist/lib/routes/organizations-invitations.d.ts.map +1 -1
- package/dist/lib/routes/organizations-invitations.js +17 -7
- package/dist/lib/routes/organizations-invitations.js.map +1 -1
- package/dist/lib/routes/organizations-members.d.ts.map +1 -1
- package/dist/lib/routes/organizations-members.js +15 -7
- package/dist/lib/routes/organizations-members.js.map +1 -1
- package/dist/lib/routes/organizations.d.ts.map +1 -1
- package/dist/lib/routes/organizations.js +3 -0
- package/dist/lib/routes/organizations.js.map +1 -1
- package/dist/lib/routes/roles-by-name.d.ts +4 -0
- package/dist/lib/routes/roles-by-name.d.ts.map +1 -0
- package/dist/lib/routes/roles-by-name.js +112 -0
- package/dist/lib/routes/roles-by-name.js.map +1 -0
- package/dist/lib/routes/roles.d.ts +4 -0
- package/dist/lib/routes/roles.d.ts.map +1 -0
- package/dist/lib/routes/roles.js +91 -0
- package/dist/lib/routes/roles.js.map +1 -0
- package/dist/lib/routes-exports.d.ts +2 -0
- package/dist/lib/routes-exports.d.ts.map +1 -1
- package/dist/lib/routes-exports.js +3 -0
- package/dist/lib/routes-exports.js.map +1 -1
- package/dist/lib/save-state-context.svelte.d.ts +8 -0
- package/dist/lib/save-state-context.svelte.d.ts.map +1 -0
- package/dist/lib/save-state-context.svelte.js +9 -0
- package/dist/lib/save-state-context.svelte.js.map +1 -0
- package/dist/lib/schema-utils/validator.d.ts.map +1 -1
- package/dist/lib/schema-utils/validator.js +14 -0
- package/dist/lib/schema-utils/validator.js.map +1 -1
- package/dist/lib/server/index.d.ts +1 -0
- package/dist/lib/server/index.d.ts.map +1 -1
- package/dist/lib/server/index.js +1 -0
- package/dist/lib/server/index.js.map +1 -1
- package/dist/lib/services/index.d.ts +1 -0
- package/dist/lib/services/index.d.ts.map +1 -1
- package/dist/lib/services/index.js +1 -0
- package/dist/lib/services/index.js.map +1 -1
- package/dist/lib/services/roles-service.d.ts +35 -0
- package/dist/lib/services/roles-service.d.ts.map +1 -0
- package/dist/lib/services/roles-service.js +91 -0
- package/dist/lib/services/roles-service.js.map +1 -0
- package/dist/lib/services/version-service.d.ts.map +1 -1
- package/dist/lib/services/version-service.js.map +1 -1
- package/dist/lib/types/auth.d.ts +43 -9
- package/dist/lib/types/auth.d.ts.map +1 -1
- package/dist/lib/types/auth.js +31 -26
- package/dist/lib/types/auth.js.map +1 -1
- package/dist/lib/types/capabilities.d.ts +104 -0
- package/dist/lib/types/capabilities.d.ts.map +1 -0
- package/dist/lib/types/capabilities.js +217 -0
- package/dist/lib/types/capabilities.js.map +1 -0
- package/dist/lib/types/index.d.ts +1 -0
- package/dist/lib/types/index.d.ts.map +1 -1
- package/dist/lib/types/index.js +1 -0
- package/dist/lib/types/index.js.map +1 -1
- package/dist/lib/types/organization.d.ts +13 -4
- package/dist/lib/types/organization.d.ts.map +1 -1
- package/dist/lib/types/schemas.d.ts +71 -0
- package/dist/lib/types/schemas.d.ts.map +1 -1
- package/dist/lib/types/sidebar.d.ts +1 -1
- package/dist/lib/types/sidebar.d.ts.map +1 -1
- package/dist/lib/utils/mime-detect.d.ts.map +1 -1
- package/dist/lib/utils/mime-detect.js +28 -6
- package/dist/lib/utils/mime-detect.js.map +1 -1
- package/dist/local-api/collection-api.d.ts +2 -0
- package/dist/local-api/collection-api.d.ts.map +1 -1
- package/dist/local-api/collection-api.js +98 -30
- package/dist/local-api/index.d.ts.map +1 -1
- package/dist/local-api/permissions.d.ts +26 -26
- package/dist/local-api/permissions.d.ts.map +1 -1
- package/dist/local-api/permissions.js +122 -80
- package/dist/permissions-context.svelte.d.ts +44 -0
- package/dist/permissions-context.svelte.d.ts.map +1 -0
- package/dist/permissions-context.svelte.js +79 -0
- package/dist/plugins/README.md +20 -20
- package/dist/routes/assets.d.ts.map +1 -1
- package/dist/routes/document-versions.d.ts.map +1 -1
- package/dist/routes/organizations-by-id.d.ts.map +1 -1
- package/dist/routes/organizations-by-id.js +6 -3
- package/dist/routes/organizations-invitations.d.ts.map +1 -1
- package/dist/routes/organizations-invitations.js +17 -7
- package/dist/routes/organizations-members.d.ts.map +1 -1
- package/dist/routes/organizations-members.js +15 -7
- package/dist/routes/organizations.d.ts.map +1 -1
- package/dist/routes/organizations.js +3 -0
- package/dist/routes/roles-by-name.d.ts +4 -0
- package/dist/routes/roles-by-name.d.ts.map +1 -0
- package/dist/routes/roles-by-name.js +111 -0
- package/dist/routes/roles.d.ts +4 -0
- package/dist/routes/roles.d.ts.map +1 -0
- package/dist/routes/roles.js +90 -0
- package/dist/routes-exports.d.ts +2 -0
- package/dist/routes-exports.d.ts.map +1 -1
- package/dist/routes-exports.js +3 -0
- package/dist/save-state-context.svelte.d.ts +8 -0
- package/dist/save-state-context.svelte.d.ts.map +1 -0
- package/dist/save-state-context.svelte.js +8 -0
- package/dist/schema-utils/validator.d.ts.map +1 -1
- package/dist/schema-utils/validator.js +14 -0
- package/dist/server/index.d.ts +1 -0
- package/dist/server/index.d.ts.map +1 -1
- package/dist/server/index.js +1 -0
- package/dist/services/index.d.ts +1 -0
- package/dist/services/index.d.ts.map +1 -1
- package/dist/services/index.js +1 -0
- package/dist/services/roles-service.d.ts +35 -0
- package/dist/services/roles-service.d.ts.map +1 -0
- package/dist/services/roles-service.js +90 -0
- package/dist/services/version-service.d.ts.map +1 -1
- package/dist/types/auth.d.ts +43 -9
- package/dist/types/auth.d.ts.map +1 -1
- package/dist/types/auth.js +31 -26
- package/dist/types/capabilities.d.ts +104 -0
- package/dist/types/capabilities.d.ts.map +1 -0
- package/dist/types/capabilities.js +216 -0
- package/dist/types/index.d.ts +1 -0
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/index.js +1 -0
- package/dist/types/organization.d.ts +13 -4
- package/dist/types/organization.d.ts.map +1 -1
- package/dist/types/schemas.d.ts +71 -0
- package/dist/types/schemas.d.ts.map +1 -1
- package/dist/types/sidebar.d.ts +1 -1
- package/dist/types/sidebar.d.ts.map +1 -1
- package/dist/utils/mime-detect.d.ts.map +1 -1
- package/dist/utils/mime-detect.js +28 -6
- package/package.json +6 -2
|
@@ -10,6 +10,8 @@
|
|
|
10
10
|
import { Rule } from '../../field-validation/rule';
|
|
11
11
|
import { hasUnpublishedChanges } from '../../utils/content-hash';
|
|
12
12
|
import { setSchemaContext } from '../../schema-context.svelte';
|
|
13
|
+
import { setSaveStateContext } from '../../save-state-context.svelte';
|
|
14
|
+
import { usePermissions } from '../../permissions-context.svelte';
|
|
13
15
|
import { getDefaultValueForFieldType } from '../../utils/field-defaults';
|
|
14
16
|
import { cmsLogger } from '../../utils/logger';
|
|
15
17
|
import { toast } from 'svelte-sonner';
|
|
@@ -26,9 +28,16 @@
|
|
|
26
28
|
onAutoSaved?: (documentId: string, title: string) => void;
|
|
27
29
|
onDeleted?: () => void;
|
|
28
30
|
onPublished?: (documentId: string) => void;
|
|
31
|
+
onUnpublished?: (documentId: string) => void;
|
|
32
|
+
onRestored?: (documentId: string) => void;
|
|
29
33
|
onOpenReference?: (documentId: string, documentType: string) => void;
|
|
30
34
|
onOpenVersionHistory?: (documentId: string) => void;
|
|
31
|
-
externalVersionPreview?: {
|
|
35
|
+
externalVersionPreview?: {
|
|
36
|
+
versionNumber: number;
|
|
37
|
+
data: Record<string, any>;
|
|
38
|
+
eventType: string;
|
|
39
|
+
createdAt?: string;
|
|
40
|
+
} | null;
|
|
32
41
|
isReadOnly?: boolean;
|
|
33
42
|
}
|
|
34
43
|
|
|
@@ -42,6 +51,8 @@
|
|
|
42
51
|
onAutoSaved,
|
|
43
52
|
onDeleted,
|
|
44
53
|
onPublished,
|
|
54
|
+
onUnpublished,
|
|
55
|
+
onRestored,
|
|
45
56
|
onOpenReference,
|
|
46
57
|
onOpenVersionHistory,
|
|
47
58
|
externalVersionPreview = null,
|
|
@@ -51,6 +62,27 @@
|
|
|
51
62
|
// Set schema context for child components (ArrayField, etc.)
|
|
52
63
|
setSchemaContext(schemas);
|
|
53
64
|
|
|
65
|
+
// Read permissions from the AdminApp context for per-action gating.
|
|
66
|
+
// `isReadOnly` prop, when explicitly true, forces everything off — this is
|
|
67
|
+
// the escape hatch for hosts that embed the editor outside the RBAC shell
|
|
68
|
+
// (version preview, public previews, etc).
|
|
69
|
+
const perms = usePermissions();
|
|
70
|
+
const canCreate = $derived(!isReadOnly && perms.can('document.create'));
|
|
71
|
+
const canUpdate = $derived(!isReadOnly && perms.can('document.update'));
|
|
72
|
+
const canDelete = $derived(!isReadOnly && perms.can('document.delete'));
|
|
73
|
+
const canPublishDoc = $derived(!isReadOnly && perms.can('document.publish'));
|
|
74
|
+
const canUnpublishDoc = $derived(!isReadOnly && perms.can('document.unpublish'));
|
|
75
|
+
// Whichever capability matters for the *current* document state:
|
|
76
|
+
// - create mode (no id yet) → document.create
|
|
77
|
+
// - edit mode → document.update
|
|
78
|
+
// Used for field editability + auto-save gating.
|
|
79
|
+
const canWriteCurrentDoc = $derived(isCreating ? canCreate : canUpdate);
|
|
80
|
+
// "Show no mutation UI at all" — when true, render the read-only badge in
|
|
81
|
+
// place of the publish button cluster.
|
|
82
|
+
const isViewingReadOnly = $derived(
|
|
83
|
+
isReadOnly || (!canCreate && !canUpdate && !canDelete && !canPublishDoc && !canUnpublishDoc)
|
|
84
|
+
);
|
|
85
|
+
|
|
54
86
|
// Schema and document state
|
|
55
87
|
let schema = $state<SchemaType | null>(null);
|
|
56
88
|
let schemaLoading = $state(false);
|
|
@@ -64,15 +96,56 @@
|
|
|
64
96
|
let lastSaved = $state<Date | null>(null);
|
|
65
97
|
let publishSuccess = $state<Date | null>(null);
|
|
66
98
|
|
|
67
|
-
|
|
68
99
|
// Perspective toggle
|
|
69
100
|
let perspective = $state<'draft' | 'published'>('draft');
|
|
70
101
|
let publishedData = $state<Record<string, any> | null>(null);
|
|
71
102
|
const isViewingPublished = $derived(perspective === 'published');
|
|
72
103
|
|
|
104
|
+
// Field-level access: mirrors the server's strip/drop logic so the form
|
|
105
|
+
// only shows fields the user can see and disables fields they can't write.
|
|
106
|
+
function fieldRoleAllowed(list: string[] | undefined): boolean {
|
|
107
|
+
if (!list) return true;
|
|
108
|
+
const role = perms.role;
|
|
109
|
+
return role !== null && list.includes(role);
|
|
110
|
+
}
|
|
111
|
+
const hiddenFieldNames = $derived(
|
|
112
|
+
new Set(
|
|
113
|
+
(schema?.fields ?? []).filter((f) => !fieldRoleAllowed(f.access?.read)).map((f) => f.name)
|
|
114
|
+
)
|
|
115
|
+
);
|
|
116
|
+
function isFieldReadonly(fieldName: string): boolean {
|
|
117
|
+
const field = schema?.fields.find((f) => f.name === fieldName);
|
|
118
|
+
// In create mode, collection-level write ability is document.create;
|
|
119
|
+
// in edit mode it's document.update. Per-field `access.update` still
|
|
120
|
+
// wins as an additional restriction when the user IS able to write.
|
|
121
|
+
if (!field) return !canWriteCurrentDoc;
|
|
122
|
+
if (!fieldRoleAllowed(field.access?.update)) return true;
|
|
123
|
+
return !canWriteCurrentDoc;
|
|
124
|
+
}
|
|
125
|
+
|
|
73
126
|
// Inspect modal
|
|
74
127
|
let showInspect = $state(false);
|
|
75
128
|
|
|
129
|
+
// Field group tabs — only render when schema declares `groups`.
|
|
130
|
+
// 'all' = show every field; otherwise filter to fields whose `group`
|
|
131
|
+
// matches (supports string or string[]).
|
|
132
|
+
let activeGroup = $state<string>('all');
|
|
133
|
+
|
|
134
|
+
$effect(() => {
|
|
135
|
+
if (!schema?.groups?.length) {
|
|
136
|
+
activeGroup = 'all';
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
const defaultGroup = schema.groups.find((g) => g.default && !g.hidden);
|
|
140
|
+
activeGroup = defaultGroup?.name ?? 'all';
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
function fieldInGroup(field: { group?: string | string[] }, groupName: string): boolean {
|
|
144
|
+
if (groupName === 'all') return true;
|
|
145
|
+
if (!field.group) return false;
|
|
146
|
+
return Array.isArray(field.group) ? field.group.includes(groupName) : field.group === groupName;
|
|
147
|
+
}
|
|
148
|
+
|
|
76
149
|
function syntaxHighlightJson(json: string): string {
|
|
77
150
|
return json.replace(
|
|
78
151
|
/("(\\u[a-zA-Z0-9]{4}|\\[^u]|[^\\"])*"(\s*:)?|\b(true|false|null)\b|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?)/g,
|
|
@@ -106,7 +179,12 @@
|
|
|
106
179
|
|
|
107
180
|
// Version history
|
|
108
181
|
let showVersionHistory = $state(false);
|
|
109
|
-
let previewingVersion = $state<{
|
|
182
|
+
let previewingVersion = $state<{
|
|
183
|
+
versionNumber: number;
|
|
184
|
+
data: Record<string, any>;
|
|
185
|
+
eventType: string;
|
|
186
|
+
createdAt?: string;
|
|
187
|
+
} | null>(null);
|
|
110
188
|
const activePreview = $derived(externalVersionPreview || previewingVersion);
|
|
111
189
|
const isPreviewingVersion = $derived(!!activePreview);
|
|
112
190
|
|
|
@@ -140,22 +218,42 @@
|
|
|
140
218
|
let hasUnsavedChanges = $state(false);
|
|
141
219
|
let autoSaveTimer: ReturnType<typeof setTimeout> | null = null;
|
|
142
220
|
let hasValidationErrors = $state(false);
|
|
221
|
+
// Snapshot of the default-initialized data for a brand-new (unsaved) document.
|
|
222
|
+
// Used to detect real user edits without relying on "is the value truthy" heuristics,
|
|
223
|
+
// so unchecking a boolean counts as a change and booleans with initialValue: true
|
|
224
|
+
// don't auto-create the doc on mount.
|
|
225
|
+
let initialDocumentData: Record<string, any> | null = null;
|
|
226
|
+
|
|
227
|
+
// Share save state with nested components (e.g. ObjectModal) via context
|
|
228
|
+
setSaveStateContext({
|
|
229
|
+
get saving() {
|
|
230
|
+
return saving;
|
|
231
|
+
},
|
|
232
|
+
get hasUnsavedChanges() {
|
|
233
|
+
return hasUnsavedChanges;
|
|
234
|
+
},
|
|
235
|
+
get savedAgoText() {
|
|
236
|
+
return savedAgoText;
|
|
237
|
+
}
|
|
238
|
+
});
|
|
143
239
|
|
|
144
240
|
let orphanedFields = $state<OrphanedField[]>([]);
|
|
145
241
|
let showOrphanedFields = $state(false);
|
|
146
242
|
let schemaFields: SchemaField[] = [];
|
|
147
243
|
|
|
148
244
|
// Track previous document to detect actual switches (not create→edit transitions)
|
|
149
|
-
|
|
150
|
-
let
|
|
245
|
+
// Plain lets (not $state) — only read/written inside the clearing effect; no need for reactivity
|
|
246
|
+
let previousDocumentId: string | null | undefined = undefined;
|
|
247
|
+
let previousDocumentType: string | undefined = undefined;
|
|
151
248
|
let justCreatedDocument = $state(false); // Flag to skip loadDocumentData after creation
|
|
152
249
|
|
|
153
250
|
// Hash-based state tracking
|
|
154
251
|
const hasUnpublishedContent = $derived(
|
|
155
252
|
hasUnpublishedChanges(documentData, fullDocument?._meta?.publishedHash || null)
|
|
156
253
|
);
|
|
254
|
+
const isUnpublished = $derived(fullDocument?._meta?.status === 'unpublished');
|
|
157
255
|
const canPublish = $derived(
|
|
158
|
-
hasUnpublishedContent && !saving && documentId && !hasValidationErrors
|
|
256
|
+
(hasUnpublishedContent || isUnpublished) && !saving && documentId && !hasValidationErrors
|
|
159
257
|
);
|
|
160
258
|
|
|
161
259
|
// Get preview title based on schema config
|
|
@@ -186,6 +284,7 @@
|
|
|
186
284
|
// Clear all state immediately to prevent old data from being auto-saved
|
|
187
285
|
documentData = {};
|
|
188
286
|
fullDocument = null;
|
|
287
|
+
initialDocumentData = null;
|
|
189
288
|
hasUnsavedChanges = false;
|
|
190
289
|
saveError = null;
|
|
191
290
|
lastSaved = null;
|
|
@@ -291,7 +390,7 @@
|
|
|
291
390
|
// With LocalAPI, data is flattened at top level (not in draftData)
|
|
292
391
|
// Extract all fields except id and _meta
|
|
293
392
|
const { id: _id, _meta, ...data } = response.data;
|
|
294
|
-
cmsLogger.debug('[Document Editor', '📄 Full response.data:', _id)
|
|
393
|
+
cmsLogger.debug('[Document Editor', '📄 Full response.data:', _id);
|
|
295
394
|
cmsLogger.debug('[Document Editor]', '📄 Full response.data:', response.data);
|
|
296
395
|
cmsLogger.debug('[Document Editor]', '📄 Extracted data (after destructuring):', data);
|
|
297
396
|
cmsLogger.debug('[Document Editor]', '📄 Keys in extracted data:', Object.keys(data));
|
|
@@ -362,6 +461,7 @@
|
|
|
362
461
|
}
|
|
363
462
|
|
|
364
463
|
documentData = initialData;
|
|
464
|
+
initialDocumentData = structuredClone(initialData);
|
|
365
465
|
fullDocument = null;
|
|
366
466
|
hasUnsavedChanges = false;
|
|
367
467
|
lastSaved = null;
|
|
@@ -369,29 +469,21 @@
|
|
|
369
469
|
cmsLogger.debug('[Document Editor]', '✅ Document initialized with:', initialData);
|
|
370
470
|
}
|
|
371
471
|
|
|
372
|
-
// Check if
|
|
373
|
-
function hasMeaningfulContent(data: Record<string, any>): boolean {
|
|
374
|
-
return Object.values(data).some((value) => {
|
|
375
|
-
if (typeof value === 'string') return value.trim() !== '';
|
|
376
|
-
if (typeof value === 'boolean') return value !== false; // Assuming false is default
|
|
377
|
-
if (Array.isArray(value)) return value.length > 0;
|
|
378
|
-
if (typeof value === 'object' && value !== null) return Object.keys(value).length > 0;
|
|
379
|
-
return value !== null && value !== undefined && value !== '';
|
|
380
|
-
});
|
|
381
|
-
}
|
|
382
|
-
|
|
383
|
-
// Check if current data differs from last saved draft
|
|
472
|
+
// Check if current data differs from last saved draft (or initial defaults for a new doc)
|
|
384
473
|
function hasChangesFromSaved(): boolean {
|
|
385
|
-
|
|
474
|
+
const baseline = fullDocument
|
|
475
|
+
? (() => {
|
|
476
|
+
const { id, _meta, ...savedData } = fullDocument;
|
|
477
|
+
return savedData;
|
|
478
|
+
})()
|
|
479
|
+
: initialDocumentData;
|
|
386
480
|
|
|
387
|
-
|
|
388
|
-
const { id, _meta, ...savedData } = fullDocument;
|
|
481
|
+
if (!baseline) return true;
|
|
389
482
|
|
|
390
|
-
// Compare current documentData with saved data using stable JSON
|
|
391
483
|
const currentJson = JSON.stringify(sortObjectForComparison(documentData));
|
|
392
|
-
const
|
|
484
|
+
const baselineJson = JSON.stringify(sortObjectForComparison(baseline));
|
|
393
485
|
|
|
394
|
-
return currentJson !==
|
|
486
|
+
return currentJson !== baselineJson;
|
|
395
487
|
}
|
|
396
488
|
|
|
397
489
|
// Helper to recursively sort object keys for stable comparison
|
|
@@ -416,24 +508,18 @@
|
|
|
416
508
|
|
|
417
509
|
// Watch for changes to trigger auto-save (debounced)
|
|
418
510
|
$effect(() => {
|
|
419
|
-
const hasContent = hasMeaningfulContent(documentData);
|
|
420
511
|
const hasChanges = hasChangesFromSaved();
|
|
421
512
|
|
|
422
|
-
|
|
423
|
-
if (hasContent && hasChanges) {
|
|
424
|
-
hasUnsavedChanges = true;
|
|
425
|
-
} else if (!hasChanges) {
|
|
426
|
-
// If there are no changes from saved, mark as not having unsaved changes
|
|
427
|
-
hasUnsavedChanges = false;
|
|
428
|
-
}
|
|
513
|
+
hasUnsavedChanges = hasChanges;
|
|
429
514
|
|
|
430
515
|
if (autoSaveTimer) {
|
|
431
516
|
clearTimeout(autoSaveTimer);
|
|
432
517
|
}
|
|
433
518
|
|
|
434
|
-
// Debounced auto-save
|
|
435
|
-
//
|
|
436
|
-
|
|
519
|
+
// Debounced auto-save. Comparing against the initial-defaults snapshot (for new
|
|
520
|
+
// docs) or the saved draft (for existing docs) means any real user edit triggers
|
|
521
|
+
// save — including unchecking a boolean or clearing a field.
|
|
522
|
+
if (hasChanges && schema && canWriteCurrentDoc) {
|
|
437
523
|
autoSaveTimer = setTimeout(() => {
|
|
438
524
|
cmsLogger.debug(
|
|
439
525
|
'[Document Editor]',
|
|
@@ -556,10 +642,22 @@
|
|
|
556
642
|
if (!documentId || saving) return;
|
|
557
643
|
|
|
558
644
|
// Check for validation errors before publishing (Sanity-style)
|
|
559
|
-
await validateAllFields();
|
|
560
|
-
|
|
561
|
-
if (
|
|
562
|
-
|
|
645
|
+
const invalid = await validateAllFields();
|
|
646
|
+
|
|
647
|
+
if (invalid.length > 0) {
|
|
648
|
+
// Show up to three field titles in the toast so the user doesn't
|
|
649
|
+
// have to hunt for the red outline — longer lists are summarised.
|
|
650
|
+
const preview = invalid
|
|
651
|
+
.slice(0, 3)
|
|
652
|
+
.map((f) => f.title)
|
|
653
|
+
.join(', ');
|
|
654
|
+
const remainder = invalid.length > 3 ? ` and ${invalid.length - 3} more` : '';
|
|
655
|
+
const detail = invalid.map((f) => `${f.title}: ${f.messages.join(', ')}`).join('\n');
|
|
656
|
+
|
|
657
|
+
saveError = `Cannot publish — fix: ${preview}${remainder}`;
|
|
658
|
+
toast.error(`Fix ${invalid.length} field${invalid.length === 1 ? '' : 's'} to publish`, {
|
|
659
|
+
description: detail
|
|
660
|
+
});
|
|
563
661
|
return;
|
|
564
662
|
}
|
|
565
663
|
|
|
@@ -643,8 +741,14 @@
|
|
|
643
741
|
try {
|
|
644
742
|
const response = await documents.unpublish(documentId);
|
|
645
743
|
if (response.success) {
|
|
646
|
-
fullDocument = {
|
|
744
|
+
fullDocument = {
|
|
745
|
+
...fullDocument,
|
|
746
|
+
_meta: { ...fullDocument?._meta, status: 'unpublished' }
|
|
747
|
+
};
|
|
647
748
|
toast.success('Document unpublished — you can re-publish anytime');
|
|
749
|
+
if (onUnpublished && documentId) {
|
|
750
|
+
onUnpublished(documentId);
|
|
751
|
+
}
|
|
648
752
|
} else {
|
|
649
753
|
throw new Error(response.error || 'Failed to unpublish');
|
|
650
754
|
}
|
|
@@ -656,13 +760,15 @@
|
|
|
656
760
|
}
|
|
657
761
|
|
|
658
762
|
// Validate all fields before publishing
|
|
659
|
-
async function validateAllFields(): Promise<
|
|
763
|
+
async function validateAllFields(): Promise<
|
|
764
|
+
Array<{ name: string; title: string; messages: string[] }>
|
|
765
|
+
> {
|
|
660
766
|
if (!schema) {
|
|
661
767
|
hasValidationErrors = false;
|
|
662
|
-
return;
|
|
768
|
+
return [];
|
|
663
769
|
}
|
|
664
770
|
|
|
665
|
-
|
|
771
|
+
const invalid: Array<{ name: string; title: string; messages: string[] }> = [];
|
|
666
772
|
|
|
667
773
|
for (const field of schema.fields) {
|
|
668
774
|
if (field.validation) {
|
|
@@ -671,26 +777,36 @@
|
|
|
671
777
|
? field.validation
|
|
672
778
|
: [field.validation];
|
|
673
779
|
|
|
780
|
+
const messages: string[] = [];
|
|
674
781
|
for (const validationFn of validationFunctions) {
|
|
675
782
|
const rule = validationFn(new Rule());
|
|
676
783
|
const markers = await rule.validate(documentData[field.name], { path: [field.name] });
|
|
677
784
|
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
cmsLogger.debug(
|
|
681
|
-
`[DocumentEditor] Validation error in field '${field.name}':`,
|
|
682
|
-
markers
|
|
683
|
-
);
|
|
785
|
+
for (const m of markers) {
|
|
786
|
+
if (m.level === 'error') messages.push(m.message ?? 'Invalid');
|
|
684
787
|
}
|
|
685
788
|
}
|
|
789
|
+
|
|
790
|
+
if (messages.length > 0) {
|
|
791
|
+
invalid.push({ name: field.name, title: field.title ?? field.name, messages });
|
|
792
|
+
cmsLogger.debug(
|
|
793
|
+
`[DocumentEditor] Validation error in field '${field.name}':`,
|
|
794
|
+
messages
|
|
795
|
+
);
|
|
796
|
+
}
|
|
686
797
|
} catch (error) {
|
|
687
|
-
|
|
688
|
-
|
|
798
|
+
invalid.push({
|
|
799
|
+
name: field.name,
|
|
800
|
+
title: field.title ?? field.name,
|
|
801
|
+
messages: ['Validation check threw an error']
|
|
802
|
+
});
|
|
803
|
+
toast.error(`Validation failed for field "${field.title ?? field.name}"`);
|
|
689
804
|
}
|
|
690
805
|
}
|
|
691
806
|
}
|
|
692
807
|
|
|
693
|
-
hasValidationErrors =
|
|
808
|
+
hasValidationErrors = invalid.length > 0;
|
|
809
|
+
return invalid;
|
|
694
810
|
}
|
|
695
811
|
|
|
696
812
|
async function deleteDocument() {
|
|
@@ -783,264 +899,386 @@
|
|
|
783
899
|
}
|
|
784
900
|
</script>
|
|
785
901
|
|
|
786
|
-
<div class="relative flex h-full flex-col overflow-hidden">
|
|
787
|
-
<!-- Header
|
|
788
|
-
<div class="
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
<div class="
|
|
792
|
-
<
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
902
|
+
<div class="relative flex h-full w-full min-w-0 flex-col overflow-hidden">
|
|
903
|
+
<!-- Hero Header -->
|
|
904
|
+
<div class="bg-background w-full min-w-0 overflow-x-clip px-4 pt-4 pb-5 lg:px-6 lg:pt-5">
|
|
905
|
+
<div class="mx-auto w-full max-w-3xl">
|
|
906
|
+
<!-- Top row: breadcrumb + actions -->
|
|
907
|
+
<div class="mb-4 flex items-center justify-between gap-3">
|
|
908
|
+
<div
|
|
909
|
+
class="text-muted-foreground flex min-w-0 items-center gap-2 text-[11px] font-medium tracking-wider uppercase"
|
|
910
|
+
>
|
|
911
|
+
<span class="whitespace-nowrap">{schema?.title || documentType}</span>
|
|
912
|
+
<span aria-hidden="true">·</span>
|
|
913
|
+
<span class="truncate">{getPreviewTitle()}</span>
|
|
914
|
+
</div>
|
|
915
|
+
|
|
916
|
+
<div class="flex shrink-0 items-center gap-2">
|
|
796
917
|
{#if saving}
|
|
797
|
-
<span
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
918
|
+
<span
|
|
919
|
+
class="text-muted-foreground hidden items-center gap-1.5 text-[10px] font-medium tracking-wider whitespace-nowrap uppercase sm:inline-flex"
|
|
920
|
+
>
|
|
921
|
+
<span class="bg-muted-foreground/60 h-1.5 w-1.5 animate-pulse rounded-full"></span>
|
|
922
|
+
Saving
|
|
801
923
|
</span>
|
|
802
924
|
{:else if hasUnsavedChanges}
|
|
803
|
-
<span
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
925
|
+
<span
|
|
926
|
+
class="text-muted-foreground hidden items-center gap-1.5 text-[10px] font-medium tracking-wider whitespace-nowrap uppercase sm:inline-flex"
|
|
927
|
+
>
|
|
928
|
+
<span class="bg-muted-foreground/60 h-1.5 w-1.5 rounded-full"></span>
|
|
929
|
+
Unsaved
|
|
930
|
+
</span>
|
|
931
|
+
{:else if savedAgoText}
|
|
932
|
+
<span
|
|
933
|
+
class="text-muted-foreground hidden items-center gap-1.5 text-[10px] font-medium tracking-wider whitespace-nowrap uppercase sm:inline-flex"
|
|
934
|
+
>
|
|
935
|
+
<span class="bg-muted-foreground/60 h-1.5 w-1.5 rounded-full"></span>
|
|
936
|
+
Auto-saved
|
|
812
937
|
</span>
|
|
813
938
|
{/if}
|
|
814
|
-
</div>
|
|
815
|
-
</div>
|
|
816
|
-
</div>
|
|
817
|
-
|
|
818
|
-
<!-- Right side: Perspective toggle, actions, close -->
|
|
819
|
-
<div class="flex items-center gap-2">
|
|
820
|
-
<!-- Perspective toggle -->
|
|
821
|
-
{#if documentId && fullDocument?._meta?.publishedHash}
|
|
822
|
-
<div class="flex rounded-md border">
|
|
823
|
-
<button
|
|
824
|
-
class="px-2.5 py-1 text-xs font-medium transition-colors {perspective === 'draft' ? 'bg-primary text-primary-foreground' : 'hover:bg-muted'}"
|
|
825
|
-
onclick={() => switchPerspective('draft')}
|
|
826
|
-
>
|
|
827
|
-
Draft
|
|
828
|
-
</button>
|
|
829
|
-
<button
|
|
830
|
-
class="px-2.5 py-1 text-xs font-medium transition-colors {perspective === 'published' ? 'bg-primary text-primary-foreground' : 'hover:bg-muted'}"
|
|
831
|
-
onclick={() => switchPerspective('published')}
|
|
832
|
-
>
|
|
833
|
-
Published
|
|
834
|
-
</button>
|
|
835
|
-
</div>
|
|
836
|
-
{/if}
|
|
837
|
-
|
|
838
939
|
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
size="icon"
|
|
845
|
-
onclick={() => (showHeaderMenu = !showHeaderMenu)}
|
|
846
|
-
class="h-8 w-8"
|
|
847
|
-
>
|
|
848
|
-
<Ellipsis class="h-4 w-4" />
|
|
849
|
-
</Button>
|
|
850
|
-
{#if showHeaderMenu}
|
|
851
|
-
<div class="bg-background border-border absolute right-0 top-full z-50 mt-1 min-w-[160px] rounded-md border py-1 shadow-lg">
|
|
940
|
+
{#if documentId && fullDocument?._meta?.publishedHash}
|
|
941
|
+
{@const isPublished =
|
|
942
|
+
fullDocument?._meta?.status === 'published' && fullDocument?._meta?.publishedAt}
|
|
943
|
+
{@const isUnpub = fullDocument?._meta?.status === 'unpublished'}
|
|
944
|
+
<div class="flex items-center gap-1.5">
|
|
852
945
|
<button
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
showVersionHistory = true;
|
|
859
|
-
}
|
|
860
|
-
}}
|
|
861
|
-
class="hover:bg-muted flex w-full items-center gap-2 px-3 py-2 text-left text-sm transition-colors"
|
|
946
|
+
class="inline-flex cursor-pointer items-center gap-1.5 rounded-full border px-2.5 py-1 text-[10px] font-medium tracking-wider uppercase transition-colors {perspective ===
|
|
947
|
+
'draft'
|
|
948
|
+
? 'bg-primary/90 text-primary-foreground border-transparent'
|
|
949
|
+
: 'text-muted-foreground hover:bg-muted'}"
|
|
950
|
+
onclick={() => switchPerspective('draft')}
|
|
862
951
|
>
|
|
863
|
-
<
|
|
952
|
+
<span
|
|
953
|
+
class="bg-muted-foreground/60 h-1.5 w-1.5 rounded-full {perspective === 'draft'
|
|
954
|
+
? 'bg-primary-foreground/60'
|
|
955
|
+
: ''}"
|
|
956
|
+
></span>
|
|
957
|
+
Draft
|
|
864
958
|
</button>
|
|
865
959
|
<button
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
960
|
+
class="inline-flex cursor-pointer items-center gap-1.5 rounded-full border px-2.5 py-1 text-[10px] font-medium tracking-wider uppercase transition-colors {perspective ===
|
|
961
|
+
'published'
|
|
962
|
+
? 'bg-primary text-primary-foreground border-transparent'
|
|
963
|
+
: 'text-muted-foreground hover:bg-muted'}"
|
|
964
|
+
onclick={() => switchPerspective('published')}
|
|
871
965
|
>
|
|
872
|
-
|
|
966
|
+
{#if isPublished}
|
|
967
|
+
<span
|
|
968
|
+
class="h-1.5 w-1.5 rounded-full {perspective === 'published'
|
|
969
|
+
? 'bg-primary-foreground/60'
|
|
970
|
+
: 'bg-green-500'}"
|
|
971
|
+
></span>
|
|
972
|
+
Published · {timeAgo(new Date(fullDocument._meta.publishedAt))}
|
|
973
|
+
{:else if isUnpub}
|
|
974
|
+
<span
|
|
975
|
+
class="h-1.5 w-1.5 rounded-full {perspective === 'published'
|
|
976
|
+
? 'bg-primary-foreground/60'
|
|
977
|
+
: 'bg-muted-foreground/60'}"
|
|
978
|
+
></span>
|
|
979
|
+
Unpublished
|
|
980
|
+
{:else}
|
|
981
|
+
Published
|
|
982
|
+
{/if}
|
|
873
983
|
</button>
|
|
874
984
|
</div>
|
|
875
|
-
<div
|
|
876
|
-
class="fixed inset-0 z-40"
|
|
877
|
-
onclick={() => (showHeaderMenu = false)}
|
|
878
|
-
></div>
|
|
879
985
|
{/if}
|
|
880
|
-
</div>
|
|
881
|
-
{/if}
|
|
882
|
-
|
|
883
|
-
<!-- Close button (X) - hidden on mobile -->
|
|
884
|
-
<Button
|
|
885
|
-
variant="ghost"
|
|
886
|
-
size="icon"
|
|
887
|
-
onclick={onBack}
|
|
888
|
-
class="hidden h-8 w-8 hover:cursor-pointer lg:flex"
|
|
889
|
-
title="Close"
|
|
890
|
-
>
|
|
891
|
-
<svg class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
892
|
-
<path
|
|
893
|
-
stroke-linecap="round"
|
|
894
|
-
stroke-linejoin="round"
|
|
895
|
-
stroke-width="2"
|
|
896
|
-
d="M6 18L18 6M6 6l12 12"
|
|
897
|
-
/>
|
|
898
|
-
</svg>
|
|
899
|
-
</Button>
|
|
900
|
-
</div>
|
|
901
|
-
</div>
|
|
902
986
|
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
987
|
+
{#if documentId}
|
|
988
|
+
<div class="relative">
|
|
989
|
+
<Button
|
|
990
|
+
variant="ghost"
|
|
991
|
+
size="icon"
|
|
992
|
+
onclick={() => (showHeaderMenu = !showHeaderMenu)}
|
|
993
|
+
class="h-8 w-8 cursor-pointer"
|
|
994
|
+
>
|
|
995
|
+
<Ellipsis class="h-4 w-4" />
|
|
996
|
+
</Button>
|
|
997
|
+
{#if showHeaderMenu}
|
|
998
|
+
<div
|
|
999
|
+
class="bg-background border-rule absolute top-full right-0 z-50 mt-1 min-w-[160px] rounded-md border py-1 shadow-lg"
|
|
1000
|
+
>
|
|
1001
|
+
<button
|
|
1002
|
+
onclick={() => {
|
|
1003
|
+
showHeaderMenu = false;
|
|
1004
|
+
if (onOpenVersionHistory && documentId) {
|
|
1005
|
+
onOpenVersionHistory(documentId);
|
|
1006
|
+
} else {
|
|
1007
|
+
showVersionHistory = true;
|
|
1008
|
+
}
|
|
1009
|
+
}}
|
|
1010
|
+
class="hover:bg-muted flex w-full cursor-pointer items-center gap-2 px-3 py-2 text-left text-sm transition-colors"
|
|
1011
|
+
>
|
|
1012
|
+
<History class="h-3.5 w-3.5" /> History
|
|
1013
|
+
</button>
|
|
1014
|
+
<button
|
|
1015
|
+
onclick={() => {
|
|
1016
|
+
showHeaderMenu = false;
|
|
1017
|
+
showInspect = true;
|
|
1018
|
+
}}
|
|
1019
|
+
class="hover:bg-muted flex w-full cursor-pointer items-center gap-2 px-3 py-2 text-left text-sm transition-colors"
|
|
1020
|
+
>
|
|
1021
|
+
<Code class="h-3.5 w-3.5" /> Inspect
|
|
1022
|
+
</button>
|
|
1023
|
+
</div>
|
|
1024
|
+
<div class="fixed inset-0 z-40" onclick={() => (showHeaderMenu = false)}></div>
|
|
1025
|
+
{/if}
|
|
1026
|
+
</div>
|
|
1027
|
+
{/if}
|
|
910
1028
|
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
{:else if schema}
|
|
920
|
-
<!-- Orphaned Fields Warning -->
|
|
921
|
-
{#if showOrphanedFields && orphanedFields.length > 0}
|
|
922
|
-
<div class="space-y-3 rounded-md border border-orange-200 bg-orange-50 p-4">
|
|
923
|
-
<div class="flex items-center gap-2">
|
|
924
|
-
<svg
|
|
925
|
-
class="h-5 w-5 text-orange-600"
|
|
926
|
-
fill="none"
|
|
927
|
-
viewBox="0 0 24 24"
|
|
928
|
-
stroke="currentColor"
|
|
929
|
-
>
|
|
1029
|
+
<Button
|
|
1030
|
+
variant="ghost"
|
|
1031
|
+
size="icon"
|
|
1032
|
+
onclick={onBack}
|
|
1033
|
+
class="hidden h-8 w-8 hover:cursor-pointer lg:flex"
|
|
1034
|
+
title="Close"
|
|
1035
|
+
>
|
|
1036
|
+
<svg class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
930
1037
|
<path
|
|
931
1038
|
stroke-linecap="round"
|
|
932
1039
|
stroke-linejoin="round"
|
|
933
1040
|
stroke-width="2"
|
|
934
|
-
d="
|
|
1041
|
+
d="M6 18L18 6M6 6l12 12"
|
|
935
1042
|
/>
|
|
936
1043
|
</svg>
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
</div>
|
|
1044
|
+
</Button>
|
|
1045
|
+
</div>
|
|
1046
|
+
</div>
|
|
941
1047
|
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
1048
|
+
<!-- Title -->
|
|
1049
|
+
<h1 class="block w-full min-w-0 truncate text-3xl font-semibold tracking-tight">
|
|
1050
|
+
{getPreviewTitle()}
|
|
1051
|
+
</h1>
|
|
1052
|
+
|
|
1053
|
+
<!-- Mobile-only status row: save state + draft pill for narrow viewports -->
|
|
1054
|
+
<div class="mt-3 flex items-center justify-between gap-3 sm:hidden">
|
|
1055
|
+
<div class="flex flex-wrap items-center gap-2">
|
|
1056
|
+
{#if !fullDocument?._meta?.publishedHash && documentId && fullDocument?._meta?.status !== 'unpublished'}
|
|
1057
|
+
<span
|
|
1058
|
+
class="text-muted-foreground inline-flex items-center gap-1.5 rounded-full border px-2.5 py-1 text-[10px] font-medium tracking-wider uppercase"
|
|
1059
|
+
>
|
|
1060
|
+
<span class="bg-muted-foreground/60 h-1.5 w-1.5 rounded-full"></span>
|
|
1061
|
+
Draft
|
|
1062
|
+
</span>
|
|
1063
|
+
{/if}
|
|
1064
|
+
</div>
|
|
945
1065
|
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
1066
|
+
{#if saving}
|
|
1067
|
+
<span
|
|
1068
|
+
class="text-muted-foreground inline-flex items-center gap-1.5 text-[10px] font-medium tracking-wider whitespace-nowrap uppercase"
|
|
1069
|
+
>
|
|
1070
|
+
<span class="bg-muted-foreground/60 h-1.5 w-1.5 animate-pulse rounded-full"></span>
|
|
1071
|
+
Saving
|
|
1072
|
+
</span>
|
|
1073
|
+
{:else if hasUnsavedChanges}
|
|
1074
|
+
<span
|
|
1075
|
+
class="text-muted-foreground inline-flex items-center gap-1.5 text-[10px] font-medium tracking-wider whitespace-nowrap uppercase"
|
|
1076
|
+
>
|
|
1077
|
+
<span class="bg-muted-foreground/60 h-1.5 w-1.5 rounded-full"></span>
|
|
1078
|
+
Unsaved
|
|
1079
|
+
</span>
|
|
1080
|
+
{:else if savedAgoText}
|
|
1081
|
+
<span
|
|
1082
|
+
class="text-muted-foreground inline-flex items-center gap-1.5 text-[10px] font-medium tracking-wider whitespace-nowrap uppercase"
|
|
1083
|
+
>
|
|
1084
|
+
<span class="bg-muted-foreground/60 h-1.5 w-1.5 rounded-full"></span>
|
|
1085
|
+
Auto-saved
|
|
1086
|
+
</span>
|
|
1087
|
+
{/if}
|
|
1088
|
+
</div>
|
|
1089
|
+
</div>
|
|
1090
|
+
</div>
|
|
1091
|
+
|
|
1092
|
+
<!-- Content Form -->
|
|
1093
|
+
<div class="flex-1 overflow-auto p-4 lg:p-6">
|
|
1094
|
+
<div class="mx-auto w-full max-w-3xl space-y-4 lg:space-y-6">
|
|
1095
|
+
{#if saveError}
|
|
1096
|
+
<div class="bg-destructive/10 border-destructive/20 rounded-md border p-3">
|
|
1097
|
+
<p class="text-destructive text-sm">{saveError}</p>
|
|
1098
|
+
</div>
|
|
1099
|
+
{/if}
|
|
1100
|
+
|
|
1101
|
+
{#if schemaError}
|
|
1102
|
+
<div class="bg-destructive/10 border-destructive/20 rounded-md border p-3">
|
|
1103
|
+
<p class="text-destructive text-sm">Schema Error: {schemaError}</p>
|
|
1104
|
+
</div>
|
|
1105
|
+
{:else if schemaLoading}
|
|
1106
|
+
<div class="p-6 text-center">
|
|
1107
|
+
<div class="text-muted-foreground text-sm">Loading schema...</div>
|
|
1108
|
+
</div>
|
|
1109
|
+
{:else if schema}
|
|
1110
|
+
<!-- Orphaned Fields Warning -->
|
|
1111
|
+
{#if showOrphanedFields && orphanedFields.length > 0}
|
|
1112
|
+
<div class="space-y-3 rounded-md border border-orange-200 bg-orange-50 p-4">
|
|
1113
|
+
<div class="flex items-center gap-2">
|
|
1114
|
+
<svg
|
|
1115
|
+
class="h-5 w-5 text-orange-600"
|
|
1116
|
+
fill="none"
|
|
1117
|
+
viewBox="0 0 24 24"
|
|
1118
|
+
stroke="currentColor"
|
|
950
1119
|
>
|
|
951
|
-
<
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
1120
|
+
<path
|
|
1121
|
+
stroke-linecap="round"
|
|
1122
|
+
stroke-linejoin="round"
|
|
1123
|
+
stroke-width="2"
|
|
1124
|
+
d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-2.5L13.732 4c-.77-.833-1.964-.833-2.732 0L5.081 16.5c-.77.833.192 2.5 1.732 2.5z"
|
|
1125
|
+
/>
|
|
1126
|
+
</svg>
|
|
1127
|
+
<h4 class="text-sm font-medium text-orange-800">
|
|
1128
|
+
{orphanedFields.length} orphaned field{orphanedFields.length === 1 ? '' : 's'} detected
|
|
1129
|
+
</h4>
|
|
1130
|
+
</div>
|
|
1131
|
+
|
|
1132
|
+
<p class="text-sm text-orange-700">
|
|
1133
|
+
These fields exist in your document but are no longer defined in the schema:
|
|
1134
|
+
</p>
|
|
1135
|
+
|
|
1136
|
+
<div class="space-y-2">
|
|
1137
|
+
{#each orphanedFields as field, index (index)}
|
|
1138
|
+
<div
|
|
1139
|
+
class="flex items-center justify-between rounded border border-orange-200 bg-white p-3"
|
|
1140
|
+
>
|
|
1141
|
+
<div class="flex-1">
|
|
1142
|
+
<div class="font-mono text-sm font-medium text-orange-800">
|
|
1143
|
+
{field.path || field.key}
|
|
1144
|
+
</div>
|
|
1145
|
+
<div class="mt-1 text-xs text-orange-600">
|
|
1146
|
+
<code class="rounded bg-orange-100 px-1">{JSON.stringify(field.value)}</code>
|
|
1147
|
+
</div>
|
|
957
1148
|
</div>
|
|
1149
|
+
<Button
|
|
1150
|
+
size="sm"
|
|
1151
|
+
variant="outline"
|
|
1152
|
+
onclick={() => removeOrphanedField(field)}
|
|
1153
|
+
class="ml-3 h-8 border-red-200 px-3 text-red-600 hover:border-red-300 hover:bg-red-50"
|
|
1154
|
+
>
|
|
1155
|
+
Remove
|
|
1156
|
+
</Button>
|
|
958
1157
|
</div>
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
variant="outline"
|
|
962
|
-
onclick={() => removeOrphanedField(field)}
|
|
963
|
-
class="ml-3 h-8 border-red-200 px-3 text-red-600 hover:border-red-300 hover:bg-red-50"
|
|
964
|
-
>
|
|
965
|
-
Remove
|
|
966
|
-
</Button>
|
|
967
|
-
</div>
|
|
968
|
-
{/each}
|
|
969
|
-
</div>
|
|
1158
|
+
{/each}
|
|
1159
|
+
</div>
|
|
970
1160
|
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
1161
|
+
<div class="flex gap-2 border-t border-orange-200 pt-2">
|
|
1162
|
+
<Button
|
|
1163
|
+
size="sm"
|
|
1164
|
+
variant="outline"
|
|
1165
|
+
onclick={cleanupAllOrphanedFields}
|
|
1166
|
+
class="border-orange-600 bg-orange-600 text-white hover:bg-orange-700"
|
|
1167
|
+
>
|
|
1168
|
+
Remove All
|
|
1169
|
+
</Button>
|
|
1170
|
+
<Button
|
|
1171
|
+
size="sm"
|
|
1172
|
+
variant="ghost"
|
|
1173
|
+
onclick={dismissOrphanedFields}
|
|
1174
|
+
class="text-orange-700 hover:text-orange-800"
|
|
1175
|
+
>
|
|
1176
|
+
Dismiss
|
|
1177
|
+
</Button>
|
|
1178
|
+
</div>
|
|
988
1179
|
</div>
|
|
989
|
-
|
|
990
|
-
{/if}
|
|
1180
|
+
{/if}
|
|
991
1181
|
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
{#each schema.fields as field, index (index)}
|
|
997
|
-
{@const viewData = isPreviewingVersion && activePreview ? activePreview.data : isViewingPublished && publishedData ? publishedData : documentData}
|
|
998
|
-
<SchemaField
|
|
999
|
-
{field}
|
|
1000
|
-
value={viewData[field.name]}
|
|
1001
|
-
documentData={viewData}
|
|
1002
|
-
onUpdate={(newValue) => {
|
|
1003
|
-
if (isViewingPublished) return;
|
|
1004
|
-
documentData = { ...documentData, [field.name]: newValue };
|
|
1005
|
-
hasUnsavedChanges = true;
|
|
1006
|
-
}}
|
|
1007
|
-
{onOpenReference}
|
|
1008
|
-
schemaType={documentType}
|
|
1009
|
-
readonly={isReadOnly || isViewingPublished || isPreviewingVersion}
|
|
1010
|
-
organizationId={fullDocument?._meta?.organizationId}
|
|
1011
|
-
/>
|
|
1012
|
-
{/each}
|
|
1013
|
-
|
|
1014
|
-
{#snippet failed(error, reset)}
|
|
1015
|
-
<div class="border-destructive/30 bg-destructive/5 rounded-md border p-4 text-center">
|
|
1016
|
-
<p class="text-destructive font-medium">Document editor encountered an error</p>
|
|
1017
|
-
<p class="text-muted-foreground mt-1 text-sm">{error instanceof Error ? error.message : 'Unknown error'}</p>
|
|
1182
|
+
<!-- Field Group Tabs -->
|
|
1183
|
+
{#if schema.groups && schema.groups.length > 0}
|
|
1184
|
+
{@const visibleGroups = schema.groups.filter((g) => !g.hidden)}
|
|
1185
|
+
<div class="mb-4 flex items-center gap-1 overflow-x-auto py-1">
|
|
1018
1186
|
<button
|
|
1019
|
-
|
|
1020
|
-
onclick={
|
|
1187
|
+
type="button"
|
|
1188
|
+
onclick={() => (activeGroup = 'all')}
|
|
1189
|
+
class="cursor-pointer rounded-md px-3 py-1.5 text-sm font-medium whitespace-nowrap transition-colors {activeGroup ===
|
|
1190
|
+
'all'
|
|
1191
|
+
? 'bg-muted text-foreground'
|
|
1192
|
+
: 'text-muted-foreground hover:text-foreground'}"
|
|
1021
1193
|
>
|
|
1022
|
-
|
|
1194
|
+
All fields
|
|
1023
1195
|
</button>
|
|
1196
|
+
{#each visibleGroups as group (group.name)}
|
|
1197
|
+
<button
|
|
1198
|
+
type="button"
|
|
1199
|
+
onclick={() => (activeGroup = group.name)}
|
|
1200
|
+
class="flex cursor-pointer items-center gap-1.5 rounded-md px-3 py-1.5 text-sm font-medium whitespace-nowrap transition-colors {activeGroup ===
|
|
1201
|
+
group.name
|
|
1202
|
+
? 'bg-muted text-foreground'
|
|
1203
|
+
: 'text-muted-foreground hover:text-foreground'}"
|
|
1204
|
+
>
|
|
1205
|
+
{#if group.icon}
|
|
1206
|
+
{@const Icon = group.icon}
|
|
1207
|
+
<Icon class="h-4 w-4" />
|
|
1208
|
+
{/if}
|
|
1209
|
+
{group.title}
|
|
1210
|
+
</button>
|
|
1211
|
+
{/each}
|
|
1024
1212
|
</div>
|
|
1025
|
-
{/
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1213
|
+
{/if}
|
|
1214
|
+
|
|
1215
|
+
<!-- Dynamic Schema Fields -->
|
|
1216
|
+
<svelte:boundary
|
|
1217
|
+
onerror={(error) =>
|
|
1218
|
+
cmsLogger.error('[DocumentEditor]', 'Error in editor content:', error)}
|
|
1219
|
+
>
|
|
1220
|
+
{#each schema.fields.filter((f) => fieldInGroup(f, activeGroup) && !hiddenFieldNames.has(f.name)) as field (field.name)}
|
|
1221
|
+
{@const viewData =
|
|
1222
|
+
isPreviewingVersion && activePreview
|
|
1223
|
+
? activePreview.data
|
|
1224
|
+
: isViewingPublished && publishedData
|
|
1225
|
+
? publishedData
|
|
1226
|
+
: documentData}
|
|
1227
|
+
<SchemaField
|
|
1228
|
+
{field}
|
|
1229
|
+
value={viewData[field.name]}
|
|
1230
|
+
documentData={viewData}
|
|
1231
|
+
onUpdate={(newValue) => {
|
|
1232
|
+
if (isViewingPublished) return;
|
|
1233
|
+
documentData = { ...documentData, [field.name]: newValue };
|
|
1234
|
+
hasUnsavedChanges = true;
|
|
1235
|
+
}}
|
|
1236
|
+
{onOpenReference}
|
|
1237
|
+
schemaType={documentType}
|
|
1238
|
+
readonly={isReadOnly ||
|
|
1239
|
+
isViewingPublished ||
|
|
1240
|
+
isPreviewingVersion ||
|
|
1241
|
+
isFieldReadonly(field.name)}
|
|
1242
|
+
organizationId={fullDocument?._meta?.organizationId}
|
|
1243
|
+
/>
|
|
1244
|
+
{/each}
|
|
1245
|
+
|
|
1246
|
+
{#snippet failed(error, reset)}
|
|
1247
|
+
<div class="border-destructive/30 bg-destructive/5 rounded-md border p-4 text-center">
|
|
1248
|
+
<p class="text-destructive font-medium">Document editor encountered an error</p>
|
|
1249
|
+
<p class="text-muted-foreground mt-1 text-sm">
|
|
1250
|
+
{error instanceof Error ? error.message : 'Unknown error'}
|
|
1251
|
+
</p>
|
|
1252
|
+
<button
|
|
1253
|
+
class="bg-primary text-primary-foreground mt-3 rounded px-4 py-2 text-sm"
|
|
1254
|
+
onclick={reset}
|
|
1255
|
+
>
|
|
1256
|
+
Reload editor
|
|
1257
|
+
</button>
|
|
1258
|
+
</div>
|
|
1259
|
+
{/snippet}
|
|
1260
|
+
</svelte:boundary>
|
|
1261
|
+
{:else}
|
|
1262
|
+
<div class="border-muted-foreground/30 rounded-md border border-dashed p-4">
|
|
1263
|
+
<p class="text-muted-foreground text-center text-sm">
|
|
1264
|
+
No schema found for document type: {documentType}
|
|
1265
|
+
</p>
|
|
1266
|
+
</div>
|
|
1267
|
+
{/if}
|
|
1268
|
+
</div>
|
|
1034
1269
|
</div>
|
|
1035
1270
|
|
|
1036
1271
|
<!-- Sanity-style bottom bar -->
|
|
1037
1272
|
{#if documentId}
|
|
1038
|
-
<div class="border-
|
|
1273
|
+
<div class="border-rule bg-background relative z-50 border-t p-4">
|
|
1039
1274
|
{#if isPreviewingVersion && activePreview}
|
|
1040
1275
|
<!-- Version preview footer -->
|
|
1041
1276
|
<div class="flex items-center justify-between">
|
|
1042
1277
|
<p class="text-muted-foreground text-sm">
|
|
1043
|
-
Revision from {new Date(activePreview.createdAt || Date.now()).toLocaleString(
|
|
1278
|
+
Revision from {new Date(activePreview.createdAt || Date.now()).toLocaleString(
|
|
1279
|
+
undefined,
|
|
1280
|
+
{ month: 'short', day: 'numeric', hour: 'numeric', minute: '2-digit', hour12: true }
|
|
1281
|
+
)}
|
|
1044
1282
|
</p>
|
|
1045
1283
|
<Button
|
|
1046
1284
|
size="sm"
|
|
@@ -1068,6 +1306,9 @@
|
|
|
1068
1306
|
perspective = 'draft';
|
|
1069
1307
|
publishedData = null;
|
|
1070
1308
|
toast.success('Revision restored');
|
|
1309
|
+
if (onRestored && documentId) {
|
|
1310
|
+
onRestored(documentId);
|
|
1311
|
+
}
|
|
1071
1312
|
} catch {
|
|
1072
1313
|
toast.error('Failed to restore revision');
|
|
1073
1314
|
}
|
|
@@ -1083,78 +1324,74 @@
|
|
|
1083
1324
|
{#if fullDocument?._meta?.status === 'unpublished'}
|
|
1084
1325
|
Unpublished
|
|
1085
1326
|
{:else}
|
|
1086
|
-
Published on {fullDocument?._meta?.publishedAt
|
|
1327
|
+
Published on {fullDocument?._meta?.publishedAt
|
|
1328
|
+
? new Date(fullDocument._meta.publishedAt).toLocaleString(undefined, {
|
|
1329
|
+
month: 'short',
|
|
1330
|
+
day: 'numeric',
|
|
1331
|
+
year: 'numeric',
|
|
1332
|
+
hour: 'numeric',
|
|
1333
|
+
minute: '2-digit',
|
|
1334
|
+
hour12: true
|
|
1335
|
+
})
|
|
1336
|
+
: 'Unknown'}
|
|
1087
1337
|
{/if}
|
|
1088
1338
|
</p>
|
|
1089
1339
|
{#if fullDocument?._meta?.status === 'unpublished'}
|
|
1090
|
-
|
|
1091
|
-
size="sm"
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
>
|
|
1095
|
-
Publish
|
|
1096
|
-
</Button>
|
|
1097
|
-
{:else}
|
|
1098
|
-
<Button
|
|
1099
|
-
size="sm"
|
|
1100
|
-
variant="secondary"
|
|
1101
|
-
onclick={unpublishDocument}
|
|
1102
|
-
disabled={saving}
|
|
1103
|
-
>
|
|
1340
|
+
{#if canPublishDoc}
|
|
1341
|
+
<Button size="sm" onclick={publishDocument} disabled={saving}>Publish</Button>
|
|
1342
|
+
{/if}
|
|
1343
|
+
{:else if canUnpublishDoc}
|
|
1344
|
+
<Button size="sm" variant="secondary" onclick={unpublishDocument} disabled={saving}>
|
|
1104
1345
|
Unpublish
|
|
1105
1346
|
</Button>
|
|
1106
1347
|
{/if}
|
|
1107
1348
|
</div>
|
|
1108
1349
|
{:else}
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
{:else if hasUnsavedChanges}
|
|
1117
|
-
<Badge variant="outline">Unsaved</Badge>
|
|
1118
|
-
{:else if savedAgoText}
|
|
1119
|
-
<Badge variant="secondary">{savedAgoText}</Badge>
|
|
1120
|
-
{/if}
|
|
1121
|
-
</div>
|
|
1350
|
+
<div class="flex items-center justify-between">
|
|
1351
|
+
<!-- Left: Transient publish confirmation (save/unsaved now shown in header) -->
|
|
1352
|
+
<div class="flex items-center gap-2">
|
|
1353
|
+
{#if publishSuccess && now - publishSuccess.getTime() < 3000}
|
|
1354
|
+
<Badge variant="default">Published!</Badge>
|
|
1355
|
+
{/if}
|
|
1356
|
+
</div>
|
|
1122
1357
|
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1358
|
+
<!-- Right: Publish button + horizontal three dots menu -->
|
|
1359
|
+
<div class="flex items-center gap-2">
|
|
1360
|
+
{#if canPublishDoc && !isViewingPublished}
|
|
1361
|
+
<Button
|
|
1362
|
+
onclick={publishDocument}
|
|
1363
|
+
disabled={!canPublish}
|
|
1364
|
+
size="sm"
|
|
1365
|
+
variant={canPublish ? 'default' : 'secondary'}
|
|
1366
|
+
class="cursor-pointer"
|
|
1367
|
+
>
|
|
1368
|
+
{#if saving}
|
|
1369
|
+
Publishing...
|
|
1370
|
+
{:else if isUnpublished}
|
|
1371
|
+
Publish
|
|
1372
|
+
{:else if !hasUnpublishedContent}
|
|
1373
|
+
Published
|
|
1374
|
+
{:else}
|
|
1375
|
+
Publish Changes
|
|
1376
|
+
{/if}
|
|
1377
|
+
</Button>
|
|
1378
|
+
{:else if isViewingReadOnly}
|
|
1379
|
+
<Badge variant="secondary" class="text-xs">Read Only</Badge>
|
|
1380
|
+
{/if}
|
|
1144
1381
|
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1382
|
+
{#if canDelete}
|
|
1383
|
+
<Button
|
|
1384
|
+
variant="ghost"
|
|
1385
|
+
size="icon"
|
|
1386
|
+
class="text-muted-foreground hover:text-destructive h-8 w-8"
|
|
1387
|
+
onclick={deleteDocument}
|
|
1388
|
+
title="Delete document"
|
|
1389
|
+
>
|
|
1390
|
+
<Trash2 class="h-4 w-4" />
|
|
1391
|
+
</Button>
|
|
1392
|
+
{/if}
|
|
1393
|
+
</div>
|
|
1156
1394
|
</div>
|
|
1157
|
-
</div>
|
|
1158
1395
|
{/if}
|
|
1159
1396
|
</div>
|
|
1160
1397
|
{/if}
|
|
@@ -1165,18 +1402,29 @@
|
|
|
1165
1402
|
<!-- Backdrop -->
|
|
1166
1403
|
<button
|
|
1167
1404
|
class="flex-1 bg-black/30"
|
|
1168
|
-
onclick={() => {
|
|
1405
|
+
onclick={() => {
|
|
1406
|
+
showVersionHistory = false;
|
|
1407
|
+
previewingVersion = null;
|
|
1408
|
+
}}
|
|
1169
1409
|
></button>
|
|
1170
1410
|
<!-- Panel -->
|
|
1171
|
-
<div class="bg-background border-
|
|
1172
|
-
<div class="border-
|
|
1411
|
+
<div class="bg-background border-rule flex w-80 flex-col border-l shadow-lg">
|
|
1412
|
+
<div class="border-rule flex items-center justify-between border-b px-4 py-3">
|
|
1173
1413
|
<h3 class="text-sm font-medium">Version History</h3>
|
|
1174
1414
|
<button
|
|
1175
1415
|
class="hover:bg-muted rounded p-1 transition-colors"
|
|
1176
|
-
onclick={() => {
|
|
1416
|
+
onclick={() => {
|
|
1417
|
+
showVersionHistory = false;
|
|
1418
|
+
previewingVersion = null;
|
|
1419
|
+
}}
|
|
1177
1420
|
>
|
|
1178
1421
|
<svg class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
1179
|
-
<path
|
|
1422
|
+
<path
|
|
1423
|
+
stroke-linecap="round"
|
|
1424
|
+
stroke-linejoin="round"
|
|
1425
|
+
stroke-width="2"
|
|
1426
|
+
d="M6 18L18 6M6 6l12 12"
|
|
1427
|
+
/>
|
|
1180
1428
|
</svg>
|
|
1181
1429
|
</button>
|
|
1182
1430
|
</div>
|
|
@@ -1190,7 +1438,10 @@
|
|
|
1190
1438
|
<div class="divide-y">
|
|
1191
1439
|
{#each response.data as version}
|
|
1192
1440
|
<button
|
|
1193
|
-
class="w-full space-y-1 px-4 py-3 text-left transition-colors
|
|
1441
|
+
class="hover:bg-muted w-full space-y-1 px-4 py-3 text-left transition-colors {previewingVersion?.versionNumber ===
|
|
1442
|
+
version.versionNumber
|
|
1443
|
+
? 'bg-muted border-l-primary border-l-2'
|
|
1444
|
+
: ''}"
|
|
1194
1445
|
onclick={async () => {
|
|
1195
1446
|
try {
|
|
1196
1447
|
const res = await documents.getVersion(documentId, version.versionNumber);
|
|
@@ -1208,7 +1459,14 @@
|
|
|
1208
1459
|
>
|
|
1209
1460
|
<div class="flex items-center gap-2">
|
|
1210
1461
|
<span class="text-xs font-medium">v{version.versionNumber}</span>
|
|
1211
|
-
<Badge
|
|
1462
|
+
<Badge
|
|
1463
|
+
variant={version.eventType === 'publish'
|
|
1464
|
+
? 'default'
|
|
1465
|
+
: version.eventType === 'restore'
|
|
1466
|
+
? 'outline'
|
|
1467
|
+
: 'secondary'}
|
|
1468
|
+
class="text-[10px]"
|
|
1469
|
+
>
|
|
1212
1470
|
{version.eventType}
|
|
1213
1471
|
</Badge>
|
|
1214
1472
|
</div>
|
|
@@ -1236,33 +1494,44 @@
|
|
|
1236
1494
|
<!-- Inspect Modal -->
|
|
1237
1495
|
{#if showInspect}
|
|
1238
1496
|
<div class="absolute inset-0 z-50 flex items-center justify-center bg-black/50">
|
|
1239
|
-
<div
|
|
1497
|
+
<div
|
|
1498
|
+
class="bg-background border-rule mx-4 flex h-[80%] w-full max-w-3xl flex-col rounded-lg border shadow-xl"
|
|
1499
|
+
>
|
|
1240
1500
|
<!-- Modal header -->
|
|
1241
1501
|
<div class="flex items-center justify-between border-b px-4 py-3">
|
|
1242
1502
|
<div>
|
|
1243
1503
|
<h3 class="text-sm font-semibold">Inspecting <em>{getPreviewTitle()}</em></h3>
|
|
1244
1504
|
</div>
|
|
1245
1505
|
<Button
|
|
1246
|
-
|
|
1506
|
+
variant="ghost"
|
|
1247
1507
|
class="hover:bg-muted rounded p-1 transition-colors"
|
|
1248
1508
|
onclick={() => (showInspect = false)}
|
|
1249
1509
|
>
|
|
1250
1510
|
<svg class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
1251
|
-
<path
|
|
1511
|
+
<path
|
|
1512
|
+
stroke-linecap="round"
|
|
1513
|
+
stroke-linejoin="round"
|
|
1514
|
+
stroke-width="2"
|
|
1515
|
+
d="M6 18L18 6M6 6l12 12"
|
|
1516
|
+
/>
|
|
1252
1517
|
</svg>
|
|
1253
1518
|
</Button>
|
|
1254
1519
|
</div>
|
|
1255
1520
|
|
|
1256
1521
|
<!-- Tabs -->
|
|
1257
|
-
<div class="border-b
|
|
1522
|
+
<div class="flex border-b">
|
|
1258
1523
|
<button
|
|
1259
|
-
class="px-4 py-2 text-sm font-medium transition-colors {inspectTab === 'parsed'
|
|
1524
|
+
class="px-4 py-2 text-sm font-medium transition-colors {inspectTab === 'parsed'
|
|
1525
|
+
? 'border-primary text-foreground border-b-2'
|
|
1526
|
+
: 'text-muted-foreground hover:text-foreground'}"
|
|
1260
1527
|
onclick={() => (inspectTab = 'parsed')}
|
|
1261
1528
|
>
|
|
1262
1529
|
Parsed
|
|
1263
1530
|
</button>
|
|
1264
1531
|
<button
|
|
1265
|
-
class="px-4 py-2 text-sm font-medium transition-colors {inspectTab === 'raw'
|
|
1532
|
+
class="px-4 py-2 text-sm font-medium transition-colors {inspectTab === 'raw'
|
|
1533
|
+
? 'border-primary text-foreground border-b-2'
|
|
1534
|
+
: 'text-muted-foreground hover:text-foreground'}"
|
|
1266
1535
|
onclick={() => (inspectTab = 'raw')}
|
|
1267
1536
|
>
|
|
1268
1537
|
Raw JSON
|
|
@@ -1273,7 +1542,7 @@
|
|
|
1273
1542
|
<div class="flex-1 overflow-auto p-4 font-mono text-sm">
|
|
1274
1543
|
{#if inspectTab === 'raw'}
|
|
1275
1544
|
<pre
|
|
1276
|
-
class="
|
|
1545
|
+
class="text-xs break-all whitespace-pre-wrap select-text"
|
|
1277
1546
|
tabindex="0"
|
|
1278
1547
|
onkeydown={(e) => {
|
|
1279
1548
|
if ((e.metaKey || e.ctrlKey) && e.key === 'a') {
|
|
@@ -1285,10 +1554,19 @@
|
|
|
1285
1554
|
sel?.removeAllRanges();
|
|
1286
1555
|
sel?.addRange(range);
|
|
1287
1556
|
}
|
|
1288
|
-
}}
|
|
1289
|
-
|
|
1557
|
+
}}>{@html syntaxHighlightJson(
|
|
1558
|
+
JSON.stringify(
|
|
1559
|
+
{ id: documentId, _meta: fullDocument?._meta, ...documentData },
|
|
1560
|
+
null,
|
|
1561
|
+
2
|
|
1562
|
+
)
|
|
1563
|
+
)}</pre>
|
|
1290
1564
|
{:else}
|
|
1291
|
-
{@render parsedValue(
|
|
1565
|
+
{@render parsedValue(
|
|
1566
|
+
null,
|
|
1567
|
+
{ id: documentId, _meta: fullDocument?._meta, ...documentData },
|
|
1568
|
+
0
|
|
1569
|
+
)}
|
|
1292
1570
|
{/if}
|
|
1293
1571
|
</div>
|
|
1294
1572
|
</div>
|
|
@@ -1308,12 +1586,17 @@
|
|
|
1308
1586
|
{/if}
|
|
1309
1587
|
{/if}
|
|
1310
1588
|
{#if Array.isArray(val)}
|
|
1311
|
-
<span class="text-muted-foreground"
|
|
1589
|
+
<span class="text-muted-foreground"
|
|
1590
|
+
>[...] {val.length} {val.length === 1 ? 'item' : 'items'}</span
|
|
1591
|
+
>
|
|
1312
1592
|
{:else}
|
|
1313
|
-
<span class="text-muted-foreground"
|
|
1593
|
+
<span class="text-muted-foreground"
|
|
1594
|
+
>{...} {Object.keys(val).length}
|
|
1595
|
+
{Object.keys(val).length === 1 ? 'property' : 'properties'}</span
|
|
1596
|
+
>
|
|
1314
1597
|
{/if}
|
|
1315
1598
|
</summary>
|
|
1316
|
-
<div class="ml-4 border-l
|
|
1599
|
+
<div class="border-rule/50 ml-4 border-l pl-3">
|
|
1317
1600
|
{#if Array.isArray(val)}
|
|
1318
1601
|
{#each val as item, i}
|
|
1319
1602
|
{@render parsedValue(String(i), item, depth + 1)}
|