@aphexcms/cms-core 2.0.3 → 2.0.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 (138) hide show
  1. package/dist/cache/adapters/in-memory-cache-adapter.d.ts +21 -0
  2. package/dist/cache/adapters/in-memory-cache-adapter.d.ts.map +1 -0
  3. package/dist/cache/adapters/in-memory-cache-adapter.js +50 -0
  4. package/dist/cache/document-cache.d.ts +18 -0
  5. package/dist/cache/document-cache.d.ts.map +1 -0
  6. package/dist/cache/document-cache.js +45 -0
  7. package/dist/cache/index.d.ts +4 -0
  8. package/dist/cache/index.d.ts.map +1 -0
  9. package/dist/cache/index.js +2 -0
  10. package/dist/cache/interfaces/cache.d.ts +18 -0
  11. package/dist/cache/interfaces/cache.d.ts.map +1 -0
  12. package/dist/cache/interfaces/cache.js +1 -0
  13. package/dist/client/index.d.ts +1 -0
  14. package/dist/client/index.d.ts.map +1 -1
  15. package/dist/client/index.js +1 -0
  16. package/dist/components/admin/SchemaField.svelte +14 -0
  17. package/dist/components/admin/SchemaField.svelte.d.ts.map +1 -1
  18. package/dist/components/admin/fields/FileField.svelte +485 -0
  19. package/dist/components/admin/fields/FileField.svelte.d.ts +18 -0
  20. package/dist/components/admin/fields/FileField.svelte.d.ts.map +1 -0
  21. package/dist/components/admin/fields/ReferenceField.svelte +2 -2
  22. package/dist/components/fields/index.d.ts +1 -0
  23. package/dist/components/fields/index.d.ts.map +1 -1
  24. package/dist/components/fields/index.js +1 -0
  25. package/dist/graphql/schema.d.ts.map +1 -1
  26. package/dist/graphql/schema.js +15 -1
  27. package/dist/hooks.d.ts.map +1 -1
  28. package/dist/hooks.js +11 -0
  29. package/dist/lib/cache/adapters/in-memory-cache-adapter.d.ts +21 -0
  30. package/dist/lib/cache/adapters/in-memory-cache-adapter.d.ts.map +1 -0
  31. package/dist/lib/cache/adapters/in-memory-cache-adapter.js +51 -0
  32. package/dist/lib/cache/adapters/in-memory-cache-adapter.js.map +1 -0
  33. package/dist/lib/cache/document-cache.d.ts +18 -0
  34. package/dist/lib/cache/document-cache.d.ts.map +1 -0
  35. package/dist/lib/cache/document-cache.js +46 -0
  36. package/dist/lib/cache/document-cache.js.map +1 -0
  37. package/dist/lib/cache/index.d.ts +4 -0
  38. package/dist/lib/cache/index.d.ts.map +1 -0
  39. package/dist/lib/cache/index.js +3 -0
  40. package/dist/lib/cache/index.js.map +1 -0
  41. package/dist/lib/cache/interfaces/cache.d.ts +18 -0
  42. package/dist/lib/cache/interfaces/cache.d.ts.map +1 -0
  43. package/dist/lib/cache/interfaces/cache.js +2 -0
  44. package/dist/lib/cache/interfaces/cache.js.map +1 -0
  45. package/dist/lib/client/index.d.ts +1 -0
  46. package/dist/lib/client/index.d.ts.map +1 -1
  47. package/dist/lib/client/index.js +1 -0
  48. package/dist/lib/client/index.js.map +1 -1
  49. package/dist/lib/components/fields/index.d.ts +1 -0
  50. package/dist/lib/components/fields/index.d.ts.map +1 -1
  51. package/dist/lib/components/fields/index.js +1 -0
  52. package/dist/lib/components/fields/index.js.map +1 -1
  53. package/dist/lib/graphql/schema.d.ts.map +1 -1
  54. package/dist/lib/graphql/schema.js +15 -1
  55. package/dist/lib/graphql/schema.js.map +1 -1
  56. package/dist/lib/hooks.d.ts.map +1 -1
  57. package/dist/lib/hooks.js +11 -0
  58. package/dist/lib/hooks.js.map +1 -1
  59. package/dist/lib/local-api/collection-api.d.ts +5 -1
  60. package/dist/lib/local-api/collection-api.d.ts.map +1 -1
  61. package/dist/lib/local-api/collection-api.js +63 -10
  62. package/dist/lib/local-api/collection-api.js.map +1 -1
  63. package/dist/lib/local-api/index.d.ts +13 -0
  64. package/dist/lib/local-api/index.d.ts.map +1 -1
  65. package/dist/lib/local-api/index.js +26 -2
  66. package/dist/lib/local-api/index.js.map +1 -1
  67. package/dist/lib/routes/assets.d.ts.map +1 -1
  68. package/dist/lib/routes/assets.js +14 -0
  69. package/dist/lib/routes/assets.js.map +1 -1
  70. package/dist/lib/routes/documents-by-id.js +18 -18
  71. package/dist/lib/routes/documents-by-id.js.map +1 -1
  72. package/dist/lib/routes/documents-publish.js +12 -12
  73. package/dist/lib/routes/documents-publish.js.map +1 -1
  74. package/dist/lib/schema-utils/validator.d.ts.map +1 -1
  75. package/dist/lib/schema-utils/validator.js +1 -0
  76. package/dist/lib/schema-utils/validator.js.map +1 -1
  77. package/dist/lib/server/index.d.ts +1 -0
  78. package/dist/lib/server/index.d.ts.map +1 -1
  79. package/dist/lib/server/index.js +1 -0
  80. package/dist/lib/server/index.js.map +1 -1
  81. package/dist/lib/services/hierarchy-service.d.ts +26 -0
  82. package/dist/lib/services/hierarchy-service.d.ts.map +1 -0
  83. package/dist/lib/services/hierarchy-service.js +64 -0
  84. package/dist/lib/services/hierarchy-service.js.map +1 -0
  85. package/dist/lib/services/index.d.ts +1 -0
  86. package/dist/lib/services/index.d.ts.map +1 -1
  87. package/dist/lib/services/index.js +1 -0
  88. package/dist/lib/services/index.js.map +1 -1
  89. package/dist/lib/storage/adapters/local-storage-adapter.d.ts.map +1 -1
  90. package/dist/lib/storage/adapters/local-storage-adapter.js +0 -14
  91. package/dist/lib/storage/adapters/local-storage-adapter.js.map +1 -1
  92. package/dist/lib/storage/interfaces/storage.d.ts +0 -1
  93. package/dist/lib/storage/interfaces/storage.d.ts.map +1 -1
  94. package/dist/lib/types/asset.d.ts +9 -0
  95. package/dist/lib/types/asset.d.ts.map +1 -1
  96. package/dist/lib/types/config.d.ts +7 -0
  97. package/dist/lib/types/config.d.ts.map +1 -1
  98. package/dist/lib/types/schemas.d.ts +10 -2
  99. package/dist/lib/types/schemas.d.ts.map +1 -1
  100. package/dist/lib/utils/mime-detect.d.ts +22 -0
  101. package/dist/lib/utils/mime-detect.d.ts.map +1 -0
  102. package/dist/lib/utils/mime-detect.js +201 -0
  103. package/dist/lib/utils/mime-detect.js.map +1 -0
  104. package/dist/local-api/collection-api.d.ts +5 -1
  105. package/dist/local-api/collection-api.d.ts.map +1 -1
  106. package/dist/local-api/collection-api.js +63 -10
  107. package/dist/local-api/index.d.ts +13 -0
  108. package/dist/local-api/index.d.ts.map +1 -1
  109. package/dist/local-api/index.js +26 -2
  110. package/dist/routes/assets.d.ts.map +1 -1
  111. package/dist/routes/assets.js +14 -0
  112. package/dist/routes/documents-by-id.js +18 -18
  113. package/dist/routes/documents-publish.js +12 -12
  114. package/dist/schema-utils/validator.d.ts.map +1 -1
  115. package/dist/schema-utils/validator.js +1 -0
  116. package/dist/server/index.d.ts +1 -0
  117. package/dist/server/index.d.ts.map +1 -1
  118. package/dist/server/index.js +1 -0
  119. package/dist/services/hierarchy-service.d.ts +26 -0
  120. package/dist/services/hierarchy-service.d.ts.map +1 -0
  121. package/dist/services/hierarchy-service.js +63 -0
  122. package/dist/services/index.d.ts +1 -0
  123. package/dist/services/index.d.ts.map +1 -1
  124. package/dist/services/index.js +1 -0
  125. package/dist/storage/adapters/local-storage-adapter.d.ts.map +1 -1
  126. package/dist/storage/adapters/local-storage-adapter.js +0 -14
  127. package/dist/storage/interfaces/storage.d.ts +0 -1
  128. package/dist/storage/interfaces/storage.d.ts.map +1 -1
  129. package/dist/types/asset.d.ts +9 -0
  130. package/dist/types/asset.d.ts.map +1 -1
  131. package/dist/types/config.d.ts +7 -0
  132. package/dist/types/config.d.ts.map +1 -1
  133. package/dist/types/schemas.d.ts +10 -2
  134. package/dist/types/schemas.d.ts.map +1 -1
  135. package/dist/utils/mime-detect.d.ts +22 -0
  136. package/dist/utils/mime-detect.d.ts.map +1 -0
  137. package/dist/utils/mime-detect.js +200 -0
  138. package/package.json +1 -1
@@ -0,0 +1,485 @@
1
+ <script lang="ts">
2
+ import { Button } from '@aphexcms/ui/shadcn/button';
3
+ import { Upload, File as FileIcon, Download, Copy, CircleX } from '@lucide/svelte';
4
+ import type { FileValue } from '../../../types/asset';
5
+ import type { FileField as FileFieldType } from '../../../types/schemas';
6
+ import { assets } from '../../../api/assets';
7
+ import { toast } from 'svelte-sonner';
8
+ import {
9
+ DropdownMenu,
10
+ DropdownMenuTrigger,
11
+ DropdownMenuContent,
12
+ DropdownMenuItem,
13
+ DropdownMenuSeparator,
14
+ DropdownMenuGroup
15
+ } from '@aphexcms/ui/shadcn/dropdown-menu';
16
+ import { Ellipsis } from '@lucide/svelte';
17
+ import elementEvents from '../../../utils/element-events';
18
+ import { copyUrlToClipboard, downloadFile } from '../../../utils/asset-actions';
19
+ import AssetBrowserModal from '../AssetBrowserModal.svelte';
20
+
21
+ interface Props {
22
+ field: FileFieldType;
23
+ value: FileValue | null;
24
+ validationClasses?: string;
25
+ onUpdate: (value: FileValue | null) => void;
26
+ schemaType?: string;
27
+ fieldPath?: string;
28
+ readonly?: boolean;
29
+ compact?: boolean;
30
+ arrayItem?: boolean;
31
+ organizationId?: string;
32
+ }
33
+
34
+ let {
35
+ field,
36
+ value,
37
+ onUpdate,
38
+ validationClasses,
39
+ schemaType,
40
+ fieldPath,
41
+ readonly = false,
42
+ compact = false,
43
+ arrayItem = false,
44
+ organizationId
45
+ }: Props = $props();
46
+
47
+ let isDragging = $state(false);
48
+ let isUploading = $state(false);
49
+ let uploadError = $state<string | null>(null);
50
+ let fileInputRef: HTMLInputElement;
51
+ let showAssetBrowser = $state(false);
52
+
53
+ // Build accept string for file input from field.accept array
54
+ const acceptString = $derived(field.accept ? field.accept.join(',') : undefined);
55
+
56
+ async function uploadFile(file: File): Promise<FileValue | null> {
57
+ isUploading = true;
58
+ uploadError = null;
59
+
60
+ try {
61
+ const formData = new FormData();
62
+ formData.append('file', file);
63
+
64
+ if (organizationId) formData.append('organizationId', organizationId);
65
+ if (schemaType) formData.append('schemaType', schemaType);
66
+ if (fieldPath) formData.append('fieldPath', fieldPath);
67
+
68
+ // Pass allowed MIME types for server-side validation
69
+ if (field.accept) {
70
+ formData.append('allowedMimeTypes', JSON.stringify(field.accept));
71
+ }
72
+ if (field.maxSize) {
73
+ formData.append('maxSize', String(field.maxSize));
74
+ }
75
+
76
+ const result = await assets.upload(formData);
77
+
78
+ if (!result.success) {
79
+ throw new Error(result.error || 'Upload failed');
80
+ }
81
+
82
+ const asset = result.data;
83
+
84
+ return {
85
+ _type: 'file',
86
+ asset: {
87
+ _type: 'reference',
88
+ _ref: asset!.id
89
+ }
90
+ };
91
+ } catch (error) {
92
+ uploadError = error instanceof Error ? error.message : 'Upload failed';
93
+ return null;
94
+ } finally {
95
+ isUploading = false;
96
+ }
97
+ }
98
+
99
+ async function handleFileSelect(files: FileList | null) {
100
+ if (readonly || !files || files.length === 0) return;
101
+
102
+ const file = files[0]!;
103
+
104
+ // Client-side size check
105
+ if (field.maxSize && file.size > field.maxSize) {
106
+ const maxMB = (field.maxSize / (1024 * 1024)).toFixed(1);
107
+ uploadError = `File exceeds maximum size of ${maxMB} MB`;
108
+ return;
109
+ }
110
+
111
+ const fileValue = await uploadFile(file);
112
+ if (fileValue) {
113
+ onUpdate(fileValue);
114
+ }
115
+ }
116
+
117
+ function handleDragOver(event: DragEvent) {
118
+ if (readonly) return;
119
+ event.preventDefault();
120
+ isDragging = true;
121
+ }
122
+
123
+ function handleDragLeave(event: DragEvent) {
124
+ if (readonly) return;
125
+ event.preventDefault();
126
+ isDragging = false;
127
+ }
128
+
129
+ function handleDrop(event: DragEvent) {
130
+ if (readonly) return;
131
+ event.preventDefault();
132
+ isDragging = false;
133
+ handleFileSelect(event.dataTransfer?.files || null);
134
+ }
135
+
136
+ function handleFileInputChange(event: Event) {
137
+ if (readonly) return;
138
+ const target = event.target as HTMLInputElement;
139
+ handleFileSelect(target.files);
140
+ }
141
+
142
+ function openFileDialog() {
143
+ if (readonly) return;
144
+ fileInputRef?.click();
145
+ }
146
+
147
+ function removeFile() {
148
+ if (readonly) return;
149
+ onUpdate(null);
150
+ uploadError = null;
151
+ }
152
+
153
+ // Asset data state
154
+ let assetData = $state<any>(null);
155
+ let loadingAsset = $state(false);
156
+ let lastAssetId = $state<string | null>(null);
157
+
158
+ $effect(() => {
159
+ async function loadAsset() {
160
+ const assetId = value?.asset?._ref || null;
161
+
162
+ if (assetId !== lastAssetId) {
163
+ lastAssetId = assetId;
164
+
165
+ if (assetId) {
166
+ loadingAsset = true;
167
+ try {
168
+ const result = await assets.getById(assetId);
169
+ if (result.success) {
170
+ assetData = result.data;
171
+ } else {
172
+ toast.error('Failed to fetch asset details');
173
+ assetData = null;
174
+ }
175
+ } catch (error) {
176
+ toast.error('Failed to load file asset');
177
+ assetData = null;
178
+ } finally {
179
+ loadingAsset = false;
180
+ }
181
+ } else {
182
+ assetData = null;
183
+ loadingAsset = false;
184
+ }
185
+ }
186
+ }
187
+ loadAsset();
188
+ });
189
+
190
+ const fileUrl = $derived(assetData?.url || null);
191
+
192
+ const displayName = $derived(
193
+ assetData?.originalFilename || assetData?.filename || value?.asset?._ref || 'File'
194
+ );
195
+
196
+ const fileSize = $derived(
197
+ assetData?.size
198
+ ? assetData.size > 1024 * 1024
199
+ ? `${(assetData.size / (1024 * 1024)).toFixed(1)} MB`
200
+ : `${(assetData.size / 1024).toFixed(1)} KB`
201
+ : null
202
+ );
203
+
204
+ const fileMime = $derived(assetData?.mimeType || null);
205
+
206
+ function downloadAsset() {
207
+ if (fileUrl) {
208
+ downloadFile(fileUrl, displayName);
209
+ }
210
+ }
211
+
212
+ async function copyUrl() {
213
+ if (fileUrl) {
214
+ await copyUrlToClipboard(fileUrl);
215
+ }
216
+ }
217
+ </script>
218
+
219
+ <!-- Hidden file input -->
220
+ <input
221
+ bind:this={fileInputRef}
222
+ type="file"
223
+ accept={acceptString}
224
+ style="display: none"
225
+ onchange={handleFileInputChange}
226
+ />
227
+
228
+ {#if arrayItem}
229
+ <!-- Minimal row for array items -->
230
+ {#if value && value.asset}
231
+ <div class="flex items-center gap-3">
232
+ <div
233
+ class="bg-muted flex h-10 w-10 shrink-0 items-center justify-center overflow-hidden rounded"
234
+ >
235
+ {#if loadingAsset}
236
+ <div class="border-primary h-4 w-4 animate-spin rounded-full border-b-2"></div>
237
+ {:else}
238
+ <FileIcon size={18} class="text-muted-foreground" />
239
+ {/if}
240
+ </div>
241
+ <div class="min-w-0 flex-1">
242
+ <span class="truncate text-sm">{displayName}</span>
243
+ {#if fileSize}
244
+ <span class="text-muted-foreground text-xs"> - {fileSize}</span>
245
+ {/if}
246
+ </div>
247
+ </div>
248
+ {:else}
249
+ <span class="text-muted-foreground text-sm">No file</span>
250
+ {/if}
251
+ {:else if compact}
252
+ <!-- Compact mode for arrays -->
253
+ {#if value && value.asset}
254
+ <div class="border-border flex items-center gap-3 rounded-md border p-2 {validationClasses}">
255
+ <div
256
+ class="bg-muted flex h-10 w-10 flex-shrink-0 items-center justify-center overflow-hidden rounded"
257
+ >
258
+ {#if loadingAsset}
259
+ <div class="border-primary h-4 w-4 animate-spin rounded-full border-b-2"></div>
260
+ {:else}
261
+ <FileIcon size={20} class="text-muted-foreground" />
262
+ {/if}
263
+ </div>
264
+
265
+ <div class="flex-1 overflow-hidden">
266
+ <p class="truncate text-sm font-medium">{displayName}</p>
267
+ {#if fileSize || fileMime}
268
+ <p class="text-muted-foreground text-xs">
269
+ {fileMime || ''}{fileSize ? ` - ${fileSize}` : ''}
270
+ </p>
271
+ {/if}
272
+ </div>
273
+
274
+ {#if !readonly}
275
+ <DropdownMenu>
276
+ <DropdownMenuTrigger>
277
+ <Button variant="ghost" size="sm" class="h-8 w-8 p-0">
278
+ <Ellipsis size={16} />
279
+ </Button>
280
+ </DropdownMenuTrigger>
281
+ <DropdownMenuContent align="end">
282
+ <DropdownMenuGroup>
283
+ <DropdownMenuItem onclick={openFileDialog} disabled={isUploading}>
284
+ <Upload size={16} />
285
+ Replace
286
+ </DropdownMenuItem>
287
+ <DropdownMenuItem
288
+ onclick={() => {
289
+ showAssetBrowser = true;
290
+ }}
291
+ disabled={isUploading}
292
+ >
293
+ <FileIcon size={16} />
294
+ Browse media
295
+ </DropdownMenuItem>
296
+ </DropdownMenuGroup>
297
+ <DropdownMenuSeparator />
298
+ <DropdownMenuItem
299
+ onclick={removeFile}
300
+ disabled={isUploading}
301
+ class="text-destructive focus:text-destructive"
302
+ >
303
+ <CircleX size={16} />
304
+ Clear field
305
+ </DropdownMenuItem>
306
+ </DropdownMenuContent>
307
+ </DropdownMenu>
308
+ {/if}
309
+ </div>
310
+ {:else}
311
+ <Button
312
+ variant="outline"
313
+ class="w-full justify-start"
314
+ onclick={openFileDialog}
315
+ disabled={isUploading || readonly}
316
+ type="button"
317
+ >
318
+ {#if isUploading}
319
+ <div class="border-primary mr-2 h-4 w-4 animate-spin rounded-full border-b-2"></div>
320
+ Uploading...
321
+ {:else}
322
+ <Upload size={16} class="mr-2" />
323
+ Upload File
324
+ {/if}
325
+ </Button>
326
+ {/if}
327
+ {:else}
328
+ <!-- Full mode -->
329
+ {#if value && value.asset}
330
+ <div class="border-border overflow-hidden rounded-md border {validationClasses}">
331
+ <!-- File info card -->
332
+ <div class="flex items-center gap-4 p-4">
333
+ <div
334
+ class="bg-muted flex h-14 w-14 flex-shrink-0 items-center justify-center rounded-lg"
335
+ >
336
+ {#if loadingAsset}
337
+ <div class="border-primary h-6 w-6 animate-spin rounded-full border-b-2"></div>
338
+ {:else}
339
+ <FileIcon size={28} class="text-muted-foreground" />
340
+ {/if}
341
+ </div>
342
+
343
+ <div class="min-w-0 flex-1">
344
+ <p class="truncate text-sm font-medium">{displayName}</p>
345
+ <div class="text-muted-foreground flex items-center gap-2 text-xs">
346
+ {#if fileMime}
347
+ <span>{fileMime}</span>
348
+ {/if}
349
+ {#if fileSize}
350
+ <span>{fileSize}</span>
351
+ {/if}
352
+ </div>
353
+ </div>
354
+
355
+ <!-- Controls -->
356
+ {#if !readonly}
357
+ <DropdownMenu>
358
+ <DropdownMenuTrigger>
359
+ <Button variant="secondary" size="icon" class="h-8 w-8">
360
+ <Ellipsis size={16} />
361
+ </Button>
362
+ </DropdownMenuTrigger>
363
+ <DropdownMenuContent align="end">
364
+ <DropdownMenuGroup>
365
+ <DropdownMenuItem onclick={openFileDialog} disabled={isUploading}>
366
+ <Upload size={16} />
367
+ Replace
368
+ </DropdownMenuItem>
369
+ <DropdownMenuItem
370
+ onclick={() => {
371
+ showAssetBrowser = true;
372
+ }}
373
+ disabled={isUploading}
374
+ >
375
+ <FileIcon size={16} />
376
+ Browse media
377
+ </DropdownMenuItem>
378
+ </DropdownMenuGroup>
379
+ <DropdownMenuSeparator />
380
+ <DropdownMenuGroup>
381
+ <DropdownMenuItem onclick={downloadAsset} disabled={!fileUrl}>
382
+ <Download size={16} />
383
+ Download
384
+ </DropdownMenuItem>
385
+ <DropdownMenuItem onclick={copyUrl} disabled={!fileUrl}>
386
+ <Copy size={16} />
387
+ Copy URL
388
+ </DropdownMenuItem>
389
+ </DropdownMenuGroup>
390
+ <DropdownMenuSeparator />
391
+ <DropdownMenuItem
392
+ onclick={removeFile}
393
+ disabled={isUploading}
394
+ class="text-destructive focus:text-destructive"
395
+ >
396
+ <CircleX size={16} />
397
+ Clear field
398
+ </DropdownMenuItem>
399
+ </DropdownMenuContent>
400
+ </DropdownMenu>
401
+ {/if}
402
+ </div>
403
+ </div>
404
+ {:else}
405
+ <!-- Upload bar -->
406
+ <div
407
+ class="border-border flex h-10 items-center overflow-hidden rounded-md border transition-colors {validationClasses} {readonly
408
+ ? ''
409
+ : isDragging
410
+ ? 'bg-primary/5'
411
+ : ''}"
412
+ use:elementEvents={{
413
+ events: [
414
+ { name: 'dragover', handler: handleDragOver },
415
+ { name: 'drop', handler: handleDrop },
416
+ { name: 'dragleave', handler: handleDragLeave }
417
+ ]
418
+ }}
419
+ >
420
+ <div class="flex flex-1 items-center gap-2 px-3">
421
+ {#if isUploading}
422
+ <div
423
+ class="border-primary h-4 w-4 animate-spin rounded-full border-2 border-t-transparent"
424
+ ></div>
425
+ <span class="text-muted-foreground text-sm">Uploading...</span>
426
+ {:else}
427
+ <FileIcon size={16} class="text-muted-foreground" />
428
+ <span class="text-muted-foreground text-sm">
429
+ {readonly ? 'No file' : isDragging ? 'Drop file here' : 'Drag or select a file'}
430
+ </span>
431
+ {/if}
432
+ </div>
433
+
434
+ <div class="flex items-center gap-1 pr-2">
435
+ <button
436
+ onclick={openFileDialog}
437
+ disabled={isUploading || readonly}
438
+ type="button"
439
+ class="text-muted-foreground hover:text-foreground flex items-center gap-1 px-2 py-1 text-sm transition-colors disabled:opacity-50"
440
+ >
441
+ <Upload size={14} />
442
+ Upload
443
+ </button>
444
+ <button
445
+ disabled={isUploading || readonly}
446
+ type="button"
447
+ onclick={() => {
448
+ showAssetBrowser = true;
449
+ }}
450
+ class="text-muted-foreground hover:text-foreground flex items-center gap-1 px-2 py-1 text-sm transition-colors disabled:opacity-50"
451
+ >
452
+ <FileIcon size={14} />
453
+ Select
454
+ </button>
455
+ </div>
456
+ </div>
457
+ {/if}
458
+ {/if}
459
+
460
+ {#if uploadError}
461
+ <p class="text-destructive mt-2 text-sm">{uploadError}</p>
462
+ {/if}
463
+
464
+ {#if field.accept}
465
+ <p class="text-muted-foreground mt-1 text-xs">
466
+ Accepted: {field.accept.join(', ')}
467
+ </p>
468
+ {/if}
469
+
470
+ <!-- Asset Browser Modal -->
471
+ <AssetBrowserModal
472
+ bind:open={showAssetBrowser}
473
+ onOpenChange={(v) => (showAssetBrowser = v)}
474
+ assetTypeFilter="file"
475
+ onSelect={(asset) => {
476
+ const fileValue: FileValue = {
477
+ _type: 'file',
478
+ asset: {
479
+ _type: 'reference',
480
+ _ref: asset.id
481
+ }
482
+ };
483
+ onUpdate(fileValue);
484
+ }}
485
+ />
@@ -0,0 +1,18 @@
1
+ import type { FileValue } from '../../../types/asset.js';
2
+ import type { FileField as FileFieldType } from '../../../types/schemas.js';
3
+ interface Props {
4
+ field: FileFieldType;
5
+ value: FileValue | null;
6
+ validationClasses?: string;
7
+ onUpdate: (value: FileValue | null) => void;
8
+ schemaType?: string;
9
+ fieldPath?: string;
10
+ readonly?: boolean;
11
+ compact?: boolean;
12
+ arrayItem?: boolean;
13
+ organizationId?: string;
14
+ }
15
+ declare const FileField: import("svelte").Component<Props, {}, "">;
16
+ type FileField = ReturnType<typeof FileField>;
17
+ export default FileField;
18
+ //# sourceMappingURL=FileField.svelte.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"FileField.svelte.d.ts","sourceRoot":"","sources":["../../../../src/lib/components/admin/fields/FileField.svelte.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,sBAAsB,CAAC;AACtD,OAAO,KAAK,EAAE,SAAS,IAAI,aAAa,EAAE,MAAM,wBAAwB,CAAC;AAiBxE,UAAU,KAAK;IACd,KAAK,EAAE,aAAa,CAAC;IACrB,KAAK,EAAE,SAAS,GAAG,IAAI,CAAC;IACxB,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAC3B,QAAQ,EAAE,CAAC,KAAK,EAAE,SAAS,GAAG,IAAI,KAAK,IAAI,CAAC;IAC5C,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,cAAc,CAAC,EAAE,MAAM,CAAC;CACxB;AAgaF,QAAA,MAAM,SAAS,2CAAwC,CAAC;AACxD,KAAK,SAAS,GAAG,UAAU,CAAC,OAAO,SAAS,CAAC,CAAC;AAC9C,eAAe,SAAS,CAAC"}
@@ -214,7 +214,7 @@
214
214
  <Command.Empty>
215
215
  <div class="flex flex-col items-center gap-2 py-4">
216
216
  <p class="text-muted-foreground text-sm">
217
- No {pluralize(targetType)} found
217
+ No {pluralize(targetType || '')} found
218
218
  </p>
219
219
  <Button size="sm" onclick={createNewDocument} disabled={creating} class="gap-1">
220
220
  <PlusIcon class="h-3 w-3" />
@@ -254,7 +254,7 @@
254
254
  </Command.Group>
255
255
  {:else}
256
256
  <Command.Empty>
257
- No {pluralize(targetType)} available
257
+ No {pluralize(targetType || '')} available
258
258
  </Command.Empty>
259
259
  {/if}
260
260
  </Command.List>
@@ -4,6 +4,7 @@ export { default as NumberField } from '../admin/fields/NumberField.svelte';
4
4
  export { default as BooleanField } from '../admin/fields/BooleanField.svelte';
5
5
  export { default as SlugField } from '../admin/fields/SlugField.svelte';
6
6
  export { default as ImageField } from '../admin/fields/ImageField.svelte';
7
+ export { default as FileField } from '../admin/fields/FileField.svelte';
7
8
  export { default as ArrayField } from '../admin/fields/ArrayField.svelte';
8
9
  export { default as ReferenceField } from '../admin/fields/ReferenceField.svelte';
9
10
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/lib/components/fields/index.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,OAAO,IAAI,WAAW,EAAE,MAAM,oCAAoC,CAAC;AAC5E,OAAO,EAAE,OAAO,IAAI,aAAa,EAAE,MAAM,sCAAsC,CAAC;AAChF,OAAO,EAAE,OAAO,IAAI,WAAW,EAAE,MAAM,oCAAoC,CAAC;AAC5E,OAAO,EAAE,OAAO,IAAI,YAAY,EAAE,MAAM,qCAAqC,CAAC;AAC9E,OAAO,EAAE,OAAO,IAAI,SAAS,EAAE,MAAM,kCAAkC,CAAC;AACxE,OAAO,EAAE,OAAO,IAAI,UAAU,EAAE,MAAM,mCAAmC,CAAC;AAC1E,OAAO,EAAE,OAAO,IAAI,UAAU,EAAE,MAAM,mCAAmC,CAAC;AAC1E,OAAO,EAAE,OAAO,IAAI,cAAc,EAAE,MAAM,uCAAuC,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/lib/components/fields/index.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,OAAO,IAAI,WAAW,EAAE,MAAM,oCAAoC,CAAC;AAC5E,OAAO,EAAE,OAAO,IAAI,aAAa,EAAE,MAAM,sCAAsC,CAAC;AAChF,OAAO,EAAE,OAAO,IAAI,WAAW,EAAE,MAAM,oCAAoC,CAAC;AAC5E,OAAO,EAAE,OAAO,IAAI,YAAY,EAAE,MAAM,qCAAqC,CAAC;AAC9E,OAAO,EAAE,OAAO,IAAI,SAAS,EAAE,MAAM,kCAAkC,CAAC;AACxE,OAAO,EAAE,OAAO,IAAI,UAAU,EAAE,MAAM,mCAAmC,CAAC;AAC1E,OAAO,EAAE,OAAO,IAAI,SAAS,EAAE,MAAM,kCAAkC,CAAC;AACxE,OAAO,EAAE,OAAO,IAAI,UAAU,EAAE,MAAM,mCAAmC,CAAC;AAC1E,OAAO,EAAE,OAAO,IAAI,cAAc,EAAE,MAAM,uCAAuC,CAAC"}
@@ -5,5 +5,6 @@ export { default as NumberField } from '../admin/fields/NumberField.svelte';
5
5
  export { default as BooleanField } from '../admin/fields/BooleanField.svelte';
6
6
  export { default as SlugField } from '../admin/fields/SlugField.svelte';
7
7
  export { default as ImageField } from '../admin/fields/ImageField.svelte';
8
+ export { default as FileField } from '../admin/fields/FileField.svelte';
8
9
  export { default as ArrayField } from '../admin/fields/ArrayField.svelte';
9
10
  export { default as ReferenceField } from '../admin/fields/ReferenceField.svelte';
@@ -1 +1 @@
1
- {"version":3,"file":"schema.d.ts","sourceRoot":"","sources":["../../src/lib/graphql/schema.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAkD,MAAM,kBAAkB,CAAC;AAkWnG,wBAAgB,qBAAqB,CAAC,WAAW,EAAE,UAAU,EAAE,GAAG,MAAM,CAgFvE"}
1
+ {"version":3,"file":"schema.d.ts","sourceRoot":"","sources":["../../src/lib/graphql/schema.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAkD,MAAM,kBAAkB,CAAC;AAqWnG,wBAAgB,qBAAqB,CAAC,WAAW,EAAE,UAAU,EAAE,GAAG,MAAM,CA2FvE"}
@@ -18,6 +18,8 @@ function getGraphQLType(field, schemaTypes, parentName = '') {
18
18
  return 'Boolean';
19
19
  case 'image':
20
20
  return 'Image';
21
+ case 'file':
22
+ return 'FileAsset';
21
23
  case 'array':
22
24
  return handleArrayField(field, schemaTypes, parentName);
23
25
  case 'object':
@@ -234,7 +236,8 @@ function getInputFieldType(field, _schemaTypes, _parentName) {
234
236
  case 'reference':
235
237
  return 'String'; // Reference IDs
236
238
  case 'image':
237
- return 'JSON'; // Images are complex objects
239
+ case 'file':
240
+ return 'JSON'; // Complex asset references
238
241
  case 'array':
239
242
  // For arrays, we'll use JSON for simplicity
240
243
  // In a more advanced implementation, you'd create specific input types
@@ -323,6 +326,17 @@ export function generateGraphQLSchema(schemaTypes) {
323
326
  type ImageAsset {
324
327
  _ref: String!
325
328
  _type: String!
329
+ }
330
+
331
+ type FileAsset {
332
+ _type: String!
333
+ asset: FileAssetRef
334
+ url: String
335
+ }
336
+
337
+ type FileAssetRef {
338
+ _ref: String!
339
+ _type: String!
326
340
  }`;
327
341
  const scalarDefs = `# JSON scalar for flexible data
328
342
  scalar JSON`;
@@ -1 +1 @@
1
- {"version":3,"file":"hooks.d.ts","sourceRoot":"","sources":["../src/lib/hooks.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,eAAe,CAAC;AAC5C,OAAO,KAAK,EAAE,SAAS,EAA8B,MAAM,eAAe,CAAC;AAC3E,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,YAAY,CAAC;AAClD,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,0BAA0B,CAAC;AAC7D,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,8BAA8B,CAAC;AACnE,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,eAAe,CAAC;AAClD,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,iBAAiB,CAAC;AACpD,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,iBAAiB,CAAC;AAKvD,OAAO,EAAa,SAAS,EAAE,MAAM,UAAU,CAAC;AAChD,OAAO,EAAkB,KAAK,QAAQ,EAAE,MAAM,mBAAmB,CAAC;AAGlE,MAAM,WAAW,YAAY;IAC5B,MAAM,EAAE,SAAS,CAAC;IAClB,YAAY,EAAE,YAAY,CAAC;IAC3B,cAAc,EAAE,cAAc,CAAC;IAC/B,eAAe,EAAE,eAAe,CAAC;IACjC,YAAY,CAAC,EAAE,YAAY,GAAG,IAAI,CAAC;IACnC,SAAS,EAAE,SAAS,CAAC;IACrB,QAAQ,EAAE,QAAQ,CAAC;IACnB,IAAI,CAAC,EAAE,YAAY,CAAC;IACpB,eAAe,CAAC,EAAE,eAAe,GAAG,IAAI,CAAC;IACzC,YAAY,CAAC,EAAE,GAAG,CACjB,MAAM,EACN;QAAE,OAAO,EAAE,CAAC,KAAK,EAAE,GAAG,KAAK,OAAO,CAAC,QAAQ,CAAC,GAAG,QAAQ,CAAC;QAAC,UAAU,EAAE,MAAM,CAAA;KAAE,CAC7E,CAAC;CACF;AAgFD,wBAAgB,aAAa,CAAC,MAAM,EAAE,SAAS,GAAG,MAAM,CAqKvD"}
1
+ {"version":3,"file":"hooks.d.ts","sourceRoot":"","sources":["../src/lib/hooks.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,eAAe,CAAC;AAC5C,OAAO,KAAK,EAAE,SAAS,EAA8B,MAAM,eAAe,CAAC;AAC3E,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,YAAY,CAAC;AAClD,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,0BAA0B,CAAC;AAC7D,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,8BAA8B,CAAC;AACnE,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,eAAe,CAAC;AAClD,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,iBAAiB,CAAC;AACpD,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,iBAAiB,CAAC;AAKvD,OAAO,EAAa,SAAS,EAAE,MAAM,UAAU,CAAC;AAChD,OAAO,EAAkB,KAAK,QAAQ,EAAE,MAAM,mBAAmB,CAAC;AAGlE,MAAM,WAAW,YAAY;IAC5B,MAAM,EAAE,SAAS,CAAC;IAClB,YAAY,EAAE,YAAY,CAAC;IAC3B,cAAc,EAAE,cAAc,CAAC;IAC/B,eAAe,EAAE,eAAe,CAAC;IACjC,YAAY,CAAC,EAAE,YAAY,GAAG,IAAI,CAAC;IACnC,SAAS,EAAE,SAAS,CAAC;IACrB,QAAQ,EAAE,QAAQ,CAAC;IACnB,IAAI,CAAC,EAAE,YAAY,CAAC;IACpB,eAAe,CAAC,EAAE,eAAe,GAAG,IAAI,CAAC;IACzC,YAAY,CAAC,EAAE,GAAG,CACjB,MAAM,EACN;QAAE,OAAO,EAAE,CAAC,KAAK,EAAE,GAAG,KAAK,OAAO,CAAC,QAAQ,CAAC,GAAG,QAAQ,CAAC;QAAC,UAAU,EAAE,MAAM,CAAA;KAAE,CAC7E,CAAC;CACF;AAiFD,wBAAgB,aAAa,CAAC,MAAM,EAAE,SAAS,GAAG,MAAM,CAiLvD"}
package/dist/hooks.js CHANGED
@@ -6,6 +6,7 @@ import { createCMS } from './engine.js';
6
6
  import { createLocalAPI } from './local-api/index.js';
7
7
  let cmsInstances = null;
8
8
  let schemaError = null;
9
+ let initPromise = null;
9
10
  /**
10
11
  * Check if schemas are dirty (changed via Vite HMR)
11
12
  * Vite plugin sets a global flag when schema files change
@@ -77,7 +78,13 @@ export function createCMSHook(config) {
77
78
  // Note: In dev mode, /storage/ might be accessible via Vite dev server
78
79
  // In production, only /static/ folder is served - /storage/ is private
79
80
  // Initialize CMS instances once at application startup
81
+ // Use a promise lock to prevent concurrent requests from racing initialization
82
+ if (initPromise) {
83
+ await initPromise;
84
+ }
80
85
  if (!cmsInstances) {
86
+ let resolveInit;
87
+ initPromise = new Promise((r) => (resolveInit = r));
81
88
  cmsLogger.info('[CMS]', 'Initializing...');
82
89
  const databaseAdapter = config.database;
83
90
  // Use the storage adapter from config, or create the default local one.
@@ -175,11 +182,15 @@ export function createCMSHook(config) {
175
182
  graphqlSettings,
176
183
  pluginRoutes
177
184
  };
185
+ resolveInit();
178
186
  }
179
187
  else if (checkSchemasDirty()) {
180
188
  // HMR: Schemas changed - reset instances so full re-initialization
181
189
  // runs on the next request with fresh config from Vite
182
190
  cmsLogger.info('[CMS]', 'Schema change detected, re-initializing...');
191
+ if (cmsInstances?.config.cache) {
192
+ cmsInstances.config.cache.flush();
193
+ }
183
194
  cmsInstances = null;
184
195
  schemaError = null;
185
196
  return resolve(event); // Let the next request trigger full init
@@ -0,0 +1,21 @@
1
+ import type { CacheAdapter } from '../interfaces/cache.js';
2
+ export interface InMemoryCacheOptions {
3
+ /** Maximum number of entries. Oldest entries are evicted when exceeded. Defaults to 1000. */
4
+ maxSize?: number;
5
+ /** Default TTL in seconds. No expiry if omitted. */
6
+ defaultTTL?: number;
7
+ }
8
+ export declare class InMemoryCacheAdapter implements CacheAdapter {
9
+ readonly name = "in-memory";
10
+ private store;
11
+ private maxSize;
12
+ private defaultTTL;
13
+ constructor(options?: InMemoryCacheOptions);
14
+ get<T>(key: string): Promise<T | null>;
15
+ set<T>(key: string, value: T, ttl?: number): Promise<void>;
16
+ delete(key: string): Promise<void>;
17
+ invalidateByPrefix(prefix: string): Promise<void>;
18
+ flush(): Promise<void>;
19
+ isHealthy(): Promise<boolean>;
20
+ }
21
+ //# sourceMappingURL=in-memory-cache-adapter.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"in-memory-cache-adapter.d.ts","sourceRoot":"","sources":["../../../../src/lib/cache/adapters/in-memory-cache-adapter.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,qBAAqB,CAAC;AAOxD,MAAM,WAAW,oBAAoB;IACpC,6FAA6F;IAC7F,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,oDAAoD;IACpD,UAAU,CAAC,EAAE,MAAM,CAAC;CACpB;AAED,qBAAa,oBAAqB,YAAW,YAAY;IACxD,QAAQ,CAAC,IAAI,eAAe;IAE5B,OAAO,CAAC,KAAK,CAAiC;IAC9C,OAAO,CAAC,OAAO,CAAS;IACxB,OAAO,CAAC,UAAU,CAAqB;gBAE3B,OAAO,GAAE,oBAAyB;IAKxC,GAAG,CAAC,CAAC,EAAE,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,CAAC,GAAG,IAAI,CAAC;IAYtC,GAAG,CAAC,CAAC,EAAE,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,CAAC,EAAE,GAAG,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAgB1D,MAAM,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAIlC,kBAAkB,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAQjD,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;IAItB,SAAS,IAAI,OAAO,CAAC,OAAO,CAAC;CAGnC"}