@aphexcms/cms-core 0.1.1 → 0.1.3
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/package.json +22 -5
- package/src/api/assets.ts +0 -75
- package/src/api/client.ts +0 -150
- package/src/api/documents.ts +0 -102
- package/src/api/index.ts +0 -7
- package/src/api/organizations.ts +0 -154
- package/src/api/types.ts +0 -34
- package/src/app.d.ts +0 -19
- package/src/auth/MULTI_TENANCY_PLAN.md +0 -1183
- package/src/auth/auth-errors.ts +0 -23
- package/src/auth/auth-hooks.ts +0 -132
- package/src/auth/provider.ts +0 -25
- package/src/client/index.ts +0 -47
- package/src/components/AdminApp.svelte +0 -1078
- package/src/components/admin/AdminLayout.svelte +0 -115
- package/src/components/admin/DocumentEditor.svelte +0 -795
- package/src/components/admin/DocumentTypesList.svelte +0 -97
- package/src/components/admin/ObjectModal.svelte +0 -135
- package/src/components/admin/SchemaField.svelte +0 -171
- package/src/components/admin/fields/ArrayField.svelte +0 -266
- package/src/components/admin/fields/BooleanField.svelte +0 -35
- package/src/components/admin/fields/ImageField.svelte +0 -284
- package/src/components/admin/fields/NumberField.svelte +0 -82
- package/src/components/admin/fields/ReferenceField.svelte +0 -260
- package/src/components/admin/fields/SlugField.svelte +0 -74
- package/src/components/admin/fields/StringField.svelte +0 -40
- package/src/components/admin/fields/TextareaField.svelte +0 -40
- package/src/components/fields/index.ts +0 -9
- package/src/components/index.ts +0 -16
- package/src/components/layout/OrganizationSwitcher.svelte +0 -218
- package/src/components/layout/Sidebar.svelte +0 -88
- package/src/components/layout/sidebar/AppSidebar.svelte +0 -63
- package/src/components/layout/sidebar/NavMain.svelte +0 -95
- package/src/components/layout/sidebar/NavSecondary.svelte +0 -69
- package/src/components/layout/sidebar/NavUser.svelte +0 -85
- package/src/config.ts +0 -18
- package/src/db/adapters/index.ts +0 -3
- package/src/db/index.ts +0 -5
- package/src/db/interfaces/asset.ts +0 -61
- package/src/db/interfaces/document.ts +0 -53
- package/src/db/interfaces/index.ts +0 -98
- package/src/db/interfaces/organization.ts +0 -51
- package/src/db/interfaces/schema.ts +0 -13
- package/src/db/interfaces/user.ts +0 -16
- package/src/db/utils/reference-resolver.ts +0 -119
- package/src/define.ts +0 -7
- package/src/email/index.ts +0 -5
- package/src/email/interfaces/email.ts +0 -45
- package/src/engine.ts +0 -85
- package/src/field-validation/rule.ts +0 -287
- package/src/field-validation/utils.ts +0 -91
- package/src/hooks.ts +0 -142
- package/src/index.ts +0 -5
- package/src/lib/is-mobile.svelte.ts +0 -9
- package/src/lib/utils.ts +0 -13
- package/src/plugins/README.md +0 -154
- package/src/routes/assets-by-id.ts +0 -161
- package/src/routes/assets-cdn.ts +0 -185
- package/src/routes/assets.ts +0 -116
- package/src/routes/documents-by-id.ts +0 -188
- package/src/routes/documents-publish.ts +0 -211
- package/src/routes/documents.ts +0 -172
- package/src/routes/index.ts +0 -13
- package/src/routes/organizations-by-id.ts +0 -258
- package/src/routes/organizations-invitations.ts +0 -183
- package/src/routes/organizations-members.ts +0 -301
- package/src/routes/organizations-switch.ts +0 -74
- package/src/routes/organizations.ts +0 -146
- package/src/routes/schemas-by-type.ts +0 -35
- package/src/routes/schemas.ts +0 -19
- package/src/routes-exports.ts +0 -42
- package/src/schema-context.svelte.ts +0 -24
- package/src/schema-utils/cleanup.ts +0 -116
- package/src/schema-utils/index.ts +0 -4
- package/src/schema-utils/utils.ts +0 -47
- package/src/schema-utils/validator.ts +0 -58
- package/src/server/index.ts +0 -40
- package/src/services/asset-service.ts +0 -256
- package/src/services/index.ts +0 -6
- package/src/storage/adapters/index.ts +0 -2
- package/src/storage/adapters/local-storage-adapter.ts +0 -215
- package/src/storage/index.ts +0 -8
- package/src/storage/interfaces/index.ts +0 -2
- package/src/storage/interfaces/storage.ts +0 -114
- package/src/storage/providers/storage.ts +0 -83
- package/src/types/asset.ts +0 -81
- package/src/types/auth.ts +0 -80
- package/src/types/config.ts +0 -45
- package/src/types/document.ts +0 -38
- package/src/types/index.ts +0 -8
- package/src/types/organization.ts +0 -119
- package/src/types/schemas.ts +0 -151
- package/src/types/sidebar.ts +0 -37
- package/src/types/user.ts +0 -17
- package/src/utils/content-hash.ts +0 -75
- package/src/utils/image-url.ts +0 -204
- package/src/utils/index.ts +0 -12
- package/src/utils/slug.ts +0 -33
|
@@ -1,795 +0,0 @@
|
|
|
1
|
-
<script lang="ts">
|
|
2
|
-
import { Button } from '@aphexcms/ui/shadcn/button';
|
|
3
|
-
import { Badge } from '@aphexcms/ui/shadcn/badge';
|
|
4
|
-
import { documents } from '../../api/documents.js';
|
|
5
|
-
import { ApiError } from '../../api/client.js';
|
|
6
|
-
import SchemaField from './SchemaField.svelte';
|
|
7
|
-
import { findOrphanedFields, type OrphanedField } from '../../schema-utils/cleanup.js';
|
|
8
|
-
import type { SchemaType } from 'src/types/schemas.js';
|
|
9
|
-
import { Rule } from '../../field-validation/rule.js';
|
|
10
|
-
import { hasUnpublishedChanges } from '../../utils/content-hash.js';
|
|
11
|
-
import { setSchemaContext } from '../../schema-context.svelte.js';
|
|
12
|
-
|
|
13
|
-
interface Props {
|
|
14
|
-
schemas: SchemaType[];
|
|
15
|
-
documentType: string;
|
|
16
|
-
documentId?: string | null;
|
|
17
|
-
isCreating: boolean;
|
|
18
|
-
onBack: () => void;
|
|
19
|
-
onSaved?: (documentId: string) => void;
|
|
20
|
-
onAutoSaved?: (documentId: string, title: string) => void;
|
|
21
|
-
onDeleted?: () => void;
|
|
22
|
-
onPublished?: (documentId: string) => void;
|
|
23
|
-
onOpenReference?: (documentId: string, documentType: string) => void;
|
|
24
|
-
isReadOnly?: boolean;
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
let {
|
|
28
|
-
schemas,
|
|
29
|
-
documentType,
|
|
30
|
-
documentId,
|
|
31
|
-
isCreating,
|
|
32
|
-
onBack,
|
|
33
|
-
onSaved,
|
|
34
|
-
onAutoSaved,
|
|
35
|
-
onDeleted,
|
|
36
|
-
onPublished,
|
|
37
|
-
onOpenReference,
|
|
38
|
-
isReadOnly = false
|
|
39
|
-
}: Props = $props();
|
|
40
|
-
|
|
41
|
-
// Set schema context for child components (ArrayField, etc.)
|
|
42
|
-
setSchemaContext(schemas);
|
|
43
|
-
|
|
44
|
-
// Schema and document state
|
|
45
|
-
let schema = $state<SchemaType | null>(null);
|
|
46
|
-
let schemaLoading = $state(false);
|
|
47
|
-
let schemaError = $state<string | null>(null);
|
|
48
|
-
|
|
49
|
-
// Document data state
|
|
50
|
-
let documentData = $state<Record<string, any>>({});
|
|
51
|
-
let fullDocument = $state<any>(null); // Store full document with publishedHash
|
|
52
|
-
let saving = $state(false);
|
|
53
|
-
let saveError = $state<string | null>(null);
|
|
54
|
-
let lastSaved = $state<Date | null>(null);
|
|
55
|
-
let publishSuccess = $state<Date | null>(null);
|
|
56
|
-
|
|
57
|
-
// Menu dropdown state
|
|
58
|
-
let showDropdown = $state(false);
|
|
59
|
-
|
|
60
|
-
// Auto-save functionality (every 2 seconds when there are changes)
|
|
61
|
-
let hasUnsavedChanges = $state(false);
|
|
62
|
-
let autoSaveTimer: ReturnType<typeof setTimeout> | null = null;
|
|
63
|
-
let hasValidationErrors = $state(false);
|
|
64
|
-
|
|
65
|
-
let orphanedFields = $state<OrphanedField[]>([]);
|
|
66
|
-
let showOrphanedFields = $state(false);
|
|
67
|
-
let schemaFields: SchemaField[] = [];
|
|
68
|
-
|
|
69
|
-
// Track previous document to detect actual switches (not create→edit transitions)
|
|
70
|
-
let previousDocumentId = $state<string | null | undefined>(undefined);
|
|
71
|
-
let previousDocumentType = $state<string | undefined>(undefined);
|
|
72
|
-
let justCreatedDocument = $state(false); // Flag to skip loadDocumentData after creation
|
|
73
|
-
|
|
74
|
-
// Hash-based state tracking
|
|
75
|
-
const hasUnpublishedContent = $derived(
|
|
76
|
-
hasUnpublishedChanges(documentData, fullDocument?.publishedHash || null)
|
|
77
|
-
);
|
|
78
|
-
const canPublish = $derived(
|
|
79
|
-
hasUnpublishedContent && !saving && documentId && !hasValidationErrors
|
|
80
|
-
);
|
|
81
|
-
|
|
82
|
-
// Get preview title based on schema config
|
|
83
|
-
function getPreviewTitle(): string {
|
|
84
|
-
if (!schema?.preview?.select?.title) {
|
|
85
|
-
return documentData.title || `Untitled`;
|
|
86
|
-
}
|
|
87
|
-
return documentData[schema.preview.select.title] || `Untitled`;
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
// CRITICAL: Clear state IMMEDIATELY when switching documents to prevent cross-contamination
|
|
91
|
-
$effect(() => {
|
|
92
|
-
// This effect runs first - watching documentId and documentType
|
|
93
|
-
const _docId = documentId;
|
|
94
|
-
const _docType = documentType;
|
|
95
|
-
|
|
96
|
-
console.log('hasValidationErrors: ', hasValidationErrors);
|
|
97
|
-
|
|
98
|
-
// Detect if we're actually switching documents vs just transitioning from creating→editing
|
|
99
|
-
const isSwitchingDocuments =
|
|
100
|
-
previousDocumentId !== undefined && // Not first render
|
|
101
|
-
previousDocumentType !== undefined &&
|
|
102
|
-
(previousDocumentType !== _docType || // Different document type
|
|
103
|
-
(previousDocumentId !== null && previousDocumentId !== _docId)); // Different document ID (but not create→edit)
|
|
104
|
-
|
|
105
|
-
// Only clear state if actually switching documents
|
|
106
|
-
if (isSwitchingDocuments) {
|
|
107
|
-
// Clear all state immediately to prevent old data from being auto-saved
|
|
108
|
-
documentData = {};
|
|
109
|
-
fullDocument = null;
|
|
110
|
-
hasUnsavedChanges = false;
|
|
111
|
-
saveError = null;
|
|
112
|
-
lastSaved = null;
|
|
113
|
-
publishSuccess = null;
|
|
114
|
-
|
|
115
|
-
// Cancel pending auto-save
|
|
116
|
-
if (autoSaveTimer) {
|
|
117
|
-
clearTimeout(autoSaveTimer);
|
|
118
|
-
autoSaveTimer = null;
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
console.log('🧹 Cleared state for document switch:', _docType, _docId || 'new');
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
// Update tracking
|
|
125
|
-
previousDocumentId = _docId;
|
|
126
|
-
previousDocumentType = _docType;
|
|
127
|
-
});
|
|
128
|
-
|
|
129
|
-
// Load schema when documentType is available or when switching to create mode
|
|
130
|
-
$effect(() => {
|
|
131
|
-
if (documentType) {
|
|
132
|
-
loadSchema();
|
|
133
|
-
}
|
|
134
|
-
});
|
|
135
|
-
|
|
136
|
-
// Load existing document data when editing
|
|
137
|
-
$effect(() => {
|
|
138
|
-
if (!isCreating && documentId) {
|
|
139
|
-
// Skip loading if we just created this document (data is already in memory)
|
|
140
|
-
if (justCreatedDocument) {
|
|
141
|
-
console.log('⏭️ Skipping loadDocumentData - just created document');
|
|
142
|
-
justCreatedDocument = false;
|
|
143
|
-
return;
|
|
144
|
-
}
|
|
145
|
-
loadDocumentData();
|
|
146
|
-
}
|
|
147
|
-
});
|
|
148
|
-
|
|
149
|
-
// Reset to defaults when creating new document
|
|
150
|
-
$effect(() => {
|
|
151
|
-
if (isCreating && schema) {
|
|
152
|
-
resetToDefaults();
|
|
153
|
-
}
|
|
154
|
-
});
|
|
155
|
-
|
|
156
|
-
// Check for orphaned fields when document data or schema changes
|
|
157
|
-
$effect(() => {
|
|
158
|
-
if (documentData && schema && Object.keys(documentData).length > 0) {
|
|
159
|
-
const cleanupResult = findOrphanedFields(documentData, schema);
|
|
160
|
-
orphanedFields = cleanupResult.orphanedFields;
|
|
161
|
-
showOrphanedFields = cleanupResult.hasOrphanedFields;
|
|
162
|
-
}
|
|
163
|
-
});
|
|
164
|
-
|
|
165
|
-
function loadSchema() {
|
|
166
|
-
schemaLoading = true;
|
|
167
|
-
schemaError = null;
|
|
168
|
-
|
|
169
|
-
console.log('[Document Editor] RUNNING LOAD SCHEMA');
|
|
170
|
-
console.log('[Document Editor] SCHEMAS: ', schemas);
|
|
171
|
-
|
|
172
|
-
try {
|
|
173
|
-
// Find schema from provided schemas
|
|
174
|
-
const foundSchema = schemas.find((s) => s.name === documentType);
|
|
175
|
-
|
|
176
|
-
if (foundSchema) {
|
|
177
|
-
schema = foundSchema;
|
|
178
|
-
} else {
|
|
179
|
-
throw new Error(`Schema type '${documentType}' not found`);
|
|
180
|
-
}
|
|
181
|
-
} catch (err) {
|
|
182
|
-
console.error('Failed to load schema:', err);
|
|
183
|
-
schemaError = err instanceof Error ? err.message : 'Failed to load schema';
|
|
184
|
-
} finally {
|
|
185
|
-
schemaLoading = false;
|
|
186
|
-
}
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
async function loadDocumentData() {
|
|
190
|
-
if (!documentId) return;
|
|
191
|
-
|
|
192
|
-
console.log('📄 Loading document data for:', documentId);
|
|
193
|
-
|
|
194
|
-
try {
|
|
195
|
-
const response = await documents.getById(documentId);
|
|
196
|
-
|
|
197
|
-
if (response.success && response.data) {
|
|
198
|
-
// Store full document for hash comparison
|
|
199
|
-
fullDocument = response.data;
|
|
200
|
-
|
|
201
|
-
// Load the draft data if available, otherwise published data
|
|
202
|
-
const data = response.data.draftData || response.data.publishedData || {};
|
|
203
|
-
console.log('📄 Loaded document data:', data);
|
|
204
|
-
console.log('📄 Published hash:', response.data.publishedHash);
|
|
205
|
-
|
|
206
|
-
documentData = { ...data };
|
|
207
|
-
hasUnsavedChanges = false; // Just loaded, so no unsaved changes
|
|
208
|
-
} else {
|
|
209
|
-
console.error('❌ Failed to load document data:', response.error);
|
|
210
|
-
saveError = response.error || 'Failed to load document';
|
|
211
|
-
}
|
|
212
|
-
} catch (err) {
|
|
213
|
-
console.error('❌ Error loading document data:', err);
|
|
214
|
-
saveError = err instanceof ApiError ? err.message : 'Failed to load document';
|
|
215
|
-
}
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
function resetToDefaults() {
|
|
219
|
-
if (!schema) return;
|
|
220
|
-
|
|
221
|
-
console.log('🔄 Resetting document data to defaults for new document');
|
|
222
|
-
|
|
223
|
-
// Reset document data with field defaults
|
|
224
|
-
const initialData: Record<string, any> = {};
|
|
225
|
-
schema.fields.forEach((field) => {
|
|
226
|
-
if (field.type === 'boolean' && 'initialValue' in field) {
|
|
227
|
-
initialData[field.name] = field.initialValue;
|
|
228
|
-
} else {
|
|
229
|
-
initialData[field.name] = '';
|
|
230
|
-
}
|
|
231
|
-
});
|
|
232
|
-
|
|
233
|
-
documentData = initialData;
|
|
234
|
-
fullDocument = null;
|
|
235
|
-
hasUnsavedChanges = false;
|
|
236
|
-
lastSaved = null;
|
|
237
|
-
saveError = null;
|
|
238
|
-
console.log('✅ Document data reset to:', initialData);
|
|
239
|
-
}
|
|
240
|
-
|
|
241
|
-
// Check if document has meaningful content (not just empty initialized values)
|
|
242
|
-
function hasMeaningfulContent(data: Record<string, any>): boolean {
|
|
243
|
-
return Object.values(data).some((value) => {
|
|
244
|
-
if (typeof value === 'string') return value.trim() !== '';
|
|
245
|
-
if (typeof value === 'boolean') return value !== false; // Assuming false is default
|
|
246
|
-
if (Array.isArray(value)) return value.length > 0;
|
|
247
|
-
if (typeof value === 'object' && value !== null) return Object.keys(value).length > 0;
|
|
248
|
-
return value !== null && value !== undefined && value !== '';
|
|
249
|
-
});
|
|
250
|
-
}
|
|
251
|
-
|
|
252
|
-
// Watch for changes to trigger auto-save (debounced)
|
|
253
|
-
$effect(() => {
|
|
254
|
-
const hasContent = hasMeaningfulContent(documentData);
|
|
255
|
-
|
|
256
|
-
// Only set hasUnsavedChanges if we actually have meaningful data
|
|
257
|
-
if (hasContent) {
|
|
258
|
-
hasUnsavedChanges = true;
|
|
259
|
-
}
|
|
260
|
-
|
|
261
|
-
if (autoSaveTimer) {
|
|
262
|
-
clearTimeout(autoSaveTimer);
|
|
263
|
-
}
|
|
264
|
-
|
|
265
|
-
// Debounced auto-save - waits for 800ms pause in typing (like Notion/modern apps)
|
|
266
|
-
// Only auto-save if there's meaningful content and not in read-only mode
|
|
267
|
-
if (hasContent && schema && !isReadOnly) {
|
|
268
|
-
autoSaveTimer = setTimeout(() => {
|
|
269
|
-
console.log('🔄 Auto-saving after typing pause...', { documentId });
|
|
270
|
-
saveDocument(true); // auto-save
|
|
271
|
-
}, 1200); // Shorter delay - saves faster but still waits for typing pauses
|
|
272
|
-
}
|
|
273
|
-
|
|
274
|
-
return () => {
|
|
275
|
-
if (autoSaveTimer) {
|
|
276
|
-
clearTimeout(autoSaveTimer);
|
|
277
|
-
}
|
|
278
|
-
};
|
|
279
|
-
});
|
|
280
|
-
|
|
281
|
-
async function saveDocument(isAutoSave = false) {
|
|
282
|
-
if (saving) return;
|
|
283
|
-
|
|
284
|
-
saving = true;
|
|
285
|
-
saveError = null;
|
|
286
|
-
|
|
287
|
-
try {
|
|
288
|
-
let response;
|
|
289
|
-
|
|
290
|
-
// ALWAYS allow saving drafts (even with validation errors) - Sanity-style
|
|
291
|
-
if (isCreating) {
|
|
292
|
-
// Create new document
|
|
293
|
-
console.log('🔄 Creating new document with data:', {
|
|
294
|
-
type: documentType,
|
|
295
|
-
draftData: documentData
|
|
296
|
-
});
|
|
297
|
-
response = await documents.create({
|
|
298
|
-
type: documentType,
|
|
299
|
-
draftData: documentData
|
|
300
|
-
});
|
|
301
|
-
|
|
302
|
-
console.log('📝 Document creation response:', response);
|
|
303
|
-
|
|
304
|
-
if (response.success && response.data) {
|
|
305
|
-
console.log('✅ Document created successfully with ID:', response.data.id);
|
|
306
|
-
// Set flag to prevent loadDocumentData from overwriting our data
|
|
307
|
-
justCreatedDocument = true;
|
|
308
|
-
// Store the response data for hash comparison
|
|
309
|
-
fullDocument = response.data;
|
|
310
|
-
// Always call onSaved to switch to edit mode after creation
|
|
311
|
-
onSaved?.(response.data.id);
|
|
312
|
-
} else {
|
|
313
|
-
console.error('❌ Document creation failed:', response);
|
|
314
|
-
}
|
|
315
|
-
} else if (documentId) {
|
|
316
|
-
// Update existing document
|
|
317
|
-
response = await documents.updateById(documentId, {
|
|
318
|
-
draftData: documentData
|
|
319
|
-
});
|
|
320
|
-
}
|
|
321
|
-
|
|
322
|
-
if (response?.success) {
|
|
323
|
-
lastSaved = new Date();
|
|
324
|
-
hasUnsavedChanges = false;
|
|
325
|
-
if (isAutoSave) {
|
|
326
|
-
// Trigger validation on all fields after auto-save
|
|
327
|
-
validateAllFields(); // Update validation status
|
|
328
|
-
schemaFields.forEach((fieldComponent, index) => {
|
|
329
|
-
const field = schema.fields[index];
|
|
330
|
-
if (fieldComponent && field) {
|
|
331
|
-
fieldComponent.performValidation(documentData[field.name], {});
|
|
332
|
-
}
|
|
333
|
-
}); // Notify parent of autosave with current title
|
|
334
|
-
if (onAutoSaved && documentId) {
|
|
335
|
-
onAutoSaved(documentId, getPreviewTitle());
|
|
336
|
-
}
|
|
337
|
-
}
|
|
338
|
-
} else {
|
|
339
|
-
throw new Error(response?.error || 'Failed to save document');
|
|
340
|
-
}
|
|
341
|
-
} catch (err) {
|
|
342
|
-
console.error('Failed to save document:', err);
|
|
343
|
-
|
|
344
|
-
// Extract validation errors if present
|
|
345
|
-
if (err instanceof ApiError && err.response?.validationErrors) {
|
|
346
|
-
const validationErrors = err.response.validationErrors;
|
|
347
|
-
const errorMessages = validationErrors
|
|
348
|
-
.map((ve: any) => `${ve.field}: ${ve.errors.join(', ')}`)
|
|
349
|
-
.join('; ');
|
|
350
|
-
saveError = `Validation failed: ${errorMessages}`;
|
|
351
|
-
} else {
|
|
352
|
-
saveError = err instanceof ApiError ? err.message : 'Failed to save document';
|
|
353
|
-
}
|
|
354
|
-
} finally {
|
|
355
|
-
saving = false;
|
|
356
|
-
}
|
|
357
|
-
}
|
|
358
|
-
|
|
359
|
-
async function publishDocument() {
|
|
360
|
-
if (!documentId || saving) return;
|
|
361
|
-
|
|
362
|
-
// Check for validation errors before publishing (Sanity-style)
|
|
363
|
-
await validateAllFields();
|
|
364
|
-
|
|
365
|
-
if (hasValidationErrors) {
|
|
366
|
-
saveError = 'Cannot publish: Please fix validation errors first';
|
|
367
|
-
return;
|
|
368
|
-
}
|
|
369
|
-
|
|
370
|
-
saving = true;
|
|
371
|
-
saveError = null;
|
|
372
|
-
|
|
373
|
-
try {
|
|
374
|
-
const response = await documents.publish(documentId);
|
|
375
|
-
|
|
376
|
-
if (response.success && response.data) {
|
|
377
|
-
// Update local state with new published hash
|
|
378
|
-
fullDocument = response.data;
|
|
379
|
-
lastSaved = new Date();
|
|
380
|
-
publishSuccess = new Date();
|
|
381
|
-
console.log('✅ Document published successfully');
|
|
382
|
-
console.log('📄 New published hash:', response.data.publishedHash);
|
|
383
|
-
|
|
384
|
-
// Notify parent that document was published
|
|
385
|
-
if (onPublished && documentId) {
|
|
386
|
-
onPublished(documentId);
|
|
387
|
-
}
|
|
388
|
-
} else {
|
|
389
|
-
throw new Error(response.error || 'Failed to publish document');
|
|
390
|
-
}
|
|
391
|
-
} catch (err) {
|
|
392
|
-
console.error('Failed to publish document:', err);
|
|
393
|
-
|
|
394
|
-
// Extract validation errors if present
|
|
395
|
-
if (err instanceof ApiError && err.response?.validationErrors) {
|
|
396
|
-
const validationErrors = err.response.validationErrors;
|
|
397
|
-
const errorMessages = validationErrors
|
|
398
|
-
.map((ve: any) => `${ve.field}: ${ve.errors.join(', ')}`)
|
|
399
|
-
.join('; ');
|
|
400
|
-
saveError = `Cannot publish - Validation failed: ${errorMessages}`;
|
|
401
|
-
} else {
|
|
402
|
-
saveError = err instanceof ApiError ? err.message : 'Failed to publish document';
|
|
403
|
-
}
|
|
404
|
-
} finally {
|
|
405
|
-
saving = false;
|
|
406
|
-
}
|
|
407
|
-
}
|
|
408
|
-
|
|
409
|
-
// Validate all fields before publishing
|
|
410
|
-
async function validateAllFields(): Promise<void> {
|
|
411
|
-
if (!schema) {
|
|
412
|
-
hasValidationErrors = false;
|
|
413
|
-
return;
|
|
414
|
-
}
|
|
415
|
-
|
|
416
|
-
let errorsFound = false;
|
|
417
|
-
|
|
418
|
-
for (const field of schema.fields) {
|
|
419
|
-
if (field.validation) {
|
|
420
|
-
try {
|
|
421
|
-
const validationFunctions = Array.isArray(field.validation)
|
|
422
|
-
? field.validation
|
|
423
|
-
: [field.validation];
|
|
424
|
-
|
|
425
|
-
for (const validationFn of validationFunctions) {
|
|
426
|
-
const rule = validationFn(new Rule());
|
|
427
|
-
const markers = await rule.validate(documentData[field.name], { path: [field.name] });
|
|
428
|
-
|
|
429
|
-
if (markers.some((m) => m.level === 'error')) {
|
|
430
|
-
errorsFound = true;
|
|
431
|
-
console.log(`❌ Validation error in field '${field.name}':`, markers);
|
|
432
|
-
}
|
|
433
|
-
}
|
|
434
|
-
} catch (error) {
|
|
435
|
-
errorsFound = true;
|
|
436
|
-
console.error(`Validation failed for field '${field.name}':`, error);
|
|
437
|
-
}
|
|
438
|
-
}
|
|
439
|
-
}
|
|
440
|
-
|
|
441
|
-
hasValidationErrors = errorsFound;
|
|
442
|
-
}
|
|
443
|
-
|
|
444
|
-
async function deleteDocument() {
|
|
445
|
-
if (!documentId || saving) return;
|
|
446
|
-
|
|
447
|
-
const confirmDelete = confirm(
|
|
448
|
-
`Are you sure you want to delete this document? This action cannot be undone.`
|
|
449
|
-
);
|
|
450
|
-
if (!confirmDelete) return;
|
|
451
|
-
|
|
452
|
-
saving = true;
|
|
453
|
-
saveError = null;
|
|
454
|
-
|
|
455
|
-
try {
|
|
456
|
-
const response = await documents.deleteById(documentId);
|
|
457
|
-
|
|
458
|
-
if (response.success) {
|
|
459
|
-
console.log('✅ Document deleted successfully');
|
|
460
|
-
onDeleted?.();
|
|
461
|
-
} else {
|
|
462
|
-
throw new Error(response.error || 'Failed to delete document');
|
|
463
|
-
}
|
|
464
|
-
} catch (err) {
|
|
465
|
-
console.error('Failed to delete document:', err);
|
|
466
|
-
saveError = err instanceof ApiError ? err.message : 'Failed to delete document';
|
|
467
|
-
} finally {
|
|
468
|
-
saving = false;
|
|
469
|
-
}
|
|
470
|
-
}
|
|
471
|
-
|
|
472
|
-
// Schema cleanup functions
|
|
473
|
-
function removeOrphanedField(fieldToRemove: OrphanedField) {
|
|
474
|
-
// Remove the specific field from document data
|
|
475
|
-
const newData = { ...documentData };
|
|
476
|
-
|
|
477
|
-
if (fieldToRemove.level === 'document') {
|
|
478
|
-
delete newData[fieldToRemove.key];
|
|
479
|
-
} else {
|
|
480
|
-
// Handle nested field removal (more complex path-based deletion)
|
|
481
|
-
removeFieldByPath(newData, fieldToRemove.path);
|
|
482
|
-
}
|
|
483
|
-
|
|
484
|
-
documentData = newData;
|
|
485
|
-
hasUnsavedChanges = true;
|
|
486
|
-
|
|
487
|
-
// Remove from orphaned fields list
|
|
488
|
-
orphanedFields = orphanedFields.filter((f) => f !== fieldToRemove);
|
|
489
|
-
|
|
490
|
-
// Hide warning if no more orphaned fields
|
|
491
|
-
if (orphanedFields.length === 0) {
|
|
492
|
-
showOrphanedFields = false;
|
|
493
|
-
}
|
|
494
|
-
}
|
|
495
|
-
|
|
496
|
-
function removeFieldByPath(obj: any, path: string) {
|
|
497
|
-
const parts = path.split('.');
|
|
498
|
-
let current = obj;
|
|
499
|
-
|
|
500
|
-
for (let i = 0; i < parts.length - 1; i++) {
|
|
501
|
-
const part = parts[i];
|
|
502
|
-
if (part.includes('[') && part.includes(']')) {
|
|
503
|
-
// Handle array index like "items[0]"
|
|
504
|
-
const [key, indexStr] = part.split('[');
|
|
505
|
-
const index = parseInt(indexStr.replace(']', ''));
|
|
506
|
-
current = current[key][index];
|
|
507
|
-
} else {
|
|
508
|
-
current = current[part];
|
|
509
|
-
}
|
|
510
|
-
}
|
|
511
|
-
|
|
512
|
-
const lastPart = parts[parts.length - 1];
|
|
513
|
-
delete current[lastPart];
|
|
514
|
-
}
|
|
515
|
-
|
|
516
|
-
function cleanupAllOrphanedFields() {
|
|
517
|
-
if (!schema) return;
|
|
518
|
-
|
|
519
|
-
const cleanupResult = findOrphanedFields(documentData, schema);
|
|
520
|
-
documentData = cleanupResult.cleanedData;
|
|
521
|
-
hasUnsavedChanges = true;
|
|
522
|
-
showOrphanedFields = false;
|
|
523
|
-
orphanedFields = [];
|
|
524
|
-
}
|
|
525
|
-
|
|
526
|
-
function dismissOrphanedFields() {
|
|
527
|
-
showOrphanedFields = false;
|
|
528
|
-
}
|
|
529
|
-
</script>
|
|
530
|
-
|
|
531
|
-
<div class="relative flex h-full flex-col">
|
|
532
|
-
<!-- Header Toolbar (Sanity-style) -->
|
|
533
|
-
<div class="border-border bg-background flex h-14 items-center justify-between border-b px-4">
|
|
534
|
-
<!-- Left side: Document info and status -->
|
|
535
|
-
<div class="flex items-center gap-3 overflow-hidden">
|
|
536
|
-
<div class="min-w-0 flex-1">
|
|
537
|
-
<h3 class="truncate text-sm font-medium">
|
|
538
|
-
{getPreviewTitle()}
|
|
539
|
-
</h3>
|
|
540
|
-
<div class="flex items-center gap-2">
|
|
541
|
-
{#if saving}
|
|
542
|
-
<span class="text-muted-foreground text-xs">Saving...</span>
|
|
543
|
-
{:else if lastSaved}
|
|
544
|
-
<span class="text-muted-foreground text-xs">
|
|
545
|
-
Saved {lastSaved.toLocaleTimeString()}
|
|
546
|
-
</span>
|
|
547
|
-
{:else if hasUnsavedChanges}
|
|
548
|
-
<span class="text-muted-foreground text-xs">Unsaved changes</span>
|
|
549
|
-
{/if}
|
|
550
|
-
|
|
551
|
-
<!-- Created by -->
|
|
552
|
-
{#if fullDocument?.createdBy}
|
|
553
|
-
<span class="text-muted-foreground hidden text-xs sm:inline">
|
|
554
|
-
• Created by {typeof fullDocument.createdBy === 'string'
|
|
555
|
-
? fullDocument.createdBy
|
|
556
|
-
: fullDocument.createdBy.name || fullDocument.createdBy.email}
|
|
557
|
-
</span>
|
|
558
|
-
{/if}
|
|
559
|
-
</div>
|
|
560
|
-
</div>
|
|
561
|
-
</div>
|
|
562
|
-
|
|
563
|
-
<!-- Right side: Actions and close button -->
|
|
564
|
-
<div class="flex items-center gap-2">
|
|
565
|
-
<!-- Status badges -->
|
|
566
|
-
{#if saving}
|
|
567
|
-
<Badge variant="secondary" class="hidden sm:flex">Saving...</Badge>
|
|
568
|
-
{:else if publishSuccess && new Date().getTime() - publishSuccess.getTime() < 3000}
|
|
569
|
-
<Badge variant="default" class="hidden sm:flex">Published!</Badge>
|
|
570
|
-
{:else if hasUnpublishedContent}
|
|
571
|
-
<Badge variant="outline" class="hidden sm:flex">Unpublished</Badge>
|
|
572
|
-
{:else if lastSaved}
|
|
573
|
-
<Badge variant="secondary" class="hidden sm:flex">Saved</Badge>
|
|
574
|
-
{/if}
|
|
575
|
-
|
|
576
|
-
<!-- Close button (X) - hidden on mobile -->
|
|
577
|
-
<Button
|
|
578
|
-
variant="ghost"
|
|
579
|
-
size="icon"
|
|
580
|
-
onclick={onBack}
|
|
581
|
-
class="hidden h-8 w-8 hover:cursor-pointer lg:flex"
|
|
582
|
-
title="Close"
|
|
583
|
-
>
|
|
584
|
-
<svg class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
585
|
-
<path
|
|
586
|
-
stroke-linecap="round"
|
|
587
|
-
stroke-linejoin="round"
|
|
588
|
-
stroke-width="2"
|
|
589
|
-
d="M6 18L18 6M6 6l12 12"
|
|
590
|
-
/>
|
|
591
|
-
</svg>
|
|
592
|
-
</Button>
|
|
593
|
-
</div>
|
|
594
|
-
</div>
|
|
595
|
-
|
|
596
|
-
<!-- Content Form -->
|
|
597
|
-
<div class="flex-1 space-y-4 overflow-auto p-4 lg:space-y-6 lg:p-6">
|
|
598
|
-
{#if saveError}
|
|
599
|
-
<div class="bg-destructive/10 border-destructive/20 rounded-md border p-3">
|
|
600
|
-
<p class="text-destructive text-sm">{saveError}</p>
|
|
601
|
-
</div>
|
|
602
|
-
{/if}
|
|
603
|
-
|
|
604
|
-
{#if schemaError}
|
|
605
|
-
<div class="bg-destructive/10 border-destructive/20 rounded-md border p-3">
|
|
606
|
-
<p class="text-destructive text-sm">Schema Error: {schemaError}</p>
|
|
607
|
-
</div>
|
|
608
|
-
{:else if schemaLoading}
|
|
609
|
-
<div class="p-6 text-center">
|
|
610
|
-
<div class="text-muted-foreground text-sm">Loading schema...</div>
|
|
611
|
-
</div>
|
|
612
|
-
{:else if schema}
|
|
613
|
-
<!-- Orphaned Fields Warning -->
|
|
614
|
-
{#if showOrphanedFields && orphanedFields.length > 0}
|
|
615
|
-
<div class="space-y-3 rounded-md border border-orange-200 bg-orange-50 p-4">
|
|
616
|
-
<div class="flex items-center gap-2">
|
|
617
|
-
<svg
|
|
618
|
-
class="h-5 w-5 text-orange-600"
|
|
619
|
-
fill="none"
|
|
620
|
-
viewBox="0 0 24 24"
|
|
621
|
-
stroke="currentColor"
|
|
622
|
-
>
|
|
623
|
-
<path
|
|
624
|
-
stroke-linecap="round"
|
|
625
|
-
stroke-linejoin="round"
|
|
626
|
-
stroke-width="2"
|
|
627
|
-
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"
|
|
628
|
-
/>
|
|
629
|
-
</svg>
|
|
630
|
-
<h4 class="text-sm font-medium text-orange-800">
|
|
631
|
-
{orphanedFields.length} orphaned field{orphanedFields.length === 1 ? '' : 's'} detected
|
|
632
|
-
</h4>
|
|
633
|
-
</div>
|
|
634
|
-
|
|
635
|
-
<p class="text-sm text-orange-700">
|
|
636
|
-
These fields exist in your document but are no longer defined in the schema:
|
|
637
|
-
</p>
|
|
638
|
-
|
|
639
|
-
<div class="space-y-2">
|
|
640
|
-
{#each orphanedFields as field, index (index)}
|
|
641
|
-
<div
|
|
642
|
-
class="flex items-center justify-between rounded border border-orange-200 bg-white p-3"
|
|
643
|
-
>
|
|
644
|
-
<div class="flex-1">
|
|
645
|
-
<div class="font-mono text-sm font-medium text-orange-800">
|
|
646
|
-
{field.path || field.key}
|
|
647
|
-
</div>
|
|
648
|
-
<div class="mt-1 text-xs text-orange-600">
|
|
649
|
-
<code class="rounded bg-orange-100 px-1">{JSON.stringify(field.value)}</code>
|
|
650
|
-
</div>
|
|
651
|
-
</div>
|
|
652
|
-
<Button
|
|
653
|
-
size="sm"
|
|
654
|
-
variant="outline"
|
|
655
|
-
onclick={() => removeOrphanedField(field)}
|
|
656
|
-
class="ml-3 h-8 border-red-200 px-3 text-red-600 hover:border-red-300 hover:bg-red-50"
|
|
657
|
-
>
|
|
658
|
-
Remove
|
|
659
|
-
</Button>
|
|
660
|
-
</div>
|
|
661
|
-
{/each}
|
|
662
|
-
</div>
|
|
663
|
-
|
|
664
|
-
<div class="flex gap-2 border-t border-orange-200 pt-2">
|
|
665
|
-
<Button
|
|
666
|
-
size="sm"
|
|
667
|
-
variant="outline"
|
|
668
|
-
onclick={cleanupAllOrphanedFields}
|
|
669
|
-
class="border-orange-600 bg-orange-600 text-white hover:bg-orange-700"
|
|
670
|
-
>
|
|
671
|
-
Remove All
|
|
672
|
-
</Button>
|
|
673
|
-
<Button
|
|
674
|
-
size="sm"
|
|
675
|
-
variant="ghost"
|
|
676
|
-
onclick={dismissOrphanedFields}
|
|
677
|
-
class="text-orange-700 hover:text-orange-800"
|
|
678
|
-
>
|
|
679
|
-
Dismiss
|
|
680
|
-
</Button>
|
|
681
|
-
</div>
|
|
682
|
-
</div>
|
|
683
|
-
{/if}
|
|
684
|
-
|
|
685
|
-
<!-- Dynamic Schema Fields -->
|
|
686
|
-
{#each schema.fields as field, index (index)}
|
|
687
|
-
<SchemaField
|
|
688
|
-
bind:this={schemaFields[index]}
|
|
689
|
-
{field}
|
|
690
|
-
value={documentData[field.name]}
|
|
691
|
-
{documentData}
|
|
692
|
-
onUpdate={(newValue) => {
|
|
693
|
-
documentData = { ...documentData, [field.name]: newValue };
|
|
694
|
-
hasUnsavedChanges = true;
|
|
695
|
-
}}
|
|
696
|
-
{onOpenReference}
|
|
697
|
-
schemaType={documentType}
|
|
698
|
-
readonly={isReadOnly}
|
|
699
|
-
/>
|
|
700
|
-
{/each}
|
|
701
|
-
{:else}
|
|
702
|
-
<div class="border-muted-foreground/30 rounded-md border border-dashed p-4">
|
|
703
|
-
<p class="text-muted-foreground text-center text-sm">
|
|
704
|
-
No schema found for document type: {documentType}
|
|
705
|
-
</p>
|
|
706
|
-
</div>
|
|
707
|
-
{/if}
|
|
708
|
-
</div>
|
|
709
|
-
|
|
710
|
-
<!-- Sanity-style bottom bar -->
|
|
711
|
-
{#if documentId}
|
|
712
|
-
<div class="border-border bg-background border-t p-4">
|
|
713
|
-
<div class="flex items-center justify-between">
|
|
714
|
-
<!-- Left: Save status badges -->
|
|
715
|
-
<div class="flex items-center gap-2">
|
|
716
|
-
{#if saving}
|
|
717
|
-
<Badge variant="secondary">Saving...</Badge>
|
|
718
|
-
{:else if publishSuccess && new Date().getTime() - publishSuccess.getTime() < 3000}
|
|
719
|
-
<Badge variant="default">Published!</Badge>
|
|
720
|
-
{:else if hasUnsavedChanges}
|
|
721
|
-
<Badge variant="outline">Unsaved</Badge>
|
|
722
|
-
{:else if hasUnpublishedContent}
|
|
723
|
-
<Badge variant="outline">Unpublished Changes</Badge>
|
|
724
|
-
{:else if lastSaved}
|
|
725
|
-
<Badge variant="secondary">Saved</Badge>
|
|
726
|
-
{/if}
|
|
727
|
-
</div>
|
|
728
|
-
|
|
729
|
-
<!-- Right: Publish button + horizontal three dots menu -->
|
|
730
|
-
<div class="flex items-center gap-2">
|
|
731
|
-
{#if !isReadOnly}
|
|
732
|
-
<Button
|
|
733
|
-
onclick={publishDocument}
|
|
734
|
-
disabled={!canPublish}
|
|
735
|
-
size="sm"
|
|
736
|
-
variant={canPublish ? 'default' : 'secondary'}
|
|
737
|
-
class="cursor-pointer"
|
|
738
|
-
>
|
|
739
|
-
{#if saving}
|
|
740
|
-
Publishing...
|
|
741
|
-
{:else if !hasUnpublishedContent}
|
|
742
|
-
Published
|
|
743
|
-
{:else}
|
|
744
|
-
Publish Changes
|
|
745
|
-
{/if}
|
|
746
|
-
</Button>
|
|
747
|
-
{:else}
|
|
748
|
-
<Badge variant="secondary" class="text-xs">Read Only</Badge>
|
|
749
|
-
{/if}
|
|
750
|
-
|
|
751
|
-
<!-- Horizontal three dots menu (only for non-read-only users) -->
|
|
752
|
-
{#if !isReadOnly}
|
|
753
|
-
<div class="relative">
|
|
754
|
-
<Button
|
|
755
|
-
onclick={() => (showDropdown = !showDropdown)}
|
|
756
|
-
variant="ghost"
|
|
757
|
-
class="hover:bg-muted flex h-8 w-8 items-center justify-center rounded transition-colors"
|
|
758
|
-
>
|
|
759
|
-
<svg class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
760
|
-
<path
|
|
761
|
-
stroke-linecap="round"
|
|
762
|
-
stroke-linejoin="round"
|
|
763
|
-
stroke-width="2"
|
|
764
|
-
d="M5 12h.01M12 12h.01M19 12h.01"
|
|
765
|
-
/>
|
|
766
|
-
</svg>
|
|
767
|
-
</Button>
|
|
768
|
-
|
|
769
|
-
{#if showDropdown}
|
|
770
|
-
<!-- Dropdown menu -->
|
|
771
|
-
<div
|
|
772
|
-
class="bg-background border-border absolute bottom-full right-0 z-50 mb-2 min-w-[140px] rounded-md border py-1 shadow-lg"
|
|
773
|
-
>
|
|
774
|
-
<Button
|
|
775
|
-
variant="ghost"
|
|
776
|
-
onclick={() => {
|
|
777
|
-
showDropdown = false;
|
|
778
|
-
deleteDocument();
|
|
779
|
-
}}
|
|
780
|
-
class="hover:bg-muted text-destructive flex w-full items-center gap-2 px-3 py-2 text-left text-sm transition-colors"
|
|
781
|
-
>
|
|
782
|
-
Delete document
|
|
783
|
-
</Button>
|
|
784
|
-
</div>
|
|
785
|
-
|
|
786
|
-
<!-- Click outside to close -->
|
|
787
|
-
<div class="fixed inset-0 z-40" onclick={() => (showDropdown = false)}></div>
|
|
788
|
-
{/if}
|
|
789
|
-
</div>
|
|
790
|
-
{/if}
|
|
791
|
-
</div>
|
|
792
|
-
</div>
|
|
793
|
-
</div>
|
|
794
|
-
{/if}
|
|
795
|
-
</div>
|