@ekrist1/vulse 0.1.6-alpha.3 → 0.1.7-alpha.4
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/cli/migrate.d.ts.map +1 -1
- package/dist/cli/migrate.js +3 -4
- package/dist/cli/setup.d.ts.map +1 -1
- package/dist/cli/setup.js +11 -10
- package/dist/core/blueprints/compile.d.ts +1 -1
- package/dist/core/blueprints/compile.d.ts.map +1 -1
- package/dist/core/blueprints/compile.js +3 -3
- package/dist/core/blueprints/mutations.js +2 -2
- package/dist/core/forms/rate-limit.d.ts +1 -1
- package/dist/core/forms/rate-limit.d.ts.map +1 -1
- package/dist/core/forms/rate-limit.js +3 -3
- package/dist/core/forms/unique.d.ts +1 -1
- package/dist/core/forms/unique.d.ts.map +1 -1
- package/dist/core/forms/unique.js +4 -4
- package/dist/core/globals/compile.d.ts +1 -1
- package/dist/core/globals/compile.d.ts.map +1 -1
- package/dist/core/globals/compile.js +2 -2
- package/dist/core/globals/definition.d.ts +1 -1
- package/dist/core/globals/definition.d.ts.map +1 -1
- package/dist/core/globals/definition.js +3 -3
- package/dist/core/repos/globals.js +3 -3
- package/dist/core/sha256.d.ts +3 -0
- package/dist/core/sha256.d.ts.map +1 -0
- package/dist/core/sha256.js +9 -0
- package/dist/integration/index.js +1 -1
- package/dist/integration/install-hook.d.ts.map +1 -1
- package/dist/integration/install-hook.js +6 -4
- package/dist/integration/wrangler-config.d.ts +12 -0
- package/dist/integration/wrangler-config.d.ts.map +1 -0
- package/dist/integration/wrangler-config.js +97 -0
- package/dist/integration/wrangler-patch.d.ts +1 -0
- package/dist/integration/wrangler-patch.d.ts.map +1 -1
- package/dist/integration/wrangler-patch.js +2 -1
- package/dist/server/routes/form-submit.js +1 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +11 -3
- package/src/admin/assets/logo-mark.svg +5 -0
- package/src/admin/client/active-locale.ts +17 -0
- package/src/admin/client/api.ts +21 -0
- package/src/admin/client/form-from-zod.ts +7 -0
- package/src/admin/client/live-preview-enabled.ts +5 -0
- package/src/admin/components/AdminShell.astro +45 -0
- package/src/admin/components/AuthSettings.vue +60 -0
- package/src/admin/components/BlockEditor.vue +53 -0
- package/src/admin/components/BlueprintEditor.vue +1783 -0
- package/src/admin/components/CollectionKindIcon.vue +26 -0
- package/src/admin/components/CollectionTree.vue +220 -0
- package/src/admin/components/EntryEditorWithPreview.vue +130 -0
- package/src/admin/components/EntryForm.vue +411 -0
- package/src/admin/components/EntryList.vue +121 -0
- package/src/admin/components/EntryStatusBadge.vue +24 -0
- package/src/admin/components/FormEditor.vue +233 -0
- package/src/admin/components/FormList.vue +54 -0
- package/src/admin/components/GlobalSetEditor.vue +272 -0
- package/src/admin/components/GlobalSetList.vue +55 -0
- package/src/admin/components/LivePreviewPanel.vue +171 -0
- package/src/admin/components/LoginForm.vue +53 -0
- package/src/admin/components/MediaLibrary.vue +106 -0
- package/src/admin/components/MediaPicker.vue +49 -0
- package/src/admin/components/RevisionDiff.vue +11 -0
- package/src/admin/components/RevisionList.vue +134 -0
- package/src/admin/components/SeoFields.vue +113 -0
- package/src/admin/components/SetEditor.vue +137 -0
- package/src/admin/components/SetList.vue +32 -0
- package/src/admin/components/SettingsForm.vue +189 -0
- package/src/admin/components/SideNav.vue +152 -0
- package/src/admin/components/SubmissionDetail.vue +45 -0
- package/src/admin/components/SubmissionList.vue +89 -0
- package/src/admin/components/ToastHost.vue +33 -0
- package/src/admin/components/TreeRow.vue +163 -0
- package/src/admin/components/UserEditor.vue +186 -0
- package/src/admin/components/UserList.vue +46 -0
- package/src/admin/components/blocks/BlockItem.vue +32 -0
- package/src/admin/components/blocks/BlockToolbar.vue +12 -0
- package/src/admin/components/blocks/edit/CodeEdit.vue +18 -0
- package/src/admin/components/blocks/edit/EmbedEdit.vue +14 -0
- package/src/admin/components/blocks/edit/HeadingEdit.vue +19 -0
- package/src/admin/components/blocks/edit/ImageEdit.vue +40 -0
- package/src/admin/components/blocks/edit/ListEdit.vue +36 -0
- package/src/admin/components/blocks/edit/ParagraphEdit.vue +14 -0
- package/src/admin/components/blocks/edit/QuoteEdit.vue +18 -0
- package/src/admin/components/fields/BlocksField.vue +123 -0
- package/src/admin/components/fields/BlocksSetsPicker.vue +59 -0
- package/src/admin/components/fields/BoolField.vue +10 -0
- package/src/admin/components/fields/DateField.vue +22 -0
- package/src/admin/components/fields/EntriesField.vue +153 -0
- package/src/admin/components/fields/EntryField.vue +138 -0
- package/src/admin/components/fields/EnumField.vue +81 -0
- package/src/admin/components/fields/FieldRenderer.vue +87 -0
- package/src/admin/components/fields/GridField.vue +173 -0
- package/src/admin/components/fields/LinkField.vue +219 -0
- package/src/admin/components/fields/MediaField.vue +69 -0
- package/src/admin/components/fields/NumberField.vue +12 -0
- package/src/admin/components/fields/ObjectField.vue +18 -0
- package/src/admin/components/fields/RefField.vue +170 -0
- package/src/admin/components/fields/RepeaterField.vue +27 -0
- package/src/admin/components/fields/ReplicatorField.vue +121 -0
- package/src/admin/components/fields/TextField.vue +11 -0
- package/src/admin/components/fields/TextareaField.vue +11 -0
- package/src/admin/components/fields/VulseAccordionGroupNodeView.vue +82 -0
- package/src/admin/components/fields/VulseAccordionNodeView.vue +128 -0
- package/src/admin/components/fields/VulseCalloutNodeView.vue +81 -0
- package/src/admin/components/fields/VulseIframeNodeView.vue +112 -0
- package/src/admin/components/fields/VulseSetNodeView.vue +68 -0
- package/src/admin/components/fields/VulseVideoNodeView.vue +104 -0
- package/src/admin/components/fields/blocks-editor-extensions.ts +26 -0
- package/src/admin/components/fields/emoji-extension.ts +54 -0
- package/src/admin/components/fields/link-extension.ts +48 -0
- package/src/admin/components/fields/set-node-utils.ts +115 -0
- package/src/admin/components/fields/url-utils.ts +85 -0
- package/src/admin/components/fields/vulse-accordion-extension.ts +64 -0
- package/src/admin/components/fields/vulse-accordion-group-extension.ts +49 -0
- package/src/admin/components/fields/vulse-callout-extension.ts +53 -0
- package/src/admin/components/fields/vulse-iframe-extension.ts +96 -0
- package/src/admin/components/fields/vulse-set-extension.ts +66 -0
- package/src/admin/components/fields/vulse-video-extension.ts +65 -0
- package/src/admin/composables/toast.ts +35 -0
- package/src/admin/composables/useEntrySearch.ts +112 -0
- package/src/admin/composables/useSets.ts +31 -0
- package/src/admin/pages/collections/[name]/[id]/revisions.astro +27 -0
- package/src/admin/pages/collections/[name]/[id].astro +90 -0
- package/src/admin/pages/collections/[name]/index.astro +31 -0
- package/src/admin/pages/collections/[name]/new.astro +38 -0
- package/src/admin/pages/forms/[handle]/submissions/[id].astro +9 -0
- package/src/admin/pages/forms/[handle]/submissions/index.astro +9 -0
- package/src/admin/pages/forms/[handle].astro +9 -0
- package/src/admin/pages/forms/index.astro +7 -0
- package/src/admin/pages/forms/new.astro +7 -0
- package/src/admin/pages/index.astro +36 -0
- package/src/admin/pages/login.astro +14 -0
- package/src/admin/pages/media.astro +8 -0
- package/src/admin/pages/schema/[handle].astro +10 -0
- package/src/admin/pages/schema/new.astro +9 -0
- package/src/admin/pages/settings/auth.astro +9 -0
- package/src/admin/pages/settings/globals/[handle].astro +9 -0
- package/src/admin/pages/settings/globals/index.astro +7 -0
- package/src/admin/pages/settings/globals/new.astro +7 -0
- package/src/admin/pages/settings/index.astro +8 -0
- package/src/admin/pages/settings/sets/[handle].astro +9 -0
- package/src/admin/pages/settings/sets/index.astro +7 -0
- package/src/admin/pages/settings/sets/new.astro +7 -0
- package/src/admin/pages/users/[id].astro +10 -0
- package/src/admin/pages/users/index.astro +8 -0
- package/src/admin/styles/admin.css +166 -0
- package/src/core/access.ts +9 -0
- package/src/core/blocks/schema.ts +66 -0
- package/src/core/blueprints/code-to-definition.ts +156 -0
- package/src/core/blueprints/compile.ts +176 -0
- package/src/core/blueprints/define.ts +12 -0
- package/src/core/blueprints/definition.ts +185 -0
- package/src/core/blueprints/load.ts +144 -0
- package/src/core/blueprints/mutations.ts +236 -0
- package/src/core/blueprints/preview-path.ts +33 -0
- package/src/core/blueprints/reflect-fields.ts +305 -0
- package/src/core/blueprints/registry.ts +14 -0
- package/src/core/blueprints/seed.ts +20 -0
- package/src/core/blueprints/select-helpers.ts +30 -0
- package/src/core/blueprints/seo.ts +180 -0
- package/src/core/blueprints/types.ts +59 -0
- package/src/core/blueprints/zod-helpers.ts +86 -0
- package/src/core/db.ts +11 -0
- package/src/core/errors.ts +34 -0
- package/src/core/forms/compile.ts +84 -0
- package/src/core/forms/definition.ts +102 -0
- package/src/core/forms/rate-limit.ts +52 -0
- package/src/core/forms/unique.ts +38 -0
- package/src/core/globals/compile.ts +35 -0
- package/src/core/globals/definition.ts +27 -0
- package/src/core/locales.ts +45 -0
- package/src/core/migrations.ts +48 -0
- package/src/core/parse-content.ts +85 -0
- package/src/core/plugins/definition.ts +150 -0
- package/src/core/preview-content.ts +21 -0
- package/src/core/repos/entries.ts +504 -0
- package/src/core/repos/forms.ts +270 -0
- package/src/core/repos/globals.ts +179 -0
- package/src/core/repos/media.ts +106 -0
- package/src/core/repos/preview-sessions.ts +108 -0
- package/src/core/repos/revisions.ts +60 -0
- package/src/core/repos/settings.ts +23 -0
- package/src/core/schema.ts +244 -0
- package/src/core/sets/compile.ts +12 -0
- package/src/core/sets/definition.ts +10 -0
- package/src/core/sets/service.ts +82 -0
- package/src/core/sets/validate-tree.ts +57 -0
- package/src/core/sha256.ts +10 -0
- package/src/core/slug.ts +30 -0
- package/src/scaffold/collection-write.ts +83 -0
- package/src/scaffold/collection.ts +277 -0
- package/src/server/assets/live-preview-bridge.content.ts +2 -0
- package/src/server/assets/live-preview-bridge.js +535 -0
- package/src/server/better-auth.ts +82 -0
- package/src/server/cf-images.ts +34 -0
- package/src/server/cron.ts +37 -0
- package/src/server/email.ts +17 -0
- package/src/server/endpoints/api-auth.ts +10 -0
- package/src/server/endpoints/api-vulse-blueprints.ts +23 -0
- package/src/server/endpoints/api-vulse-entries-locales.ts +12 -0
- package/src/server/endpoints/api-vulse-entries-move.ts +7 -0
- package/src/server/endpoints/api-vulse-entries-publish.ts +7 -0
- package/src/server/endpoints/api-vulse-entries-tree.ts +7 -0
- package/src/server/endpoints/api-vulse-entries.ts +23 -0
- package/src/server/endpoints/api-vulse-form-handle.ts +30 -0
- package/src/server/endpoints/api-vulse-form-public.ts +7 -0
- package/src/server/endpoints/api-vulse-form-submit.ts +7 -0
- package/src/server/endpoints/api-vulse-form-upload.ts +7 -0
- package/src/server/endpoints/api-vulse-forms.ts +12 -0
- package/src/server/endpoints/api-vulse-globals-handle.ts +20 -0
- package/src/server/endpoints/api-vulse-globals-public-handle.ts +7 -0
- package/src/server/endpoints/api-vulse-globals-public.ts +7 -0
- package/src/server/endpoints/api-vulse-globals-value.ts +8 -0
- package/src/server/endpoints/api-vulse-globals.ts +12 -0
- package/src/server/endpoints/api-vulse-media-file.ts +7 -0
- package/src/server/endpoints/api-vulse-media-id.ts +12 -0
- package/src/server/endpoints/api-vulse-media.ts +12 -0
- package/src/server/endpoints/api-vulse-preview-bridge.ts +11 -0
- package/src/server/endpoints/api-vulse-preview-sessions-id.ts +10 -0
- package/src/server/endpoints/api-vulse-preview-sessions.ts +7 -0
- package/src/server/endpoints/api-vulse-preview-start.ts +7 -0
- package/src/server/endpoints/api-vulse-preview-stop.ts +7 -0
- package/src/server/endpoints/api-vulse-revisions-restore.ts +7 -0
- package/src/server/endpoints/api-vulse-revisions.ts +7 -0
- package/src/server/endpoints/api-vulse-search.ts +7 -0
- package/src/server/endpoints/api-vulse-sets.ts +23 -0
- package/src/server/endpoints/api-vulse-settings.ts +12 -0
- package/src/server/endpoints/api-vulse-users-id.ts +12 -0
- package/src/server/endpoints/api-vulse-users-reset-password.ts +7 -0
- package/src/server/endpoints/api-vulse-users-role.ts +9 -0
- package/src/server/endpoints/api-vulse-users.ts +7 -0
- package/src/server/endpoints/with-runtime.ts +11 -0
- package/src/server/env.ts +23 -0
- package/src/server/envelope.ts +21 -0
- package/src/server/forms/email.ts +11 -0
- package/src/server/forms/process-submission.ts +95 -0
- package/src/server/forms/queue.ts +25 -0
- package/src/server/forms/templates.ts +24 -0
- package/src/server/forms/webhook.ts +19 -0
- package/src/server/handler.ts +66 -0
- package/src/server/image-probe.ts +35 -0
- package/src/server/loader.ts +54 -0
- package/src/server/plugins.ts +214 -0
- package/src/server/preview.ts +25 -0
- package/src/server/r2.ts +13 -0
- package/src/server/routes/blueprints.ts +62 -0
- package/src/server/routes/entries.ts +255 -0
- package/src/server/routes/form-submit.ts +168 -0
- package/src/server/routes/form-upload.ts +100 -0
- package/src/server/routes/forms.ts +88 -0
- package/src/server/routes/globals-public.ts +30 -0
- package/src/server/routes/globals.ts +93 -0
- package/src/server/routes/media.ts +145 -0
- package/src/server/routes/preview-sessions.ts +76 -0
- package/src/server/routes/preview.ts +36 -0
- package/src/server/routes/revisions.ts +29 -0
- package/src/server/routes/search.ts +31 -0
- package/src/server/routes/sets.ts +40 -0
- package/src/server/routes/settings.ts +24 -0
- package/src/server/routes/users.ts +127 -0
- package/src/server/runtime.ts +99 -0
- package/src/server/sdk/collections.ts +98 -0
- package/src/server/sdk/index.ts +25 -0
- package/src/server/sdk/media.ts +11 -0
- package/src/server/sdk/search.ts +90 -0
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { EditorContent, useEditor } from '@tiptap/vue-3'
|
|
3
|
+
import { computed, onMounted, watch } from 'vue'
|
|
4
|
+
import { useSets } from '../../composables/useSets.js'
|
|
5
|
+
import { EMPTY_BLOCKS_DOC, blocksEditorExtensions } from './blocks-editor-extensions.js'
|
|
6
|
+
import { sanitizeLinkHref } from './url-utils.js'
|
|
7
|
+
|
|
8
|
+
const props = defineProps<{
|
|
9
|
+
label: string
|
|
10
|
+
modelValue: unknown
|
|
11
|
+
error?: string
|
|
12
|
+
blocksSets?: string[]
|
|
13
|
+
}>()
|
|
14
|
+
const emit = defineEmits<{ (e: 'update:modelValue', v: unknown): void }>()
|
|
15
|
+
|
|
16
|
+
const { get, hydrate } = useSets()
|
|
17
|
+
onMounted(() => { void hydrate() })
|
|
18
|
+
|
|
19
|
+
const availableSetHandles = computed<string[]>(() => {
|
|
20
|
+
const declared = props.blocksSets ?? []
|
|
21
|
+
return declared.filter((h) => !!get(h))
|
|
22
|
+
})
|
|
23
|
+
|
|
24
|
+
function insertSet(handle: string) {
|
|
25
|
+
if (!handle) return
|
|
26
|
+
editor.value?.chain().focus().insertVulseSet(handle).run()
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const editor = useEditor({
|
|
30
|
+
extensions: blocksEditorExtensions,
|
|
31
|
+
content: (props.modelValue as object) ?? EMPTY_BLOCKS_DOC,
|
|
32
|
+
onUpdate: ({ editor: ed }) => {
|
|
33
|
+
emit('update:modelValue', ed.getJSON())
|
|
34
|
+
},
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
watch(
|
|
38
|
+
() => props.modelValue,
|
|
39
|
+
(v) => {
|
|
40
|
+
if (!editor.value) return
|
|
41
|
+
const current = JSON.stringify(editor.value.getJSON())
|
|
42
|
+
const incoming = JSON.stringify(v)
|
|
43
|
+
if (current !== incoming && v) {
|
|
44
|
+
editor.value.commands.setContent(v as object, false)
|
|
45
|
+
}
|
|
46
|
+
},
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
function insertCallout(tone: 'info' | 'warn') {
|
|
50
|
+
editor.value?.chain().focus().insertVulseCallout(tone).run()
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function toggleLink() {
|
|
54
|
+
const currentHref = (editor.value?.getAttributes('link').href as string | undefined) ?? ''
|
|
55
|
+
const raw = window.prompt('Link URL', currentHref)
|
|
56
|
+
if (raw === null) return
|
|
57
|
+
const href = sanitizeLinkHref(raw)
|
|
58
|
+
if (!href) {
|
|
59
|
+
editor.value?.chain().focus().unsetVulseLink().run()
|
|
60
|
+
return
|
|
61
|
+
}
|
|
62
|
+
if (editor.value?.state.selection.empty) {
|
|
63
|
+
editor.value?.chain().focus()
|
|
64
|
+
.insertContent({ type: 'text', text: href, marks: [{ type: 'link', attrs: { href } }] })
|
|
65
|
+
.run()
|
|
66
|
+
return
|
|
67
|
+
}
|
|
68
|
+
editor.value?.chain().focus().extendMarkRange('link').setVulseLink(href).run()
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function insertEmoji() {
|
|
72
|
+
const value = window.prompt('Emoji', '🙂')
|
|
73
|
+
if (!value?.trim()) return
|
|
74
|
+
editor.value?.chain().focus().insertEmoji(value.trim()).run()
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function insertAccordion() {
|
|
78
|
+
editor.value?.chain().focus().insertVulseAccordionGroup('Accordion').run()
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function insertIframe() {
|
|
82
|
+
editor.value?.chain().focus().insertVulseIframe().run()
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function insertVideo() {
|
|
86
|
+
editor.value?.chain().focus().insertVulseVideo().run()
|
|
87
|
+
}
|
|
88
|
+
</script>
|
|
89
|
+
|
|
90
|
+
<template>
|
|
91
|
+
<div :data-testid="`field-${label}`">
|
|
92
|
+
<span class="block text-sm font-medium text-zinc-700 capitalize">{{ label }}</span>
|
|
93
|
+
<div class="mt-1 rounded border border-zinc-300 bg-white">
|
|
94
|
+
<div class="flex flex-wrap gap-1 border-b border-zinc-200 bg-zinc-50 px-2 py-1 text-xs">
|
|
95
|
+
<button type="button" class="rounded px-2 py-1 hover:bg-zinc-200" @click="editor?.chain().focus().toggleBold().run()">B</button>
|
|
96
|
+
<button type="button" class="rounded px-2 py-1 hover:bg-zinc-200 italic" @click="editor?.chain().focus().toggleItalic().run()">I</button>
|
|
97
|
+
<button type="button" class="rounded px-2 py-1 hover:bg-zinc-200" @click="toggleLink">Link</button>
|
|
98
|
+
<button type="button" class="rounded px-2 py-1 hover:bg-zinc-200" @click="editor?.chain().focus().toggleHeading({ level: 2 }).run()">H2</button>
|
|
99
|
+
<button type="button" class="rounded px-2 py-1 hover:bg-zinc-200" @click="editor?.chain().focus().toggleHeading({ level: 3 }).run()">H3</button>
|
|
100
|
+
<button type="button" class="rounded px-2 py-1 hover:bg-zinc-200" @click="editor?.chain().focus().toggleHeading({ level: 4 }).run()">H4</button>
|
|
101
|
+
<button type="button" class="rounded px-2 py-1 hover:bg-zinc-200" @click="editor?.chain().focus().toggleBulletList().run()">• List</button>
|
|
102
|
+
<button type="button" class="rounded px-2 py-1 hover:bg-zinc-200" @click="editor?.chain().focus().toggleOrderedList().run()">1. List</button>
|
|
103
|
+
<span class="mx-1 w-px bg-zinc-300" />
|
|
104
|
+
<button type="button" class="rounded px-2 py-1 hover:bg-zinc-200" @click="insertEmoji">Emoji</button>
|
|
105
|
+
<button type="button" class="rounded px-2 py-1 hover:bg-zinc-200" @click="insertCallout('info')">+ Info</button>
|
|
106
|
+
<button type="button" class="rounded px-2 py-1 hover:bg-zinc-200" @click="insertCallout('warn')">+ Warn</button>
|
|
107
|
+
<button type="button" class="rounded px-2 py-1 hover:bg-zinc-200" @click="insertAccordion">Accordion</button>
|
|
108
|
+
<button type="button" class="rounded px-2 py-1 hover:bg-zinc-200" @click="insertIframe">Iframe</button>
|
|
109
|
+
<button type="button" class="rounded px-2 py-1 hover:bg-zinc-200" @click="insertVideo">Video</button>
|
|
110
|
+
<select
|
|
111
|
+
v-if="availableSetHandles.length > 0"
|
|
112
|
+
class="rounded border border-zinc-300 px-2 py-1 text-xs"
|
|
113
|
+
@change="insertSet(($event.target as HTMLSelectElement).value); ($event.target as HTMLSelectElement).value = ''"
|
|
114
|
+
>
|
|
115
|
+
<option value="" disabled selected>+ Insert set</option>
|
|
116
|
+
<option v-for="h in availableSetHandles" :key="h" :value="h">{{ get(h)?.label ?? h }}</option>
|
|
117
|
+
</select>
|
|
118
|
+
</div>
|
|
119
|
+
<EditorContent v-if="editor" :editor="editor" class="prose max-w-none p-3 text-sm" />
|
|
120
|
+
</div>
|
|
121
|
+
<span v-if="error" class="mt-1 block text-xs text-red-600">{{ error }}</span>
|
|
122
|
+
</div>
|
|
123
|
+
</template>
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { computed, onMounted } from 'vue'
|
|
3
|
+
import { useSets } from '../../composables/useSets.js'
|
|
4
|
+
|
|
5
|
+
const props = defineProps<{ modelValue: string[] }>()
|
|
6
|
+
const emit = defineEmits<{ (e: 'update:modelValue', v: string[]): void }>()
|
|
7
|
+
|
|
8
|
+
const { sets, hydrate } = useSets()
|
|
9
|
+
|
|
10
|
+
onMounted(() => {
|
|
11
|
+
void hydrate()
|
|
12
|
+
})
|
|
13
|
+
|
|
14
|
+
const setList = computed(() => [...sets.value.values()])
|
|
15
|
+
const selected = computed(() => props.modelValue ?? [])
|
|
16
|
+
|
|
17
|
+
function toggle(handle: string, checked: boolean) {
|
|
18
|
+
const next = checked
|
|
19
|
+
? [...selected.value, handle]
|
|
20
|
+
: selected.value.filter((entry) => entry !== handle)
|
|
21
|
+
emit('update:modelValue', next)
|
|
22
|
+
}
|
|
23
|
+
</script>
|
|
24
|
+
|
|
25
|
+
<template>
|
|
26
|
+
<div class="mt-2 rounded border border-zinc-200 bg-zinc-50 px-3 py-3">
|
|
27
|
+
<span class="block text-xs font-medium text-zinc-700">Global sets</span>
|
|
28
|
+
<p class="mt-1 text-xs text-zinc-500">
|
|
29
|
+
Choose reusable sets from
|
|
30
|
+
<a href="/admin/settings/sets" class="text-zinc-700 underline">Settings → Sets</a>.
|
|
31
|
+
Editors can insert them inside this blocks field.
|
|
32
|
+
</p>
|
|
33
|
+
<div v-if="setList.length === 0" class="mt-2 text-xs text-zinc-500">
|
|
34
|
+
No sets defined yet.
|
|
35
|
+
<a href="/admin/settings/sets/new" class="text-zinc-700 underline">Create one</a>.
|
|
36
|
+
</div>
|
|
37
|
+
<div v-else class="mt-2 grid grid-cols-1 gap-1 sm:grid-cols-2">
|
|
38
|
+
<label
|
|
39
|
+
v-for="set in setList"
|
|
40
|
+
:key="set.handle"
|
|
41
|
+
class="flex items-center gap-2 rounded border border-transparent px-2 py-1.5 text-sm hover:border-zinc-200 hover:bg-white"
|
|
42
|
+
>
|
|
43
|
+
<input
|
|
44
|
+
type="checkbox"
|
|
45
|
+
:value="set.handle"
|
|
46
|
+
:checked="selected.includes(set.handle)"
|
|
47
|
+
@change="toggle(set.handle, ($event.target as HTMLInputElement).checked)"
|
|
48
|
+
/>
|
|
49
|
+
<span>
|
|
50
|
+
{{ set.label }}
|
|
51
|
+
<span class="font-mono text-xs text-zinc-500">({{ set.handle }})</span>
|
|
52
|
+
</span>
|
|
53
|
+
</label>
|
|
54
|
+
</div>
|
|
55
|
+
<p v-if="selected.length > 0" class="mt-2 text-xs text-zinc-500">
|
|
56
|
+
{{ selected.length }} set{{ selected.length === 1 ? '' : 's' }} selected.
|
|
57
|
+
</p>
|
|
58
|
+
</div>
|
|
59
|
+
</template>
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
defineProps<{ modelValue: boolean; label: string }>()
|
|
3
|
+
defineEmits<{ (e: 'update:modelValue', v: boolean): void }>()
|
|
4
|
+
</script>
|
|
5
|
+
<template>
|
|
6
|
+
<label class="flex items-center gap-2">
|
|
7
|
+
<input type="checkbox" :checked="modelValue" @change="$emit('update:modelValue', ($event.target as HTMLInputElement).checked)" />
|
|
8
|
+
<span class="text-sm text-zinc-600">{{ label }}</span>
|
|
9
|
+
</label>
|
|
10
|
+
</template>
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { computed } from 'vue'
|
|
3
|
+
const props = defineProps<{ modelValue: string | Date | null | undefined; label: string }>()
|
|
4
|
+
const emit = defineEmits<{ (e: 'update:modelValue', v: string | null): void }>()
|
|
5
|
+
const local = computed({
|
|
6
|
+
get() {
|
|
7
|
+
if (!props.modelValue) return ''
|
|
8
|
+
const d = props.modelValue instanceof Date ? props.modelValue : new Date(props.modelValue)
|
|
9
|
+
if (Number.isNaN(d.getTime())) return ''
|
|
10
|
+
return d.toISOString().slice(0, 16)
|
|
11
|
+
},
|
|
12
|
+
set(v: string) {
|
|
13
|
+
emit('update:modelValue', v ? new Date(v).toISOString() : null)
|
|
14
|
+
},
|
|
15
|
+
})
|
|
16
|
+
</script>
|
|
17
|
+
<template>
|
|
18
|
+
<label class="block">
|
|
19
|
+
<span class="text-sm text-zinc-600">{{ label }}</span>
|
|
20
|
+
<input v-model="local" type="datetime-local" class="mt-1 w-full rounded border px-3 py-2" />
|
|
21
|
+
</label>
|
|
22
|
+
</template>
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { computed, ref, watch } from 'vue'
|
|
3
|
+
import { entryOptionLabel, useEntrySearch } from '../../composables/useEntrySearch.js'
|
|
4
|
+
|
|
5
|
+
const props = defineProps<{
|
|
6
|
+
modelValue: string[]
|
|
7
|
+
label: string
|
|
8
|
+
collections: string[]
|
|
9
|
+
max?: number
|
|
10
|
+
}>()
|
|
11
|
+
|
|
12
|
+
const emit = defineEmits<{ (e: 'update:modelValue', v: string[]): void }>()
|
|
13
|
+
|
|
14
|
+
const selected = ref<Array<{ id: string; collection: string; label: string }>>([])
|
|
15
|
+
|
|
16
|
+
const {
|
|
17
|
+
open,
|
|
18
|
+
query,
|
|
19
|
+
options,
|
|
20
|
+
loading,
|
|
21
|
+
resolveLabel,
|
|
22
|
+
openDropdown,
|
|
23
|
+
closeDropdown,
|
|
24
|
+
onBlur,
|
|
25
|
+
} = useEntrySearch(() => props.collections)
|
|
26
|
+
|
|
27
|
+
const atMax = computed(() => props.max !== undefined && selected.value.length >= props.max)
|
|
28
|
+
|
|
29
|
+
const availableOptions = computed(() =>
|
|
30
|
+
options.value.filter(
|
|
31
|
+
(o) => !selected.value.some((s) => s.id === o.id && s.collection === o.collection),
|
|
32
|
+
),
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
async function syncSelected(ids: string[]) {
|
|
36
|
+
if (ids.length === 0) {
|
|
37
|
+
selected.value = []
|
|
38
|
+
return
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const next: Array<{ id: string; collection: string; label: string }> = []
|
|
42
|
+
for (const id of ids) {
|
|
43
|
+
let found = selected.value.find((s) => s.id === id)
|
|
44
|
+
if (found) {
|
|
45
|
+
next.push(found)
|
|
46
|
+
continue
|
|
47
|
+
}
|
|
48
|
+
for (const collection of props.collections) {
|
|
49
|
+
try {
|
|
50
|
+
const label = await resolveLabel(id, collection)
|
|
51
|
+
if (label && label !== id) {
|
|
52
|
+
next.push({ id, collection, label })
|
|
53
|
+
break
|
|
54
|
+
}
|
|
55
|
+
} catch {
|
|
56
|
+
// try next collection
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
if (!next.some((s) => s.id === id)) {
|
|
60
|
+
next.push({ id, collection: props.collections[0] ?? '', label: id })
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
selected.value = next
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function addOption(option: { id: string; collection: string; title?: string; email?: string }) {
|
|
67
|
+
if (atMax.value) return
|
|
68
|
+
if (selected.value.some((s) => s.id === option.id)) return
|
|
69
|
+
const next = [
|
|
70
|
+
...selected.value,
|
|
71
|
+
{ id: option.id, collection: option.collection, label: entryOptionLabel(option) },
|
|
72
|
+
]
|
|
73
|
+
selected.value = next
|
|
74
|
+
emit('update:modelValue', next.map((s) => s.id))
|
|
75
|
+
query.value = ''
|
|
76
|
+
if (props.max === 1) closeDropdown()
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function removeAt(index: number) {
|
|
80
|
+
const next = [...selected.value]
|
|
81
|
+
next.splice(index, 1)
|
|
82
|
+
selected.value = next
|
|
83
|
+
emit('update:modelValue', next.map((s) => s.id))
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
watch(
|
|
87
|
+
() => props.modelValue,
|
|
88
|
+
(value) => {
|
|
89
|
+
void syncSelected(value ?? [])
|
|
90
|
+
},
|
|
91
|
+
{ immediate: true },
|
|
92
|
+
)
|
|
93
|
+
</script>
|
|
94
|
+
|
|
95
|
+
<template>
|
|
96
|
+
<label class="block">
|
|
97
|
+
<span class="text-sm text-zinc-600">{{ label }}</span>
|
|
98
|
+
<div class="relative mt-1" @blur="onBlur">
|
|
99
|
+
<div v-if="selected.length" class="mb-2 flex flex-wrap gap-2">
|
|
100
|
+
<span
|
|
101
|
+
v-for="(item, i) in selected"
|
|
102
|
+
:key="item.id"
|
|
103
|
+
class="inline-flex items-center gap-1 rounded-full border border-zinc-200 bg-zinc-50 px-2 py-1 text-sm"
|
|
104
|
+
>
|
|
105
|
+
{{ item.label }}
|
|
106
|
+
<button type="button" class="text-zinc-400 hover:text-red-600" @click="removeAt(i)">×</button>
|
|
107
|
+
</span>
|
|
108
|
+
</div>
|
|
109
|
+
|
|
110
|
+
<button
|
|
111
|
+
v-if="!atMax"
|
|
112
|
+
type="button"
|
|
113
|
+
class="vulse-input flex w-full items-center justify-between bg-white text-left"
|
|
114
|
+
:class="open && 'border-zinc-400'"
|
|
115
|
+
@click="open ? closeDropdown() : openDropdown()"
|
|
116
|
+
>
|
|
117
|
+
<span class="text-zinc-400">{{ max === 1 ? 'Select entry…' : 'Add entry…' }}</span>
|
|
118
|
+
<span class="text-xs text-zinc-400">{{ open ? '▴' : '▾' }}</span>
|
|
119
|
+
</button>
|
|
120
|
+
<p v-else-if="max" class="text-xs text-zinc-500">Maximum of {{ max }} selected.</p>
|
|
121
|
+
|
|
122
|
+
<div
|
|
123
|
+
v-if="open"
|
|
124
|
+
class="absolute z-20 mt-1 w-full overflow-hidden rounded-md border border-zinc-200 bg-white shadow-lg"
|
|
125
|
+
>
|
|
126
|
+
<div class="border-b border-zinc-200 p-2">
|
|
127
|
+
<input
|
|
128
|
+
v-model="query"
|
|
129
|
+
type="search"
|
|
130
|
+
class="vulse-input bg-white"
|
|
131
|
+
placeholder="Search entries…"
|
|
132
|
+
autofocus
|
|
133
|
+
@keydown.esc.prevent="closeDropdown()"
|
|
134
|
+
/>
|
|
135
|
+
</div>
|
|
136
|
+
<ul class="max-h-48 overflow-auto py-1 text-sm">
|
|
137
|
+
<li v-if="loading" class="px-3 py-2 text-zinc-500">Loading…</li>
|
|
138
|
+
<li v-else-if="availableOptions.length === 0" class="px-3 py-2 text-zinc-500">No matches</li>
|
|
139
|
+
<li v-for="option in availableOptions" v-else :key="`${option.collection}:${option.id}`">
|
|
140
|
+
<button
|
|
141
|
+
type="button"
|
|
142
|
+
class="flex w-full items-center px-3 py-2 text-left hover:bg-zinc-100"
|
|
143
|
+
@click="addOption(option)"
|
|
144
|
+
>
|
|
145
|
+
<span v-if="collections.length > 1" class="mr-2 text-xs text-zinc-400">{{ option.collection }}</span>
|
|
146
|
+
{{ entryOptionLabel(option) }}
|
|
147
|
+
</button>
|
|
148
|
+
</li>
|
|
149
|
+
</ul>
|
|
150
|
+
</div>
|
|
151
|
+
</div>
|
|
152
|
+
</label>
|
|
153
|
+
</template>
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { ref, toRef, watch } from 'vue'
|
|
3
|
+
import { entryOptionLabel, useEntrySearch } from '../../composables/useEntrySearch.js'
|
|
4
|
+
|
|
5
|
+
const props = defineProps<{
|
|
6
|
+
modelValue: string | null
|
|
7
|
+
label: string
|
|
8
|
+
collections: string[]
|
|
9
|
+
}>()
|
|
10
|
+
|
|
11
|
+
const emit = defineEmits<{ (e: 'update:modelValue', v: string | null): void }>()
|
|
12
|
+
|
|
13
|
+
const selectedCollection = ref(props.collections[0] ?? '')
|
|
14
|
+
const selectedLabel = ref('')
|
|
15
|
+
|
|
16
|
+
const {
|
|
17
|
+
open,
|
|
18
|
+
query,
|
|
19
|
+
options,
|
|
20
|
+
loading,
|
|
21
|
+
resolveLabel,
|
|
22
|
+
openDropdown,
|
|
23
|
+
closeDropdown,
|
|
24
|
+
onBlur,
|
|
25
|
+
} = useEntrySearch(() => {
|
|
26
|
+
const col = selectedCollection.value || props.collections[0]
|
|
27
|
+
return col ? [col] : props.collections
|
|
28
|
+
})
|
|
29
|
+
|
|
30
|
+
function selectOption(option: { id: string; collection: string; title?: string; email?: string }) {
|
|
31
|
+
selectedCollection.value = option.collection
|
|
32
|
+
emit('update:modelValue', option.id)
|
|
33
|
+
selectedLabel.value = entryOptionLabel(option)
|
|
34
|
+
query.value = ''
|
|
35
|
+
closeDropdown()
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function clearSelection() {
|
|
39
|
+
emit('update:modelValue', null)
|
|
40
|
+
selectedLabel.value = ''
|
|
41
|
+
query.value = ''
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
watch(
|
|
45
|
+
() => props.modelValue,
|
|
46
|
+
(value) => {
|
|
47
|
+
if (!value) {
|
|
48
|
+
selectedLabel.value = ''
|
|
49
|
+
return
|
|
50
|
+
}
|
|
51
|
+
const col = selectedCollection.value || props.collections[0]
|
|
52
|
+
if (!col) return
|
|
53
|
+
void resolveLabel(value, col).then((label) => {
|
|
54
|
+
selectedLabel.value = label
|
|
55
|
+
})
|
|
56
|
+
},
|
|
57
|
+
{ immediate: true },
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
watch(
|
|
61
|
+
() => props.collections,
|
|
62
|
+
(cols) => {
|
|
63
|
+
if (cols.length === 1) selectedCollection.value = cols[0]!
|
|
64
|
+
},
|
|
65
|
+
{ immediate: true },
|
|
66
|
+
)
|
|
67
|
+
</script>
|
|
68
|
+
|
|
69
|
+
<template>
|
|
70
|
+
<label class="block">
|
|
71
|
+
<span class="text-sm text-zinc-600">{{ label }}</span>
|
|
72
|
+
<div class="mt-1 space-y-2">
|
|
73
|
+
<select
|
|
74
|
+
v-if="collections.length > 1"
|
|
75
|
+
v-model="selectedCollection"
|
|
76
|
+
class="vulse-input bg-white text-sm"
|
|
77
|
+
@change="clearSelection()"
|
|
78
|
+
>
|
|
79
|
+
<option v-for="col in collections" :key="col" :value="col">{{ col }}</option>
|
|
80
|
+
</select>
|
|
81
|
+
|
|
82
|
+
<div class="relative" @blur="onBlur">
|
|
83
|
+
<div class="flex gap-2">
|
|
84
|
+
<button
|
|
85
|
+
type="button"
|
|
86
|
+
class="vulse-input flex flex-1 items-center justify-between bg-white text-left"
|
|
87
|
+
:class="open && 'border-zinc-400'"
|
|
88
|
+
@click="open ? closeDropdown() : openDropdown()"
|
|
89
|
+
>
|
|
90
|
+
<span :class="modelValue ? 'text-zinc-900' : 'text-zinc-400'">
|
|
91
|
+
{{ modelValue ? selectedLabel || modelValue : 'Select entry…' }}
|
|
92
|
+
</span>
|
|
93
|
+
<span class="text-xs text-zinc-400">{{ open ? '▴' : '▾' }}</span>
|
|
94
|
+
</button>
|
|
95
|
+
<button
|
|
96
|
+
v-if="modelValue"
|
|
97
|
+
type="button"
|
|
98
|
+
class="rounded border border-zinc-300 bg-white px-3 py-2 text-sm text-zinc-600 hover:bg-zinc-50"
|
|
99
|
+
@click="clearSelection"
|
|
100
|
+
>
|
|
101
|
+
Clear
|
|
102
|
+
</button>
|
|
103
|
+
</div>
|
|
104
|
+
|
|
105
|
+
<div
|
|
106
|
+
v-if="open"
|
|
107
|
+
class="absolute z-20 mt-1 w-full overflow-hidden rounded-md border border-zinc-200 bg-white shadow-lg"
|
|
108
|
+
>
|
|
109
|
+
<div class="border-b border-zinc-200 p-2">
|
|
110
|
+
<input
|
|
111
|
+
v-model="query"
|
|
112
|
+
type="search"
|
|
113
|
+
class="vulse-input bg-white"
|
|
114
|
+
placeholder="Search entries…"
|
|
115
|
+
autofocus
|
|
116
|
+
@keydown.esc.prevent="closeDropdown()"
|
|
117
|
+
/>
|
|
118
|
+
</div>
|
|
119
|
+
<ul class="max-h-48 overflow-auto py-1 text-sm">
|
|
120
|
+
<li v-if="loading" class="px-3 py-2 text-zinc-500">Loading…</li>
|
|
121
|
+
<li v-else-if="options.length === 0" class="px-3 py-2 text-zinc-500">No matches</li>
|
|
122
|
+
<li v-for="option in options" v-else :key="`${option.collection}:${option.id}`">
|
|
123
|
+
<button
|
|
124
|
+
type="button"
|
|
125
|
+
class="flex w-full items-center px-3 py-2 text-left hover:bg-zinc-100"
|
|
126
|
+
:class="option.id === modelValue && 'bg-zinc-50 font-medium'"
|
|
127
|
+
@click="selectOption(option)"
|
|
128
|
+
>
|
|
129
|
+
<span v-if="collections.length > 1" class="mr-2 text-xs text-zinc-400">{{ option.collection }}</span>
|
|
130
|
+
{{ entryOptionLabel(option) }}
|
|
131
|
+
</button>
|
|
132
|
+
</li>
|
|
133
|
+
</ul>
|
|
134
|
+
</div>
|
|
135
|
+
</div>
|
|
136
|
+
</div>
|
|
137
|
+
</label>
|
|
138
|
+
</template>
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { computed } from 'vue'
|
|
3
|
+
|
|
4
|
+
const props = defineProps<{
|
|
5
|
+
modelValue: string | string[]
|
|
6
|
+
label: string
|
|
7
|
+
options: { key: string; label: string }[]
|
|
8
|
+
multiple?: boolean
|
|
9
|
+
placeholder?: string
|
|
10
|
+
clearable?: boolean
|
|
11
|
+
required?: boolean
|
|
12
|
+
}>()
|
|
13
|
+
|
|
14
|
+
const emit = defineEmits<{ (e: 'update:modelValue', v: string | string[]): void }>()
|
|
15
|
+
|
|
16
|
+
const normalizedOptions = computed(() =>
|
|
17
|
+
props.options.length > 0 ? props.options : [{ key: '', label: '—' }],
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
function onSingleChange(event: Event) {
|
|
21
|
+
const value = (event.target as HTMLSelectElement).value
|
|
22
|
+
emit('update:modelValue', value)
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function onMultipleChange(event: Event) {
|
|
26
|
+
const select = event.target as HTMLSelectElement
|
|
27
|
+
emit(
|
|
28
|
+
'update:modelValue',
|
|
29
|
+
Array.from(select.selectedOptions).map((o) => o.value),
|
|
30
|
+
)
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function clearSingle() {
|
|
34
|
+
emit('update:modelValue', '')
|
|
35
|
+
}
|
|
36
|
+
</script>
|
|
37
|
+
|
|
38
|
+
<template>
|
|
39
|
+
<label class="block">
|
|
40
|
+
<span class="text-sm text-zinc-600">{{ label }}</span>
|
|
41
|
+
<div v-if="multiple" class="mt-1">
|
|
42
|
+
<select
|
|
43
|
+
multiple
|
|
44
|
+
class="w-full rounded border px-3 py-2"
|
|
45
|
+
:value="modelValue as string[]"
|
|
46
|
+
@change="onMultipleChange"
|
|
47
|
+
>
|
|
48
|
+
<option v-for="o in normalizedOptions" :key="o.key" :value="o.key">{{ o.label }}</option>
|
|
49
|
+
</select>
|
|
50
|
+
<p class="mt-1 text-xs text-zinc-500">Hold Ctrl/Cmd to select multiple options.</p>
|
|
51
|
+
</div>
|
|
52
|
+
<div v-else class="mt-1 flex gap-2">
|
|
53
|
+
<select
|
|
54
|
+
:value="(modelValue as string) ?? ''"
|
|
55
|
+
class="w-full rounded border px-3 py-2"
|
|
56
|
+
:required="required"
|
|
57
|
+
@change="onSingleChange"
|
|
58
|
+
>
|
|
59
|
+
<option v-if="placeholder || clearable" value="" disabled :selected="!(modelValue as string)">
|
|
60
|
+
{{ placeholder || 'Choose…' }}
|
|
61
|
+
</option>
|
|
62
|
+
<option
|
|
63
|
+
v-for="o in normalizedOptions"
|
|
64
|
+
:key="o.key"
|
|
65
|
+
:value="o.key"
|
|
66
|
+
:disabled="!o.key"
|
|
67
|
+
>
|
|
68
|
+
{{ o.label }}
|
|
69
|
+
</option>
|
|
70
|
+
</select>
|
|
71
|
+
<button
|
|
72
|
+
v-if="clearable && modelValue"
|
|
73
|
+
type="button"
|
|
74
|
+
class="rounded border border-zinc-300 bg-white px-3 py-2 text-sm text-zinc-600 hover:bg-zinc-50"
|
|
75
|
+
@click="clearSingle"
|
|
76
|
+
>
|
|
77
|
+
Clear
|
|
78
|
+
</button>
|
|
79
|
+
</div>
|
|
80
|
+
</label>
|
|
81
|
+
</template>
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { computed } from 'vue'
|
|
3
|
+
import type { LinkValue } from '../../../core/blueprints/definition.js'
|
|
4
|
+
import type { FieldDescriptor } from '../../client/form-from-zod'
|
|
5
|
+
import TextField from './TextField.vue'
|
|
6
|
+
import TextareaField from './TextareaField.vue'
|
|
7
|
+
import NumberField from './NumberField.vue'
|
|
8
|
+
import BoolField from './BoolField.vue'
|
|
9
|
+
import DateField from './DateField.vue'
|
|
10
|
+
import EnumField from './EnumField.vue'
|
|
11
|
+
import ObjectField from './ObjectField.vue'
|
|
12
|
+
import RepeaterField from './RepeaterField.vue'
|
|
13
|
+
import ReplicatorField from './ReplicatorField.vue'
|
|
14
|
+
import RefField from './RefField.vue'
|
|
15
|
+
import EntryField from './EntryField.vue'
|
|
16
|
+
import EntriesField from './EntriesField.vue'
|
|
17
|
+
import LinkField from './LinkField.vue'
|
|
18
|
+
import MediaField from './MediaField.vue'
|
|
19
|
+
import BlocksField from './BlocksField.vue'
|
|
20
|
+
import GridField from './GridField.vue'
|
|
21
|
+
|
|
22
|
+
const props = defineProps<{
|
|
23
|
+
field: FieldDescriptor
|
|
24
|
+
modelValue: unknown
|
|
25
|
+
fieldErrors?: Record<string, string>
|
|
26
|
+
tree?: boolean
|
|
27
|
+
linkCollections?: string[]
|
|
28
|
+
}>()
|
|
29
|
+
defineEmits<{ (e: 'update:modelValue', v: unknown): void }>()
|
|
30
|
+
|
|
31
|
+
const ownError = computed<string | undefined>(() => props.fieldErrors?.[props.field.path])
|
|
32
|
+
|
|
33
|
+
const selectOptions = computed(() => {
|
|
34
|
+
if (props.field.selectOptions?.length) return props.field.selectOptions
|
|
35
|
+
return (props.field.options ?? []).map((key) => ({ key, label: key }))
|
|
36
|
+
})
|
|
37
|
+
</script>
|
|
38
|
+
<template>
|
|
39
|
+
<div :class="['vulse-field', ownError && 'vulse-field-error']">
|
|
40
|
+
<TextField v-if="field.widget === 'text'" :model-value="(modelValue as string) ?? ''" :label="field.label ?? field.path" :required="field.required" @update:modelValue="$emit('update:modelValue', $event)" />
|
|
41
|
+
<TextareaField v-else-if="field.widget === 'textarea'" :model-value="(modelValue as string) ?? ''" :label="field.label ?? field.path" :required="field.required" @update:modelValue="$emit('update:modelValue', $event)" />
|
|
42
|
+
<NumberField v-else-if="field.widget === 'number'" :model-value="modelValue as number" :label="field.label ?? field.path" :required="field.required" @update:modelValue="$emit('update:modelValue', $event)" />
|
|
43
|
+
<BoolField v-else-if="field.widget === 'bool'" :model-value="!!modelValue" :label="field.label ?? field.path" @update:modelValue="$emit('update:modelValue', $event)" />
|
|
44
|
+
<DateField v-else-if="field.widget === 'date'" :model-value="modelValue as string | null" :label="field.label ?? field.path" @update:modelValue="$emit('update:modelValue', $event)" />
|
|
45
|
+
<EnumField
|
|
46
|
+
v-else-if="field.widget === 'enum'"
|
|
47
|
+
:model-value="field.selectMultiple ? ((modelValue as string[]) ?? []) : ((modelValue as string) ?? '')"
|
|
48
|
+
:label="field.label ?? field.path"
|
|
49
|
+
:options="selectOptions"
|
|
50
|
+
:multiple="field.selectMultiple"
|
|
51
|
+
:placeholder="field.selectPlaceholder"
|
|
52
|
+
:clearable="field.selectClearable"
|
|
53
|
+
:required="field.required"
|
|
54
|
+
@update:modelValue="$emit('update:modelValue', $event)"
|
|
55
|
+
/>
|
|
56
|
+
<RefField v-else-if="field.widget === 'ref'" :model-value="modelValue as string | null" :label="field.label ?? field.path" :ref-target="field.refTarget!" @update:modelValue="$emit('update:modelValue', $event)" />
|
|
57
|
+
<EntryField v-else-if="field.widget === 'entry'" :model-value="modelValue as string | null" :label="field.label ?? field.path" :collections="field.entryCollections ?? []" @update:modelValue="$emit('update:modelValue', $event)" />
|
|
58
|
+
<EntriesField v-else-if="field.widget === 'entries'" :model-value="(modelValue as string[]) ?? []" :label="field.label ?? field.path" :collections="field.entriesCollections ?? []" :max="field.entriesMax" @update:modelValue="$emit('update:modelValue', $event)" />
|
|
59
|
+
<LinkField
|
|
60
|
+
v-else-if="field.widget === 'link'"
|
|
61
|
+
:model-value="modelValue as LinkValue | null"
|
|
62
|
+
:label="field.label ?? field.path"
|
|
63
|
+
:collections="field.linkCollections"
|
|
64
|
+
:tree="tree"
|
|
65
|
+
@update:modelValue="$emit('update:modelValue', $event)"
|
|
66
|
+
/>
|
|
67
|
+
<MediaField v-else-if="field.widget === 'media'" :model-value="modelValue" :label="field.label ?? field.path" @update:modelValue="$emit('update:modelValue', $event)" />
|
|
68
|
+
<BlocksField v-else-if="field.widget === 'blocks'" :model-value="modelValue" :label="field.label ?? field.path" :blocks-sets="field.blocksSets" @update:modelValue="$emit('update:modelValue', $event)" />
|
|
69
|
+
<ObjectField v-else-if="field.widget === 'object'" :model-value="(modelValue as Record<string, unknown>) ?? {}" :label="field.label ?? field.path" :fields="field.children ?? []" @update:modelValue="$emit('update:modelValue', $event)" />
|
|
70
|
+
<ReplicatorField v-else-if="field.widget === 'replicator'" :model-value="modelValue" :label="field.label ?? field.path" :replicator-sets="field.replicatorSets" @update:modelValue="$emit('update:modelValue', $event)" />
|
|
71
|
+
<RepeaterField v-else-if="field.widget === 'repeater'" :model-value="(modelValue as Record<string, unknown>[]) ?? []" :label="field.label ?? field.path" :item-fields="field.itemFields ?? []" @update:modelValue="$emit('update:modelValue', $event)" />
|
|
72
|
+
<GridField
|
|
73
|
+
v-else-if="field.widget === 'grid'"
|
|
74
|
+
:model-value="(modelValue as Record<string, unknown>[]) ?? []"
|
|
75
|
+
:label="field.label ?? field.path"
|
|
76
|
+
:item-fields="field.itemFields ?? []"
|
|
77
|
+
:mode="field.gridMode"
|
|
78
|
+
:min-rows="field.gridMinRows"
|
|
79
|
+
:max-rows="field.gridMaxRows"
|
|
80
|
+
:add-label="field.gridAddLabel"
|
|
81
|
+
:tree="tree"
|
|
82
|
+
:link-collections="linkCollections"
|
|
83
|
+
@update:modelValue="$emit('update:modelValue', $event)"
|
|
84
|
+
/>
|
|
85
|
+
<p v-if="ownError" class="mt-1 text-xs text-red-600">{{ ownError }}</p>
|
|
86
|
+
</div>
|
|
87
|
+
</template>
|