@aphexcms/cms-core 0.1.0 → 0.1.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +22 -5
- package/src/api/assets.ts +0 -75
- package/src/api/client.ts +0 -150
- package/src/api/documents.ts +0 -102
- package/src/api/index.ts +0 -7
- package/src/api/organizations.ts +0 -154
- package/src/api/types.ts +0 -34
- package/src/app.d.ts +0 -19
- package/src/auth/MULTI_TENANCY_PLAN.md +0 -1183
- package/src/auth/auth-errors.ts +0 -23
- package/src/auth/auth-hooks.ts +0 -132
- package/src/auth/provider.ts +0 -25
- package/src/client/index.ts +0 -47
- package/src/components/AdminApp.svelte +0 -1078
- package/src/components/admin/AdminLayout.svelte +0 -115
- package/src/components/admin/DocumentEditor.svelte +0 -795
- package/src/components/admin/DocumentTypesList.svelte +0 -97
- package/src/components/admin/ObjectModal.svelte +0 -135
- package/src/components/admin/SchemaField.svelte +0 -171
- package/src/components/admin/fields/ArrayField.svelte +0 -266
- package/src/components/admin/fields/BooleanField.svelte +0 -35
- package/src/components/admin/fields/ImageField.svelte +0 -284
- package/src/components/admin/fields/NumberField.svelte +0 -82
- package/src/components/admin/fields/ReferenceField.svelte +0 -260
- package/src/components/admin/fields/SlugField.svelte +0 -74
- package/src/components/admin/fields/StringField.svelte +0 -40
- package/src/components/admin/fields/TextareaField.svelte +0 -40
- package/src/components/fields/index.ts +0 -9
- package/src/components/index.ts +0 -16
- package/src/components/layout/OrganizationSwitcher.svelte +0 -218
- package/src/components/layout/Sidebar.svelte +0 -88
- package/src/components/layout/sidebar/AppSidebar.svelte +0 -63
- package/src/components/layout/sidebar/NavMain.svelte +0 -95
- package/src/components/layout/sidebar/NavSecondary.svelte +0 -69
- package/src/components/layout/sidebar/NavUser.svelte +0 -85
- package/src/config.ts +0 -18
- package/src/db/adapters/index.ts +0 -3
- package/src/db/index.ts +0 -5
- package/src/db/interfaces/asset.ts +0 -61
- package/src/db/interfaces/document.ts +0 -53
- package/src/db/interfaces/index.ts +0 -98
- package/src/db/interfaces/organization.ts +0 -51
- package/src/db/interfaces/schema.ts +0 -13
- package/src/db/interfaces/user.ts +0 -16
- package/src/db/utils/reference-resolver.ts +0 -119
- package/src/define.ts +0 -7
- package/src/email/index.ts +0 -5
- package/src/email/interfaces/email.ts +0 -45
- package/src/engine.ts +0 -85
- package/src/field-validation/rule.ts +0 -287
- package/src/field-validation/utils.ts +0 -91
- package/src/hooks.ts +0 -142
- package/src/index.ts +0 -5
- package/src/lib/is-mobile.svelte.ts +0 -9
- package/src/lib/utils.ts +0 -13
- package/src/plugins/README.md +0 -154
- package/src/routes/assets-by-id.ts +0 -161
- package/src/routes/assets-cdn.ts +0 -185
- package/src/routes/assets.ts +0 -116
- package/src/routes/documents-by-id.ts +0 -188
- package/src/routes/documents-publish.ts +0 -211
- package/src/routes/documents.ts +0 -172
- package/src/routes/index.ts +0 -13
- package/src/routes/organizations-by-id.ts +0 -258
- package/src/routes/organizations-invitations.ts +0 -183
- package/src/routes/organizations-members.ts +0 -301
- package/src/routes/organizations-switch.ts +0 -74
- package/src/routes/organizations.ts +0 -146
- package/src/routes/schemas-by-type.ts +0 -35
- package/src/routes/schemas.ts +0 -19
- package/src/routes-exports.ts +0 -42
- package/src/schema-context.svelte.ts +0 -24
- package/src/schema-utils/cleanup.ts +0 -116
- package/src/schema-utils/index.ts +0 -4
- package/src/schema-utils/utils.ts +0 -47
- package/src/schema-utils/validator.ts +0 -58
- package/src/server/index.ts +0 -40
- package/src/services/asset-service.ts +0 -256
- package/src/services/index.ts +0 -6
- package/src/storage/adapters/index.ts +0 -2
- package/src/storage/adapters/local-storage-adapter.ts +0 -215
- package/src/storage/index.ts +0 -8
- package/src/storage/interfaces/index.ts +0 -2
- package/src/storage/interfaces/storage.ts +0 -114
- package/src/storage/providers/storage.ts +0 -83
- package/src/types/asset.ts +0 -81
- package/src/types/auth.ts +0 -80
- package/src/types/config.ts +0 -45
- package/src/types/document.ts +0 -38
- package/src/types/index.ts +0 -8
- package/src/types/organization.ts +0 -119
- package/src/types/schemas.ts +0 -151
- package/src/types/sidebar.ts +0 -37
- package/src/types/user.ts +0 -17
- package/src/utils/content-hash.ts +0 -75
- package/src/utils/image-url.ts +0 -204
- package/src/utils/index.ts +0 -12
- package/src/utils/slug.ts +0 -33
|
@@ -1,284 +0,0 @@
|
|
|
1
|
-
<script lang="ts">
|
|
2
|
-
import { Button } from '@aphex/ui/shadcn/button';
|
|
3
|
-
import { Trash2, Upload, Image as ImageIcon, FileImage } from 'lucide-svelte';
|
|
4
|
-
import type { ImageValue } from '../../../types/asset.js';
|
|
5
|
-
import type { ImageField as ImageFieldType } from '../../../types/schemas.js';
|
|
6
|
-
import { assets } from '../../../api/assets';
|
|
7
|
-
|
|
8
|
-
interface Props {
|
|
9
|
-
field: ImageFieldType;
|
|
10
|
-
value: ImageValue | null;
|
|
11
|
-
validationClasses?: string;
|
|
12
|
-
onUpdate: (value: ImageValue | null) => void;
|
|
13
|
-
schemaType?: string;
|
|
14
|
-
fieldPath?: string;
|
|
15
|
-
readonly?: boolean;
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
let {
|
|
19
|
-
field,
|
|
20
|
-
value,
|
|
21
|
-
onUpdate,
|
|
22
|
-
validationClasses,
|
|
23
|
-
schemaType,
|
|
24
|
-
fieldPath,
|
|
25
|
-
readonly = false
|
|
26
|
-
}: Props = $props();
|
|
27
|
-
|
|
28
|
-
// Component state
|
|
29
|
-
let isDragging = $state(false);
|
|
30
|
-
let isUploading = $state(false);
|
|
31
|
-
let uploadError = $state<string | null>(null);
|
|
32
|
-
let fileInputRef: HTMLInputElement;
|
|
33
|
-
|
|
34
|
-
// Upload file to server
|
|
35
|
-
async function uploadFile(file: File): Promise<ImageValue | null> {
|
|
36
|
-
isUploading = true;
|
|
37
|
-
uploadError = null;
|
|
38
|
-
|
|
39
|
-
try {
|
|
40
|
-
const formData = new FormData();
|
|
41
|
-
formData.append('file', file);
|
|
42
|
-
|
|
43
|
-
// Add field metadata for privacy checking
|
|
44
|
-
if (schemaType) formData.append('schemaType', schemaType);
|
|
45
|
-
if (fieldPath) formData.append('fieldPath', fieldPath);
|
|
46
|
-
|
|
47
|
-
const result = await assets.upload(formData);
|
|
48
|
-
|
|
49
|
-
if (!result.success) {
|
|
50
|
-
throw new Error(result.error || 'Upload failed');
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
// Extract asset from response
|
|
54
|
-
const asset = result.data;
|
|
55
|
-
|
|
56
|
-
// Return Sanity-style image value
|
|
57
|
-
return {
|
|
58
|
-
_type: 'image',
|
|
59
|
-
asset: {
|
|
60
|
-
_type: 'reference',
|
|
61
|
-
_ref: asset!.id
|
|
62
|
-
}
|
|
63
|
-
};
|
|
64
|
-
} catch (error) {
|
|
65
|
-
uploadError = error instanceof Error ? error.message : 'Upload failed';
|
|
66
|
-
return null;
|
|
67
|
-
} finally {
|
|
68
|
-
isUploading = false;
|
|
69
|
-
}
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
// Handle file selection
|
|
73
|
-
async function handleFileSelect(files: FileList | null) {
|
|
74
|
-
if (readonly || !files || files.length === 0) return;
|
|
75
|
-
|
|
76
|
-
const file = files[0];
|
|
77
|
-
|
|
78
|
-
const imageValue = await uploadFile(file);
|
|
79
|
-
if (imageValue) {
|
|
80
|
-
onUpdate(imageValue);
|
|
81
|
-
}
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
// Drag and drop handlers
|
|
85
|
-
function handleDragOver(event: DragEvent) {
|
|
86
|
-
if (readonly) return;
|
|
87
|
-
event.preventDefault();
|
|
88
|
-
isDragging = true;
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
function handleDragLeave(event: DragEvent) {
|
|
92
|
-
if (readonly) return;
|
|
93
|
-
event.preventDefault();
|
|
94
|
-
isDragging = false;
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
function handleDrop(event: DragEvent) {
|
|
98
|
-
if (readonly) return;
|
|
99
|
-
event.preventDefault();
|
|
100
|
-
isDragging = false;
|
|
101
|
-
handleFileSelect(event.dataTransfer?.files || null);
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
// File input handlers
|
|
105
|
-
function handleFileInputChange(event: Event) {
|
|
106
|
-
if (readonly) return;
|
|
107
|
-
const target = event.target as HTMLInputElement;
|
|
108
|
-
handleFileSelect(target.files);
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
function openFileDialog() {
|
|
112
|
-
if (readonly) return;
|
|
113
|
-
fileInputRef?.click();
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
// Remove image
|
|
117
|
-
function removeImage() {
|
|
118
|
-
if (readonly) return;
|
|
119
|
-
onUpdate(null);
|
|
120
|
-
uploadError = null;
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
// Asset data state
|
|
124
|
-
let assetData = $state<any>(null);
|
|
125
|
-
let loadingAsset = $state(false);
|
|
126
|
-
|
|
127
|
-
// Fetch asset details when asset reference changes
|
|
128
|
-
$effect(() => {
|
|
129
|
-
async function loadAsset() {
|
|
130
|
-
if (value?.asset?._ref) {
|
|
131
|
-
loadingAsset = true;
|
|
132
|
-
try {
|
|
133
|
-
const result = await assets.getById(value.asset._ref);
|
|
134
|
-
if (result.success) {
|
|
135
|
-
assetData = result.data;
|
|
136
|
-
} else {
|
|
137
|
-
console.error('Failed to fetch asset details');
|
|
138
|
-
assetData = null;
|
|
139
|
-
}
|
|
140
|
-
} catch (error) {
|
|
141
|
-
console.error('Error fetching asset:', error);
|
|
142
|
-
assetData = null;
|
|
143
|
-
} finally {
|
|
144
|
-
loadingAsset = false;
|
|
145
|
-
}
|
|
146
|
-
} else {
|
|
147
|
-
assetData = null;
|
|
148
|
-
}
|
|
149
|
-
}
|
|
150
|
-
loadAsset();
|
|
151
|
-
});
|
|
152
|
-
|
|
153
|
-
// Get asset URL for preview
|
|
154
|
-
const previewUrl = $derived(assetData?.url || null);
|
|
155
|
-
</script>
|
|
156
|
-
|
|
157
|
-
<!-- Hidden file input -->
|
|
158
|
-
<input
|
|
159
|
-
bind:this={fileInputRef}
|
|
160
|
-
type="file"
|
|
161
|
-
accept={field.accept || 'image/*'}
|
|
162
|
-
style="display: none"
|
|
163
|
-
onchange={handleFileInputChange}
|
|
164
|
-
/>
|
|
165
|
-
|
|
166
|
-
{#if value && value.asset}
|
|
167
|
-
<!-- Image preview with controls -->
|
|
168
|
-
<div class="border-border overflow-hidden rounded-md border {validationClasses}">
|
|
169
|
-
<div class="group relative">
|
|
170
|
-
<!-- Image preview (Sanity-style aspect ratio ~2.75:1) -->
|
|
171
|
-
<div class="bg-muted flex items-center justify-center" style="aspect-ratio: 2.75 / 1;">
|
|
172
|
-
{#if loadingAsset}
|
|
173
|
-
<div class="text-muted-foreground flex flex-col items-center gap-2">
|
|
174
|
-
<div class="border-primary h-8 w-8 animate-spin rounded-full border-b-2"></div>
|
|
175
|
-
<span class="text-sm">Loading image...</span>
|
|
176
|
-
</div>
|
|
177
|
-
{:else if previewUrl}
|
|
178
|
-
<img
|
|
179
|
-
src={previewUrl}
|
|
180
|
-
alt={assetData?.alt || 'Uploaded image'}
|
|
181
|
-
class="h-full w-full object-contain"
|
|
182
|
-
loading="lazy"
|
|
183
|
-
/>
|
|
184
|
-
{:else}
|
|
185
|
-
<div class="text-muted-foreground flex flex-col items-center gap-2">
|
|
186
|
-
<ImageIcon size={32} />
|
|
187
|
-
<span class="text-sm">Image: {value.asset._ref}</span>
|
|
188
|
-
<span class="text-xs">Failed to load preview</span>
|
|
189
|
-
</div>
|
|
190
|
-
{/if}
|
|
191
|
-
</div>
|
|
192
|
-
|
|
193
|
-
<!-- Overlay controls (hidden for read-only) -->
|
|
194
|
-
{#if !readonly}
|
|
195
|
-
<div
|
|
196
|
-
class="absolute inset-0 flex items-center justify-center gap-2 bg-black/50 opacity-0 transition-opacity group-hover:opacity-100"
|
|
197
|
-
>
|
|
198
|
-
<Button variant="secondary" size="sm" onclick={openFileDialog} disabled={isUploading}>
|
|
199
|
-
<Upload size={16} class="mr-1" />
|
|
200
|
-
Replace
|
|
201
|
-
</Button>
|
|
202
|
-
<Button variant="destructive" size="sm" onclick={removeImage} disabled={isUploading}>
|
|
203
|
-
<Trash2 size={16} class="mr-1" />
|
|
204
|
-
Remove
|
|
205
|
-
</Button>
|
|
206
|
-
</div>
|
|
207
|
-
{/if}
|
|
208
|
-
</div>
|
|
209
|
-
|
|
210
|
-
<!-- Additional image controls/metadata could go here -->
|
|
211
|
-
{#if field.fields}
|
|
212
|
-
<div class="border-border space-y-2 border-t p-3">
|
|
213
|
-
<!-- Custom fields like caption, alt text, etc. would be rendered here -->
|
|
214
|
-
<p class="text-muted-foreground text-xs">Custom fields coming soon...</p>
|
|
215
|
-
</div>
|
|
216
|
-
{/if}
|
|
217
|
-
</div>
|
|
218
|
-
{:else}
|
|
219
|
-
<!-- Sanity-style upload bar -->
|
|
220
|
-
<div class="border-border overflow-hidden rounded-md border {validationClasses}">
|
|
221
|
-
<div class="flex items-center">
|
|
222
|
-
<!-- Drag and drop area (left side) -->
|
|
223
|
-
<div
|
|
224
|
-
class="flex-1 px-4 py-3 transition-colors {readonly
|
|
225
|
-
? ''
|
|
226
|
-
: isDragging
|
|
227
|
-
? 'bg-primary/5'
|
|
228
|
-
: 'hover:bg-muted/50'}"
|
|
229
|
-
ondragover={readonly ? undefined : handleDragOver}
|
|
230
|
-
ondragleave={readonly ? undefined : handleDragLeave}
|
|
231
|
-
ondrop={readonly ? undefined : handleDrop}
|
|
232
|
-
tabindex={readonly ? -1 : 0}
|
|
233
|
-
role={readonly ? undefined : 'button'}
|
|
234
|
-
>
|
|
235
|
-
{#if isUploading}
|
|
236
|
-
<div class="flex items-center gap-3">
|
|
237
|
-
<div class="border-primary h-5 w-5 animate-spin rounded-full border-b-2"></div>
|
|
238
|
-
<span class="text-muted-foreground text-sm">Uploading...</span>
|
|
239
|
-
</div>
|
|
240
|
-
{:else}
|
|
241
|
-
<div class="flex items-center gap-3">
|
|
242
|
-
<FileImage size={20} class="text-muted-foreground" />
|
|
243
|
-
<span class="text-muted-foreground text-sm">
|
|
244
|
-
{readonly ? 'No image' : isDragging ? 'Drop image here' : 'Drag or paste image here'}
|
|
245
|
-
</span>
|
|
246
|
-
</div>
|
|
247
|
-
{/if}
|
|
248
|
-
</div>
|
|
249
|
-
|
|
250
|
-
<!-- Buttons (right side) -->
|
|
251
|
-
<div class="border-border bg-muted/20 flex items-center gap-2 border-l px-3 py-2">
|
|
252
|
-
<Button
|
|
253
|
-
variant="outline"
|
|
254
|
-
size="sm"
|
|
255
|
-
onclick={openFileDialog}
|
|
256
|
-
disabled={isUploading || readonly}
|
|
257
|
-
type="button"
|
|
258
|
-
>
|
|
259
|
-
<Upload size={16} class="mr-1" />
|
|
260
|
-
Upload
|
|
261
|
-
</Button>
|
|
262
|
-
|
|
263
|
-
<Button
|
|
264
|
-
variant="outline"
|
|
265
|
-
size="sm"
|
|
266
|
-
disabled={isUploading || readonly}
|
|
267
|
-
type="button"
|
|
268
|
-
onclick={() => {
|
|
269
|
-
// TODO: Open asset browser/selector
|
|
270
|
-
console.log('Open asset selector');
|
|
271
|
-
}}
|
|
272
|
-
>
|
|
273
|
-
<ImageIcon size={16} class="mr-1" />
|
|
274
|
-
Select
|
|
275
|
-
</Button>
|
|
276
|
-
</div>
|
|
277
|
-
</div>
|
|
278
|
-
</div>
|
|
279
|
-
{/if}
|
|
280
|
-
|
|
281
|
-
<!-- Error display -->
|
|
282
|
-
{#if uploadError}
|
|
283
|
-
<p class="text-destructive mt-2 text-sm">{uploadError}</p>
|
|
284
|
-
{/if}
|
|
@@ -1,82 +0,0 @@
|
|
|
1
|
-
<script lang="ts">
|
|
2
|
-
import { Input } from '@aphex/ui/shadcn/input';
|
|
3
|
-
import type { Field } from '../../../types/schemas.js';
|
|
4
|
-
|
|
5
|
-
interface Props {
|
|
6
|
-
field: Field;
|
|
7
|
-
value: number | null;
|
|
8
|
-
onUpdate: (value: number | null) => void;
|
|
9
|
-
validationClasses?: string;
|
|
10
|
-
onBlur?: (event: any) => void;
|
|
11
|
-
onFocus?: (event: any) => void;
|
|
12
|
-
readonly?: boolean;
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
let {
|
|
16
|
-
field,
|
|
17
|
-
value,
|
|
18
|
-
onUpdate,
|
|
19
|
-
validationClasses,
|
|
20
|
-
onBlur,
|
|
21
|
-
onFocus,
|
|
22
|
-
readonly = false
|
|
23
|
-
}: Props = $props();
|
|
24
|
-
|
|
25
|
-
// Convert value to string for input, handle null/undefined
|
|
26
|
-
let inputValue = $derived(value?.toString() || '');
|
|
27
|
-
|
|
28
|
-
function handleInput(event: Event) {
|
|
29
|
-
const target = event.target as HTMLInputElement;
|
|
30
|
-
const newValue = target.value;
|
|
31
|
-
|
|
32
|
-
// Convert to number and update
|
|
33
|
-
if (newValue === '') {
|
|
34
|
-
onUpdate(null);
|
|
35
|
-
} else {
|
|
36
|
-
const numValue = parseFloat(newValue);
|
|
37
|
-
if (!isNaN(numValue)) {
|
|
38
|
-
onUpdate(numValue);
|
|
39
|
-
}
|
|
40
|
-
}
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
function handleKeydown(event: KeyboardEvent) {
|
|
44
|
-
// Allow: backspace, delete, tab, escape, enter
|
|
45
|
-
if (
|
|
46
|
-
[8, 9, 27, 13, 46].includes(event.keyCode) ||
|
|
47
|
-
// Allow: Ctrl+A, Ctrl+C, Ctrl+V, Ctrl+X
|
|
48
|
-
(event.keyCode === 65 && event.ctrlKey) ||
|
|
49
|
-
(event.keyCode === 67 && event.ctrlKey) ||
|
|
50
|
-
(event.keyCode === 86 && event.ctrlKey) ||
|
|
51
|
-
(event.keyCode === 88 && event.ctrlKey) ||
|
|
52
|
-
// Allow: home, end, left, right
|
|
53
|
-
(event.keyCode >= 35 && event.keyCode <= 39)
|
|
54
|
-
) {
|
|
55
|
-
return;
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
// Ensure that it's a number or decimal point and stop the keypress
|
|
59
|
-
if (
|
|
60
|
-
(event.shiftKey || event.keyCode < 48 || event.keyCode > 57) &&
|
|
61
|
-
(event.keyCode < 96 || event.keyCode > 105) &&
|
|
62
|
-
event.keyCode !== 190 &&
|
|
63
|
-
event.keyCode !== 110
|
|
64
|
-
) {
|
|
65
|
-
event.preventDefault();
|
|
66
|
-
}
|
|
67
|
-
}
|
|
68
|
-
</script>
|
|
69
|
-
|
|
70
|
-
<Input
|
|
71
|
-
id={field.name}
|
|
72
|
-
type="number"
|
|
73
|
-
step="any"
|
|
74
|
-
placeholder={field.description || `Enter ${field.title?.toLowerCase() || 'number'}`}
|
|
75
|
-
value={inputValue}
|
|
76
|
-
oninput={handleInput}
|
|
77
|
-
onkeydown={handleKeydown}
|
|
78
|
-
onblur={onBlur}
|
|
79
|
-
onfocus={onFocus}
|
|
80
|
-
class={validationClasses}
|
|
81
|
-
disabled={readonly}
|
|
82
|
-
/>
|
|
@@ -1,260 +0,0 @@
|
|
|
1
|
-
<script lang="ts">
|
|
2
|
-
import CheckIcon from '@lucide/svelte/icons/check';
|
|
3
|
-
import ChevronsUpDownIcon from '@lucide/svelte/icons/chevrons-up-down';
|
|
4
|
-
import PlusIcon from '@lucide/svelte/icons/plus';
|
|
5
|
-
import XIcon from '@lucide/svelte/icons/x';
|
|
6
|
-
import { tick } from 'svelte';
|
|
7
|
-
import * as Command from '@aphex/ui/shadcn/command';
|
|
8
|
-
import * as Popover from '@aphex/ui/shadcn/popover';
|
|
9
|
-
import { Button } from '@aphex/ui/shadcn/button';
|
|
10
|
-
import { cn } from '@aphex/ui/utils';
|
|
11
|
-
import type { Field, ReferenceField as ReferenceFieldType } from '../../../types/schemas.js';
|
|
12
|
-
import { documents } from '../../../api/documents.js';
|
|
13
|
-
|
|
14
|
-
interface Props {
|
|
15
|
-
field: Field;
|
|
16
|
-
value: string | null; // Document ID
|
|
17
|
-
onUpdate: (value: string | null) => void;
|
|
18
|
-
onOpenReference?: (documentId: string, documentType: string) => void;
|
|
19
|
-
readonly?: boolean;
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
let { field, value, onUpdate, onOpenReference, readonly = false }: Props = $props();
|
|
23
|
-
|
|
24
|
-
// Cast to reference field type
|
|
25
|
-
const referenceField = field as ReferenceFieldType;
|
|
26
|
-
const targetType = referenceField.to?.[0]?.type;
|
|
27
|
-
|
|
28
|
-
// State
|
|
29
|
-
let open = $state(false);
|
|
30
|
-
let searchResults = $state<any[]>([]);
|
|
31
|
-
let selectedDocument = $state<any>(null);
|
|
32
|
-
let loading = $state(false);
|
|
33
|
-
let creating = $state(false);
|
|
34
|
-
let triggerRef = $state<HTMLButtonElement>(null!);
|
|
35
|
-
|
|
36
|
-
// Load selected document details when value changes
|
|
37
|
-
$effect(() => {
|
|
38
|
-
async function loadDocument() {
|
|
39
|
-
if (value) {
|
|
40
|
-
try {
|
|
41
|
-
const doc = await documents.getById(value);
|
|
42
|
-
if (doc.success) {
|
|
43
|
-
selectedDocument = doc.data;
|
|
44
|
-
}
|
|
45
|
-
} catch (err) {
|
|
46
|
-
console.error('Failed to load referenced document:', err);
|
|
47
|
-
selectedDocument = null;
|
|
48
|
-
}
|
|
49
|
-
} else {
|
|
50
|
-
selectedDocument = null;
|
|
51
|
-
}
|
|
52
|
-
}
|
|
53
|
-
loadDocument();
|
|
54
|
-
});
|
|
55
|
-
|
|
56
|
-
// Load documents when dropdown opens
|
|
57
|
-
$effect(() => {
|
|
58
|
-
async function loadDocuments() {
|
|
59
|
-
if (open && targetType) {
|
|
60
|
-
console.log('[ReferenceField] Opening select box, loading documents for type:', targetType);
|
|
61
|
-
loading = true;
|
|
62
|
-
try {
|
|
63
|
-
const result = await documents.list({
|
|
64
|
-
docType: targetType,
|
|
65
|
-
limit: 10
|
|
66
|
-
});
|
|
67
|
-
console.log('[ReferenceField] Documents loaded:', result);
|
|
68
|
-
if (result.success && result.data) {
|
|
69
|
-
searchResults = result.data;
|
|
70
|
-
console.log('[ReferenceField] Search results:', searchResults.length, 'documents');
|
|
71
|
-
}
|
|
72
|
-
} catch (err) {
|
|
73
|
-
console.error('[ReferenceField] Failed to load documents:', err);
|
|
74
|
-
searchResults = [];
|
|
75
|
-
} finally {
|
|
76
|
-
loading = false;
|
|
77
|
-
}
|
|
78
|
-
}
|
|
79
|
-
}
|
|
80
|
-
loadDocuments();
|
|
81
|
-
});
|
|
82
|
-
|
|
83
|
-
function closeAndFocusTrigger() {
|
|
84
|
-
open = false;
|
|
85
|
-
tick().then(() => {
|
|
86
|
-
triggerRef?.focus();
|
|
87
|
-
});
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
function selectDocument(doc: any) {
|
|
91
|
-
if (readonly) return;
|
|
92
|
-
onUpdate(doc.id);
|
|
93
|
-
closeAndFocusTrigger();
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
function clearSelection() {
|
|
97
|
-
if (readonly) return;
|
|
98
|
-
onUpdate(null);
|
|
99
|
-
selectedDocument = null;
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
function openReference() {
|
|
103
|
-
if (selectedDocument && targetType && onOpenReference) {
|
|
104
|
-
onOpenReference(selectedDocument.id, targetType);
|
|
105
|
-
}
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
async function createNewDocument() {
|
|
109
|
-
if (readonly || !targetType) return;
|
|
110
|
-
|
|
111
|
-
creating = true;
|
|
112
|
-
try {
|
|
113
|
-
const result = await documents.create({
|
|
114
|
-
type: targetType,
|
|
115
|
-
draftData: {
|
|
116
|
-
title: 'Untitled'
|
|
117
|
-
}
|
|
118
|
-
});
|
|
119
|
-
|
|
120
|
-
if (result.success && result.data) {
|
|
121
|
-
onUpdate(result.data.id);
|
|
122
|
-
closeAndFocusTrigger();
|
|
123
|
-
}
|
|
124
|
-
} catch (err) {
|
|
125
|
-
console.error('Failed to create document:', err);
|
|
126
|
-
} finally {
|
|
127
|
-
creating = false;
|
|
128
|
-
}
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
function getDocumentTitle(doc: any): string {
|
|
132
|
-
// Try to get title from draft data first, then published data
|
|
133
|
-
const data = doc.draftData || doc.publishedData || {};
|
|
134
|
-
return data.title || data.name || data.heading || 'Untitled';
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
const selectedLabel = $derived(selectedDocument ? getDocumentTitle(selectedDocument) : null);
|
|
138
|
-
</script>
|
|
139
|
-
|
|
140
|
-
{#if selectedDocument}
|
|
141
|
-
<!-- Selected document display -->
|
|
142
|
-
<div class="border-border bg-muted/30 flex items-center gap-2 rounded-md border p-3">
|
|
143
|
-
<div class="flex-1">
|
|
144
|
-
<div class="text-sm font-medium">{getDocumentTitle(selectedDocument)}</div>
|
|
145
|
-
<div class="text-muted-foreground text-xs">
|
|
146
|
-
{targetType} • {selectedDocument.status === 'published' ? '🟢' : '🟡'}
|
|
147
|
-
{selectedDocument.status}
|
|
148
|
-
</div>
|
|
149
|
-
</div>
|
|
150
|
-
<Button
|
|
151
|
-
variant="ghost"
|
|
152
|
-
size="sm"
|
|
153
|
-
onclick={openReference}
|
|
154
|
-
class="text-muted-foreground hover:text-foreground h-8 w-8 p-0"
|
|
155
|
-
title="Edit referenced document"
|
|
156
|
-
>
|
|
157
|
-
<svg class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
158
|
-
<path
|
|
159
|
-
stroke-linecap="round"
|
|
160
|
-
stroke-linejoin="round"
|
|
161
|
-
stroke-width="2"
|
|
162
|
-
d="M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14"
|
|
163
|
-
/>
|
|
164
|
-
</svg>
|
|
165
|
-
</Button>
|
|
166
|
-
{#if !readonly}
|
|
167
|
-
<Button
|
|
168
|
-
variant="ghost"
|
|
169
|
-
size="sm"
|
|
170
|
-
onclick={clearSelection}
|
|
171
|
-
class="text-muted-foreground hover:text-destructive h-8 w-8 p-0"
|
|
172
|
-
title="Clear selection"
|
|
173
|
-
>
|
|
174
|
-
<XIcon class="h-4 w-4" />
|
|
175
|
-
</Button>
|
|
176
|
-
{/if}
|
|
177
|
-
</div>
|
|
178
|
-
{:else}
|
|
179
|
-
<!-- Search/select interface -->
|
|
180
|
-
{#if readonly}
|
|
181
|
-
<!-- Read-only state: show placeholder -->
|
|
182
|
-
<div
|
|
183
|
-
class="border-input bg-muted/50 flex h-10 w-full items-center justify-between rounded-md border px-3 py-2 text-sm"
|
|
184
|
-
>
|
|
185
|
-
<span class="text-muted-foreground">No reference selected</span>
|
|
186
|
-
</div>
|
|
187
|
-
{:else}
|
|
188
|
-
<Popover.Root bind:open>
|
|
189
|
-
<Popover.Trigger bind:ref={triggerRef}>
|
|
190
|
-
{#snippet child({ props })}
|
|
191
|
-
<Button
|
|
192
|
-
{...props}
|
|
193
|
-
variant="outline"
|
|
194
|
-
class="w-full justify-between"
|
|
195
|
-
role="combobox"
|
|
196
|
-
aria-expanded={open}
|
|
197
|
-
>
|
|
198
|
-
{selectedLabel || `Select ${targetType}...`}
|
|
199
|
-
<ChevronsUpDownIcon class="opacity-50" />
|
|
200
|
-
</Button>
|
|
201
|
-
{/snippet}
|
|
202
|
-
</Popover.Trigger>
|
|
203
|
-
<Popover.Content class="!z-[9999] w-[400px] p-0">
|
|
204
|
-
<Command.Root>
|
|
205
|
-
<Command.List>
|
|
206
|
-
{#if loading}
|
|
207
|
-
<Command.Loading>Loading...</Command.Loading>
|
|
208
|
-
{:else if searchResults.length === 0}
|
|
209
|
-
<Command.Empty>
|
|
210
|
-
<div class="flex flex-col items-center gap-2 py-4">
|
|
211
|
-
<p class="text-muted-foreground text-sm">
|
|
212
|
-
No {targetType}s found
|
|
213
|
-
</p>
|
|
214
|
-
<Button size="sm" onclick={createNewDocument} disabled={creating} class="gap-1">
|
|
215
|
-
<PlusIcon class="h-3 w-3" />
|
|
216
|
-
{creating ? 'Creating...' : `Create new ${targetType}`}
|
|
217
|
-
</Button>
|
|
218
|
-
</div>
|
|
219
|
-
</Command.Empty>
|
|
220
|
-
{:else if searchResults.length > 0}
|
|
221
|
-
<Command.Group>
|
|
222
|
-
{#each searchResults as doc (doc.id)}
|
|
223
|
-
<Command.Item
|
|
224
|
-
value={doc.id}
|
|
225
|
-
onSelect={() => selectDocument(doc)}
|
|
226
|
-
class="flex items-center justify-between"
|
|
227
|
-
>
|
|
228
|
-
<div class="flex items-center gap-2">
|
|
229
|
-
<CheckIcon class={cn('h-4 w-4', value !== doc.id && 'text-transparent')} />
|
|
230
|
-
<div>
|
|
231
|
-
<div class="text-sm font-medium">{getDocumentTitle(doc)}</div>
|
|
232
|
-
<div class="text-muted-foreground text-xs">
|
|
233
|
-
{doc.status === 'published' ? '🟢' : '🟡'}
|
|
234
|
-
{doc.status}
|
|
235
|
-
</div>
|
|
236
|
-
</div>
|
|
237
|
-
</div>
|
|
238
|
-
</Command.Item>
|
|
239
|
-
{/each}
|
|
240
|
-
</Command.Group>
|
|
241
|
-
<Command.Separator />
|
|
242
|
-
<Command.Group>
|
|
243
|
-
<Command.Item onSelect={createNewDocument} class="justify-center">
|
|
244
|
-
<div class="flex items-center gap-1">
|
|
245
|
-
<PlusIcon class="h-3 w-3" />
|
|
246
|
-
{creating ? 'Creating...' : `Create new ${targetType}`}
|
|
247
|
-
</div>
|
|
248
|
-
</Command.Item>
|
|
249
|
-
</Command.Group>
|
|
250
|
-
{:else}
|
|
251
|
-
<Command.Empty>
|
|
252
|
-
No {targetType}s available
|
|
253
|
-
</Command.Empty>
|
|
254
|
-
{/if}
|
|
255
|
-
</Command.List>
|
|
256
|
-
</Command.Root>
|
|
257
|
-
</Popover.Content>
|
|
258
|
-
</Popover.Root>
|
|
259
|
-
{/if}
|
|
260
|
-
{/if}
|
|
@@ -1,74 +0,0 @@
|
|
|
1
|
-
<script lang="ts">
|
|
2
|
-
import { Input } from '@aphex/ui/shadcn/input';
|
|
3
|
-
import { Button } from '@aphex/ui/shadcn/button';
|
|
4
|
-
import type { Field } from '../../../types/schemas.js';
|
|
5
|
-
import { generateSlug } from '../../../utils/index.js';
|
|
6
|
-
|
|
7
|
-
interface Props {
|
|
8
|
-
field: Field;
|
|
9
|
-
value: any;
|
|
10
|
-
documentData?: Record<string, any>;
|
|
11
|
-
onUpdate: (value: any) => void;
|
|
12
|
-
validationClasses?: string;
|
|
13
|
-
onBlur?: (event: any) => void;
|
|
14
|
-
onFocus?: (event: any) => void;
|
|
15
|
-
readonly?: boolean;
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
let {
|
|
19
|
-
field,
|
|
20
|
-
value,
|
|
21
|
-
documentData,
|
|
22
|
-
onUpdate,
|
|
23
|
-
validationClasses,
|
|
24
|
-
onBlur,
|
|
25
|
-
onFocus,
|
|
26
|
-
readonly = false
|
|
27
|
-
}: Props = $props();
|
|
28
|
-
|
|
29
|
-
function handleInputChange(event: Event) {
|
|
30
|
-
const target = event.target as HTMLInputElement;
|
|
31
|
-
onUpdate(target.value);
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
// Generate slug from title
|
|
35
|
-
function generateSlugFromTitle() {
|
|
36
|
-
if (documentData?.title) {
|
|
37
|
-
const generatedSlug = generateSlug(documentData.title);
|
|
38
|
-
onUpdate(generatedSlug);
|
|
39
|
-
}
|
|
40
|
-
}
|
|
41
|
-
</script>
|
|
42
|
-
|
|
43
|
-
<div class="space-y-2">
|
|
44
|
-
<div class="flex gap-2">
|
|
45
|
-
<Input
|
|
46
|
-
id={field.name}
|
|
47
|
-
value={value || ''}
|
|
48
|
-
placeholder="document-slug"
|
|
49
|
-
oninput={handleInputChange}
|
|
50
|
-
onblur={onBlur}
|
|
51
|
-
onfocus={onFocus}
|
|
52
|
-
class="flex-1 {validationClasses}"
|
|
53
|
-
disabled={readonly}
|
|
54
|
-
/>
|
|
55
|
-
<Button
|
|
56
|
-
variant="outline"
|
|
57
|
-
size="sm"
|
|
58
|
-
onclick={generateSlugFromTitle}
|
|
59
|
-
disabled={!documentData?.title || readonly}
|
|
60
|
-
class="shrink-0"
|
|
61
|
-
>
|
|
62
|
-
Generate from Title
|
|
63
|
-
</Button>
|
|
64
|
-
</div>
|
|
65
|
-
{#if documentData?.title}
|
|
66
|
-
<p class="text-muted-foreground text-xs">
|
|
67
|
-
Click "Generate from Title" to create slug from: "{documentData.title}"
|
|
68
|
-
</p>
|
|
69
|
-
{:else}
|
|
70
|
-
<p class="text-muted-foreground text-xs">
|
|
71
|
-
Enter a title first to generate a slug automatically
|
|
72
|
-
</p>
|
|
73
|
-
{/if}
|
|
74
|
-
</div>
|