@edgedev/create-edge-app 1.1.27 → 1.1.28
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/edge/components/cms/block.vue +334 -26
- package/edge/components/cms/blockEditor.vue +50 -3
- package/edge/components/cms/codeEditor.vue +15 -0
- package/edge/components/cms/init_blocks/footer.html +111 -19
- package/edge/components/cms/init_blocks/image.html +8 -0
- package/edge/components/cms/init_blocks/post_content.html +3 -2
- package/edge/components/cms/init_blocks/post_title_header.html +8 -6
- package/edge/components/cms/init_blocks/posts_list.html +6 -5
- package/edge/components/cms/mediaCard.vue +13 -2
- package/edge/components/cms/mediaManager.vue +16 -2
- package/edge/components/cms/menu.vue +253 -42
- package/edge/components/cms/page.vue +151 -18
- package/edge/components/cms/site.vue +517 -372
- package/edge/components/cms/siteSettingsForm.vue +616 -0
- package/edge/components/cms/themeDefaultMenu.vue +258 -22
- package/edge/components/cms/themeEditor.vue +95 -11
- package/edge/components/editor.vue +1 -0
- package/edge/components/formSubtypes/myOrgs.vue +112 -1
- package/edge/components/orgSwitcher.vue +1 -1
- package/edge/components/organizationMembers.vue +171 -21
- package/edge/components/shad/html.vue +6 -0
- package/edge/components/sideBar.vue +7 -4
- package/edge/components/sideBarContent.vue +1 -1
- package/edge/components/userMenu.vue +50 -14
- package/edge/composables/siteSettingsTemplate.js +79 -0
- package/edge/composables/structuredDataTemplates.js +36 -0
- package/package.json +1 -1
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
<script setup>
|
|
2
2
|
import { useVModel } from '@vueuse/core'
|
|
3
|
-
import { ImagePlus, Plus } from 'lucide-vue-next'
|
|
3
|
+
import { ImagePlus, Loader2, Plus, Sparkles } from 'lucide-vue-next'
|
|
4
4
|
const props = defineProps({
|
|
5
5
|
modelValue: {
|
|
6
6
|
type: Object,
|
|
@@ -49,6 +49,7 @@ function extractFieldsInOrder(template) {
|
|
|
49
49
|
}
|
|
50
50
|
|
|
51
51
|
const modelValue = useVModel(props, 'modelValue', emit)
|
|
52
|
+
const blockFormRef = ref(null)
|
|
52
53
|
|
|
53
54
|
const state = reactive({
|
|
54
55
|
open: false,
|
|
@@ -61,8 +62,23 @@ const state = reactive({
|
|
|
61
62
|
loading: true,
|
|
62
63
|
afterLoad: false,
|
|
63
64
|
imageOpen: false,
|
|
65
|
+
imageOpenByField: {},
|
|
66
|
+
aiDialogOpen: false,
|
|
67
|
+
aiInstructions: '',
|
|
68
|
+
aiSelectedFields: {},
|
|
69
|
+
aiGenerating: false,
|
|
70
|
+
aiError: '',
|
|
71
|
+
validationErrors: [],
|
|
64
72
|
})
|
|
65
73
|
|
|
74
|
+
const isLightName = (value) => {
|
|
75
|
+
if (!value)
|
|
76
|
+
return false
|
|
77
|
+
return String(value).toLowerCase().includes('light')
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const previewBackgroundClass = value => (isLightName(value) ? 'bg-neutral-900/90' : 'bg-neutral-100')
|
|
81
|
+
|
|
66
82
|
const ensureQueryItemsDefaults = (meta) => {
|
|
67
83
|
Object.keys(meta || {}).forEach((key) => {
|
|
68
84
|
const cfg = meta[key]
|
|
@@ -159,19 +175,59 @@ const openEditor = async () => {
|
|
|
159
175
|
}
|
|
160
176
|
}
|
|
161
177
|
modelValue.value.blockUpdatedAt = new Date().toISOString()
|
|
178
|
+
state.validationErrors = []
|
|
162
179
|
state.open = true
|
|
163
180
|
state.afterLoad = true
|
|
164
181
|
}
|
|
165
182
|
|
|
166
|
-
const
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
183
|
+
const normalizeValidationNumber = (value) => {
|
|
184
|
+
if (value === null || value === undefined || value === '')
|
|
185
|
+
return null
|
|
186
|
+
const parsed = Number(value)
|
|
187
|
+
return Number.isNaN(parsed) ? null : parsed
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
const stringLength = (value) => {
|
|
191
|
+
if (value === null || value === undefined)
|
|
192
|
+
return 0
|
|
193
|
+
return String(value).trim().length
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
const validateValueAgainstRules = (value, rules, label, typeHint) => {
|
|
197
|
+
if (!rules || typeof rules !== 'object')
|
|
198
|
+
return []
|
|
199
|
+
|
|
200
|
+
const errors = []
|
|
201
|
+
if (rules.required) {
|
|
202
|
+
const isEmptyArray = Array.isArray(value) && value.length === 0
|
|
203
|
+
const isEmptyString = typeof value === 'string' && stringLength(value) === 0
|
|
204
|
+
if (value === null || value === undefined || isEmptyArray || isEmptyString) {
|
|
205
|
+
errors.push(`${label} is required.`)
|
|
206
|
+
return errors
|
|
207
|
+
}
|
|
171
208
|
}
|
|
172
|
-
|
|
173
|
-
|
|
209
|
+
|
|
210
|
+
if (typeHint === 'number') {
|
|
211
|
+
const numericValue = normalizeValidationNumber(value)
|
|
212
|
+
if (numericValue !== null) {
|
|
213
|
+
if (rules.min !== undefined && numericValue < rules.min)
|
|
214
|
+
errors.push(`${label} must be at least ${rules.min}.`)
|
|
215
|
+
if (rules.max !== undefined && numericValue > rules.max)
|
|
216
|
+
errors.push(`${label} must be ${rules.max} or less.`)
|
|
217
|
+
}
|
|
218
|
+
return errors
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
const length = Array.isArray(value) ? value.length : stringLength(value)
|
|
222
|
+
if (rules.min !== undefined && length < rules.min) {
|
|
223
|
+
errors.push(`${label} must be at least ${rules.min} ${Array.isArray(value) ? 'items' : 'characters'}.`)
|
|
224
|
+
}
|
|
225
|
+
if (rules.max !== undefined && length > rules.max) {
|
|
226
|
+
errors.push(`${label} must be ${rules.max} ${Array.isArray(value) ? 'items' : 'characters'} or less.`)
|
|
227
|
+
}
|
|
228
|
+
return errors
|
|
174
229
|
}
|
|
230
|
+
|
|
175
231
|
const orderedMeta = computed(() => {
|
|
176
232
|
const metaObj = state.metaUpdate || {}
|
|
177
233
|
const tpl = modelValue.value?.content || ''
|
|
@@ -209,6 +265,161 @@ const genTitleFromField = (field) => {
|
|
|
209
265
|
.replace(/([a-z])([A-Z])/g, '$1 $2')
|
|
210
266
|
.replace(/^./, str => str.toUpperCase())
|
|
211
267
|
}
|
|
268
|
+
|
|
269
|
+
const collectValidationErrors = () => {
|
|
270
|
+
const errors = []
|
|
271
|
+
for (const entry of orderedMeta.value) {
|
|
272
|
+
const label = genTitleFromField(entry)
|
|
273
|
+
const value = state.draft?.[entry.field]
|
|
274
|
+
|
|
275
|
+
if (entry.meta?.type === 'array' && !entry.meta?.api && !entry.meta?.collection) {
|
|
276
|
+
const itemCount = Array.isArray(value) ? value.length : 0
|
|
277
|
+
if (itemCount < 1) {
|
|
278
|
+
errors.push(`${label} requires at least one item.`)
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
if (Array.isArray(value) && entry.meta?.schema) {
|
|
282
|
+
value.forEach((item, index) => {
|
|
283
|
+
for (const schemaItem of entry.meta.schema) {
|
|
284
|
+
const itemLabel = `${label} ${index + 1} · ${genTitleFromField(schemaItem)}`
|
|
285
|
+
const itemValue = item?.[schemaItem.field]
|
|
286
|
+
errors.push(...validateValueAgainstRules(itemValue, schemaItem.validation, itemLabel, schemaItem.type))
|
|
287
|
+
}
|
|
288
|
+
})
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
const topLevelErrors = validateValueAgainstRules(value, entry.meta?.validation, label, entry.meta?.type)
|
|
293
|
+
errors.push(...topLevelErrors)
|
|
294
|
+
}
|
|
295
|
+
return errors
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
const save = () => {
|
|
299
|
+
const validationErrors = collectValidationErrors()
|
|
300
|
+
if (validationErrors.length) {
|
|
301
|
+
state.validationErrors = validationErrors
|
|
302
|
+
return
|
|
303
|
+
}
|
|
304
|
+
state.validationErrors = []
|
|
305
|
+
const updated = {
|
|
306
|
+
...modelValue.value,
|
|
307
|
+
values: JSON.parse(JSON.stringify(state.draft)),
|
|
308
|
+
meta: sanitizeQueryItems(state.meta),
|
|
309
|
+
}
|
|
310
|
+
modelValue.value = updated
|
|
311
|
+
state.open = false
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
const aiFieldOptions = computed(() => {
|
|
315
|
+
return orderedMeta.value
|
|
316
|
+
.map(entry => ({
|
|
317
|
+
id: entry.field,
|
|
318
|
+
label: genTitleFromField(entry),
|
|
319
|
+
type: entry.meta?.type || 'text',
|
|
320
|
+
}))
|
|
321
|
+
.filter(option => option.type !== 'image' && option.type !== 'color' && !/url/i.test(option.id) && !/color/i.test(option.id))
|
|
322
|
+
})
|
|
323
|
+
|
|
324
|
+
const selectedAiFieldIds = computed(() => {
|
|
325
|
+
return aiFieldOptions.value
|
|
326
|
+
.filter(option => state.aiSelectedFields?.[option.id])
|
|
327
|
+
.map(option => option.id)
|
|
328
|
+
})
|
|
329
|
+
|
|
330
|
+
const allAiFieldsSelected = computed({
|
|
331
|
+
get: () => {
|
|
332
|
+
if (!aiFieldOptions.value.length)
|
|
333
|
+
return false
|
|
334
|
+
return aiFieldOptions.value.every(option => state.aiSelectedFields?.[option.id])
|
|
335
|
+
},
|
|
336
|
+
set: (value) => {
|
|
337
|
+
const next = {}
|
|
338
|
+
aiFieldOptions.value.forEach((option) => {
|
|
339
|
+
next[option.id] = value
|
|
340
|
+
})
|
|
341
|
+
state.aiSelectedFields = next
|
|
342
|
+
},
|
|
343
|
+
})
|
|
344
|
+
|
|
345
|
+
const resetAiSelections = () => {
|
|
346
|
+
const next = {}
|
|
347
|
+
aiFieldOptions.value.forEach((option) => {
|
|
348
|
+
next[option.id] = true
|
|
349
|
+
})
|
|
350
|
+
state.aiSelectedFields = next
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
const openAiDialog = () => {
|
|
354
|
+
state.aiError = ''
|
|
355
|
+
state.aiInstructions = ''
|
|
356
|
+
resetAiSelections()
|
|
357
|
+
state.aiDialogOpen = true
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
const closeAiDialog = () => {
|
|
361
|
+
state.aiDialogOpen = false
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
const generateWithAi = async () => {
|
|
365
|
+
if (state.aiGenerating)
|
|
366
|
+
return
|
|
367
|
+
const selectedFields = selectedAiFieldIds.value
|
|
368
|
+
if (!selectedFields.length) {
|
|
369
|
+
state.aiError = 'Select at least one field for AI generation.'
|
|
370
|
+
return
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
state.aiGenerating = true
|
|
374
|
+
state.aiError = ''
|
|
375
|
+
|
|
376
|
+
try {
|
|
377
|
+
const fields = aiFieldOptions.value.filter(option => selectedFields.includes(option.id))
|
|
378
|
+
const currentValues = selectedFields.reduce((acc, field) => {
|
|
379
|
+
acc[field] = state.draft?.[field]
|
|
380
|
+
return acc
|
|
381
|
+
}, {})
|
|
382
|
+
const meta = selectedFields.reduce((acc, field) => {
|
|
383
|
+
acc[field] = state.meta?.[field]
|
|
384
|
+
return acc
|
|
385
|
+
}, {})
|
|
386
|
+
|
|
387
|
+
const response = await edgeFirebase.runFunction('cms-generateBlockFields', {
|
|
388
|
+
orgId: edgeGlobal.edgeState.currentOrganization,
|
|
389
|
+
uid: edgeFirebase?.user?.uid || '',
|
|
390
|
+
blockId: modelValue.value?.blockId || props.blockId,
|
|
391
|
+
blockName: modelValue.value?.name || '',
|
|
392
|
+
content: modelValue.value?.content || '',
|
|
393
|
+
instructions: state.aiInstructions || '',
|
|
394
|
+
fields,
|
|
395
|
+
currentValues,
|
|
396
|
+
meta,
|
|
397
|
+
})
|
|
398
|
+
|
|
399
|
+
const aiFields = response?.data?.fields || {}
|
|
400
|
+
Object.keys(aiFields).forEach((field) => {
|
|
401
|
+
if (selectedFields.includes(field)) {
|
|
402
|
+
state.draft[field] = aiFields[field]
|
|
403
|
+
blockFormRef.value?.setFieldValue?.(field, aiFields[field])
|
|
404
|
+
}
|
|
405
|
+
})
|
|
406
|
+
|
|
407
|
+
const missingFields = selectedFields.filter(field => !(field in aiFields))
|
|
408
|
+
if (missingFields.length) {
|
|
409
|
+
state.aiError = `AI skipped: ${missingFields.join(', ')}`
|
|
410
|
+
return
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
closeAiDialog()
|
|
414
|
+
}
|
|
415
|
+
catch (error) {
|
|
416
|
+
console.error('Failed to generate block fields with AI', error)
|
|
417
|
+
state.aiError = 'AI generation failed. Try again.'
|
|
418
|
+
}
|
|
419
|
+
finally {
|
|
420
|
+
state.aiGenerating = false
|
|
421
|
+
}
|
|
422
|
+
}
|
|
212
423
|
const addToArray = async (field) => {
|
|
213
424
|
state.reload = true
|
|
214
425
|
state.draft[field].push(JSON.parse(JSON.stringify(state.arrayItems[field])))
|
|
@@ -317,13 +528,28 @@ const getTagsFromPosts = computed(() => {
|
|
|
317
528
|
<Sheet v-model:open="state.open">
|
|
318
529
|
<edge-cms-block-sheet-content v-if="state.afterLoad" class="w-full md:w-1/2 max-w-none sm:max-w-none max-w-2xl">
|
|
319
530
|
<SheetHeader>
|
|
320
|
-
<
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
531
|
+
<div class="flex flex-col gap-3 pr-10 md:flex-row md:items-start md:justify-between">
|
|
532
|
+
<div class="min-w-0">
|
|
533
|
+
<SheetTitle>Edit Block</SheetTitle>
|
|
534
|
+
<SheetDescription v-if="modelValue.synced" class="text-sm text-red-500">
|
|
535
|
+
This is a synced block. Changes made here will be reflected across all instances of this block on your site.
|
|
536
|
+
</SheetDescription>
|
|
537
|
+
</div>
|
|
538
|
+
<edge-shad-button
|
|
539
|
+
type="button"
|
|
540
|
+
size="sm"
|
|
541
|
+
class="h-8 gap-2 md:self-start"
|
|
542
|
+
variant="outline"
|
|
543
|
+
:disabled="!aiFieldOptions.length"
|
|
544
|
+
@click="openAiDialog"
|
|
545
|
+
>
|
|
546
|
+
<Sparkles class="w-4 h-4" />
|
|
547
|
+
Generate with AI
|
|
548
|
+
</edge-shad-button>
|
|
549
|
+
</div>
|
|
324
550
|
</SheetHeader>
|
|
325
551
|
|
|
326
|
-
<edge-shad-form>
|
|
552
|
+
<edge-shad-form ref="blockFormRef">
|
|
327
553
|
<div v-if="orderedMeta.length === 0">
|
|
328
554
|
<Alert variant="info" class="mt-4 mb-4">
|
|
329
555
|
<AlertTitle>No editable fields found</AlertTitle>
|
|
@@ -463,9 +689,12 @@ const getTagsFromPosts = computed(() => {
|
|
|
463
689
|
</div>
|
|
464
690
|
</div>
|
|
465
691
|
<div v-else-if="entry.meta?.type === 'image'" class="w-full">
|
|
466
|
-
<div class="
|
|
692
|
+
<div class="mb-2 text-sm font-medium text-foreground">
|
|
693
|
+
{{ genTitleFromField(entry) }}
|
|
694
|
+
</div>
|
|
695
|
+
<div class="relative py-2 rounded-md flex items-center justify-center" :class="previewBackgroundClass(state.draft[entry.field])">
|
|
467
696
|
<div class="bg-black/80 absolute left-0 top-0 w-full h-full opacity-0 hover:opacity-100 transition-opacity flex items-center justify-center z-10 cursor-pointer">
|
|
468
|
-
<Dialog v-model:open="state.
|
|
697
|
+
<Dialog v-model:open="state.imageOpenByField[entry.field]">
|
|
469
698
|
<DialogTrigger as-child>
|
|
470
699
|
<edge-shad-button variant="outline" class="bg-white text-black hover:bg-gray-200">
|
|
471
700
|
<ImagePlus class="h-5 w-5 mr-2" />
|
|
@@ -482,18 +711,22 @@ const getTagsFromPosts = computed(() => {
|
|
|
482
711
|
:site="props.siteId"
|
|
483
712
|
:select-mode="true"
|
|
484
713
|
:default-tags="entry.meta.tags"
|
|
485
|
-
@select="(url) => { state.draft[entry.field] = url; state.
|
|
714
|
+
@select="(url) => { state.draft[entry.field] = url; state.imageOpenByField[entry.field] = false }"
|
|
486
715
|
/>
|
|
487
716
|
<edge-cms-media-manager
|
|
488
717
|
v-else
|
|
489
718
|
:site="props.siteId"
|
|
490
719
|
:select-mode="true"
|
|
491
|
-
@select="(url) => { state.draft[entry.field] = url; state.
|
|
720
|
+
@select="(url) => { state.draft[entry.field] = url; state.imageOpenByField[entry.field] = false }"
|
|
492
721
|
/>
|
|
493
722
|
</DialogContent>
|
|
494
723
|
</Dialog>
|
|
495
724
|
</div>
|
|
496
|
-
<img
|
|
725
|
+
<img
|
|
726
|
+
v-if="state.draft[entry.field]"
|
|
727
|
+
:src="state.draft[entry.field]"
|
|
728
|
+
class="max-h-40 max-w-full h-auto w-auto object-contain"
|
|
729
|
+
>
|
|
497
730
|
</div>
|
|
498
731
|
</div>
|
|
499
732
|
<div v-else-if="entry.meta?.option">
|
|
@@ -514,15 +747,90 @@ const getTagsFromPosts = computed(() => {
|
|
|
514
747
|
</template>
|
|
515
748
|
</div>
|
|
516
749
|
|
|
517
|
-
<
|
|
518
|
-
<
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
750
|
+
<div class="sticky bottom-0 bg-background px-6 pb-4 pt-2">
|
|
751
|
+
<Alert v-if="state.validationErrors.length" variant="destructive" class="mb-3">
|
|
752
|
+
<AlertTitle>Fix the highlighted fields</AlertTitle>
|
|
753
|
+
<AlertDescription class="text-sm">
|
|
754
|
+
<div v-for="(error, index) in state.validationErrors" :key="`${error}-${index}`">
|
|
755
|
+
{{ error }}
|
|
756
|
+
</div>
|
|
757
|
+
</AlertDescription>
|
|
758
|
+
</Alert>
|
|
759
|
+
<SheetFooter class="flex justify-between">
|
|
760
|
+
<edge-shad-button variant="destructive" class="text-white" @click="state.open = false">
|
|
761
|
+
Cancel
|
|
762
|
+
</edge-shad-button>
|
|
763
|
+
<edge-shad-button class=" bg-slate-800 hover:bg-slate-400 w-full" @click="save">
|
|
764
|
+
Save changes
|
|
765
|
+
</edge-shad-button>
|
|
766
|
+
</SheetFooter>
|
|
767
|
+
</div>
|
|
525
768
|
</edge-shad-form>
|
|
769
|
+
|
|
770
|
+
<edge-shad-dialog v-model="state.aiDialogOpen">
|
|
771
|
+
<DialogContent class="max-w-[640px]">
|
|
772
|
+
<DialogHeader>
|
|
773
|
+
<DialogTitle>Generate with AI</DialogTitle>
|
|
774
|
+
<DialogDescription>
|
|
775
|
+
Choose which fields the AI should fill and add any optional instructions.
|
|
776
|
+
</DialogDescription>
|
|
777
|
+
</DialogHeader>
|
|
778
|
+
<div class="space-y-4">
|
|
779
|
+
<edge-shad-textarea
|
|
780
|
+
v-model="state.aiInstructions"
|
|
781
|
+
name="aiInstructions"
|
|
782
|
+
label="Instructions (Optional)"
|
|
783
|
+
placeholder="Share tone, audience, and any details the AI should include."
|
|
784
|
+
/>
|
|
785
|
+
<div class="space-y-2">
|
|
786
|
+
<div class="flex items-center justify-between text-xs font-semibold uppercase tracking-wide text-muted-foreground">
|
|
787
|
+
<span>Fields</span>
|
|
788
|
+
<span>{{ selectedAiFieldIds.length }} selected</span>
|
|
789
|
+
</div>
|
|
790
|
+
<edge-shad-checkbox v-model="allAiFieldsSelected" name="aiSelectAll">
|
|
791
|
+
Select all fields
|
|
792
|
+
</edge-shad-checkbox>
|
|
793
|
+
<div v-if="aiFieldOptions.length" class="grid gap-2 md:grid-cols-2">
|
|
794
|
+
<edge-shad-checkbox
|
|
795
|
+
v-for="option in aiFieldOptions"
|
|
796
|
+
:key="option.id"
|
|
797
|
+
v-model="state.aiSelectedFields[option.id]"
|
|
798
|
+
:name="`ai-field-${option.id}`"
|
|
799
|
+
>
|
|
800
|
+
{{ option.label }}
|
|
801
|
+
<span class="ml-2 text-xs text-muted-foreground">({{ option.type }})</span>
|
|
802
|
+
</edge-shad-checkbox>
|
|
803
|
+
</div>
|
|
804
|
+
<Alert v-else variant="info">
|
|
805
|
+
<AlertTitle>No editable fields</AlertTitle>
|
|
806
|
+
<AlertDescription class="text-sm">
|
|
807
|
+
Add editable fields to this block to enable AI generation.
|
|
808
|
+
</AlertDescription>
|
|
809
|
+
</Alert>
|
|
810
|
+
</div>
|
|
811
|
+
<Alert v-if="state.aiError" variant="destructive">
|
|
812
|
+
<AlertTitle>AI generation failed</AlertTitle>
|
|
813
|
+
<AlertDescription class="text-sm">
|
|
814
|
+
{{ state.aiError }}
|
|
815
|
+
</AlertDescription>
|
|
816
|
+
</Alert>
|
|
817
|
+
</div>
|
|
818
|
+
<DialogFooter class="pt-4 flex justify-between">
|
|
819
|
+
<edge-shad-button type="button" variant="destructive" class="text-white" @click="closeAiDialog">
|
|
820
|
+
Cancel
|
|
821
|
+
</edge-shad-button>
|
|
822
|
+
<edge-shad-button
|
|
823
|
+
type="button"
|
|
824
|
+
class="w-full"
|
|
825
|
+
:disabled="state.aiGenerating || !selectedAiFieldIds.length"
|
|
826
|
+
@click="generateWithAi"
|
|
827
|
+
>
|
|
828
|
+
<Loader2 v-if="state.aiGenerating" class="w-4 h-4 mr-2 animate-spin" />
|
|
829
|
+
Generate
|
|
830
|
+
</edge-shad-button>
|
|
831
|
+
</DialogFooter>
|
|
832
|
+
</DialogContent>
|
|
833
|
+
</edge-shad-dialog>
|
|
526
834
|
</edge-cms-block-sheet-content>
|
|
527
835
|
</Sheet>
|
|
528
836
|
</div>
|
|
@@ -38,6 +38,7 @@ const state = reactive({
|
|
|
38
38
|
initialBlocksSeeded: false,
|
|
39
39
|
seedingInitialBlocks: false,
|
|
40
40
|
previewViewport: 'full',
|
|
41
|
+
previewBlock: null,
|
|
41
42
|
})
|
|
42
43
|
|
|
43
44
|
const blockSchema = toTypedSchema(z.object({
|
|
@@ -95,6 +96,8 @@ const PLACEHOLDERS = {
|
|
|
95
96
|
|
|
96
97
|
const contentEditorRef = ref(null)
|
|
97
98
|
|
|
99
|
+
const ignorePreviewDelete = () => {}
|
|
100
|
+
|
|
98
101
|
const BLOCK_CONTENT_SNIPPETS = [
|
|
99
102
|
{
|
|
100
103
|
label: 'Text Field',
|
|
@@ -339,6 +342,8 @@ function handleEditorLineClick(payload, workingDoc) {
|
|
|
339
342
|
const tag = findTagAtOffset(workingDoc.content, offset)
|
|
340
343
|
if (!tag)
|
|
341
344
|
return
|
|
345
|
+
if (tag.type === 'if')
|
|
346
|
+
return
|
|
342
347
|
|
|
343
348
|
const parsedCfg = safeParseConfig(tag.rawCfg)
|
|
344
349
|
state.jsonEditorError = ''
|
|
@@ -416,6 +421,42 @@ function handleJsonEditorSave() {
|
|
|
416
421
|
closeJsonEditor()
|
|
417
422
|
}
|
|
418
423
|
|
|
424
|
+
const buildPreviewBlock = (workingDoc, parsed) => {
|
|
425
|
+
const content = workingDoc?.content || ''
|
|
426
|
+
const nextValues = {}
|
|
427
|
+
const previousValues = state.previewBlock?.values || {}
|
|
428
|
+
Object.keys(parsed.values || {}).forEach((field) => {
|
|
429
|
+
if (previousValues[field] !== undefined)
|
|
430
|
+
nextValues[field] = previousValues[field]
|
|
431
|
+
else
|
|
432
|
+
nextValues[field] = parsed.values[field]
|
|
433
|
+
})
|
|
434
|
+
|
|
435
|
+
const previousMeta = state.previewBlock?.meta || {}
|
|
436
|
+
const nextMeta = {}
|
|
437
|
+
Object.keys(parsed.meta || {}).forEach((field) => {
|
|
438
|
+
if (previousMeta[field]) {
|
|
439
|
+
nextMeta[field] = {
|
|
440
|
+
...previousMeta[field],
|
|
441
|
+
...parsed.meta[field],
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
else {
|
|
445
|
+
nextMeta[field] = parsed.meta[field]
|
|
446
|
+
}
|
|
447
|
+
})
|
|
448
|
+
|
|
449
|
+
return {
|
|
450
|
+
id: state.previewBlock?.id || 'preview',
|
|
451
|
+
blockId: props.blockId,
|
|
452
|
+
name: workingDoc?.name || state.previewBlock?.name || '',
|
|
453
|
+
content,
|
|
454
|
+
values: nextValues,
|
|
455
|
+
meta: nextMeta,
|
|
456
|
+
synced: !!workingDoc?.synced,
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
|
|
419
460
|
const theme = computed(() => {
|
|
420
461
|
const theme = edgeGlobal.edgeState.blockEditorTheme || ''
|
|
421
462
|
let themeContents = null
|
|
@@ -443,7 +484,9 @@ watch(headObject, (newHeadElements) => {
|
|
|
443
484
|
}, { immediate: true, deep: true })
|
|
444
485
|
|
|
445
486
|
const editorDocUpdates = (workingDoc) => {
|
|
446
|
-
|
|
487
|
+
const parsed = blockModel(workingDoc.content)
|
|
488
|
+
state.workingDoc = parsed
|
|
489
|
+
state.previewBlock = buildPreviewBlock(workingDoc, parsed)
|
|
447
490
|
console.log('Editor workingDoc update:', state.workingDoc)
|
|
448
491
|
}
|
|
449
492
|
|
|
@@ -670,11 +713,15 @@ const getTagsFromBlocks = computed(() => {
|
|
|
670
713
|
class="w-full mx-auto bg-card border border-border rounded-lg shadow-sm md:shadow-md"
|
|
671
714
|
:style="previewViewportStyle"
|
|
672
715
|
>
|
|
673
|
-
<edge-cms-block
|
|
716
|
+
<edge-cms-block
|
|
717
|
+
v-if="state.previewBlock"
|
|
718
|
+
v-model="state.previewBlock"
|
|
674
719
|
:site-id="edgeGlobal.edgeState.blockEditorSite"
|
|
675
720
|
:theme="theme"
|
|
676
|
-
:
|
|
721
|
+
:edit-mode="true"
|
|
677
722
|
:viewport-mode="previewViewportMode"
|
|
723
|
+
:block-id="state.previewBlock.id"
|
|
724
|
+
@delete="ignorePreviewDelete"
|
|
678
725
|
/>
|
|
679
726
|
</div>
|
|
680
727
|
</div>
|
|
@@ -166,6 +166,12 @@ const redo = () => {
|
|
|
166
166
|
|
|
167
167
|
const editorCompRef = ref(null)
|
|
168
168
|
const editorInstanceRef = shallowRef(null)
|
|
169
|
+
let editorDomNode = null
|
|
170
|
+
|
|
171
|
+
const stopEnterPropagation = (event) => {
|
|
172
|
+
if (event.key === 'Enter')
|
|
173
|
+
event.stopPropagation()
|
|
174
|
+
}
|
|
169
175
|
|
|
170
176
|
const setCursor = () => {
|
|
171
177
|
const editor = editorInstanceRef.value
|
|
@@ -226,6 +232,9 @@ const runChatGpt = async () => {
|
|
|
226
232
|
const handleMount = (editor) => {
|
|
227
233
|
editorInstanceRef.value = editor
|
|
228
234
|
editorInstanceRef.value?.getAction('editor.action.formatDocument').run()
|
|
235
|
+
editorDomNode = editor.getDomNode?.()
|
|
236
|
+
if (editorDomNode)
|
|
237
|
+
editorDomNode.addEventListener('keydown', stopEnterPropagation)
|
|
229
238
|
editor.onMouseDown((event) => {
|
|
230
239
|
const position = event?.target?.position
|
|
231
240
|
const model = editor.getModel?.()
|
|
@@ -314,6 +323,12 @@ const getChanges = () => {
|
|
|
314
323
|
formatCode()
|
|
315
324
|
}
|
|
316
325
|
}
|
|
326
|
+
|
|
327
|
+
onBeforeUnmount(() => {
|
|
328
|
+
if (editorDomNode)
|
|
329
|
+
editorDomNode.removeEventListener('keydown', stopEnterPropagation)
|
|
330
|
+
editorDomNode = null
|
|
331
|
+
})
|
|
317
332
|
</script>
|
|
318
333
|
|
|
319
334
|
<template>
|