@aphexcms/cms-core 2.0.5 → 2.0.6
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/documents.d.ts +18 -0
- package/dist/api/documents.d.ts.map +1 -1
- package/dist/api/documents.js +22 -1
- package/dist/components/AdminApp.svelte +53 -1
- package/dist/components/AdminApp.svelte.d.ts.map +1 -1
- package/dist/components/admin/DocumentEditor.svelte +440 -51
- package/dist/components/admin/DocumentEditor.svelte.d.ts +7 -0
- package/dist/components/admin/DocumentEditor.svelte.d.ts.map +1 -1
- package/dist/components/admin/MediaBrowser.svelte +42 -13
- package/dist/components/admin/MediaBrowser.svelte.d.ts.map +1 -1
- package/dist/components/admin/fields/ArrayField.svelte +102 -2
- package/dist/components/admin/fields/ArrayField.svelte.d.ts.map +1 -1
- package/dist/db/interfaces/document.d.ts +14 -0
- package/dist/db/interfaces/document.d.ts.map +1 -1
- package/dist/db/interfaces/index.d.ts +6 -0
- package/dist/db/interfaces/index.d.ts.map +1 -1
- package/dist/graphql/resolvers.js +1 -1
- package/dist/lib/api/documents.d.ts +18 -0
- package/dist/lib/api/documents.d.ts.map +1 -1
- package/dist/lib/api/documents.js +22 -1
- package/dist/lib/api/documents.js.map +1 -1
- package/dist/lib/db/interfaces/document.d.ts +14 -0
- package/dist/lib/db/interfaces/document.d.ts.map +1 -1
- package/dist/lib/db/interfaces/index.d.ts +6 -0
- package/dist/lib/db/interfaces/index.d.ts.map +1 -1
- package/dist/lib/graphql/resolvers.js +1 -1
- package/dist/lib/graphql/resolvers.js.map +1 -1
- package/dist/lib/local-api/collection-api.d.ts +5 -1
- package/dist/lib/local-api/collection-api.d.ts.map +1 -1
- package/dist/lib/local-api/collection-api.js +26 -6
- package/dist/lib/local-api/collection-api.js.map +1 -1
- package/dist/lib/local-api/index.d.ts +2 -0
- package/dist/lib/local-api/index.d.ts.map +1 -1
- package/dist/lib/local-api/index.js +7 -2
- package/dist/lib/local-api/index.js.map +1 -1
- package/dist/lib/routes/document-versions.d.ts +5 -0
- package/dist/lib/routes/document-versions.d.ts.map +1 -0
- package/dist/lib/routes/document-versions.js +100 -0
- package/dist/lib/routes/document-versions.js.map +1 -0
- package/dist/lib/routes-exports.d.ts +1 -0
- package/dist/lib/routes-exports.d.ts.map +1 -1
- package/dist/lib/routes-exports.js +1 -0
- package/dist/lib/routes-exports.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/version-service.d.ts +39 -0
- package/dist/lib/services/version-service.d.ts.map +1 -0
- package/dist/lib/services/version-service.js +129 -0
- package/dist/lib/services/version-service.js.map +1 -0
- package/dist/lib/types/config.d.ts +7 -0
- package/dist/lib/types/config.d.ts.map +1 -1
- package/dist/lib/types/document.d.ts +2 -2
- package/dist/lib/types/document.d.ts.map +1 -1
- 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/schemas.d.ts +3 -0
- package/dist/lib/types/schemas.d.ts.map +1 -1
- package/dist/lib/types/version.d.ts +15 -0
- package/dist/lib/types/version.d.ts.map +1 -0
- package/dist/lib/types/version.js +2 -0
- package/dist/lib/types/version.js.map +1 -0
- package/dist/local-api/collection-api.d.ts +5 -1
- package/dist/local-api/collection-api.d.ts.map +1 -1
- package/dist/local-api/collection-api.js +26 -6
- package/dist/local-api/index.d.ts +2 -0
- package/dist/local-api/index.d.ts.map +1 -1
- package/dist/local-api/index.js +7 -2
- package/dist/routes/document-versions.d.ts +5 -0
- package/dist/routes/document-versions.d.ts.map +1 -0
- package/dist/routes/document-versions.js +99 -0
- package/dist/routes-exports.d.ts +1 -0
- package/dist/routes-exports.d.ts.map +1 -1
- package/dist/routes-exports.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/version-service.d.ts +39 -0
- package/dist/services/version-service.d.ts.map +1 -0
- package/dist/services/version-service.js +128 -0
- package/dist/types/config.d.ts +7 -0
- package/dist/types/config.d.ts.map +1 -1
- package/dist/types/document.d.ts +2 -2
- package/dist/types/document.d.ts.map +1 -1
- 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/schemas.d.ts +3 -0
- package/dist/types/schemas.d.ts.map +1 -1
- package/dist/types/version.d.ts +15 -0
- package/dist/types/version.d.ts.map +1 -0
- package/dist/types/version.js +1 -0
- package/package.json +1 -1
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
import elementEvents from '../../utils/element-events';
|
|
15
15
|
import { cmsLogger } from '../../utils/logger';
|
|
16
16
|
import { toast } from 'svelte-sonner';
|
|
17
|
+
import { History, EyeOff, Trash2, Ellipsis, Search, Code } from '@lucide/svelte';
|
|
17
18
|
|
|
18
19
|
interface Props {
|
|
19
20
|
schemas: SchemaType[];
|
|
@@ -26,6 +27,8 @@
|
|
|
26
27
|
onDeleted?: () => void;
|
|
27
28
|
onPublished?: (documentId: string) => void;
|
|
28
29
|
onOpenReference?: (documentId: string, documentType: string) => void;
|
|
30
|
+
onOpenVersionHistory?: (documentId: string) => void;
|
|
31
|
+
externalVersionPreview?: { versionNumber: number; data: Record<string, any>; eventType: string; createdAt?: string } | null;
|
|
29
32
|
isReadOnly?: boolean;
|
|
30
33
|
}
|
|
31
34
|
|
|
@@ -40,6 +43,8 @@
|
|
|
40
43
|
onDeleted,
|
|
41
44
|
onPublished,
|
|
42
45
|
onOpenReference,
|
|
46
|
+
onOpenVersionHistory,
|
|
47
|
+
externalVersionPreview = null,
|
|
43
48
|
isReadOnly = false
|
|
44
49
|
}: Props = $props();
|
|
45
50
|
|
|
@@ -59,6 +64,52 @@
|
|
|
59
64
|
let lastSaved = $state<Date | null>(null);
|
|
60
65
|
let publishSuccess = $state<Date | null>(null);
|
|
61
66
|
|
|
67
|
+
|
|
68
|
+
// Perspective toggle
|
|
69
|
+
let perspective = $state<'draft' | 'published'>('draft');
|
|
70
|
+
let publishedData = $state<Record<string, any> | null>(null);
|
|
71
|
+
const isViewingPublished = $derived(perspective === 'published');
|
|
72
|
+
|
|
73
|
+
// Inspect modal
|
|
74
|
+
let showInspect = $state(false);
|
|
75
|
+
|
|
76
|
+
function syntaxHighlightJson(json: string): string {
|
|
77
|
+
return json.replace(
|
|
78
|
+
/("(\\u[a-zA-Z0-9]{4}|\\[^u]|[^\\"])*"(\s*:)?|\b(true|false|null)\b|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?)/g,
|
|
79
|
+
(match) => {
|
|
80
|
+
let cls = 'text-green-400'; // number
|
|
81
|
+
if (/^"/.test(match)) {
|
|
82
|
+
if (/:$/.test(match)) {
|
|
83
|
+
// key
|
|
84
|
+
const key = match.slice(0, -1); // remove trailing colon
|
|
85
|
+
return `<span class="text-blue-400">${escapeHtml(key)}</span>:`;
|
|
86
|
+
} else {
|
|
87
|
+
cls = 'text-yellow-500'; // string
|
|
88
|
+
}
|
|
89
|
+
} else if (/true|false/.test(match)) {
|
|
90
|
+
cls = 'text-orange-400'; // boolean
|
|
91
|
+
} else if (/null/.test(match)) {
|
|
92
|
+
cls = 'text-red-400'; // null
|
|
93
|
+
}
|
|
94
|
+
return `<span class="${cls}">${escapeHtml(match)}</span>`;
|
|
95
|
+
}
|
|
96
|
+
);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function escapeHtml(str: string): string {
|
|
100
|
+
return str.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>');
|
|
101
|
+
}
|
|
102
|
+
let inspectTab = $state<'parsed' | 'raw'>('parsed');
|
|
103
|
+
|
|
104
|
+
// Header options dropdown
|
|
105
|
+
let showHeaderMenu = $state(false);
|
|
106
|
+
|
|
107
|
+
// Version history
|
|
108
|
+
let showVersionHistory = $state(false);
|
|
109
|
+
let previewingVersion = $state<{ versionNumber: number; data: Record<string, any>; eventType: string; createdAt?: string } | null>(null);
|
|
110
|
+
const activePreview = $derived(externalVersionPreview || previewingVersion);
|
|
111
|
+
const isPreviewingVersion = $derived(!!activePreview);
|
|
112
|
+
|
|
62
113
|
// Ticker to keep relative time updating
|
|
63
114
|
let now = $state(Date.now());
|
|
64
115
|
let tickerInterval: ReturnType<typeof setInterval> | null = null;
|
|
@@ -142,6 +193,9 @@
|
|
|
142
193
|
saveError = null;
|
|
143
194
|
lastSaved = null;
|
|
144
195
|
publishSuccess = null;
|
|
196
|
+
perspective = 'draft';
|
|
197
|
+
publishedData = null;
|
|
198
|
+
previewingVersion = null;
|
|
145
199
|
|
|
146
200
|
// Cancel pending auto-save
|
|
147
201
|
if (autoSaveTimer) {
|
|
@@ -344,6 +398,7 @@
|
|
|
344
398
|
}
|
|
345
399
|
|
|
346
400
|
// Helper to recursively sort object keys for stable comparison
|
|
401
|
+
// Strips _key fields since those are auto-generated and not real content changes
|
|
347
402
|
function sortObjectForComparison(item: any): any {
|
|
348
403
|
if (item === null || typeof item !== 'object') return item;
|
|
349
404
|
|
|
@@ -351,6 +406,9 @@
|
|
|
351
406
|
return item.map(sortObjectForComparison);
|
|
352
407
|
}
|
|
353
408
|
|
|
409
|
+
const { _key, ...rest } = item;
|
|
410
|
+
item = rest;
|
|
411
|
+
|
|
354
412
|
const sortedKeys = Object.keys(item).sort();
|
|
355
413
|
const sortedObj: any = {};
|
|
356
414
|
for (const key of sortedKeys) {
|
|
@@ -548,6 +606,54 @@
|
|
|
548
606
|
}
|
|
549
607
|
}
|
|
550
608
|
|
|
609
|
+
async function switchPerspective(newPerspective: 'draft' | 'published') {
|
|
610
|
+
if (newPerspective === perspective) return;
|
|
611
|
+
|
|
612
|
+
if (newPerspective === 'published') {
|
|
613
|
+
if (!documentId) return;
|
|
614
|
+
// Fetch published version of the document
|
|
615
|
+
try {
|
|
616
|
+
const response = await documents.getById(`${documentId}?perspective=published`);
|
|
617
|
+
if (response.success && response.data) {
|
|
618
|
+
publishedData = response.data;
|
|
619
|
+
perspective = 'published';
|
|
620
|
+
} else {
|
|
621
|
+
toast.error('No published version available');
|
|
622
|
+
}
|
|
623
|
+
} catch {
|
|
624
|
+
toast.error('Failed to load published version');
|
|
625
|
+
}
|
|
626
|
+
} else {
|
|
627
|
+
perspective = 'draft';
|
|
628
|
+
publishedData = null;
|
|
629
|
+
}
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
async function unpublishDocument() {
|
|
633
|
+
if (!documentId || saving) return;
|
|
634
|
+
|
|
635
|
+
const confirmUnpublish = confirm('Unpublish this document? It will be removed from published queries but the data is preserved.');
|
|
636
|
+
if (!confirmUnpublish) return;
|
|
637
|
+
|
|
638
|
+
saving = true;
|
|
639
|
+
saveError = null;
|
|
640
|
+
|
|
641
|
+
try {
|
|
642
|
+
const response = await documents.unpublish(documentId);
|
|
643
|
+
if (response.success) {
|
|
644
|
+
fullDocument = { ...fullDocument, _meta: { ...fullDocument?._meta, status: 'unpublished' } };
|
|
645
|
+
toast.success('Document unpublished — you can re-publish anytime');
|
|
646
|
+
showDropdown = false;
|
|
647
|
+
} else {
|
|
648
|
+
throw new Error(response.error || 'Failed to unpublish');
|
|
649
|
+
}
|
|
650
|
+
} catch (err) {
|
|
651
|
+
toast.error(err instanceof ApiError ? err.message : 'Failed to unpublish document');
|
|
652
|
+
} finally {
|
|
653
|
+
saving = false;
|
|
654
|
+
}
|
|
655
|
+
}
|
|
656
|
+
|
|
551
657
|
// Validate all fields before publishing
|
|
552
658
|
async function validateAllFields(): Promise<void> {
|
|
553
659
|
if (!schema) {
|
|
@@ -705,13 +811,69 @@
|
|
|
705
811
|
</div>
|
|
706
812
|
</div>
|
|
707
813
|
|
|
708
|
-
<!-- Right side:
|
|
814
|
+
<!-- Right side: Perspective toggle, actions, close -->
|
|
709
815
|
<div class="flex items-center gap-2">
|
|
710
|
-
<!--
|
|
711
|
-
{#if
|
|
712
|
-
<
|
|
713
|
-
|
|
714
|
-
|
|
816
|
+
<!-- Perspective toggle -->
|
|
817
|
+
{#if documentId && fullDocument?._meta?.publishedHash}
|
|
818
|
+
<div class="flex rounded-md border">
|
|
819
|
+
<button
|
|
820
|
+
class="px-2.5 py-1 text-xs font-medium transition-colors {perspective === 'draft' ? 'bg-primary text-primary-foreground' : 'hover:bg-muted'}"
|
|
821
|
+
onclick={() => switchPerspective('draft')}
|
|
822
|
+
>
|
|
823
|
+
Draft
|
|
824
|
+
</button>
|
|
825
|
+
<button
|
|
826
|
+
class="px-2.5 py-1 text-xs font-medium transition-colors {perspective === 'published' ? 'bg-primary text-primary-foreground' : 'hover:bg-muted'}"
|
|
827
|
+
onclick={() => switchPerspective('published')}
|
|
828
|
+
>
|
|
829
|
+
Published
|
|
830
|
+
</button>
|
|
831
|
+
</div>
|
|
832
|
+
{/if}
|
|
833
|
+
|
|
834
|
+
|
|
835
|
+
<!-- Options dropdown -->
|
|
836
|
+
{#if documentId}
|
|
837
|
+
<div class="relative">
|
|
838
|
+
<Button
|
|
839
|
+
variant="ghost"
|
|
840
|
+
size="icon"
|
|
841
|
+
onclick={() => (showHeaderMenu = !showHeaderMenu)}
|
|
842
|
+
class="h-8 w-8"
|
|
843
|
+
>
|
|
844
|
+
<Ellipsis class="h-4 w-4" />
|
|
845
|
+
</Button>
|
|
846
|
+
{#if showHeaderMenu}
|
|
847
|
+
<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">
|
|
848
|
+
<button
|
|
849
|
+
onclick={() => {
|
|
850
|
+
showHeaderMenu = false;
|
|
851
|
+
if (onOpenVersionHistory && documentId) {
|
|
852
|
+
onOpenVersionHistory(documentId);
|
|
853
|
+
} else {
|
|
854
|
+
showVersionHistory = true;
|
|
855
|
+
}
|
|
856
|
+
}}
|
|
857
|
+
class="hover:bg-muted flex w-full items-center gap-2 px-3 py-2 text-left text-sm transition-colors"
|
|
858
|
+
>
|
|
859
|
+
<History class="h-3.5 w-3.5" /> History
|
|
860
|
+
</button>
|
|
861
|
+
<button
|
|
862
|
+
onclick={() => {
|
|
863
|
+
showHeaderMenu = false;
|
|
864
|
+
showInspect = true;
|
|
865
|
+
}}
|
|
866
|
+
class="hover:bg-muted flex w-full items-center gap-2 px-3 py-2 text-left text-sm transition-colors"
|
|
867
|
+
>
|
|
868
|
+
<Code class="h-3.5 w-3.5" /> Inspect
|
|
869
|
+
</button>
|
|
870
|
+
</div>
|
|
871
|
+
<div
|
|
872
|
+
class="fixed inset-0 z-40"
|
|
873
|
+
onclick={() => (showHeaderMenu = false)}
|
|
874
|
+
></div>
|
|
875
|
+
{/if}
|
|
876
|
+
</div>
|
|
715
877
|
{/if}
|
|
716
878
|
|
|
717
879
|
<!-- Close button (X) - hidden on mobile -->
|
|
@@ -828,17 +990,19 @@
|
|
|
828
990
|
onerror={(error) => cmsLogger.error('[DocumentEditor]', 'Error in editor content:', error)}
|
|
829
991
|
>
|
|
830
992
|
{#each schema.fields as field, index (index)}
|
|
993
|
+
{@const viewData = isPreviewingVersion && activePreview ? activePreview.data : isViewingPublished && publishedData ? publishedData : documentData}
|
|
831
994
|
<SchemaField
|
|
832
995
|
{field}
|
|
833
|
-
value={
|
|
834
|
-
{
|
|
996
|
+
value={viewData[field.name]}
|
|
997
|
+
documentData={viewData}
|
|
835
998
|
onUpdate={(newValue) => {
|
|
999
|
+
if (isViewingPublished) return;
|
|
836
1000
|
documentData = { ...documentData, [field.name]: newValue };
|
|
837
1001
|
hasUnsavedChanges = true;
|
|
838
1002
|
}}
|
|
839
1003
|
{onOpenReference}
|
|
840
1004
|
schemaType={documentType}
|
|
841
|
-
readonly={isReadOnly}
|
|
1005
|
+
readonly={isReadOnly || isViewingPublished || isPreviewingVersion}
|
|
842
1006
|
organizationId={fullDocument?._meta?.organizationId}
|
|
843
1007
|
/>
|
|
844
1008
|
{/each}
|
|
@@ -868,6 +1032,80 @@
|
|
|
868
1032
|
<!-- Sanity-style bottom bar -->
|
|
869
1033
|
{#if documentId}
|
|
870
1034
|
<div class="border-border bg-background border-t p-4">
|
|
1035
|
+
{#if isPreviewingVersion && activePreview}
|
|
1036
|
+
<!-- Version preview footer -->
|
|
1037
|
+
<div class="flex items-center justify-between">
|
|
1038
|
+
<p class="text-muted-foreground text-sm">
|
|
1039
|
+
Revision from {new Date(activePreview.createdAt || Date.now()).toLocaleString(undefined, { month: 'short', day: 'numeric', hour: 'numeric', minute: '2-digit', hour12: true })}
|
|
1040
|
+
</p>
|
|
1041
|
+
{#if fullDocument?._meta?.publishedHash && fullDocument?._meta?.status !== 'unpublished'}
|
|
1042
|
+
<p class="text-muted-foreground text-xs">Unpublish first to restore</p>
|
|
1043
|
+
{:else}
|
|
1044
|
+
<Button
|
|
1045
|
+
size="sm"
|
|
1046
|
+
onclick={async () => {
|
|
1047
|
+
if (!documentId || !activePreview) return;
|
|
1048
|
+
try {
|
|
1049
|
+
await documents.restoreVersion(documentId, activePreview.versionNumber);
|
|
1050
|
+
const docRes = await documents.getById(documentId);
|
|
1051
|
+
if (docRes.success && docRes.data) {
|
|
1052
|
+
const doc = docRes.data as Record<string, any>;
|
|
1053
|
+
fullDocument = doc;
|
|
1054
|
+
const newData: Record<string, any> = {};
|
|
1055
|
+
if (schema) {
|
|
1056
|
+
for (const field of schema.fields) {
|
|
1057
|
+
if (doc[field.name] !== undefined) {
|
|
1058
|
+
newData[field.name] = doc[field.name];
|
|
1059
|
+
}
|
|
1060
|
+
}
|
|
1061
|
+
}
|
|
1062
|
+
documentData = newData;
|
|
1063
|
+
hasUnsavedChanges = false;
|
|
1064
|
+
lastSaved = new Date();
|
|
1065
|
+
}
|
|
1066
|
+
previewingVersion = null;
|
|
1067
|
+
perspective = 'draft';
|
|
1068
|
+
publishedData = null;
|
|
1069
|
+
toast.success('Revision restored');
|
|
1070
|
+
} catch {
|
|
1071
|
+
toast.error('Failed to restore revision');
|
|
1072
|
+
}
|
|
1073
|
+
}}
|
|
1074
|
+
>
|
|
1075
|
+
Restore
|
|
1076
|
+
</Button>
|
|
1077
|
+
{/if}
|
|
1078
|
+
</div>
|
|
1079
|
+
{:else if isViewingPublished}
|
|
1080
|
+
<!-- Published view footer -->
|
|
1081
|
+
<div class="flex items-center justify-between">
|
|
1082
|
+
<p class="text-muted-foreground text-sm">
|
|
1083
|
+
{#if fullDocument?._meta?.status === 'unpublished'}
|
|
1084
|
+
Unpublished
|
|
1085
|
+
{:else}
|
|
1086
|
+
Published on {fullDocument?._meta?.publishedAt ? new Date(fullDocument._meta.publishedAt).toLocaleString(undefined, { month: 'short', day: 'numeric', year: 'numeric', hour: 'numeric', minute: '2-digit', hour12: true }) : 'Unknown'}
|
|
1087
|
+
{/if}
|
|
1088
|
+
</p>
|
|
1089
|
+
{#if fullDocument?._meta?.status === 'unpublished'}
|
|
1090
|
+
<Button
|
|
1091
|
+
size="sm"
|
|
1092
|
+
onclick={publishDocument}
|
|
1093
|
+
disabled={saving}
|
|
1094
|
+
>
|
|
1095
|
+
Publish
|
|
1096
|
+
</Button>
|
|
1097
|
+
{:else}
|
|
1098
|
+
<Button
|
|
1099
|
+
size="sm"
|
|
1100
|
+
variant="secondary"
|
|
1101
|
+
onclick={unpublishDocument}
|
|
1102
|
+
disabled={saving}
|
|
1103
|
+
>
|
|
1104
|
+
Unpublish
|
|
1105
|
+
</Button>
|
|
1106
|
+
{/if}
|
|
1107
|
+
</div>
|
|
1108
|
+
{:else}
|
|
871
1109
|
<div class="flex items-center justify-between">
|
|
872
1110
|
<!-- Left: Save status badges -->
|
|
873
1111
|
<div class="flex items-center gap-2">
|
|
@@ -884,7 +1122,7 @@
|
|
|
884
1122
|
|
|
885
1123
|
<!-- Right: Publish button + horizontal three dots menu -->
|
|
886
1124
|
<div class="flex items-center gap-2">
|
|
887
|
-
{#if !isReadOnly}
|
|
1125
|
+
{#if !isReadOnly && !isViewingPublished}
|
|
888
1126
|
<Button
|
|
889
1127
|
onclick={publishDocument}
|
|
890
1128
|
disabled={!canPublish}
|
|
@@ -900,57 +1138,208 @@
|
|
|
900
1138
|
Publish Changes
|
|
901
1139
|
{/if}
|
|
902
1140
|
</Button>
|
|
903
|
-
{:else}
|
|
1141
|
+
{:else if isReadOnly}
|
|
904
1142
|
<Badge variant="secondary" class="text-xs">Read Only</Badge>
|
|
905
1143
|
{/if}
|
|
906
1144
|
|
|
907
|
-
<!-- Horizontal three dots menu (only for non-read-only users) -->
|
|
908
1145
|
{#if !isReadOnly}
|
|
909
|
-
<
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
1146
|
+
<Button
|
|
1147
|
+
variant="ghost"
|
|
1148
|
+
size="icon"
|
|
1149
|
+
class="h-8 w-8 text-muted-foreground hover:text-destructive"
|
|
1150
|
+
onclick={deleteDocument}
|
|
1151
|
+
title="Delete document"
|
|
1152
|
+
>
|
|
1153
|
+
<Trash2 class="h-4 w-4" />
|
|
1154
|
+
</Button>
|
|
1155
|
+
{/if}
|
|
1156
|
+
</div>
|
|
1157
|
+
</div>
|
|
1158
|
+
{/if}
|
|
1159
|
+
</div>
|
|
1160
|
+
{/if}
|
|
1161
|
+
|
|
1162
|
+
<!-- Version History Panel -->
|
|
1163
|
+
{#if showVersionHistory && documentId}
|
|
1164
|
+
<div class="absolute inset-0 z-50 flex">
|
|
1165
|
+
<!-- Backdrop -->
|
|
1166
|
+
<button
|
|
1167
|
+
class="flex-1 bg-black/30"
|
|
1168
|
+
onclick={() => { showVersionHistory = false; previewingVersion = null; }}
|
|
1169
|
+
></button>
|
|
1170
|
+
<!-- Panel -->
|
|
1171
|
+
<div class="bg-background border-border flex w-80 flex-col border-l shadow-lg">
|
|
1172
|
+
<div class="border-border flex items-center justify-between border-b px-4 py-3">
|
|
1173
|
+
<h3 class="text-sm font-medium">Version History</h3>
|
|
1174
|
+
<button
|
|
1175
|
+
class="hover:bg-muted rounded p-1 transition-colors"
|
|
1176
|
+
onclick={() => { showVersionHistory = false; previewingVersion = null; }}
|
|
1177
|
+
>
|
|
1178
|
+
<svg class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
1179
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
|
|
1180
|
+
</svg>
|
|
1181
|
+
</button>
|
|
1182
|
+
</div>
|
|
1183
|
+
<div class="flex-1 overflow-auto">
|
|
1184
|
+
{#await documents.listVersions(documentId)}
|
|
1185
|
+
<div class="p-4 text-center">
|
|
1186
|
+
<span class="text-muted-foreground text-sm">Loading versions...</span>
|
|
1187
|
+
</div>
|
|
1188
|
+
{:then response}
|
|
1189
|
+
{#if response.success && response.data && response.data.length > 0}
|
|
1190
|
+
<div class="divide-y">
|
|
1191
|
+
{#each response.data as version}
|
|
1192
|
+
<button
|
|
1193
|
+
class="w-full space-y-1 px-4 py-3 text-left transition-colors hover:bg-muted {previewingVersion?.versionNumber === version.versionNumber ? 'bg-muted border-l-primary border-l-2' : ''}"
|
|
1194
|
+
onclick={async () => {
|
|
1195
|
+
try {
|
|
1196
|
+
const res = await documents.getVersion(documentId, version.versionNumber);
|
|
1197
|
+
if (res.success && res.data) {
|
|
1198
|
+
previewingVersion = {
|
|
1199
|
+
versionNumber: version.versionNumber,
|
|
1200
|
+
data: res.data.data,
|
|
1201
|
+
eventType: version.eventType
|
|
1202
|
+
};
|
|
1203
|
+
}
|
|
1204
|
+
} catch {
|
|
1205
|
+
toast.error('Failed to load version');
|
|
1206
|
+
}
|
|
935
1207
|
}}
|
|
936
|
-
class="hover:bg-muted text-destructive flex w-full items-center gap-2 px-3 py-2 text-left text-sm transition-colors"
|
|
937
1208
|
>
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
1209
|
+
<div class="flex items-center gap-2">
|
|
1210
|
+
<span class="text-xs font-medium">v{version.versionNumber}</span>
|
|
1211
|
+
<Badge variant={version.eventType === 'publish' ? 'default' : version.eventType === 'restore' ? 'outline' : 'secondary'} class="text-[10px]">
|
|
1212
|
+
{version.eventType}
|
|
1213
|
+
</Badge>
|
|
1214
|
+
</div>
|
|
1215
|
+
<p class="text-muted-foreground text-[11px]">
|
|
1216
|
+
{new Date(version.createdAt).toLocaleString()}
|
|
1217
|
+
</p>
|
|
1218
|
+
</button>
|
|
1219
|
+
{/each}
|
|
1220
|
+
</div>
|
|
1221
|
+
{:else}
|
|
1222
|
+
<div class="p-4 text-center">
|
|
1223
|
+
<span class="text-muted-foreground text-sm">No versions yet</span>
|
|
1224
|
+
</div>
|
|
1225
|
+
{/if}
|
|
1226
|
+
{:catch}
|
|
1227
|
+
<div class="p-4 text-center">
|
|
1228
|
+
<span class="text-destructive text-sm">Failed to load versions</span>
|
|
950
1229
|
</div>
|
|
1230
|
+
{/await}
|
|
1231
|
+
</div>
|
|
1232
|
+
</div>
|
|
1233
|
+
</div>
|
|
1234
|
+
{/if}
|
|
1235
|
+
|
|
1236
|
+
<!-- Inspect Modal -->
|
|
1237
|
+
{#if showInspect}
|
|
1238
|
+
<div class="absolute inset-0 z-50 flex items-center justify-center bg-black/50">
|
|
1239
|
+
<div class="bg-background border-border mx-4 flex h-[80%] w-full max-w-3xl flex-col rounded-lg border shadow-xl">
|
|
1240
|
+
<!-- Modal header -->
|
|
1241
|
+
<div class="flex items-center justify-between border-b px-4 py-3">
|
|
1242
|
+
<div>
|
|
1243
|
+
<h3 class="text-sm font-semibold">Inspecting <em>{getPreviewTitle()}</em></h3>
|
|
1244
|
+
</div>
|
|
1245
|
+
<button
|
|
1246
|
+
class="hover:bg-muted rounded p-1 transition-colors"
|
|
1247
|
+
onclick={() => (showInspect = false)}
|
|
1248
|
+
>
|
|
1249
|
+
<svg class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
1250
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
|
|
1251
|
+
</svg>
|
|
1252
|
+
</button>
|
|
1253
|
+
</div>
|
|
1254
|
+
|
|
1255
|
+
<!-- Tabs -->
|
|
1256
|
+
<div class="border-b flex">
|
|
1257
|
+
<button
|
|
1258
|
+
class="px-4 py-2 text-sm font-medium transition-colors {inspectTab === 'parsed' ? 'border-b-2 border-primary text-foreground' : 'text-muted-foreground hover:text-foreground'}"
|
|
1259
|
+
onclick={() => (inspectTab = 'parsed')}
|
|
1260
|
+
>
|
|
1261
|
+
Parsed
|
|
1262
|
+
</button>
|
|
1263
|
+
<button
|
|
1264
|
+
class="px-4 py-2 text-sm font-medium transition-colors {inspectTab === 'raw' ? 'border-b-2 border-primary text-foreground' : 'text-muted-foreground hover:text-foreground'}"
|
|
1265
|
+
onclick={() => (inspectTab = 'raw')}
|
|
1266
|
+
>
|
|
1267
|
+
Raw JSON
|
|
1268
|
+
</button>
|
|
1269
|
+
</div>
|
|
1270
|
+
|
|
1271
|
+
<!-- Content -->
|
|
1272
|
+
<div class="flex-1 overflow-auto p-4 font-mono text-sm">
|
|
1273
|
+
{#if inspectTab === 'raw'}
|
|
1274
|
+
<pre
|
|
1275
|
+
class="whitespace-pre-wrap break-all text-xs select-text"
|
|
1276
|
+
tabindex="0"
|
|
1277
|
+
onkeydown={(e) => {
|
|
1278
|
+
if ((e.metaKey || e.ctrlKey) && e.key === 'a') {
|
|
1279
|
+
e.preventDefault();
|
|
1280
|
+
e.stopPropagation();
|
|
1281
|
+
const sel = window.getSelection();
|
|
1282
|
+
const range = document.createRange();
|
|
1283
|
+
range.selectNodeContents(e.currentTarget);
|
|
1284
|
+
sel?.removeAllRanges();
|
|
1285
|
+
sel?.addRange(range);
|
|
1286
|
+
}
|
|
1287
|
+
}}
|
|
1288
|
+
>{@html syntaxHighlightJson(JSON.stringify({ id: documentId, _meta: fullDocument?._meta, ...documentData }, null, 2))}</pre>
|
|
1289
|
+
{:else}
|
|
1290
|
+
{@render parsedValue(null, { id: documentId, _meta: fullDocument?._meta, ...documentData }, 0)}
|
|
951
1291
|
{/if}
|
|
952
1292
|
</div>
|
|
953
1293
|
</div>
|
|
954
1294
|
</div>
|
|
955
1295
|
{/if}
|
|
956
1296
|
</div>
|
|
1297
|
+
|
|
1298
|
+
{#snippet parsedValue(key: string | null, val: any, depth: number)}
|
|
1299
|
+
{#if val && typeof val === 'object'}
|
|
1300
|
+
<details class="my-0.5" open={depth < 2}>
|
|
1301
|
+
<summary class="cursor-pointer text-xs leading-relaxed">
|
|
1302
|
+
{#if key !== null}
|
|
1303
|
+
{#if typeof key === 'number' || /^\d+$/.test(String(key))}
|
|
1304
|
+
<span class="text-purple-400">{key}:</span>
|
|
1305
|
+
{:else}
|
|
1306
|
+
<span class="text-blue-400">{key}:</span>
|
|
1307
|
+
{/if}
|
|
1308
|
+
{/if}
|
|
1309
|
+
{#if Array.isArray(val)}
|
|
1310
|
+
<span class="text-muted-foreground">[...] {val.length} {val.length === 1 ? 'item' : 'items'}</span>
|
|
1311
|
+
{:else}
|
|
1312
|
+
<span class="text-muted-foreground">{...} {Object.keys(val).length} {Object.keys(val).length === 1 ? 'property' : 'properties'}</span>
|
|
1313
|
+
{/if}
|
|
1314
|
+
</summary>
|
|
1315
|
+
<div class="ml-4 border-l border-border/50 pl-3">
|
|
1316
|
+
{#if Array.isArray(val)}
|
|
1317
|
+
{#each val as item, i}
|
|
1318
|
+
{@render parsedValue(String(i), item, depth + 1)}
|
|
1319
|
+
{/each}
|
|
1320
|
+
{:else}
|
|
1321
|
+
{#each Object.entries(val) as [k, v]}
|
|
1322
|
+
{@render parsedValue(k, v, depth + 1)}
|
|
1323
|
+
{/each}
|
|
1324
|
+
{/if}
|
|
1325
|
+
</div>
|
|
1326
|
+
</details>
|
|
1327
|
+
{:else}
|
|
1328
|
+
<div class="my-0.5 text-xs leading-relaxed">
|
|
1329
|
+
{#if key !== null}
|
|
1330
|
+
<span class="text-blue-400">{key}:</span>
|
|
1331
|
+
{/if}
|
|
1332
|
+
{#if typeof val === 'string'}
|
|
1333
|
+
<span class="text-yellow-500">{val}</span>
|
|
1334
|
+
{:else if typeof val === 'number'}
|
|
1335
|
+
<span class="text-green-400">{val}</span>
|
|
1336
|
+
{:else if typeof val === 'boolean'}
|
|
1337
|
+
<span class="text-orange-400">{val}</span>
|
|
1338
|
+
{:else if val === null || val === undefined}
|
|
1339
|
+
<span class="text-red-400">null</span>
|
|
1340
|
+
{:else}
|
|
1341
|
+
<span class="text-muted-foreground">{JSON.stringify(val)}</span>
|
|
1342
|
+
{/if}
|
|
1343
|
+
</div>
|
|
1344
|
+
{/if}
|
|
1345
|
+
{/snippet}
|
|
@@ -10,6 +10,13 @@ interface Props {
|
|
|
10
10
|
onDeleted?: () => void;
|
|
11
11
|
onPublished?: (documentId: string) => void;
|
|
12
12
|
onOpenReference?: (documentId: string, documentType: string) => void;
|
|
13
|
+
onOpenVersionHistory?: (documentId: string) => void;
|
|
14
|
+
externalVersionPreview?: {
|
|
15
|
+
versionNumber: number;
|
|
16
|
+
data: Record<string, any>;
|
|
17
|
+
eventType: string;
|
|
18
|
+
createdAt?: string;
|
|
19
|
+
} | null;
|
|
13
20
|
isReadOnly?: boolean;
|
|
14
21
|
}
|
|
15
22
|
declare const DocumentEditor: import("svelte").Component<Props, {}, "">;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"DocumentEditor.svelte.d.ts","sourceRoot":"","sources":["../../../src/lib/components/admin/DocumentEditor.svelte.ts"],"names":[],"mappings":"AAUA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,wBAAwB,CAAC;
|
|
1
|
+
{"version":3,"file":"DocumentEditor.svelte.d.ts","sourceRoot":"","sources":["../../../src/lib/components/admin/DocumentEditor.svelte.ts"],"names":[],"mappings":"AAUA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,wBAAwB,CAAC;AAWxD,UAAU,KAAK;IACd,OAAO,EAAE,UAAU,EAAE,CAAC;IACtB,YAAY,EAAE,MAAM,CAAC;IACrB,UAAU,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3B,UAAU,EAAE,OAAO,CAAC;IACpB,MAAM,EAAE,MAAM,IAAI,CAAC;IACnB,OAAO,CAAC,EAAE,CAAC,UAAU,EAAE,MAAM,KAAK,IAAI,CAAC;IACvC,WAAW,CAAC,EAAE,CAAC,UAAU,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;IAC1D,SAAS,CAAC,EAAE,MAAM,IAAI,CAAC;IACvB,WAAW,CAAC,EAAE,CAAC,UAAU,EAAE,MAAM,KAAK,IAAI,CAAC;IAC3C,eAAe,CAAC,EAAE,CAAC,UAAU,EAAE,MAAM,EAAE,YAAY,EAAE,MAAM,KAAK,IAAI,CAAC;IACrE,oBAAoB,CAAC,EAAE,CAAC,UAAU,EAAE,MAAM,KAAK,IAAI,CAAC;IACpD,sBAAsB,CAAC,EAAE;QAAE,aAAa,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC;QAAC,SAAS,EAAE,MAAM,CAAC;QAAC,SAAS,CAAC,EAAE,MAAM,CAAA;KAAE,GAAG,IAAI,CAAC;IAC5H,UAAU,CAAC,EAAE,OAAO,CAAC;CACrB;AAsrCF,QAAA,MAAM,cAAc,2CAAwC,CAAC;AAC7D,KAAK,cAAc,GAAG,UAAU,CAAC,OAAO,cAAc,CAAC,CAAC;AACxD,eAAe,cAAc,CAAC"}
|