@aphexcms/cms-core 0.1.3 → 0.1.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (280) hide show
  1. package/dist/api/assets.d.ts +48 -0
  2. package/dist/api/assets.d.ts.map +1 -0
  3. package/dist/api/assets.js +52 -0
  4. package/dist/api/client.d.ts +37 -0
  5. package/dist/api/client.d.ts.map +1 -0
  6. package/dist/api/client.js +125 -0
  7. package/dist/api/documents.d.ts +56 -0
  8. package/dist/api/documents.d.ts.map +1 -0
  9. package/dist/api/documents.js +77 -0
  10. package/dist/api/index.d.ts +7 -0
  11. package/dist/api/index.d.ts.map +1 -0
  12. package/dist/api/index.js +5 -0
  13. package/dist/api/organizations.d.ts +101 -0
  14. package/dist/api/organizations.d.ts.map +1 -0
  15. package/dist/api/organizations.js +92 -0
  16. package/dist/api/types.d.ts +23 -0
  17. package/dist/api/types.d.ts.map +1 -0
  18. package/dist/api/types.js +1 -0
  19. package/dist/app.d.ts +19 -0
  20. package/dist/auth/MULTI_TENANCY_PLAN.md +1183 -0
  21. package/dist/auth/auth-errors.d.ts +7 -0
  22. package/dist/auth/auth-errors.d.ts.map +1 -0
  23. package/dist/auth/auth-errors.js +13 -0
  24. package/dist/auth/auth-hooks.d.ts +6 -0
  25. package/dist/auth/auth-hooks.d.ts.map +1 -0
  26. package/dist/auth/auth-hooks.js +108 -0
  27. package/dist/auth/provider.d.ts +17 -0
  28. package/dist/auth/provider.d.ts.map +1 -0
  29. package/dist/auth/provider.js +1 -0
  30. package/dist/client/index.d.ts +24 -0
  31. package/dist/client/index.d.ts.map +1 -0
  32. package/dist/client/index.js +31 -0
  33. package/dist/components/AdminApp.svelte +1077 -0
  34. package/dist/components/AdminApp.svelte.d.ts +24 -0
  35. package/dist/components/AdminApp.svelte.d.ts.map +1 -0
  36. package/dist/components/admin/AdminLayout.svelte +115 -0
  37. package/dist/components/admin/AdminLayout.svelte.d.ts +15 -0
  38. package/dist/components/admin/AdminLayout.svelte.d.ts.map +1 -0
  39. package/dist/components/admin/DocumentEditor.svelte +795 -0
  40. package/dist/components/admin/DocumentEditor.svelte.d.ts +18 -0
  41. package/dist/components/admin/DocumentEditor.svelte.d.ts.map +1 -0
  42. package/dist/components/admin/DocumentTypesList.svelte +97 -0
  43. package/dist/components/admin/DocumentTypesList.svelte.d.ts +14 -0
  44. package/dist/components/admin/DocumentTypesList.svelte.d.ts.map +1 -0
  45. package/dist/components/admin/ObjectModal.svelte +135 -0
  46. package/dist/components/admin/ObjectModal.svelte.d.ts +15 -0
  47. package/dist/components/admin/ObjectModal.svelte.d.ts.map +1 -0
  48. package/dist/components/admin/SchemaField.svelte +171 -0
  49. package/dist/components/admin/SchemaField.svelte.d.ts +19 -0
  50. package/dist/components/admin/SchemaField.svelte.d.ts.map +1 -0
  51. package/dist/components/admin/fields/ArrayField.svelte +266 -0
  52. package/dist/components/admin/fields/ArrayField.svelte.d.ts +12 -0
  53. package/dist/components/admin/fields/ArrayField.svelte.d.ts.map +1 -0
  54. package/dist/components/admin/fields/BooleanField.svelte +35 -0
  55. package/dist/components/admin/fields/BooleanField.svelte.d.ts +13 -0
  56. package/dist/components/admin/fields/BooleanField.svelte.d.ts.map +1 -0
  57. package/dist/components/admin/fields/ImageField.svelte +284 -0
  58. package/dist/components/admin/fields/ImageField.svelte.d.ts +15 -0
  59. package/dist/components/admin/fields/ImageField.svelte.d.ts.map +1 -0
  60. package/dist/components/admin/fields/NumberField.svelte +82 -0
  61. package/dist/components/admin/fields/NumberField.svelte.d.ts +14 -0
  62. package/dist/components/admin/fields/NumberField.svelte.d.ts.map +1 -0
  63. package/dist/components/admin/fields/ReferenceField.svelte +260 -0
  64. package/dist/components/admin/fields/ReferenceField.svelte.d.ts +12 -0
  65. package/dist/components/admin/fields/ReferenceField.svelte.d.ts.map +1 -0
  66. package/dist/components/admin/fields/SlugField.svelte +74 -0
  67. package/dist/components/admin/fields/SlugField.svelte.d.ts +15 -0
  68. package/dist/components/admin/fields/SlugField.svelte.d.ts.map +1 -0
  69. package/dist/components/admin/fields/StringField.svelte +40 -0
  70. package/dist/components/admin/fields/StringField.svelte.d.ts +14 -0
  71. package/dist/components/admin/fields/StringField.svelte.d.ts.map +1 -0
  72. package/dist/components/admin/fields/TextareaField.svelte +40 -0
  73. package/dist/components/admin/fields/TextareaField.svelte.d.ts +14 -0
  74. package/dist/components/admin/fields/TextareaField.svelte.d.ts.map +1 -0
  75. package/dist/components/fields/index.d.ts +9 -0
  76. package/dist/components/fields/index.d.ts.map +1 -0
  77. package/dist/components/fields/index.js +9 -0
  78. package/dist/components/index.d.ts +7 -0
  79. package/dist/components/index.d.ts.map +1 -0
  80. package/dist/components/index.js +12 -0
  81. package/dist/components/layout/OrganizationSwitcher.svelte +218 -0
  82. package/dist/components/layout/OrganizationSwitcher.svelte.d.ts +11 -0
  83. package/dist/components/layout/OrganizationSwitcher.svelte.d.ts.map +1 -0
  84. package/dist/components/layout/Sidebar.svelte +88 -0
  85. package/dist/components/layout/Sidebar.svelte.d.ts +14 -0
  86. package/dist/components/layout/Sidebar.svelte.d.ts.map +1 -0
  87. package/dist/components/layout/sidebar/AppSidebar.svelte +63 -0
  88. package/dist/components/layout/sidebar/AppSidebar.svelte.d.ts +11 -0
  89. package/dist/components/layout/sidebar/AppSidebar.svelte.d.ts.map +1 -0
  90. package/dist/components/layout/sidebar/NavMain.svelte +95 -0
  91. package/dist/components/layout/sidebar/NavMain.svelte.d.ts +19 -0
  92. package/dist/components/layout/sidebar/NavMain.svelte.d.ts.map +1 -0
  93. package/dist/components/layout/sidebar/NavSecondary.svelte +69 -0
  94. package/dist/components/layout/sidebar/NavSecondary.svelte.d.ts +9 -0
  95. package/dist/components/layout/sidebar/NavSecondary.svelte.d.ts.map +1 -0
  96. package/dist/components/layout/sidebar/NavUser.svelte +85 -0
  97. package/dist/components/layout/sidebar/NavUser.svelte.d.ts +9 -0
  98. package/dist/components/layout/sidebar/NavUser.svelte.d.ts.map +1 -0
  99. package/dist/config.d.ts +3 -0
  100. package/dist/config.d.ts.map +1 -0
  101. package/dist/config.js +15 -0
  102. package/dist/db/adapters/index.d.ts +1 -0
  103. package/dist/db/adapters/index.d.ts.map +1 -0
  104. package/dist/db/adapters/index.js +4 -0
  105. package/dist/db/index.d.ts +2 -0
  106. package/dist/db/index.d.ts.map +1 -0
  107. package/dist/db/index.js +4 -0
  108. package/dist/db/interfaces/asset.d.ts +51 -0
  109. package/dist/db/interfaces/asset.d.ts.map +1 -0
  110. package/dist/db/interfaces/asset.js +1 -0
  111. package/dist/db/interfaces/document.d.ts +36 -0
  112. package/dist/db/interfaces/document.d.ts.map +1 -0
  113. package/dist/db/interfaces/document.js +1 -0
  114. package/dist/db/interfaces/index.d.ts +73 -0
  115. package/dist/db/interfaces/index.d.ts.map +1 -0
  116. package/dist/db/interfaces/index.js +1 -0
  117. package/dist/db/interfaces/organization.d.ts +27 -0
  118. package/dist/db/interfaces/organization.d.ts.map +1 -0
  119. package/dist/db/interfaces/organization.js +1 -0
  120. package/dist/db/interfaces/schema.d.ts +21 -0
  121. package/dist/db/interfaces/schema.d.ts.map +1 -0
  122. package/dist/db/interfaces/schema.js +1 -0
  123. package/dist/db/interfaces/user.d.ts +15 -0
  124. package/dist/db/interfaces/user.d.ts.map +1 -0
  125. package/dist/db/interfaces/user.js +1 -0
  126. package/dist/db/utils/reference-resolver.d.ts +18 -0
  127. package/dist/db/utils/reference-resolver.d.ts.map +1 -0
  128. package/dist/db/utils/reference-resolver.js +80 -0
  129. package/dist/define.d.ts +3 -0
  130. package/dist/define.d.ts.map +1 -0
  131. package/dist/define.js +4 -0
  132. package/dist/email/index.d.ts +2 -0
  133. package/dist/email/index.d.ts.map +1 -0
  134. package/dist/email/index.js +4 -0
  135. package/dist/email/interfaces/email.d.ts +42 -0
  136. package/dist/email/interfaces/email.d.ts.map +1 -0
  137. package/dist/email/interfaces/email.js +1 -0
  138. package/dist/engine.d.ts +26 -0
  139. package/dist/engine.d.ts.map +1 -0
  140. package/dist/engine.js +66 -0
  141. package/dist/field-validation/rule.d.ts +51 -0
  142. package/dist/field-validation/rule.d.ts.map +1 -0
  143. package/dist/field-validation/rule.js +221 -0
  144. package/dist/field-validation/utils.d.ts +21 -0
  145. package/dist/field-validation/utils.d.ts.map +1 -0
  146. package/dist/field-validation/utils.js +66 -0
  147. package/dist/hooks.d.ts +23 -0
  148. package/dist/hooks.d.ts.map +1 -0
  149. package/dist/hooks.js +96 -0
  150. package/dist/index.d.ts +2 -0
  151. package/dist/index.d.ts.map +1 -0
  152. package/dist/index.js +4 -0
  153. package/dist/plugins/README.md +154 -0
  154. package/dist/routes/assets-by-id.d.ts +5 -0
  155. package/dist/routes/assets-by-id.d.ts.map +1 -0
  156. package/dist/routes/assets-by-id.js +138 -0
  157. package/dist/routes/assets-cdn.d.ts +3 -0
  158. package/dist/routes/assets-cdn.d.ts.map +1 -0
  159. package/dist/routes/assets-cdn.js +155 -0
  160. package/dist/routes/assets.d.ts +4 -0
  161. package/dist/routes/assets.d.ts.map +1 -0
  162. package/dist/routes/assets.js +94 -0
  163. package/dist/routes/documents-by-id.d.ts +5 -0
  164. package/dist/routes/documents-by-id.d.ts.map +1 -0
  165. package/dist/routes/documents-by-id.js +142 -0
  166. package/dist/routes/documents-publish.d.ts +4 -0
  167. package/dist/routes/documents-publish.d.ts.map +1 -0
  168. package/dist/routes/documents-publish.js +151 -0
  169. package/dist/routes/documents.d.ts +4 -0
  170. package/dist/routes/documents.d.ts.map +1 -0
  171. package/dist/routes/documents.js +131 -0
  172. package/dist/routes/index.d.ts +6 -0
  173. package/dist/routes/index.d.ts.map +1 -0
  174. package/dist/routes/index.js +10 -0
  175. package/dist/routes/organizations-by-id.d.ts +5 -0
  176. package/dist/routes/organizations-by-id.d.ts.map +1 -0
  177. package/dist/routes/organizations-by-id.js +187 -0
  178. package/dist/routes/organizations-invitations.d.ts +4 -0
  179. package/dist/routes/organizations-invitations.d.ts.map +1 -0
  180. package/dist/routes/organizations-invitations.js +125 -0
  181. package/dist/routes/organizations-members.d.ts +5 -0
  182. package/dist/routes/organizations-members.d.ts.map +1 -0
  183. package/dist/routes/organizations-members.js +206 -0
  184. package/dist/routes/organizations-switch.d.ts +3 -0
  185. package/dist/routes/organizations-switch.d.ts.map +1 -0
  186. package/dist/routes/organizations-switch.js +53 -0
  187. package/dist/routes/organizations.d.ts +4 -0
  188. package/dist/routes/organizations.d.ts.map +1 -0
  189. package/dist/routes/organizations.js +108 -0
  190. package/dist/routes/schemas-by-type.d.ts +3 -0
  191. package/dist/routes/schemas-by-type.d.ts.map +1 -0
  192. package/dist/routes/schemas-by-type.js +25 -0
  193. package/dist/routes/schemas.d.ts +3 -0
  194. package/dist/routes/schemas.d.ts.map +1 -0
  195. package/dist/routes/schemas.js +11 -0
  196. package/dist/routes-exports.d.ts +14 -0
  197. package/dist/routes-exports.d.ts.map +1 -0
  198. package/dist/routes-exports.js +19 -0
  199. package/dist/schema-context.svelte.d.ts +10 -0
  200. package/dist/schema-context.svelte.d.ts.map +1 -0
  201. package/dist/schema-context.svelte.js +18 -0
  202. package/dist/schema-utils/cleanup.d.ts +21 -0
  203. package/dist/schema-utils/cleanup.d.ts.map +1 -0
  204. package/dist/schema-utils/cleanup.js +80 -0
  205. package/dist/schema-utils/index.d.ts +4 -0
  206. package/dist/schema-utils/index.d.ts.map +1 -0
  207. package/dist/schema-utils/index.js +4 -0
  208. package/dist/schema-utils/utils.d.ts +30 -0
  209. package/dist/schema-utils/utils.d.ts.map +1 -0
  210. package/dist/schema-utils/utils.js +37 -0
  211. package/dist/schema-utils/validator.d.ts +6 -0
  212. package/dist/schema-utils/validator.d.ts.map +1 -0
  213. package/dist/schema-utils/validator.js +45 -0
  214. package/dist/server/index.d.ts +16 -0
  215. package/dist/server/index.d.ts.map +1 -0
  216. package/dist/server/index.js +28 -0
  217. package/dist/services/asset-service.d.ts +86 -0
  218. package/dist/services/asset-service.d.ts.map +1 -0
  219. package/dist/services/asset-service.js +187 -0
  220. package/dist/services/index.d.ts +3 -0
  221. package/dist/services/index.d.ts.map +1 -0
  222. package/dist/services/index.js +4 -0
  223. package/dist/storage/adapters/index.d.ts +2 -0
  224. package/dist/storage/adapters/index.d.ts.map +1 -0
  225. package/dist/storage/adapters/index.js +2 -0
  226. package/dist/storage/adapters/local-storage-adapter.d.ts +54 -0
  227. package/dist/storage/adapters/local-storage-adapter.d.ts.map +1 -0
  228. package/dist/storage/adapters/local-storage-adapter.js +187 -0
  229. package/dist/storage/index.d.ts +3 -0
  230. package/dist/storage/index.d.ts.map +1 -0
  231. package/dist/storage/index.js +6 -0
  232. package/dist/storage/interfaces/index.d.ts +2 -0
  233. package/dist/storage/interfaces/index.d.ts.map +1 -0
  234. package/dist/storage/interfaces/index.js +2 -0
  235. package/dist/storage/interfaces/storage.d.ts +91 -0
  236. package/dist/storage/interfaces/storage.d.ts.map +1 -0
  237. package/dist/storage/interfaces/storage.js +1 -0
  238. package/dist/storage/providers/storage.d.ts +43 -0
  239. package/dist/storage/providers/storage.d.ts.map +1 -0
  240. package/dist/storage/providers/storage.js +64 -0
  241. package/dist/types/asset.d.ts +73 -0
  242. package/dist/types/asset.d.ts.map +1 -0
  243. package/dist/types/asset.js +2 -0
  244. package/dist/types/auth.d.ts +50 -0
  245. package/dist/types/auth.d.ts.map +1 -0
  246. package/dist/types/auth.js +41 -0
  247. package/dist/types/config.d.ts +47 -0
  248. package/dist/types/config.d.ts.map +1 -0
  249. package/dist/types/config.js +1 -0
  250. package/dist/types/document.d.ts +34 -0
  251. package/dist/types/document.d.ts.map +1 -0
  252. package/dist/types/document.js +1 -0
  253. package/dist/types/index.d.ts +9 -0
  254. package/dist/types/index.d.ts.map +1 -0
  255. package/dist/types/index.js +8 -0
  256. package/dist/types/organization.d.ts +105 -0
  257. package/dist/types/organization.d.ts.map +1 -0
  258. package/dist/types/organization.js +3 -0
  259. package/dist/types/schemas.d.ts +114 -0
  260. package/dist/types/schemas.d.ts.map +1 -0
  261. package/dist/types/schemas.js +1 -0
  262. package/dist/types/sidebar.d.ts +33 -0
  263. package/dist/types/sidebar.d.ts.map +1 -0
  264. package/dist/types/sidebar.js +1 -0
  265. package/dist/types/user.d.ts +14 -0
  266. package/dist/types/user.d.ts.map +1 -0
  267. package/dist/types/user.js +1 -0
  268. package/dist/utils/content-hash.d.ts +22 -0
  269. package/dist/utils/content-hash.d.ts.map +1 -0
  270. package/dist/utils/content-hash.js +67 -0
  271. package/dist/utils/image-url.d.ts +88 -0
  272. package/dist/utils/image-url.d.ts.map +1 -0
  273. package/dist/utils/image-url.js +165 -0
  274. package/dist/utils/index.d.ts +6 -0
  275. package/dist/utils/index.d.ts.map +1 -0
  276. package/dist/utils/index.js +9 -0
  277. package/dist/utils/slug.d.ts +13 -0
  278. package/dist/utils/slug.d.ts.map +1 -0
  279. package/dist/utils/slug.js +30 -0
  280. package/package.json +11 -41
@@ -0,0 +1,795 @@
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>