@actuate-media/cms-admin 0.1.3 → 0.2.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/AdminRoot.d.ts.map +1 -1
- package/dist/AdminRoot.js +16 -10
- package/dist/AdminRoot.js.map +1 -1
- package/dist/actuate-admin.css +2 -0
- package/dist/lib/useApiData.d.ts +8 -1
- package/dist/lib/useApiData.d.ts.map +1 -1
- package/dist/lib/useApiData.js +39 -7
- package/dist/lib/useApiData.js.map +1 -1
- package/dist/views/Dashboard.d.ts.map +1 -1
- package/dist/views/Dashboard.js +8 -3
- package/dist/views/Dashboard.js.map +1 -1
- package/package.json +10 -5
- package/src/AdminRoot.tsx +312 -0
- package/src/__tests__/lib/search.test.ts +138 -0
- package/src/__tests__/lib/utils.test.ts +19 -0
- package/src/__tests__/router/match-route.test.ts +47 -0
- package/src/__tests__/router/strip-base.test.ts +30 -0
- package/src/components/Breadcrumbs.tsx +92 -0
- package/src/components/CommandPalette.tsx +384 -0
- package/src/components/ErrorBoundary.tsx +52 -0
- package/src/components/FocalPointPicker.tsx +54 -0
- package/src/components/FolderTree.tsx +427 -0
- package/src/components/LivePreview.tsx +136 -0
- package/src/components/LocaleProvider.tsx +51 -0
- package/src/components/LocaleSwitcher.tsx +51 -0
- package/src/components/MediaPickerModal.tsx +183 -0
- package/src/components/PresenceIndicator.tsx +71 -0
- package/src/components/SEOPanel.tsx +767 -0
- package/src/components/ThemeProvider.tsx +98 -0
- package/src/components/TipTapEditor.tsx +469 -0
- package/src/components/VersionHistory.tsx +167 -0
- package/src/components/ui/Avatar.tsx +42 -0
- package/src/components/ui/Badge.tsx +25 -0
- package/src/components/ui/Button.tsx +52 -0
- package/src/components/ui/CommandPalette.tsx +119 -0
- package/src/components/ui/ConfirmDialog.tsx +52 -0
- package/src/components/ui/DataTable.tsx +194 -0
- package/src/components/ui/EmptyState.tsx +29 -0
- package/src/components/ui/Modal.tsx +48 -0
- package/src/components/ui/Pagination.tsx +79 -0
- package/src/components/ui/SearchInput.tsx +44 -0
- package/src/components/ui/Skeleton.tsx +48 -0
- package/src/components/ui/Toast.tsx +66 -0
- package/src/components/ui/index.ts +24 -0
- package/src/fields/ArrayField.tsx +92 -0
- package/src/fields/BlockBuilderField.tsx +421 -0
- package/src/fields/DateField.tsx +41 -0
- package/src/fields/FieldRenderer.tsx +84 -0
- package/src/fields/GroupField.tsx +41 -0
- package/src/fields/MediaField.tsx +48 -0
- package/src/fields/NavBuilderField.tsx +78 -0
- package/src/fields/NumberField.tsx +45 -0
- package/src/fields/RelationshipField.tsx +245 -0
- package/src/fields/RichTextField.tsx +26 -0
- package/src/fields/SelectField.tsx +117 -0
- package/src/fields/SlugField.tsx +65 -0
- package/src/fields/TextField.tsx +48 -0
- package/src/fields/ToggleField.tsx +36 -0
- package/src/fields/block-types.ts +95 -0
- package/src/fields/index.ts +17 -0
- package/src/hooks/useContentLock.ts +52 -0
- package/src/hooks/useDebounce.ts +14 -0
- package/src/hooks/useKeyboardShortcuts.ts +32 -0
- package/src/index.ts +55 -0
- package/src/layout/Header.tsx +135 -0
- package/src/layout/Layout.tsx +77 -0
- package/src/layout/Sidebar.tsx +216 -0
- package/src/lib/api.ts +67 -0
- package/src/lib/search.ts +59 -0
- package/src/lib/useApiData.ts +95 -0
- package/src/lib/utils.ts +6 -0
- package/src/router/index.ts +81 -0
- package/src/styles/build-input.css +11 -0
- package/src/styles/tailwind.css +7 -2
- package/src/styles/theme.css +2 -1
- package/src/views/CollectionList.tsx +270 -0
- package/src/views/Dashboard.tsx +207 -0
- package/src/views/DocumentEdit.tsx +377 -0
- package/src/views/FormEditor.tsx +533 -0
- package/src/views/FormSubmissions.tsx +316 -0
- package/src/views/Forms.tsx +106 -0
- package/src/views/Login.tsx +322 -0
- package/src/views/MediaBrowser.tsx +774 -0
- package/src/views/PageEditor.tsx +192 -0
- package/src/views/Pages.tsx +354 -0
- package/src/views/PostEditor.tsx +251 -0
- package/src/views/Posts.tsx +243 -0
- package/src/views/Redirects.tsx +293 -0
- package/src/views/SEO.tsx +458 -0
- package/src/views/Settings.tsx +811 -0
- package/src/views/SetupWizard.tsx +207 -0
- package/src/views/Users.tsx +282 -0
|
@@ -0,0 +1,377 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useState, useEffect, useCallback, useRef } from 'react';
|
|
4
|
+
import { AlertTriangle, Eye, EyeOff, Save, Copy, Loader2, Clock } from 'lucide-react';
|
|
5
|
+
import { toast } from 'sonner';
|
|
6
|
+
import { FieldRenderer } from '../fields/FieldRenderer.js';
|
|
7
|
+
import { Button } from '../components/ui/Button.js';
|
|
8
|
+
import { Badge } from '../components/ui/Badge.js';
|
|
9
|
+
import { LivePreview } from '../components/LivePreview.js';
|
|
10
|
+
import { VersionHistory } from '../components/VersionHistory.js';
|
|
11
|
+
import { PresenceIndicator } from '../components/PresenceIndicator.js';
|
|
12
|
+
import { SEOPanel } from '../components/SEOPanel.js';
|
|
13
|
+
import type { SEOData } from '../components/SEOPanel.js';
|
|
14
|
+
import { cmsApi } from '../lib/api.js';
|
|
15
|
+
|
|
16
|
+
export interface DocumentEditProps {
|
|
17
|
+
collectionSlug: string;
|
|
18
|
+
documentId?: string;
|
|
19
|
+
config: any;
|
|
20
|
+
onNavigate?: (path: string) => void;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function DocumentEdit({ collectionSlug, documentId, config, onNavigate }: DocumentEditProps) {
|
|
24
|
+
const isNew = !documentId;
|
|
25
|
+
const [values, setValues] = useState<Record<string, any>>({});
|
|
26
|
+
const [initialValues, setInitialValues] = useState<Record<string, any>>({});
|
|
27
|
+
const [seoData, setSeoData] = useState<SEOData>({});
|
|
28
|
+
const [initialSeoData, setInitialSeoData] = useState<SEOData>({});
|
|
29
|
+
const [saving, setSaving] = useState(false);
|
|
30
|
+
const [loading, setLoading] = useState(!isNew);
|
|
31
|
+
const [showPreview, setShowPreview] = useState(false);
|
|
32
|
+
const [showVersions, setShowVersions] = useState(false);
|
|
33
|
+
const [docStatus, setDocStatus] = useState<string>('DRAFT');
|
|
34
|
+
const hasLoadedRef = useRef(false);
|
|
35
|
+
|
|
36
|
+
const collections = config?.collections;
|
|
37
|
+
const collection = Array.isArray(collections)
|
|
38
|
+
? collections.find((c: any) => c.slug === collectionSlug)
|
|
39
|
+
: collections?.[collectionSlug];
|
|
40
|
+
|
|
41
|
+
const previewUrl = collection?.admin?.preview ? collection.admin.preview({}) : undefined;
|
|
42
|
+
const fields: any[] = collection?.fields
|
|
43
|
+
? (Array.isArray(collection.fields)
|
|
44
|
+
? collection.fields
|
|
45
|
+
: Object.entries(collection.fields).map(([name, def]: [string, any]) => ({ name, ...def })))
|
|
46
|
+
: [];
|
|
47
|
+
|
|
48
|
+
const useAsTitleField = collection?.admin?.useAsTitle ?? 'title';
|
|
49
|
+
const displayTitle = isNew
|
|
50
|
+
? `New ${collection?.labels?.singular ?? collectionSlug}`
|
|
51
|
+
: `Edit ${values[useAsTitleField] ?? 'Document'}`;
|
|
52
|
+
|
|
53
|
+
const isDirty = JSON.stringify(values) !== JSON.stringify(initialValues)
|
|
54
|
+
|| JSON.stringify(seoData) !== JSON.stringify(initialSeoData);
|
|
55
|
+
|
|
56
|
+
useEffect(() => {
|
|
57
|
+
if (!isNew && documentId && !hasLoadedRef.current) {
|
|
58
|
+
hasLoadedRef.current = true;
|
|
59
|
+
loadDocument();
|
|
60
|
+
}
|
|
61
|
+
}, [documentId, isNew]);
|
|
62
|
+
|
|
63
|
+
useEffect(() => {
|
|
64
|
+
if (!isDirty) return;
|
|
65
|
+
const handler = (e: BeforeUnloadEvent) => {
|
|
66
|
+
e.preventDefault();
|
|
67
|
+
};
|
|
68
|
+
window.addEventListener('beforeunload', handler);
|
|
69
|
+
return () => window.removeEventListener('beforeunload', handler);
|
|
70
|
+
}, [isDirty]);
|
|
71
|
+
|
|
72
|
+
const SEO_FIELDS: (keyof SEOData)[] = [
|
|
73
|
+
'metaTitle', 'metaDescription', 'focusKeyphrase', 'canonical',
|
|
74
|
+
'noIndex', 'noFollow', 'ogTitle', 'ogDescription', 'ogImage',
|
|
75
|
+
'twitterTitle', 'twitterDescription', 'twitterImage',
|
|
76
|
+
'isCornerstone', 'schemaType',
|
|
77
|
+
];
|
|
78
|
+
|
|
79
|
+
async function loadDocument() {
|
|
80
|
+
setLoading(true);
|
|
81
|
+
const res = await cmsApi<any>(`/collections/${collectionSlug}/${documentId}`);
|
|
82
|
+
if (res.data) {
|
|
83
|
+
const doc = res.data;
|
|
84
|
+
const docData = (doc.data && typeof doc.data === 'object') ? doc.data : {};
|
|
85
|
+
const merged = { ...docData, title: doc.title ?? docData.title, slug: doc.slug ?? docData.slug };
|
|
86
|
+
setValues(merged);
|
|
87
|
+
setInitialValues(merged);
|
|
88
|
+
setDocStatus(doc.status ?? 'DRAFT');
|
|
89
|
+
|
|
90
|
+
const loadedSeo: SEOData = {};
|
|
91
|
+
for (const key of SEO_FIELDS) {
|
|
92
|
+
if (docData[key] !== undefined) {
|
|
93
|
+
(loadedSeo as any)[key] = docData[key];
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
setSeoData(loadedSeo);
|
|
97
|
+
setInitialSeoData(loadedSeo);
|
|
98
|
+
} else if (res.error) {
|
|
99
|
+
toast.error(`Failed to load document: ${res.error}`);
|
|
100
|
+
}
|
|
101
|
+
setLoading(false);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function handleFieldChange(name: string, value: any) {
|
|
105
|
+
setValues((prev) => ({ ...prev, [name]: value }));
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
async function handleSave() {
|
|
109
|
+
setSaving(true);
|
|
110
|
+
const payload = { ...values, ...seoData };
|
|
111
|
+
try {
|
|
112
|
+
if (isNew) {
|
|
113
|
+
const res = await cmsApi<any>(`/collections/${collectionSlug}`, {
|
|
114
|
+
method: 'POST',
|
|
115
|
+
body: JSON.stringify(payload),
|
|
116
|
+
});
|
|
117
|
+
if (res.error) {
|
|
118
|
+
toast.error(res.error);
|
|
119
|
+
} else {
|
|
120
|
+
toast.success('Document created');
|
|
121
|
+
const newId = res.data?.id;
|
|
122
|
+
setInitialValues(values);
|
|
123
|
+
setInitialSeoData(seoData);
|
|
124
|
+
if (newId && onNavigate) {
|
|
125
|
+
onNavigate(`/${collectionSlug}/${newId}`);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
} else {
|
|
129
|
+
const res = await cmsApi<any>(`/collections/${collectionSlug}/${documentId}`, {
|
|
130
|
+
method: 'PUT',
|
|
131
|
+
body: JSON.stringify(payload),
|
|
132
|
+
});
|
|
133
|
+
if (res.error) {
|
|
134
|
+
toast.error(res.error);
|
|
135
|
+
} else {
|
|
136
|
+
toast.success('Changes saved');
|
|
137
|
+
setInitialValues(values);
|
|
138
|
+
setInitialSeoData(seoData);
|
|
139
|
+
if (res.data?.status) setDocStatus(res.data.status);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
} catch {
|
|
143
|
+
toast.error('An unexpected error occurred');
|
|
144
|
+
}
|
|
145
|
+
setSaving(false);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
async function handlePublish() {
|
|
149
|
+
if (isNew) return;
|
|
150
|
+
setSaving(true);
|
|
151
|
+
const res = await cmsApi<any>(`/collections/${collectionSlug}/${documentId}`, {
|
|
152
|
+
method: 'PUT',
|
|
153
|
+
body: JSON.stringify({ ...values, ...seoData, status: 'PUBLISHED' }),
|
|
154
|
+
});
|
|
155
|
+
if (res.error) {
|
|
156
|
+
toast.error(res.error);
|
|
157
|
+
} else {
|
|
158
|
+
toast.success('Document published');
|
|
159
|
+
setDocStatus('PUBLISHED');
|
|
160
|
+
setInitialValues(values);
|
|
161
|
+
setInitialSeoData(seoData);
|
|
162
|
+
}
|
|
163
|
+
setSaving(false);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
async function handleUnpublish() {
|
|
167
|
+
if (isNew) return;
|
|
168
|
+
setSaving(true);
|
|
169
|
+
const res = await cmsApi<any>(`/collections/${collectionSlug}/${documentId}`, {
|
|
170
|
+
method: 'PUT',
|
|
171
|
+
body: JSON.stringify({ ...values, ...seoData, status: 'DRAFT' }),
|
|
172
|
+
});
|
|
173
|
+
if (res.error) {
|
|
174
|
+
toast.error(res.error);
|
|
175
|
+
} else {
|
|
176
|
+
toast.success('Document unpublished');
|
|
177
|
+
setDocStatus('DRAFT');
|
|
178
|
+
setInitialValues(values);
|
|
179
|
+
setInitialSeoData(seoData);
|
|
180
|
+
}
|
|
181
|
+
setSaving(false);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
async function handleDuplicate() {
|
|
185
|
+
if (isNew || !documentId) return;
|
|
186
|
+
const res = await cmsApi<any>(`/collections/${collectionSlug}`, {
|
|
187
|
+
method: 'POST',
|
|
188
|
+
body: JSON.stringify({
|
|
189
|
+
...values,
|
|
190
|
+
title: values.title ? `${values.title} (Copy)` : undefined,
|
|
191
|
+
slug: values.slug ? `${values.slug}-copy` : undefined,
|
|
192
|
+
}),
|
|
193
|
+
});
|
|
194
|
+
if (res.error) {
|
|
195
|
+
toast.error(res.error);
|
|
196
|
+
} else {
|
|
197
|
+
toast.success('Document duplicated');
|
|
198
|
+
const newId = res.data?.id;
|
|
199
|
+
if (newId && onNavigate) {
|
|
200
|
+
onNavigate(`/${collectionSlug}/${newId}`);
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
const mainFields = fields.filter(
|
|
206
|
+
(f: any) => !['status', 'publishedAt', 'featured', 'featuredImage'].includes(f.name),
|
|
207
|
+
);
|
|
208
|
+
const sidebarFields = fields.filter((f: any) =>
|
|
209
|
+
['publishedAt', 'featured', 'featuredImage'].includes(f.name),
|
|
210
|
+
);
|
|
211
|
+
|
|
212
|
+
const contentField = fields.find(
|
|
213
|
+
(f: any) => f.type === 'richText' || f.name === 'content' || f.name === 'body',
|
|
214
|
+
);
|
|
215
|
+
const htmlContent = contentField ? (values[contentField.name] ?? '') : '';
|
|
216
|
+
const docSlug = values.slug ?? '';
|
|
217
|
+
|
|
218
|
+
const statusColor = docStatus === 'PUBLISHED' ? 'bg-green-100 text-green-800'
|
|
219
|
+
: docStatus === 'SCHEDULED' ? 'bg-blue-100 text-blue-800'
|
|
220
|
+
: 'bg-yellow-100 text-yellow-800';
|
|
221
|
+
|
|
222
|
+
if (loading) {
|
|
223
|
+
return (
|
|
224
|
+
<div className="flex items-center justify-center min-h-[300px]">
|
|
225
|
+
<Loader2 className="w-8 h-8 animate-spin text-gray-400" />
|
|
226
|
+
</div>
|
|
227
|
+
);
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
return (
|
|
231
|
+
<div className="space-y-4">
|
|
232
|
+
<div className="flex items-center justify-between">
|
|
233
|
+
<div className="flex items-center gap-3">
|
|
234
|
+
<h1 className="text-2xl font-bold">{displayTitle}</h1>
|
|
235
|
+
<span className={`inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium ${statusColor}`}>
|
|
236
|
+
{docStatus}
|
|
237
|
+
</span>
|
|
238
|
+
{documentId && <PresenceIndicator documentId={documentId} />}
|
|
239
|
+
{isDirty && (
|
|
240
|
+
<span className="text-xs text-amber-600 font-medium">Unsaved changes</span>
|
|
241
|
+
)}
|
|
242
|
+
</div>
|
|
243
|
+
<div className="flex items-center gap-2">
|
|
244
|
+
<button
|
|
245
|
+
onClick={() => setShowPreview((v) => !v)}
|
|
246
|
+
className="inline-flex items-center gap-1.5 rounded-md px-3 py-1.5 text-sm text-[var(--muted-foreground)] hover:bg-[var(--muted)] hover:text-[var(--foreground)]"
|
|
247
|
+
title={showPreview ? 'Hide preview' : 'Show preview'}
|
|
248
|
+
>
|
|
249
|
+
{showPreview ? <EyeOff className="h-4 w-4" /> : <Eye className="h-4 w-4" />}
|
|
250
|
+
Preview
|
|
251
|
+
</button>
|
|
252
|
+
{!isNew && (
|
|
253
|
+
<>
|
|
254
|
+
<button
|
|
255
|
+
onClick={() => setShowVersions(true)}
|
|
256
|
+
className="inline-flex items-center gap-1.5 rounded-md px-3 py-1.5 text-sm text-[var(--muted-foreground)] hover:bg-[var(--muted)] hover:text-[var(--foreground)]"
|
|
257
|
+
title="Version history"
|
|
258
|
+
>
|
|
259
|
+
<Clock className="h-4 w-4" />
|
|
260
|
+
History
|
|
261
|
+
</button>
|
|
262
|
+
<button
|
|
263
|
+
onClick={handleDuplicate}
|
|
264
|
+
className="inline-flex items-center gap-1.5 rounded-md px-3 py-1.5 text-sm text-[var(--muted-foreground)] hover:bg-[var(--muted)] hover:text-[var(--foreground)]"
|
|
265
|
+
title="Duplicate this document"
|
|
266
|
+
>
|
|
267
|
+
<Copy className="h-4 w-4" />
|
|
268
|
+
Duplicate
|
|
269
|
+
</button>
|
|
270
|
+
</>
|
|
271
|
+
)}
|
|
272
|
+
</div>
|
|
273
|
+
</div>
|
|
274
|
+
|
|
275
|
+
<div className={showPreview ? 'grid grid-cols-2 gap-6' : ''}>
|
|
276
|
+
<div className="space-y-6">
|
|
277
|
+
{fields.length === 0 ? (
|
|
278
|
+
<div className="flex flex-col items-center justify-center py-16 text-center rounded-lg border border-[var(--border)]">
|
|
279
|
+
<div className="w-12 h-12 rounded-full bg-gray-100 flex items-center justify-center mb-4">
|
|
280
|
+
<AlertTriangle className="w-6 h-6 text-gray-400" />
|
|
281
|
+
</div>
|
|
282
|
+
<h3 className="text-sm font-medium text-gray-900 mb-1">Collection schema not found</h3>
|
|
283
|
+
<p className="text-sm text-gray-500">No fields are defined for this collection.</p>
|
|
284
|
+
</div>
|
|
285
|
+
) : (
|
|
286
|
+
<div className="grid grid-cols-1 gap-6 lg:grid-cols-3">
|
|
287
|
+
<div className="space-y-6 lg:col-span-2">
|
|
288
|
+
{mainFields.map((field: any) => (
|
|
289
|
+
<FieldRenderer
|
|
290
|
+
key={field.name}
|
|
291
|
+
field={field}
|
|
292
|
+
value={values[field.name]}
|
|
293
|
+
onChange={(val) => handleFieldChange(field.name, val)}
|
|
294
|
+
/>
|
|
295
|
+
))}
|
|
296
|
+
</div>
|
|
297
|
+
|
|
298
|
+
<div className="space-y-6">
|
|
299
|
+
<div className="rounded-lg border border-[var(--border)] bg-[var(--card)] p-4">
|
|
300
|
+
<h3 className="mb-4 font-semibold">Status</h3>
|
|
301
|
+
<div className="space-y-3">
|
|
302
|
+
{!isNew && docStatus === 'DRAFT' && (
|
|
303
|
+
<Button variant="primary" className="w-full" onClick={handlePublish} loading={saving}>
|
|
304
|
+
Publish
|
|
305
|
+
</Button>
|
|
306
|
+
)}
|
|
307
|
+
{!isNew && docStatus === 'PUBLISHED' && (
|
|
308
|
+
<Button variant="secondary" className="w-full" onClick={handleUnpublish} loading={saving}>
|
|
309
|
+
Unpublish
|
|
310
|
+
</Button>
|
|
311
|
+
)}
|
|
312
|
+
</div>
|
|
313
|
+
</div>
|
|
314
|
+
|
|
315
|
+
{sidebarFields.length > 0 && (
|
|
316
|
+
<div className="rounded-lg border border-[var(--border)] bg-[var(--card)] p-4">
|
|
317
|
+
<h3 className="mb-4 font-semibold">Metadata</h3>
|
|
318
|
+
{sidebarFields.map((field: any) => (
|
|
319
|
+
<div key={field.name} className="mb-4">
|
|
320
|
+
<FieldRenderer
|
|
321
|
+
field={field}
|
|
322
|
+
value={values[field.name]}
|
|
323
|
+
onChange={(val) => handleFieldChange(field.name, val)}
|
|
324
|
+
/>
|
|
325
|
+
</div>
|
|
326
|
+
))}
|
|
327
|
+
</div>
|
|
328
|
+
)}
|
|
329
|
+
|
|
330
|
+
<SEOPanel
|
|
331
|
+
title={values[useAsTitleField] ?? values.title ?? ''}
|
|
332
|
+
slug={docSlug}
|
|
333
|
+
content={htmlContent}
|
|
334
|
+
seoData={seoData}
|
|
335
|
+
onChange={setSeoData}
|
|
336
|
+
siteUrl={config?.siteUrl}
|
|
337
|
+
/>
|
|
338
|
+
</div>
|
|
339
|
+
</div>
|
|
340
|
+
)}
|
|
341
|
+
|
|
342
|
+
<div className="sticky bottom-0 flex items-center justify-end gap-3 border-t border-[var(--border)] bg-[var(--background)] px-4 py-3">
|
|
343
|
+
<Button variant="secondary" onClick={() => onNavigate?.(`/${collectionSlug}`)}>Cancel</Button>
|
|
344
|
+
<Button variant="primary" loading={saving} onClick={handleSave} data-shortcut="save">
|
|
345
|
+
<Save className="h-4 w-4 mr-1.5" />
|
|
346
|
+
{isNew ? 'Create' : 'Save Changes'}
|
|
347
|
+
</Button>
|
|
348
|
+
</div>
|
|
349
|
+
</div>
|
|
350
|
+
|
|
351
|
+
{showPreview && (
|
|
352
|
+
<LivePreview
|
|
353
|
+
collection={collectionSlug}
|
|
354
|
+
documentId={documentId}
|
|
355
|
+
previewUrl={previewUrl}
|
|
356
|
+
values={values}
|
|
357
|
+
onClose={() => setShowPreview(false)}
|
|
358
|
+
/>
|
|
359
|
+
)}
|
|
360
|
+
</div>
|
|
361
|
+
|
|
362
|
+
{!isNew && documentId && (
|
|
363
|
+
<VersionHistory
|
|
364
|
+
collectionSlug={collectionSlug}
|
|
365
|
+
documentId={documentId}
|
|
366
|
+
open={showVersions}
|
|
367
|
+
onClose={() => setShowVersions(false)}
|
|
368
|
+
onRestore={(data) => {
|
|
369
|
+
const restoredData = data as Record<string, any>;
|
|
370
|
+
setValues(restoredData);
|
|
371
|
+
setInitialValues(restoredData);
|
|
372
|
+
}}
|
|
373
|
+
/>
|
|
374
|
+
)}
|
|
375
|
+
</div>
|
|
376
|
+
);
|
|
377
|
+
}
|