@aphexcms/cms-core 7.0.0 → 8.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/api/documents.d.ts +19 -0
- package/dist/api/documents.d.ts.map +1 -1
- package/dist/api/documents.js +21 -0
- package/dist/api/schemas/documents.d.ts +3 -0
- package/dist/api/schemas/documents.d.ts.map +1 -1
- package/dist/api/schemas/documents.js +12 -0
- package/dist/cli/generate-types.d.ts.map +1 -1
- package/dist/cli/generate-types.js +133 -19
- package/dist/cli/generate-types.js.map +1 -1
- package/dist/components/AdminApp.svelte +106 -52
- package/dist/components/AdminApp.svelte.d.ts.map +1 -1
- package/dist/components/admin/DocumentEditor.svelte +65 -2
- package/dist/components/admin/DocumentEditor.svelte.d.ts.map +1 -1
- package/dist/components/admin/confirm-dialog/ConfirmDialogHost.svelte +8 -6
- package/dist/components/admin/confirm-dialog/ConfirmDialogHost.svelte.d.ts.map +1 -1
- package/dist/components/admin/fields/ArrayField.svelte +149 -2
- package/dist/components/admin/fields/ArrayField.svelte.d.ts.map +1 -1
- package/dist/components/admin/fields/ReferenceField.svelte +198 -125
- package/dist/components/admin/fields/ReferenceField.svelte.d.ts +9 -2
- package/dist/components/admin/fields/ReferenceField.svelte.d.ts.map +1 -1
- package/dist/components/layout/Sidebar.svelte +1 -1
- package/dist/components/layout/sidebar/AppSidebar.svelte +2 -1
- package/dist/components/layout/sidebar/AppSidebar.svelte.d.ts.map +1 -1
- package/dist/components/layout/sidebar/NavMain.svelte +9 -2
- package/dist/components/layout/sidebar/NavMain.svelte.d.ts +2 -0
- package/dist/components/layout/sidebar/NavMain.svelte.d.ts.map +1 -1
- package/dist/db/interfaces/index.d.ts +3 -1
- package/dist/db/interfaces/index.d.ts.map +1 -1
- package/dist/db/interfaces/reference.d.ts +43 -0
- package/dist/db/interfaces/reference.d.ts.map +1 -0
- package/dist/db/interfaces/reference.js +4 -0
- package/dist/db/utils/reference-resolver.d.ts +4 -6
- package/dist/db/utils/reference-resolver.d.ts.map +1 -1
- package/dist/db/utils/reference-resolver.js +33 -31
- package/dist/document-refresh.svelte.d.ts +3 -0
- package/dist/document-refresh.svelte.d.ts.map +1 -0
- package/dist/document-refresh.svelte.js +21 -0
- package/dist/graphql/resolvers.d.ts.map +1 -1
- package/dist/graphql/resolvers.js +61 -1
- package/dist/graphql/schema.d.ts.map +1 -1
- package/dist/graphql/schema.js +10 -0
- package/dist/lib/api/documents.d.ts +19 -0
- package/dist/lib/api/documents.d.ts.map +1 -1
- package/dist/lib/api/documents.js +21 -0
- package/dist/lib/api/documents.js.map +1 -1
- package/dist/lib/api/schemas/documents.d.ts +3 -0
- package/dist/lib/api/schemas/documents.d.ts.map +1 -1
- package/dist/lib/api/schemas/documents.js +12 -0
- package/dist/lib/api/schemas/documents.js.map +1 -1
- package/dist/lib/db/interfaces/index.d.ts +3 -1
- package/dist/lib/db/interfaces/index.d.ts.map +1 -1
- package/dist/lib/db/interfaces/reference.d.ts +43 -0
- package/dist/lib/db/interfaces/reference.d.ts.map +1 -0
- package/dist/lib/db/interfaces/reference.js +5 -0
- package/dist/lib/db/interfaces/reference.js.map +1 -0
- package/dist/lib/db/utils/reference-resolver.d.ts +4 -6
- package/dist/lib/db/utils/reference-resolver.d.ts.map +1 -1
- package/dist/lib/db/utils/reference-resolver.js +33 -31
- package/dist/lib/db/utils/reference-resolver.js.map +1 -1
- package/dist/lib/document-refresh.svelte.d.ts +3 -0
- package/dist/lib/document-refresh.svelte.d.ts.map +1 -0
- package/dist/lib/document-refresh.svelte.js +22 -0
- package/dist/lib/document-refresh.svelte.js.map +1 -0
- package/dist/lib/graphql/resolvers.d.ts.map +1 -1
- package/dist/lib/graphql/resolvers.js +61 -1
- package/dist/lib/graphql/resolvers.js.map +1 -1
- package/dist/lib/graphql/schema.d.ts.map +1 -1
- package/dist/lib/graphql/schema.js +10 -0
- package/dist/lib/graphql/schema.js.map +1 -1
- package/dist/lib/local-api/collection-api.d.ts +10 -1
- package/dist/lib/local-api/collection-api.d.ts.map +1 -1
- package/dist/lib/local-api/collection-api.js +39 -1
- package/dist/lib/local-api/collection-api.js.map +1 -1
- package/dist/lib/local-api/index.d.ts +29 -0
- package/dist/lib/local-api/index.d.ts.map +1 -1
- package/dist/lib/local-api/index.js +66 -2
- package/dist/lib/local-api/index.js.map +1 -1
- package/dist/lib/schema-utils/validator.js +21 -11
- package/dist/lib/schema-utils/validator.js.map +1 -1
- package/dist/lib/server/api/index.d.ts.map +1 -1
- package/dist/lib/server/api/index.js +3 -1
- package/dist/lib/server/api/index.js.map +1 -1
- package/dist/lib/server/api/routes/documents-by-id.d.ts.map +1 -1
- package/dist/lib/server/api/routes/documents-by-id.js +23 -0
- package/dist/lib/server/api/routes/documents-by-id.js.map +1 -1
- package/dist/lib/server/api/routes/documents.d.ts.map +1 -1
- package/dist/lib/server/api/routes/documents.js +29 -1
- package/dist/lib/server/api/routes/documents.js.map +1 -1
- package/dist/lib/services/references-service.d.ts +35 -0
- package/dist/lib/services/references-service.d.ts.map +1 -0
- package/dist/lib/services/references-service.js +60 -0
- package/dist/lib/services/references-service.js.map +1 -0
- package/dist/lib/types/schemas.d.ts +8 -0
- package/dist/lib/types/schemas.d.ts.map +1 -1
- package/dist/lib/utils/preview.d.ts +2 -3
- package/dist/lib/utils/preview.d.ts.map +1 -1
- package/dist/lib/utils/preview.js +35 -8
- package/dist/lib/utils/preview.js.map +1 -1
- package/dist/lib/utils/reference-walk.d.ts +8 -0
- package/dist/lib/utils/reference-walk.d.ts.map +1 -0
- package/dist/lib/utils/reference-walk.js +43 -0
- package/dist/lib/utils/reference-walk.js.map +1 -0
- package/dist/local-api/collection-api.d.ts +10 -1
- package/dist/local-api/collection-api.d.ts.map +1 -1
- package/dist/local-api/collection-api.js +39 -1
- package/dist/local-api/index.d.ts +29 -0
- package/dist/local-api/index.d.ts.map +1 -1
- package/dist/local-api/index.js +66 -2
- package/dist/schema-utils/validator.js +21 -11
- package/dist/server/api/index.d.ts.map +1 -1
- package/dist/server/api/index.js +3 -1
- package/dist/server/api/routes/documents-by-id.d.ts.map +1 -1
- package/dist/server/api/routes/documents-by-id.js +23 -0
- package/dist/server/api/routes/documents.d.ts.map +1 -1
- package/dist/server/api/routes/documents.js +29 -1
- package/dist/services/references-service.d.ts +35 -0
- package/dist/services/references-service.d.ts.map +1 -0
- package/dist/services/references-service.js +59 -0
- package/dist/types/schemas.d.ts +8 -0
- package/dist/types/schemas.d.ts.map +1 -1
- package/dist/utils/preview.d.ts +2 -3
- package/dist/utils/preview.d.ts.map +1 -1
- package/dist/utils/preview.js +35 -8
- package/dist/utils/reference-walk.d.ts +8 -0
- package/dist/utils/reference-walk.d.ts.map +1 -0
- package/dist/utils/reference-walk.js +42 -0
- package/package.json +3 -3
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
import { getSchemaContext } from '../../../schema-context.svelte';
|
|
11
11
|
import ObjectModal from '../ObjectModal.svelte';
|
|
12
12
|
import ImageField from './ImageField.svelte';
|
|
13
|
+
import ReferenceField from './ReferenceField.svelte';
|
|
13
14
|
import { getDefaultValueForFieldType } from '../../../utils/field-defaults';
|
|
14
15
|
import { DragDropProvider } from '@dnd-kit/svelte';
|
|
15
16
|
import { createSortable, isSortable } from '@dnd-kit/svelte/sortable';
|
|
@@ -29,6 +30,9 @@
|
|
|
29
30
|
import { toast } from 'svelte-sonner';
|
|
30
31
|
import AssetBrowserModal from '../AssetBrowserModal.svelte';
|
|
31
32
|
import { resolvePreviewTitle, resolvePreviewSubtitle } from '../../../utils/preview';
|
|
33
|
+
import { documents } from '../../../api/documents';
|
|
34
|
+
import { SvelteMap } from 'svelte/reactivity';
|
|
35
|
+
import { getDocumentVersion } from '../../../document-refresh.svelte';
|
|
32
36
|
|
|
33
37
|
interface Props {
|
|
34
38
|
field: ArrayFieldType;
|
|
@@ -70,8 +74,12 @@
|
|
|
70
74
|
}
|
|
71
75
|
|
|
72
76
|
const isGridLayout = $derived(field.options?.layout === 'grid');
|
|
77
|
+
const isReferenceArray = $derived(
|
|
78
|
+
field.of && field.of.length > 0 && field.of[0]?.type === 'reference'
|
|
79
|
+
);
|
|
73
80
|
const isPrimitiveArray = $derived(
|
|
74
|
-
|
|
81
|
+
!isReferenceArray &&
|
|
82
|
+
field.of &&
|
|
75
83
|
field.of.length > 0 &&
|
|
76
84
|
field.of[0]?.type &&
|
|
77
85
|
!field.of[0].fields &&
|
|
@@ -80,6 +88,48 @@
|
|
|
80
88
|
const primitiveType = $derived(isPrimitiveArray ? field.of?.[0]?.type : null);
|
|
81
89
|
const availableTypes = $derived(getArrayTypes(schemas, field));
|
|
82
90
|
|
|
91
|
+
// Synthetic ReferenceField definition reused for every row in an
|
|
92
|
+
// array-of-references. We forward the array's first `of[]` entry's `to` so
|
|
93
|
+
// the row picker honors the schema-author's allowed target types.
|
|
94
|
+
const referenceFieldShape = $derived(
|
|
95
|
+
isReferenceArray
|
|
96
|
+
? {
|
|
97
|
+
name: field.name,
|
|
98
|
+
type: 'reference' as const,
|
|
99
|
+
title: field.title,
|
|
100
|
+
to: (field.of?.[0] as any)?.to ?? []
|
|
101
|
+
}
|
|
102
|
+
: null
|
|
103
|
+
);
|
|
104
|
+
|
|
105
|
+
// Batched hydration cache for reference rows. Without this, each row's
|
|
106
|
+
// ReferenceField fires its own getById on mount → N round-trips for an
|
|
107
|
+
// array of N refs. Instead we fetch all populated refs in a single call
|
|
108
|
+
// and hand each row its preloaded doc. The cache invalidates whenever
|
|
109
|
+
// any row's referenced doc bumps its document-refresh version.
|
|
110
|
+
const referenceCache = new SvelteMap<string, any>();
|
|
111
|
+
$effect(() => {
|
|
112
|
+
if (!isReferenceArray) return;
|
|
113
|
+
const ids = (Array.isArray(value) ? value : [])
|
|
114
|
+
.map((item: any) => item?._ref)
|
|
115
|
+
.filter((id: any): id is string => typeof id === 'string' && id.length > 0);
|
|
116
|
+
if (ids.length === 0) return;
|
|
117
|
+
// Subscribe to each id's version so external saves trigger a refetch.
|
|
118
|
+
for (const id of ids) getDocumentVersion(id);
|
|
119
|
+
(async () => {
|
|
120
|
+
try {
|
|
121
|
+
const res = await documents.getMany(ids);
|
|
122
|
+
if (res.success && res.data) {
|
|
123
|
+
for (const doc of res.data as any[]) {
|
|
124
|
+
if (doc?.id) referenceCache.set(doc.id, doc);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
} catch {
|
|
128
|
+
// Per-row ReferenceField will fall back to its own fetch on miss.
|
|
129
|
+
}
|
|
130
|
+
})();
|
|
131
|
+
});
|
|
132
|
+
|
|
83
133
|
// Modal state
|
|
84
134
|
let modalOpen = $state(false);
|
|
85
135
|
let editingIndex = $state<number | null>(null);
|
|
@@ -376,6 +426,29 @@
|
|
|
376
426
|
onUpdate(newArray);
|
|
377
427
|
}
|
|
378
428
|
|
|
429
|
+
// Reference array — append an empty reference row. The row renders the
|
|
430
|
+
// ReferenceField empty state (search picker), so the user picks the target
|
|
431
|
+
// document inline without an extra modal step.
|
|
432
|
+
function handleAddReference() {
|
|
433
|
+
if (readonly) return;
|
|
434
|
+
const newItem = { _type: 'reference', _key: generateKey(), _ref: null };
|
|
435
|
+
onUpdate([...keyedItems, newItem]);
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
function handleUpdateReference(
|
|
439
|
+
index: number,
|
|
440
|
+
newRef: { _type: 'reference'; _ref: string; _key?: string } | null
|
|
441
|
+
) {
|
|
442
|
+
if (readonly) return;
|
|
443
|
+
const newArray = [...keyedItems];
|
|
444
|
+
// Preserve the row's _key (stable across drag/drop) and only swap the _ref.
|
|
445
|
+
newArray[index] = {
|
|
446
|
+
...newArray[index],
|
|
447
|
+
_ref: newRef?._ref ?? null
|
|
448
|
+
};
|
|
449
|
+
onUpdate(newArray);
|
|
450
|
+
}
|
|
451
|
+
|
|
379
452
|
// Write-through: propagate every field change from the modal to the array immediately
|
|
380
453
|
function handleModalUpdate(updatedData: Record<string, any>) {
|
|
381
454
|
if (editingIndex === null || !editingType) return;
|
|
@@ -421,7 +494,81 @@
|
|
|
421
494
|
}
|
|
422
495
|
</script>
|
|
423
496
|
|
|
424
|
-
{#if
|
|
497
|
+
{#if isReferenceArray && referenceFieldShape}
|
|
498
|
+
<!-- Reference array — each row reuses ReferenceField. Drag handle on the
|
|
499
|
+
left, ReferenceField fills the row, an X button on empty rows lets the
|
|
500
|
+
user discard a never-picked row (populated rows use ReferenceField's
|
|
501
|
+
own context menu, which we override via onRemove to drop the row). -->
|
|
502
|
+
{#if keyedItems.length > 0}
|
|
503
|
+
{#key dndKey}
|
|
504
|
+
<DragDropProvider onDragEnd={handleDragEnd}>
|
|
505
|
+
<div class="space-y-1">
|
|
506
|
+
{#each keyedItems as item, index (item._key)}
|
|
507
|
+
{@const sortable = createSortable({ id: item._key, index, disabled: readonly })}
|
|
508
|
+
<div
|
|
509
|
+
{@attach sortable.attach}
|
|
510
|
+
class="flex items-center gap-1"
|
|
511
|
+
class:opacity-50={sortable.isDragging}
|
|
512
|
+
>
|
|
513
|
+
{#if !readonly}
|
|
514
|
+
<button
|
|
515
|
+
{@attach sortable.attachHandle}
|
|
516
|
+
class="text-muted-foreground hover:text-foreground flex h-9 w-7 shrink-0 cursor-grab items-center justify-center active:cursor-grabbing"
|
|
517
|
+
>
|
|
518
|
+
<GripVertical class="h-4 w-4" />
|
|
519
|
+
</button>
|
|
520
|
+
{/if}
|
|
521
|
+
<div class="min-w-0 flex-1">
|
|
522
|
+
<ReferenceField
|
|
523
|
+
field={referenceFieldShape}
|
|
524
|
+
value={item._ref ? { _type: 'reference', _ref: item._ref } : null}
|
|
525
|
+
onUpdate={(newRef) => handleUpdateReference(index, newRef)}
|
|
526
|
+
{onOpenReference}
|
|
527
|
+
{readonly}
|
|
528
|
+
onRemove={() => handleRemoveItem(index)}
|
|
529
|
+
preloadedDoc={item._ref ? referenceCache.get(item._ref) : undefined}
|
|
530
|
+
/>
|
|
531
|
+
</div>
|
|
532
|
+
{#if !readonly && !item._ref}
|
|
533
|
+
<DropdownMenu.Root>
|
|
534
|
+
<DropdownMenu.Trigger>
|
|
535
|
+
{#snippet child({ props })}
|
|
536
|
+
<button
|
|
537
|
+
{...props}
|
|
538
|
+
class="text-muted-foreground hover:text-foreground flex h-9 w-7 shrink-0 items-center justify-center rounded transition-colors"
|
|
539
|
+
aria-label="Row options"
|
|
540
|
+
>
|
|
541
|
+
<Ellipsis class="h-4 w-4" />
|
|
542
|
+
</button>
|
|
543
|
+
{/snippet}
|
|
544
|
+
</DropdownMenu.Trigger>
|
|
545
|
+
<DropdownMenu.Content align="end">
|
|
546
|
+
<DropdownMenu.Item
|
|
547
|
+
class="text-destructive focus:text-destructive"
|
|
548
|
+
onclick={() => handleRemoveItem(index)}
|
|
549
|
+
>
|
|
550
|
+
<Trash2 class="mr-2 h-4 w-4" />
|
|
551
|
+
Remove
|
|
552
|
+
</DropdownMenu.Item>
|
|
553
|
+
</DropdownMenu.Content>
|
|
554
|
+
</DropdownMenu.Root>
|
|
555
|
+
{/if}
|
|
556
|
+
</div>
|
|
557
|
+
{/each}
|
|
558
|
+
</div>
|
|
559
|
+
</DragDropProvider>
|
|
560
|
+
{/key}
|
|
561
|
+
{/if}
|
|
562
|
+
{#if !readonly}
|
|
563
|
+
<button
|
|
564
|
+
class="border-border/50 text-muted-foreground hover:text-foreground hover:bg-muted/50 mt-1 flex h-10 w-full items-center justify-center gap-2 rounded border border-dashed text-sm transition-colors"
|
|
565
|
+
onclick={handleAddReference}
|
|
566
|
+
>
|
|
567
|
+
<Plus class="h-4 w-4" />
|
|
568
|
+
Add reference...
|
|
569
|
+
</button>
|
|
570
|
+
{/if}
|
|
571
|
+
{:else if isPrimitiveArray}
|
|
425
572
|
<!-- Primitive array UI with DnD -->
|
|
426
573
|
{#if arrayValue.length === 0}
|
|
427
574
|
<div
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ArrayField.svelte.d.ts","sourceRoot":"","sources":["../../../../src/lib/components/admin/fields/ArrayField.svelte.ts"],"names":[],"mappings":"AASA,OAAO,KAAK,EAAE,UAAU,IAAI,cAAc,EAAc,MAAM,wBAAwB,CAAC;
|
|
1
|
+
{"version":3,"file":"ArrayField.svelte.d.ts","sourceRoot":"","sources":["../../../../src/lib/components/admin/fields/ArrayField.svelte.ts"],"names":[],"mappings":"AASA,OAAO,KAAK,EAAE,UAAU,IAAI,cAAc,EAAc,MAAM,wBAAwB,CAAC;AA8BtF,UAAU,KAAK;IACd,KAAK,EAAE,cAAc,CAAC;IACtB,KAAK,EAAE,GAAG,CAAC;IACX,QAAQ,EAAE,CAAC,KAAK,EAAE,GAAG,KAAK,IAAI,CAAC;IAC/B,eAAe,CAAC,EAAE,CAAC,UAAU,EAAE,MAAM,EAAE,YAAY,EAAE,MAAM,KAAK,IAAI,CAAC;IACrE,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,cAAc,CAAC,EAAE,MAAM,CAAC;CACxB;AAq5BF,QAAA,MAAM,UAAU,2CAAwC,CAAC;AACzD,KAAK,UAAU,GAAG,UAAU,CAAC,OAAO,UAAU,CAAC,CAAC;AAChD,eAAe,UAAU,CAAC"}
|
|
@@ -1,31 +1,45 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
import CheckIcon from '@lucide/svelte/icons/check';
|
|
3
3
|
import PlusIcon from '@lucide/svelte/icons/plus';
|
|
4
|
-
import { tick } from 'svelte';
|
|
5
4
|
import * as Command from '@aphexcms/ui/shadcn/command';
|
|
6
|
-
import * as Popover from '@aphexcms/ui/shadcn/popover';
|
|
7
5
|
import * as DropdownMenu from '@aphexcms/ui/shadcn/dropdown-menu';
|
|
8
6
|
import { Button } from '@aphexcms/ui/shadcn/button';
|
|
7
|
+
import { Input } from '@aphexcms/ui/shadcn/input';
|
|
9
8
|
import { cn } from '@aphexcms/ui/utils';
|
|
10
|
-
import { ExternalLink, Ellipsis, Trash2, FileText } from '@lucide/svelte';
|
|
9
|
+
import { ChevronDown, ExternalLink, Ellipsis, Trash2, FileText } from '@lucide/svelte';
|
|
11
10
|
import type { Field, ReferenceField as ReferenceFieldType } from '../../../types/schemas';
|
|
12
11
|
import { documents } from '../../../api/documents';
|
|
13
12
|
import { getSchemaContext } from '../../../schema-context.svelte';
|
|
14
13
|
import { getSchemaByName } from '../../../schema-utils/utils';
|
|
15
14
|
import { toast } from 'svelte-sonner';
|
|
16
|
-
import { cmsLogger } from '../../../utils/logger';
|
|
17
15
|
import { pluralize } from '../../../utils/pluralize';
|
|
18
16
|
import { resolvePreviewTitle, resolvePreviewSubtitle } from '../../../utils/preview';
|
|
17
|
+
import { getDocumentVersion } from '../../../document-refresh.svelte';
|
|
18
|
+
|
|
19
|
+
type ReferenceValue = { _type: 'reference'; _ref: string; _key?: string };
|
|
19
20
|
|
|
20
21
|
interface Props {
|
|
21
22
|
field: Field;
|
|
22
|
-
value:
|
|
23
|
-
onUpdate: (value:
|
|
23
|
+
value: ReferenceValue | null; // Reference wrapper — singular and array share this shape
|
|
24
|
+
onUpdate: (value: ReferenceValue | null) => void;
|
|
24
25
|
onOpenReference?: (documentId: string, documentType: string) => void;
|
|
25
26
|
readonly?: boolean;
|
|
27
|
+
onRemove?: () => void; // When set, the "Remove" menu item calls this instead of clearing the value (used by array-of-references rows)
|
|
28
|
+
preloadedDoc?: unknown; // When provided AND its id matches `value._ref`, skip the per-row getById and use this doc as `selectedDocument` (used by ArrayField to batch-hydrate reference rows in one HTTP call)
|
|
26
29
|
}
|
|
27
30
|
|
|
28
|
-
let {
|
|
31
|
+
let {
|
|
32
|
+
field,
|
|
33
|
+
value,
|
|
34
|
+
onUpdate,
|
|
35
|
+
onOpenReference,
|
|
36
|
+
readonly = false,
|
|
37
|
+
onRemove,
|
|
38
|
+
preloadedDoc
|
|
39
|
+
}: Props = $props();
|
|
40
|
+
|
|
41
|
+
// Pull the target ID off the wrapper (or null when nothing's selected).
|
|
42
|
+
const refId = $derived(value?._ref ?? null);
|
|
29
43
|
|
|
30
44
|
// Cast to reference field type
|
|
31
45
|
const referenceField = field as ReferenceFieldType;
|
|
@@ -41,14 +55,26 @@
|
|
|
41
55
|
let selectedDocument = $state<any>(null);
|
|
42
56
|
let loading = $state(false);
|
|
43
57
|
let creating = $state(false);
|
|
44
|
-
let
|
|
58
|
+
let query = $state('');
|
|
45
59
|
|
|
46
|
-
// Load selected document details when value changes
|
|
60
|
+
// Load selected document details when value changes — and re-load whenever
|
|
61
|
+
// the referenced document is saved elsewhere (the version counter bumps),
|
|
62
|
+
// so the row reflects edits made in a side-panel editor without needing
|
|
63
|
+
// the user to navigate away and back. When the parent (e.g. ArrayField)
|
|
64
|
+
// hands us a preloaded doc whose id matches `value`, use it directly and
|
|
65
|
+
// skip the network call.
|
|
47
66
|
$effect(() => {
|
|
67
|
+
const id = refId;
|
|
68
|
+
// Subscribe to the document's version so this effect re-fires on save.
|
|
69
|
+
getDocumentVersion(id);
|
|
70
|
+
if (id && preloadedDoc && (preloadedDoc as any).id === id) {
|
|
71
|
+
selectedDocument = preloadedDoc;
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
48
74
|
async function loadDocument() {
|
|
49
|
-
if (
|
|
75
|
+
if (id) {
|
|
50
76
|
try {
|
|
51
|
-
const doc = await documents.getById(
|
|
77
|
+
const doc = await documents.getById(id);
|
|
52
78
|
if (doc.success) {
|
|
53
79
|
selectedDocument = doc.data;
|
|
54
80
|
}
|
|
@@ -63,46 +89,84 @@
|
|
|
63
89
|
loadDocument();
|
|
64
90
|
});
|
|
65
91
|
|
|
66
|
-
//
|
|
92
|
+
// Reset query whenever the popover closes so the next open starts empty.
|
|
67
93
|
$effect(() => {
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
toast.error('Failed to load documents');
|
|
87
|
-
searchResults = [];
|
|
88
|
-
} finally {
|
|
89
|
-
loading = false;
|
|
90
|
-
}
|
|
94
|
+
if (!open) {
|
|
95
|
+
query = '';
|
|
96
|
+
searchResults = [];
|
|
97
|
+
}
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
// All docs cache — fetched once when the dropdown opens, reused for
|
|
101
|
+
// empty-query browsing and client-side filtering while typing.
|
|
102
|
+
let allDocs = $state<any[]>([]);
|
|
103
|
+
let allDocsFetched = $state(false);
|
|
104
|
+
|
|
105
|
+
async function fetchAllDocs() {
|
|
106
|
+
if (allDocsFetched || !targetType) return;
|
|
107
|
+
loading = true;
|
|
108
|
+
try {
|
|
109
|
+
const result = await documents.list({ docType: targetType, limit: 20 });
|
|
110
|
+
if (result.success && result.data) {
|
|
111
|
+
allDocs = result.data;
|
|
91
112
|
}
|
|
113
|
+
} catch {
|
|
114
|
+
toast.error('Failed to load documents');
|
|
115
|
+
} finally {
|
|
116
|
+
allDocsFetched = true;
|
|
117
|
+
loading = false;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// Fetch docs when dropdown opens; reset cache when it closes.
|
|
122
|
+
$effect(() => {
|
|
123
|
+
if (open) {
|
|
124
|
+
fetchAllDocs();
|
|
125
|
+
} else {
|
|
126
|
+
query = '';
|
|
127
|
+
searchResults = [];
|
|
128
|
+
allDocs = [];
|
|
129
|
+
allDocsFetched = false;
|
|
92
130
|
}
|
|
93
|
-
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
// Filter results client-side as the user types. Empty query shows all.
|
|
134
|
+
$effect(() => {
|
|
135
|
+
const q = query.trim().toLowerCase();
|
|
136
|
+
if (!open) return;
|
|
137
|
+
if (!q) {
|
|
138
|
+
searchResults = allDocs;
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
searchResults = allDocs.filter((doc: any) => {
|
|
142
|
+
const title = resolvePreviewTitle(doc, targetSchema).toLowerCase();
|
|
143
|
+
const subtitle = (resolvePreviewSubtitle(doc, targetSchema) ?? '').toLowerCase();
|
|
144
|
+
return title.includes(q) || subtitle.includes(q);
|
|
145
|
+
});
|
|
94
146
|
});
|
|
95
147
|
|
|
96
148
|
function closeAndFocusTrigger() {
|
|
97
149
|
open = false;
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// Tiny click-outside action for the inline results dropdown — closes the
|
|
153
|
+
// dropdown when the user clicks anywhere outside the wrapper. Cheaper
|
|
154
|
+
// than reaching for a Popover and avoids the focus-stealing problem.
|
|
155
|
+
function clickOutside(node: HTMLElement, callback: () => void) {
|
|
156
|
+
function handle(e: MouseEvent) {
|
|
157
|
+
if (!node.contains(e.target as Node)) callback();
|
|
158
|
+
}
|
|
159
|
+
document.addEventListener('mousedown', handle);
|
|
160
|
+
return {
|
|
161
|
+
destroy() {
|
|
162
|
+
document.removeEventListener('mousedown', handle);
|
|
163
|
+
}
|
|
164
|
+
};
|
|
101
165
|
}
|
|
102
166
|
|
|
103
167
|
function selectDocument(doc: any) {
|
|
104
168
|
if (readonly) return;
|
|
105
|
-
onUpdate(doc.id);
|
|
169
|
+
onUpdate({ _type: 'reference', _ref: doc.id });
|
|
106
170
|
closeAndFocusTrigger();
|
|
107
171
|
}
|
|
108
172
|
|
|
@@ -125,13 +189,11 @@
|
|
|
125
189
|
try {
|
|
126
190
|
const result = await documents.create({
|
|
127
191
|
type: targetType,
|
|
128
|
-
data: {
|
|
129
|
-
title: 'Untitled'
|
|
130
|
-
}
|
|
192
|
+
data: {}
|
|
131
193
|
});
|
|
132
194
|
|
|
133
195
|
if (result.success && result.data) {
|
|
134
|
-
onUpdate(result.data.id);
|
|
196
|
+
onUpdate({ _type: 'reference', _ref: result.data.id });
|
|
135
197
|
closeAndFocusTrigger();
|
|
136
198
|
}
|
|
137
199
|
} catch (err) {
|
|
@@ -179,9 +241,9 @@
|
|
|
179
241
|
>
|
|
180
242
|
{/if}
|
|
181
243
|
</span>
|
|
182
|
-
{#if selectedDocument.status === 'published'}
|
|
244
|
+
{#if selectedDocument._meta?.status === 'published'}
|
|
183
245
|
<span class="h-1.5 w-1.5 shrink-0 rounded-full bg-green-500" title="Published"></span>
|
|
184
|
-
{:else if selectedDocument.status === 'unpublished'}
|
|
246
|
+
{:else if selectedDocument._meta?.status === 'unpublished'}
|
|
185
247
|
<span class="bg-muted-foreground/60 h-1.5 w-1.5 shrink-0 rounded-full" title="Unpublished"
|
|
186
248
|
></span>
|
|
187
249
|
{:else}
|
|
@@ -210,7 +272,7 @@
|
|
|
210
272
|
<DropdownMenu.Separator />
|
|
211
273
|
<DropdownMenu.Item
|
|
212
274
|
class="text-destructive focus:text-destructive"
|
|
213
|
-
onclick={clearSelection}
|
|
275
|
+
onclick={() => (onRemove ? onRemove() : clearSelection())}
|
|
214
276
|
>
|
|
215
277
|
<Trash2 class="mr-2 h-4 w-4" />
|
|
216
278
|
Remove
|
|
@@ -227,86 +289,97 @@
|
|
|
227
289
|
<p class="text-muted-foreground text-sm">No reference selected</p>
|
|
228
290
|
</div>
|
|
229
291
|
{:else}
|
|
230
|
-
<!--
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
292
|
+
<!-- Inline empty state: search input + chevron + Create button. The input
|
|
293
|
+
is the primary focus target — the results dropdown is a sibling div
|
|
294
|
+
positioned absolutely under the input wrapper, with click-outside
|
|
295
|
+
closing it. No popover wrapping the input, so no double-click. -->
|
|
296
|
+
<div class="flex items-center gap-1">
|
|
297
|
+
<div class="relative min-w-0 flex-1" use:clickOutside={() => (open = false)}>
|
|
298
|
+
<div
|
|
299
|
+
class="border-input bg-background focus-within:ring-ring flex h-9 items-center rounded-md border focus-within:ring-1"
|
|
300
|
+
>
|
|
301
|
+
<Input
|
|
302
|
+
bind:value={query}
|
|
303
|
+
placeholder="Type to search..."
|
|
304
|
+
onfocus={() => (open = true)}
|
|
305
|
+
class="h-8 border-0 bg-transparent shadow-none focus-visible:ring-0"
|
|
306
|
+
/>
|
|
234
307
|
<button
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
aria-
|
|
308
|
+
type="button"
|
|
309
|
+
onclick={() => (open = !open)}
|
|
310
|
+
class="text-muted-foreground hover:text-foreground flex h-8 w-8 shrink-0 items-center justify-center"
|
|
311
|
+
aria-label="Toggle results"
|
|
239
312
|
>
|
|
240
|
-
<
|
|
241
|
-
Add {targetType || 'reference'}...
|
|
313
|
+
<ChevronDown class="h-4 w-4" />
|
|
242
314
|
</button>
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
{
|
|
249
|
-
<Command.
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
<
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
{creating ? 'Creating...' : `Create new ${targetType}`}
|
|
259
|
-
</Button>
|
|
260
|
-
</div>
|
|
261
|
-
</Command.Empty>
|
|
262
|
-
{:else}
|
|
263
|
-
<Command.Group>
|
|
264
|
-
{#each searchResults as doc (doc.id)}
|
|
265
|
-
<Command.Item
|
|
266
|
-
value={doc.id}
|
|
267
|
-
onSelect={() => selectDocument(doc)}
|
|
268
|
-
class="flex items-center justify-between"
|
|
269
|
-
>
|
|
270
|
-
<div class="flex min-w-0 flex-1 items-center gap-2">
|
|
271
|
-
<CheckIcon
|
|
272
|
-
class={cn('h-4 w-4 shrink-0', value !== doc.id && 'text-transparent')}
|
|
273
|
-
/>
|
|
274
|
-
<span class="flex min-w-0 flex-1 flex-col">
|
|
275
|
-
<span class="truncate text-sm">{getDocumentTitle(doc)}</span>
|
|
276
|
-
{#if getDocumentSubtitle(doc)}
|
|
277
|
-
<span class="text-muted-foreground truncate text-xs"
|
|
278
|
-
>{getDocumentSubtitle(doc)}</span
|
|
279
|
-
>
|
|
280
|
-
{/if}
|
|
281
|
-
</span>
|
|
315
|
+
</div>
|
|
316
|
+
{#if open}
|
|
317
|
+
<div
|
|
318
|
+
class="bg-popover text-popover-foreground absolute top-full left-0 z-[9999] mt-1 w-[400px] max-w-[min(400px,calc(100vw-2rem))] rounded-md border p-0 shadow-md"
|
|
319
|
+
>
|
|
320
|
+
<Command.Root shouldFilter={false}>
|
|
321
|
+
<Command.List class="max-h-[300px] overflow-y-auto">
|
|
322
|
+
{#if loading}
|
|
323
|
+
<Command.Loading>Loading...</Command.Loading>
|
|
324
|
+
{:else if searchResults.length === 0}
|
|
325
|
+
<Command.Empty>
|
|
326
|
+
<div class="text-muted-foreground py-4 text-center text-sm">
|
|
327
|
+
{query.trim()
|
|
328
|
+
? `No ${pluralize(targetType || '')} match "${query}"`
|
|
329
|
+
: `No ${pluralize(targetType || '')} found`}
|
|
282
330
|
</div>
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
{
|
|
287
|
-
<
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
331
|
+
</Command.Empty>
|
|
332
|
+
{:else}
|
|
333
|
+
<Command.Group>
|
|
334
|
+
{#each searchResults as doc (doc.id)}
|
|
335
|
+
<Command.Item
|
|
336
|
+
value={doc.id}
|
|
337
|
+
onSelect={() => selectDocument(doc)}
|
|
338
|
+
class="flex items-center justify-between"
|
|
339
|
+
>
|
|
340
|
+
<div class="flex min-w-0 flex-1 items-center gap-2">
|
|
341
|
+
<CheckIcon
|
|
342
|
+
class={cn('h-4 w-4 shrink-0', refId !== doc.id && 'text-transparent')}
|
|
343
|
+
/>
|
|
344
|
+
<span class="flex min-w-0 flex-1 flex-col">
|
|
345
|
+
<span class="truncate text-sm">{getDocumentTitle(doc)}</span>
|
|
346
|
+
{#if getDocumentSubtitle(doc)}
|
|
347
|
+
<span class="text-muted-foreground truncate text-xs"
|
|
348
|
+
>{getDocumentSubtitle(doc)}</span
|
|
349
|
+
>
|
|
350
|
+
{/if}
|
|
351
|
+
</span>
|
|
352
|
+
</div>
|
|
353
|
+
{#if doc._meta?.status === 'published'}
|
|
354
|
+
<span class="h-1.5 w-1.5 shrink-0 rounded-full bg-green-500" title="Published"
|
|
355
|
+
></span>
|
|
356
|
+
{:else if doc._meta?.status === 'unpublished'}
|
|
357
|
+
<span
|
|
358
|
+
class="bg-muted-foreground/60 h-1.5 w-1.5 shrink-0 rounded-full"
|
|
359
|
+
title="Unpublished"
|
|
360
|
+
></span>
|
|
361
|
+
{:else}
|
|
362
|
+
<span class="h-1.5 w-1.5 shrink-0 rounded-full bg-amber-500" title="Draft"
|
|
363
|
+
></span>
|
|
364
|
+
{/if}
|
|
365
|
+
</Command.Item>
|
|
366
|
+
{/each}
|
|
367
|
+
</Command.Group>
|
|
368
|
+
{/if}
|
|
369
|
+
</Command.List>
|
|
370
|
+
</Command.Root>
|
|
371
|
+
</div>
|
|
372
|
+
{/if}
|
|
373
|
+
</div>
|
|
374
|
+
<Button
|
|
375
|
+
variant="outline"
|
|
376
|
+
size="sm"
|
|
377
|
+
onclick={createNewDocument}
|
|
378
|
+
disabled={creating}
|
|
379
|
+
class="h-9 shrink-0 gap-1"
|
|
380
|
+
>
|
|
381
|
+
<PlusIcon class="h-4 w-4" />
|
|
382
|
+
{creating ? 'Creating...' : 'Create...'}
|
|
383
|
+
</Button>
|
|
384
|
+
</div>
|
|
312
385
|
{/if}
|
|
@@ -1,10 +1,17 @@
|
|
|
1
1
|
import type { Field } from '../../../types/schemas.js';
|
|
2
|
+
type ReferenceValue = {
|
|
3
|
+
_type: 'reference';
|
|
4
|
+
_ref: string;
|
|
5
|
+
_key?: string;
|
|
6
|
+
};
|
|
2
7
|
interface Props {
|
|
3
8
|
field: Field;
|
|
4
|
-
value:
|
|
5
|
-
onUpdate: (value:
|
|
9
|
+
value: ReferenceValue | null;
|
|
10
|
+
onUpdate: (value: ReferenceValue | null) => void;
|
|
6
11
|
onOpenReference?: (documentId: string, documentType: string) => void;
|
|
7
12
|
readonly?: boolean;
|
|
13
|
+
onRemove?: () => void;
|
|
14
|
+
preloadedDoc?: unknown;
|
|
8
15
|
}
|
|
9
16
|
declare const ReferenceField: import("svelte").Component<Props, {}, "">;
|
|
10
17
|
type ReferenceField = ReturnType<typeof ReferenceField>;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ReferenceField.svelte.d.ts","sourceRoot":"","sources":["../../../../src/lib/components/admin/fields/ReferenceField.svelte.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"ReferenceField.svelte.d.ts","sourceRoot":"","sources":["../../../../src/lib/components/admin/fields/ReferenceField.svelte.ts"],"names":[],"mappings":"AAWA,OAAO,KAAK,EAAE,KAAK,EAAwC,MAAM,wBAAwB,CAAC;AAUzF,KAAK,cAAc,GAAG;IAAE,KAAK,EAAE,WAAW,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,IAAI,CAAC,EAAE,MAAM,CAAA;CAAE,CAAC;AAE1E,UAAU,KAAK;IACd,KAAK,EAAE,KAAK,CAAC;IACb,KAAK,EAAE,cAAc,GAAG,IAAI,CAAC;IAC7B,QAAQ,EAAE,CAAC,KAAK,EAAE,cAAc,GAAG,IAAI,KAAK,IAAI,CAAC;IACjD,eAAe,CAAC,EAAE,CAAC,UAAU,EAAE,MAAM,EAAE,YAAY,EAAE,MAAM,KAAK,IAAI,CAAC;IACrE,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,QAAQ,CAAC,EAAE,MAAM,IAAI,CAAC;IACtB,YAAY,CAAC,EAAE,OAAO,CAAC;CACvB;AAqUF,QAAA,MAAM,cAAc,2CAAwC,CAAC;AAC7D,KAAK,cAAc,GAAG,UAAU,CAAC,OAAO,cAAc,CAAC,CAAC;AACxD,eAAe,cAAc,CAAC"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"AppSidebar.svelte.d.ts","sourceRoot":"","sources":["../../../../src/lib/components/layout/sidebar/AppSidebar.svelte.ts"],"names":[],"mappings":"AAGA,OAAO,EACL,OAAO,EAKP,MAAM,6BAA6B,CAAC;AAItC,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,wBAAwB,CAAC;AAC1D,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,QAAQ,CAAC;AAG5C,KAAK,KAAK,GAAG,cAAc,CAAC,OAAO,OAAO,CAAC,GAAG;IAC7C,IAAI,EAAE,WAAW,CAAC;IAClB,SAAS,CAAC,EAAE,MAAM,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CACvC,CAAC;
|
|
1
|
+
{"version":3,"file":"AppSidebar.svelte.d.ts","sourceRoot":"","sources":["../../../../src/lib/components/layout/sidebar/AppSidebar.svelte.ts"],"names":[],"mappings":"AAGA,OAAO,EACL,OAAO,EAKP,MAAM,6BAA6B,CAAC;AAItC,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,wBAAwB,CAAC;AAC1D,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,QAAQ,CAAC;AAG5C,KAAK,KAAK,GAAG,cAAc,CAAC,OAAO,OAAO,CAAC,GAAG;IAC7C,IAAI,EAAE,WAAW,CAAC;IAClB,SAAS,CAAC,EAAE,MAAM,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CACvC,CAAC;AAsDH,QAAA,MAAM,UAAU,2CAAwC,CAAC;AACzD,KAAK,UAAU,GAAG,UAAU,CAAC,OAAO,UAAU,CAAC,CAAC;AAChD,eAAe,UAAU,CAAC"}
|