@edgedev/create-edge-app 1.2.34 → 1.2.35
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/README.md +1 -0
- package/agents.md +95 -2
- package/deploy.sh +60 -1
- package/edge/components/cms/block.vue +763 -301
- package/edge/components/cms/blockEditor.vue +342 -29
- package/edge/components/cms/blockPicker.vue +3 -3
- package/edge/components/cms/blocksManager.vue +48 -13
- package/edge/components/cms/htmlContent.vue +601 -10
- package/edge/components/cms/init_blocks/contact_us.html +55 -47
- package/edge/components/cms/init_blocks/newsletter.html +56 -96
- package/edge/components/cms/menu.vue +92 -31
- package/edge/components/cms/page.vue +165 -50
- package/edge/components/cms/posts.vue +13 -4
- package/edge/components/cms/site.vue +15 -4
- package/edge/components/cms/siteSettingsForm.vue +19 -9
- package/edge/components/cms/themeDefaultMenu.vue +20 -2
- package/edge/composables/siteSettingsTemplate.js +2 -0
- package/edge-pull.sh +16 -2
- package/edge-push.sh +9 -1
- package/edge-remote.sh +20 -0
- package/edge-status.sh +9 -5
- package/edge-update-all.sh +127 -0
- package/package.json +1 -1
|
@@ -34,6 +34,18 @@ const props = defineProps({
|
|
|
34
34
|
type: Boolean,
|
|
35
35
|
default: false,
|
|
36
36
|
},
|
|
37
|
+
disableInteractivePreviewInEdit: {
|
|
38
|
+
type: Boolean,
|
|
39
|
+
default: true,
|
|
40
|
+
},
|
|
41
|
+
overrideClicksInEditMode: {
|
|
42
|
+
type: Boolean,
|
|
43
|
+
default: false,
|
|
44
|
+
},
|
|
45
|
+
allowPreviewContentEdit: {
|
|
46
|
+
type: Boolean,
|
|
47
|
+
default: false,
|
|
48
|
+
},
|
|
37
49
|
})
|
|
38
50
|
const emit = defineEmits(['update:modelValue', 'delete'])
|
|
39
51
|
const edgeFirebase = inject('edgeFirebase')
|
|
@@ -143,9 +155,11 @@ function extractFieldsInOrder(template) {
|
|
|
143
155
|
|
|
144
156
|
const modelValue = useVModel(props, 'modelValue', emit)
|
|
145
157
|
const blockFormRef = ref(null)
|
|
158
|
+
const previewContentEditorRef = ref(null)
|
|
146
159
|
|
|
147
160
|
const state = reactive({
|
|
148
161
|
open: false,
|
|
162
|
+
editorMode: 'fields',
|
|
149
163
|
draft: {},
|
|
150
164
|
delete: false,
|
|
151
165
|
meta: {},
|
|
@@ -162,6 +176,10 @@ const state = reactive({
|
|
|
162
176
|
aiGenerating: false,
|
|
163
177
|
aiError: '',
|
|
164
178
|
validationErrors: [],
|
|
179
|
+
blockContentDraft: '',
|
|
180
|
+
blockContentDocId: '',
|
|
181
|
+
blockContentUpdating: false,
|
|
182
|
+
blockContentError: '',
|
|
165
183
|
})
|
|
166
184
|
|
|
167
185
|
const INTERACTIVE_CLICK_SELECTOR = [
|
|
@@ -172,6 +190,14 @@ const INTERACTIVE_CLICK_SELECTOR = [
|
|
|
172
190
|
'.cms-nav-panel',
|
|
173
191
|
'.cms-nav-close',
|
|
174
192
|
'.cms-nav-link',
|
|
193
|
+
'.cms-nav-folder-toggle',
|
|
194
|
+
'.cms-nav-folder-menu',
|
|
195
|
+
'[data-cms-nav-folder-toggle]',
|
|
196
|
+
'[data-cms-nav-folder-menu]',
|
|
197
|
+
].join(', ')
|
|
198
|
+
const EDITOR_CONTROL_CLICK_SELECTOR = [
|
|
199
|
+
'[data-cms-block-control]',
|
|
200
|
+
'[data-cms-block-ignore-editor-click]',
|
|
175
201
|
].join(', ')
|
|
176
202
|
|
|
177
203
|
const hasFixedPositionInContent = computed(() => {
|
|
@@ -207,17 +233,26 @@ const effectivePreviewType = computed(() => {
|
|
|
207
233
|
return normalizePreviewType(inheritedPreviewType.value ?? resolvedPreviewType.value)
|
|
208
234
|
})
|
|
209
235
|
|
|
236
|
+
const canOpenFieldEditor = computed(() => props.editMode)
|
|
237
|
+
const canOpenPreviewContentEditor = computed(() => !props.editMode && props.allowPreviewContentEdit)
|
|
238
|
+
const canOpenEditor = computed(() => canOpenFieldEditor.value || canOpenPreviewContentEditor.value)
|
|
239
|
+
|
|
210
240
|
const shouldContainFixedPreview = computed(() => {
|
|
211
241
|
return (props.editMode || props.containFixed) && hasFixedPositionInContent.value
|
|
212
242
|
})
|
|
213
243
|
|
|
244
|
+
const shouldDisableInteractivePreview = computed(() => {
|
|
245
|
+
return props.editMode && props.disableInteractivePreviewInEdit
|
|
246
|
+
})
|
|
247
|
+
|
|
214
248
|
const blockWrapperClass = computed(() => ({
|
|
215
249
|
'overflow-visible': shouldContainFixedPreview.value,
|
|
216
|
-
'min-h-[88px]': props.editMode && shouldContainFixedPreview.value,
|
|
250
|
+
'min-h-[88px]': props.editMode && shouldContainFixedPreview.value && shouldDisableInteractivePreview.value,
|
|
251
|
+
'min-h-[calc(100vh-360px)]': props.editMode && shouldContainFixedPreview.value && !shouldDisableInteractivePreview.value,
|
|
217
252
|
'z-30': shouldContainFixedPreview.value,
|
|
218
253
|
'bg-white text-black': props.editMode && effectivePreviewType.value === 'light',
|
|
219
254
|
'bg-neutral-950 text-neutral-50': props.editMode && effectivePreviewType.value === 'dark',
|
|
220
|
-
'cms-nav-edit-static':
|
|
255
|
+
'cms-nav-edit-static': shouldDisableInteractivePreview.value,
|
|
221
256
|
}))
|
|
222
257
|
|
|
223
258
|
const blockWrapperStyle = computed(() => {
|
|
@@ -236,6 +271,94 @@ const isLightName = (value) => {
|
|
|
236
271
|
|
|
237
272
|
const previewBackgroundClass = value => (isLightName(value) ? 'bg-neutral-900/90' : 'bg-neutral-100')
|
|
238
273
|
|
|
274
|
+
const PLACEHOLDERS = {
|
|
275
|
+
text: 'Lorem ipsum dolor sit amet.',
|
|
276
|
+
textarea: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.',
|
|
277
|
+
richtext: '<p>Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.</p>',
|
|
278
|
+
arrayItem: [
|
|
279
|
+
'Lorem ipsum dolor sit amet.',
|
|
280
|
+
'Consectetur adipiscing elit.',
|
|
281
|
+
'Sed do eiusmod tempor incididunt.',
|
|
282
|
+
],
|
|
283
|
+
image: 'https://imagedelivery.net/h7EjKG0X9kOxmLp41mxOng/f1f7f610-dfa9-4011-08a3-7a98d95e7500/thumbnail',
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
const BLOCK_CONTENT_SNIPPETS = [
|
|
287
|
+
{
|
|
288
|
+
label: 'Text Field',
|
|
289
|
+
snippet: '{{{#text {"field": "fieldName", "value": "" }}}}',
|
|
290
|
+
description: 'Simple text field placeholder',
|
|
291
|
+
},
|
|
292
|
+
{
|
|
293
|
+
label: 'Text with Options',
|
|
294
|
+
snippet: '{{{#text {"field":"fieldName","title":"Field Label","option":{"field":"fieldName","options":[{"title":"Option 1","name":"option1"},{"title":"Option 2","name":"option2"}],"optionsKey":"title","optionsValue":"name"},"value":"option1"}}}}',
|
|
295
|
+
description: 'Text field with selectable options',
|
|
296
|
+
},
|
|
297
|
+
{
|
|
298
|
+
label: 'Text Area',
|
|
299
|
+
snippet: '{{{#textarea {"field": "fieldName", "value": "" }}}}',
|
|
300
|
+
description: 'Textarea field placeholder',
|
|
301
|
+
},
|
|
302
|
+
{
|
|
303
|
+
label: 'Rich Text',
|
|
304
|
+
snippet: '{{{#richtext {"field": "content", "value": "" }}}}',
|
|
305
|
+
description: 'Rich text field placeholder',
|
|
306
|
+
},
|
|
307
|
+
{
|
|
308
|
+
label: 'Image',
|
|
309
|
+
snippet: '{{{#image {"field": "imageField", "value": "", "tags": ["Backgrounds"] }}}}',
|
|
310
|
+
description: 'Image field placeholder',
|
|
311
|
+
},
|
|
312
|
+
{
|
|
313
|
+
label: 'Array (Basic)',
|
|
314
|
+
snippet: `{{{#array {"field": "items", "value": [] }}}}
|
|
315
|
+
<!-- iterate with {{item}} -->
|
|
316
|
+
{{{/array}}}`,
|
|
317
|
+
description: 'Basic repeating array block',
|
|
318
|
+
},
|
|
319
|
+
{
|
|
320
|
+
label: 'Subarray',
|
|
321
|
+
snippet: `{{{#subarray:child {"field": "item.children", "limit": 0 }}}}
|
|
322
|
+
{{child}}
|
|
323
|
+
{{{/subarray}}}`,
|
|
324
|
+
description: 'Nested array inside an array item',
|
|
325
|
+
},
|
|
326
|
+
{
|
|
327
|
+
label: 'If / Else',
|
|
328
|
+
snippet: `{{{#if {"cond": "condition" }}}}
|
|
329
|
+
<!-- content when condition is true -->
|
|
330
|
+
{{{#else}}}
|
|
331
|
+
<!-- content when condition is false -->
|
|
332
|
+
{{{/if}}}`,
|
|
333
|
+
description: 'Conditional block with optional else',
|
|
334
|
+
},
|
|
335
|
+
]
|
|
336
|
+
|
|
337
|
+
const insertPreviewSnippet = (snippet) => {
|
|
338
|
+
if (!snippet)
|
|
339
|
+
return
|
|
340
|
+
const editor = previewContentEditorRef.value
|
|
341
|
+
if (!editor || typeof editor.insertSnippet !== 'function')
|
|
342
|
+
return
|
|
343
|
+
editor.insertSnippet(snippet)
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
const previewBlockDisplayName = computed(() => {
|
|
347
|
+
const blockDocId = String(state.blockContentDocId || sourceBlockDocId.value || '').trim()
|
|
348
|
+
const blockDoc = blockDocId
|
|
349
|
+
? edgeFirebase.data?.[`${edgeGlobal.edgeState.organizationDocPath}/blocks`]?.[blockDocId]
|
|
350
|
+
: null
|
|
351
|
+
|
|
352
|
+
const candidates = [
|
|
353
|
+
blockDoc?.name,
|
|
354
|
+
modelValue.value?.name,
|
|
355
|
+
state.blockContentDocId,
|
|
356
|
+
sourceBlockDocId.value,
|
|
357
|
+
]
|
|
358
|
+
const found = candidates.find(value => String(value || '').trim())
|
|
359
|
+
return String(found || '').trim() || 'Block'
|
|
360
|
+
})
|
|
361
|
+
|
|
239
362
|
const ensureQueryItemsDefaults = (meta) => {
|
|
240
363
|
Object.keys(meta || {}).forEach((key) => {
|
|
241
364
|
const cfg = meta[key]
|
|
@@ -304,11 +427,241 @@ const resetArrayItems = (field, metaSource = null) => {
|
|
|
304
427
|
}
|
|
305
428
|
}
|
|
306
429
|
|
|
430
|
+
const TAG_START_RE = /\{\{\{\#([A-Za-z0-9_-]+)\s*\{/g
|
|
431
|
+
|
|
432
|
+
function* iterateTags(html) {
|
|
433
|
+
TAG_START_RE.lastIndex = 0
|
|
434
|
+
for (;;) {
|
|
435
|
+
const m = TAG_START_RE.exec(html)
|
|
436
|
+
if (!m)
|
|
437
|
+
break
|
|
438
|
+
|
|
439
|
+
const type = m[1]
|
|
440
|
+
const configStart = TAG_START_RE.lastIndex - 1
|
|
441
|
+
if (configStart < 0 || html[configStart] !== '{')
|
|
442
|
+
continue
|
|
443
|
+
|
|
444
|
+
const configEnd = findMatchingBrace(html, configStart)
|
|
445
|
+
if (configEnd === -1)
|
|
446
|
+
continue
|
|
447
|
+
|
|
448
|
+
const rawCfg = html.slice(configStart, configEnd + 1)
|
|
449
|
+
const closeTriple = html.indexOf('}}}', configEnd)
|
|
450
|
+
const tagEnd = closeTriple !== -1 ? closeTriple + 3 : configEnd + 1
|
|
451
|
+
|
|
452
|
+
yield { type, rawCfg, tagEnd }
|
|
453
|
+
|
|
454
|
+
TAG_START_RE.lastIndex = tagEnd
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
const parseBlockContentModel = (html) => {
|
|
459
|
+
const values = {}
|
|
460
|
+
const meta = {}
|
|
461
|
+
if (!html)
|
|
462
|
+
return { values, meta }
|
|
463
|
+
|
|
464
|
+
for (const { type, rawCfg } of iterateTags(html)) {
|
|
465
|
+
const cfg = safeParseTagConfig(rawCfg)
|
|
466
|
+
if (!cfg || !cfg.field)
|
|
467
|
+
continue
|
|
468
|
+
|
|
469
|
+
const field = String(cfg.field)
|
|
470
|
+
const title = cfg.title != null ? String(cfg.title) : ''
|
|
471
|
+
const { value: _omitValue, field: _omitField, ...rest } = cfg
|
|
472
|
+
meta[field] = { type, ...rest, title }
|
|
473
|
+
|
|
474
|
+
let val = cfg.value
|
|
475
|
+
if (type === 'image')
|
|
476
|
+
val = !val ? PLACEHOLDERS.image : String(val)
|
|
477
|
+
else if (type === 'text')
|
|
478
|
+
val = !val ? PLACEHOLDERS.text : String(val)
|
|
479
|
+
else if (type === 'array') {
|
|
480
|
+
if (meta[field]?.limit > 0) {
|
|
481
|
+
val = Array(meta[field].limit).fill('placeholder')
|
|
482
|
+
}
|
|
483
|
+
else {
|
|
484
|
+
if (Array.isArray(val)) {
|
|
485
|
+
if (val.length === 0)
|
|
486
|
+
val = PLACEHOLDERS.arrayItem
|
|
487
|
+
}
|
|
488
|
+
else {
|
|
489
|
+
val = PLACEHOLDERS.arrayItem
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
else if (type === 'textarea')
|
|
494
|
+
val = !val ? PLACEHOLDERS.textarea : String(val)
|
|
495
|
+
else if (type === 'richtext')
|
|
496
|
+
val = !val ? PLACEHOLDERS.richtext : String(val)
|
|
497
|
+
|
|
498
|
+
values[field] = val
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
return { values, meta }
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
const buildUpdatedBlockDocFromContent = (content, sourceDoc = {}) => {
|
|
505
|
+
const parsed = parseBlockContentModel(content)
|
|
506
|
+
const previousValues = sourceDoc?.values || {}
|
|
507
|
+
const previousMeta = sourceDoc?.meta || {}
|
|
508
|
+
const nextValues = {}
|
|
509
|
+
const nextMeta = {}
|
|
510
|
+
|
|
511
|
+
Object.keys(parsed.values || {}).forEach((field) => {
|
|
512
|
+
if (previousValues[field] !== undefined)
|
|
513
|
+
nextValues[field] = previousValues[field]
|
|
514
|
+
else
|
|
515
|
+
nextValues[field] = parsed.values[field]
|
|
516
|
+
})
|
|
517
|
+
|
|
518
|
+
Object.keys(parsed.meta || {}).forEach((field) => {
|
|
519
|
+
if (previousMeta[field]) {
|
|
520
|
+
nextMeta[field] = {
|
|
521
|
+
...previousMeta[field],
|
|
522
|
+
...parsed.meta[field],
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
else {
|
|
526
|
+
nextMeta[field] = parsed.meta[field]
|
|
527
|
+
}
|
|
528
|
+
})
|
|
529
|
+
|
|
530
|
+
return { values: nextValues, meta: nextMeta }
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
const blockContentSourceDoc = computed(() => {
|
|
534
|
+
const blockDocId = String(state.blockContentDocId || sourceBlockDocId.value || '').trim()
|
|
535
|
+
if (!blockDocId)
|
|
536
|
+
return null
|
|
537
|
+
return edgeFirebase.data?.[`${edgeGlobal.edgeState.organizationDocPath}/blocks`]?.[blockDocId] || null
|
|
538
|
+
})
|
|
539
|
+
|
|
540
|
+
const blockContentPreviewBlock = computed(() => {
|
|
541
|
+
const content = String(state.blockContentDraft ?? '')
|
|
542
|
+
const sourceDoc = modelValue.value || blockContentSourceDoc.value || {}
|
|
543
|
+
const { values, meta } = buildUpdatedBlockDocFromContent(content, sourceDoc)
|
|
544
|
+
const previewType = modelValue.value?.previewType ?? blockContentSourceDoc.value?.previewType
|
|
545
|
+
return {
|
|
546
|
+
id: modelValue.value?.id || 'preview-content',
|
|
547
|
+
blockId: String(state.blockContentDocId || sourceBlockDocId.value || '').trim(),
|
|
548
|
+
name: previewBlockDisplayName.value,
|
|
549
|
+
previewType: normalizePreviewType(previewType),
|
|
550
|
+
content,
|
|
551
|
+
values,
|
|
552
|
+
meta,
|
|
553
|
+
synced: !!sourceDoc?.synced,
|
|
554
|
+
}
|
|
555
|
+
})
|
|
556
|
+
|
|
557
|
+
const previewContentSurfaceClass = computed(() => {
|
|
558
|
+
const previewType = normalizePreviewType(blockContentPreviewBlock.value?.previewType)
|
|
559
|
+
return previewType === 'light'
|
|
560
|
+
? 'bg-white text-black'
|
|
561
|
+
: 'bg-neutral-950 text-neutral-50'
|
|
562
|
+
})
|
|
563
|
+
|
|
564
|
+
const previewContentCanvasClass = computed(() => {
|
|
565
|
+
const content = String(blockContentPreviewBlock.value?.content || '')
|
|
566
|
+
const hasFixedContent = /\bfixed\b/.test(content)
|
|
567
|
+
return hasFixedContent ? 'min-h-[calc(100vh-380px)]' : 'min-h-[220px]'
|
|
568
|
+
})
|
|
569
|
+
|
|
570
|
+
const openPreviewContentEditor = async () => {
|
|
571
|
+
const blockDocId = sourceBlockDocId.value
|
|
572
|
+
if (!blockDocId)
|
|
573
|
+
return
|
|
574
|
+
|
|
575
|
+
const blocksPath = `${edgeGlobal.edgeState.organizationDocPath}/blocks`
|
|
576
|
+
if (!edgeFirebase.data?.[blocksPath])
|
|
577
|
+
await edgeFirebase.startSnapshot(blocksPath)
|
|
578
|
+
|
|
579
|
+
const blockData = edgeFirebase.data?.[blocksPath]?.[blockDocId]
|
|
580
|
+
if (!blockData) {
|
|
581
|
+
state.blockContentError = 'Unable to load block content.'
|
|
582
|
+
edgeFirebase?.toast?.error?.('Unable to load block content.')
|
|
583
|
+
return
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
state.editorMode = 'content'
|
|
587
|
+
state.blockContentDocId = blockDocId
|
|
588
|
+
state.blockContentDraft = String(modelValue.value?.content || blockData.content || '')
|
|
589
|
+
state.blockContentError = ''
|
|
590
|
+
state.blockContentUpdating = false
|
|
591
|
+
state.validationErrors = []
|
|
592
|
+
state.open = true
|
|
593
|
+
state.afterLoad = true
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
const updateBlockContent = async () => {
|
|
597
|
+
if (state.blockContentUpdating)
|
|
598
|
+
return
|
|
599
|
+
const blockDocId = String(state.blockContentDocId || sourceBlockDocId.value || '').trim()
|
|
600
|
+
if (!blockDocId)
|
|
601
|
+
return
|
|
602
|
+
|
|
603
|
+
const blocksPath = `${edgeGlobal.edgeState.organizationDocPath}/blocks`
|
|
604
|
+
const blockData = edgeFirebase.data?.[blocksPath]?.[blockDocId] || {}
|
|
605
|
+
const nextContent = String(state.blockContentDraft || '')
|
|
606
|
+
// Update shared block defaults from the source block doc.
|
|
607
|
+
const { values: blockValues, meta: blockMeta } = buildUpdatedBlockDocFromContent(nextContent, blockData)
|
|
608
|
+
// Preserve page/block-instance values when editing block content from preview mode.
|
|
609
|
+
const { values: instanceValues, meta: instanceMeta } = buildUpdatedBlockDocFromContent(nextContent, modelValue.value || {})
|
|
610
|
+
const blockUpdatedAt = new Date().toISOString()
|
|
611
|
+
|
|
612
|
+
const previousModelValue = edgeGlobal.dupObject(modelValue.value || {})
|
|
613
|
+
modelValue.value = {
|
|
614
|
+
...(modelValue.value || {}),
|
|
615
|
+
content: nextContent,
|
|
616
|
+
values: instanceValues,
|
|
617
|
+
meta: instanceMeta,
|
|
618
|
+
blockUpdatedAt,
|
|
619
|
+
blockId: blockData?.docId || blockDocId,
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
state.blockContentError = ''
|
|
623
|
+
state.blockContentUpdating = true
|
|
624
|
+
try {
|
|
625
|
+
const results = await edgeFirebase.changeDoc(blocksPath, blockDocId, {
|
|
626
|
+
content: nextContent,
|
|
627
|
+
values: blockValues,
|
|
628
|
+
meta: blockMeta,
|
|
629
|
+
blockUpdatedAt,
|
|
630
|
+
})
|
|
631
|
+
if (results?.success === false) {
|
|
632
|
+
throw new Error(results?.error || 'Failed to update block content.')
|
|
633
|
+
}
|
|
634
|
+
edgeFirebase?.toast?.success?.('Block content updated.')
|
|
635
|
+
state.open = false
|
|
636
|
+
}
|
|
637
|
+
catch (error) {
|
|
638
|
+
modelValue.value = previousModelValue
|
|
639
|
+
state.blockContentError = error?.message || 'Unable to save block content.'
|
|
640
|
+
edgeFirebase?.toast?.error?.(state.blockContentError)
|
|
641
|
+
}
|
|
642
|
+
finally {
|
|
643
|
+
state.blockContentUpdating = false
|
|
644
|
+
}
|
|
645
|
+
}
|
|
646
|
+
|
|
307
647
|
const openEditor = async (event) => {
|
|
308
|
-
if (!
|
|
648
|
+
if (!canOpenEditor.value)
|
|
309
649
|
return
|
|
310
650
|
const target = event?.target
|
|
311
|
-
if (target?.closest?.(
|
|
651
|
+
if (target?.closest?.(EDITOR_CONTROL_CLICK_SELECTOR))
|
|
652
|
+
return
|
|
653
|
+
const shouldOverrideEditClicks = props.editMode && props.overrideClicksInEditMode
|
|
654
|
+
if (shouldOverrideEditClicks) {
|
|
655
|
+
event?.preventDefault?.()
|
|
656
|
+
event?.stopPropagation?.()
|
|
657
|
+
}
|
|
658
|
+
if (canOpenPreviewContentEditor.value) {
|
|
659
|
+
event?.preventDefault?.()
|
|
660
|
+
event?.stopPropagation?.()
|
|
661
|
+
await openPreviewContentEditor()
|
|
662
|
+
return
|
|
663
|
+
}
|
|
664
|
+
if (!shouldOverrideEditClicks && target?.closest?.(INTERACTIVE_CLICK_SELECTOR))
|
|
312
665
|
return
|
|
313
666
|
const blockData = edgeFirebase.data[`${edgeGlobal.edgeState.organizationDocPath}/blocks`]?.[modelValue.value.blockId]
|
|
314
667
|
const templateMeta = blockData?.meta || modelValue.value?.meta || {}
|
|
@@ -348,6 +701,7 @@ const openEditor = async (event) => {
|
|
|
348
701
|
}
|
|
349
702
|
modelValue.value.blockUpdatedAt = new Date().toISOString()
|
|
350
703
|
state.validationErrors = []
|
|
704
|
+
state.editorMode = 'fields'
|
|
351
705
|
state.open = true
|
|
352
706
|
state.afterLoad = true
|
|
353
707
|
}
|
|
@@ -669,28 +1023,33 @@ const getTagsFromPosts = computed(() => {
|
|
|
669
1023
|
<template>
|
|
670
1024
|
<div>
|
|
671
1025
|
<div
|
|
672
|
-
:class="[{ 'cursor-pointer':
|
|
1026
|
+
:class="[{ 'cursor-pointer': canOpenEditor }, blockWrapperClass]"
|
|
673
1027
|
:style="blockWrapperStyle"
|
|
674
1028
|
class="relative group"
|
|
675
|
-
@click="openEditor($event)"
|
|
1029
|
+
@click.capture="openEditor($event)"
|
|
676
1030
|
>
|
|
677
1031
|
<!-- Content -->
|
|
678
|
-
<
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
1032
|
+
<div :class="props.editMode && props.overrideClicksInEditMode ? 'pointer-events-none' : ''">
|
|
1033
|
+
<edge-cms-block-api :site-id="props.siteId" :theme="props.theme" :content="modelValue?.content" :values="modelValue?.values" :meta="modelValue?.meta" :viewport-mode="props.viewportMode" @pending="state.loading = $event" />
|
|
1034
|
+
<edge-cms-block-render
|
|
1035
|
+
v-if="state.loading"
|
|
1036
|
+
:content="loadingRender(modelValue?.content)"
|
|
1037
|
+
:values="modelValue?.values"
|
|
1038
|
+
:meta="modelValue?.meta"
|
|
1039
|
+
:theme="props.theme"
|
|
1040
|
+
:viewport-mode="props.viewportMode"
|
|
1041
|
+
/>
|
|
1042
|
+
</div>
|
|
687
1043
|
<!-- Darken overlay on hover -->
|
|
688
|
-
<div v-if="props.editMode" class="pointer-events-none absolute inset-0 bg-black/50 opacity-0 transition-opacity duration-200 group-hover:opacity-100 z-
|
|
1044
|
+
<div v-if="props.editMode" class="pointer-events-none absolute inset-0 bg-black/50 opacity-0 transition-opacity duration-200 group-hover:opacity-100 z-[10000]" />
|
|
689
1045
|
|
|
690
1046
|
<!-- Hover controls -->
|
|
691
|
-
<div
|
|
1047
|
+
<div
|
|
1048
|
+
v-if="props.editMode"
|
|
1049
|
+
class="pointer-events-none absolute inset-0 opacity-0 group-hover:opacity-100 transition-opacity duration-200 z-[10001]"
|
|
1050
|
+
>
|
|
692
1051
|
<!-- Delete button top right -->
|
|
693
|
-
<div v-if="props.allowDelete" class="absolute top-2 right-2">
|
|
1052
|
+
<div v-if="props.allowDelete" data-cms-block-control class="pointer-events-auto absolute top-2 right-2">
|
|
694
1053
|
<edge-shad-button
|
|
695
1054
|
variant="destructive"
|
|
696
1055
|
size="icon"
|
|
@@ -729,317 +1088,420 @@ const getTagsFromPosts = computed(() => {
|
|
|
729
1088
|
</edge-shad-dialog>
|
|
730
1089
|
|
|
731
1090
|
<Sheet v-model:open="state.open">
|
|
732
|
-
<edge-cms-block-sheet-content
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
1091
|
+
<edge-cms-block-sheet-content
|
|
1092
|
+
v-if="state.afterLoad"
|
|
1093
|
+
:side="state.editorMode === 'content' ? 'left' : 'right'"
|
|
1094
|
+
:class="state.editorMode === 'content'
|
|
1095
|
+
? 'w-full max-w-none sm:max-w-none md:max-w-none'
|
|
1096
|
+
: 'w-full md:w-1/2 max-w-none sm:max-w-none max-w-2xl'"
|
|
1097
|
+
>
|
|
1098
|
+
<template v-if="state.editorMode === 'content'">
|
|
1099
|
+
<SheetHeader>
|
|
1100
|
+
<div class="flex flex-col gap-2 pr-10">
|
|
1101
|
+
<div class="min-w-0">
|
|
1102
|
+
<SheetTitle>Edit Block Content: {{ previewBlockDisplayName }}</SheetTitle>
|
|
1103
|
+
<SheetDescription class="text-sm text-muted-foreground">
|
|
1104
|
+
Update this block template and save it globally. Changes will sync to every page using this block.
|
|
1105
|
+
</SheetDescription>
|
|
1106
|
+
</div>
|
|
740
1107
|
</div>
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
class="h-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
1108
|
+
</SheetHeader>
|
|
1109
|
+
<div class="px-6 pb-1 pt-3 flex h-[calc(100vh-116px)] flex-col gap-2 overflow-hidden">
|
|
1110
|
+
<div class="flex-1 min-h-0 grid grid-cols-1 xl:grid-cols-2 gap-3">
|
|
1111
|
+
<div class="min-h-0">
|
|
1112
|
+
<edge-cms-code-editor
|
|
1113
|
+
ref="previewContentEditorRef"
|
|
1114
|
+
v-model="state.blockContentDraft"
|
|
1115
|
+
title="Block Content"
|
|
1116
|
+
language="handlebars"
|
|
1117
|
+
name="preview-block-content"
|
|
1118
|
+
:enable-formatting="false"
|
|
1119
|
+
height="calc(100vh - 295px)"
|
|
1120
|
+
class="h-full min-h-0"
|
|
1121
|
+
>
|
|
1122
|
+
<template #end-actions>
|
|
1123
|
+
<DropdownMenu>
|
|
1124
|
+
<DropdownMenuTrigger as-child>
|
|
1125
|
+
<edge-shad-button
|
|
1126
|
+
type="button"
|
|
1127
|
+
size="sm"
|
|
1128
|
+
variant="outline"
|
|
1129
|
+
class="h-8 px-2 text-[11px] uppercase tracking-wide"
|
|
1130
|
+
>
|
|
1131
|
+
Dynamic Content
|
|
1132
|
+
</edge-shad-button>
|
|
1133
|
+
</DropdownMenuTrigger>
|
|
1134
|
+
<DropdownMenuContent align="end" class="w-72">
|
|
1135
|
+
<DropdownMenuItem
|
|
1136
|
+
v-for="snippet in BLOCK_CONTENT_SNIPPETS"
|
|
1137
|
+
:key="snippet.label"
|
|
1138
|
+
class="cursor-pointer flex-col items-start gap-0.5"
|
|
1139
|
+
@click="insertPreviewSnippet(snippet.snippet)"
|
|
1140
|
+
>
|
|
1141
|
+
<span class="text-sm font-medium">{{ snippet.label }}</span>
|
|
1142
|
+
<span class="text-xs text-muted-foreground whitespace-normal">{{ snippet.description }}</span>
|
|
1143
|
+
</DropdownMenuItem>
|
|
1144
|
+
</DropdownMenuContent>
|
|
1145
|
+
</DropdownMenu>
|
|
1146
|
+
</template>
|
|
1147
|
+
</edge-cms-code-editor>
|
|
1148
|
+
</div>
|
|
1149
|
+
<div class="min-h-0 rounded-md border border-border bg-card overflow-hidden flex flex-col">
|
|
1150
|
+
<div class="px-3 py-2 border-b border-border text-xs font-semibold uppercase tracking-wide text-muted-foreground bg-muted/40">
|
|
1151
|
+
Live Preview
|
|
1152
|
+
</div>
|
|
1153
|
+
<div class="flex-1 min-h-0 overflow-y-auto p-3">
|
|
1154
|
+
<div class="relative overflow-visible rounded-none" :class="[previewContentSurfaceClass, previewContentCanvasClass]" style="transform: translateZ(0);">
|
|
1155
|
+
<edge-cms-block-api
|
|
1156
|
+
:site-id="props.siteId"
|
|
1157
|
+
:theme="props.theme"
|
|
1158
|
+
:content="blockContentPreviewBlock.content"
|
|
1159
|
+
:values="blockContentPreviewBlock.values"
|
|
1160
|
+
:meta="blockContentPreviewBlock.meta"
|
|
1161
|
+
:viewport-mode="props.viewportMode"
|
|
1162
|
+
/>
|
|
1163
|
+
</div>
|
|
1164
|
+
</div>
|
|
1165
|
+
</div>
|
|
1166
|
+
</div>
|
|
1167
|
+
<Alert v-if="state.blockContentError" variant="destructive" class="mb-2">
|
|
1168
|
+
<AlertTitle>Save failed</AlertTitle>
|
|
759
1169
|
<AlertDescription class="text-sm">
|
|
760
|
-
|
|
1170
|
+
{{ state.blockContentError }}
|
|
761
1171
|
</AlertDescription>
|
|
762
1172
|
</Alert>
|
|
1173
|
+
<SheetFooter class="flex justify-between pt-1 pb-0">
|
|
1174
|
+
<edge-shad-button
|
|
1175
|
+
variant="destructive"
|
|
1176
|
+
class="text-white"
|
|
1177
|
+
:disabled="state.blockContentUpdating"
|
|
1178
|
+
@click="state.open = false"
|
|
1179
|
+
>
|
|
1180
|
+
Cancel
|
|
1181
|
+
</edge-shad-button>
|
|
1182
|
+
<edge-shad-button
|
|
1183
|
+
class="bg-slate-800 hover:bg-slate-400 w-full"
|
|
1184
|
+
:disabled="state.blockContentUpdating"
|
|
1185
|
+
@click="updateBlockContent"
|
|
1186
|
+
>
|
|
1187
|
+
<Loader2 v-if="state.blockContentUpdating" class="w-4 h-4 mr-2 animate-spin" />
|
|
1188
|
+
Update
|
|
1189
|
+
</edge-shad-button>
|
|
1190
|
+
</SheetFooter>
|
|
763
1191
|
</div>
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
<Grip class="handle pointer" />
|
|
823
|
-
</div>
|
|
824
|
-
<div class="px-2 py-2 w-[98%] flex gap-1">
|
|
825
|
-
<template v-for="schemaItem in entry.meta.schema" :key="schemaItem.field">
|
|
826
|
-
<Popover>
|
|
827
|
-
<PopoverTrigger as-child>
|
|
828
|
-
<Alert class="w-[200px] text-xs py-1 px-2 cursor-pointer hover:bg-primary hover:text-white">
|
|
829
|
-
<AlertTitle> {{ genTitleFromField(schemaItem) }}</AlertTitle>
|
|
830
|
-
<AlertDescription class="text-sm truncate max-w-[200px]">
|
|
831
|
-
{{ element[schemaItem.field] }}
|
|
832
|
-
</AlertDescription>
|
|
833
|
-
</Alert>
|
|
834
|
-
</PopoverTrigger>
|
|
835
|
-
<PopoverContent class="!w-80 mr-20">
|
|
836
|
-
<Card class="border-none shadow-none p-4">
|
|
1192
|
+
</template>
|
|
1193
|
+
<template v-else>
|
|
1194
|
+
<SheetHeader>
|
|
1195
|
+
<div class="flex flex-col gap-3 pr-10 md:flex-row md:items-start md:justify-between">
|
|
1196
|
+
<div class="min-w-0">
|
|
1197
|
+
<SheetTitle>Edit Block</SheetTitle>
|
|
1198
|
+
<SheetDescription v-if="modelValue.synced" class="text-sm text-red-500">
|
|
1199
|
+
This is a synced block. Changes made here will be reflected across all instances of this block on your site.
|
|
1200
|
+
</SheetDescription>
|
|
1201
|
+
</div>
|
|
1202
|
+
<edge-shad-button
|
|
1203
|
+
type="button"
|
|
1204
|
+
size="sm"
|
|
1205
|
+
class="h-8 gap-2 md:self-start"
|
|
1206
|
+
variant="outline"
|
|
1207
|
+
:disabled="!aiFieldOptions.length"
|
|
1208
|
+
@click="openAiDialog"
|
|
1209
|
+
>
|
|
1210
|
+
<Sparkles class="w-4 h-4" />
|
|
1211
|
+
Generate with AI
|
|
1212
|
+
</edge-shad-button>
|
|
1213
|
+
</div>
|
|
1214
|
+
</SheetHeader>
|
|
1215
|
+
|
|
1216
|
+
<edge-shad-form ref="blockFormRef">
|
|
1217
|
+
<div v-if="editableMetaEntries.length === 0">
|
|
1218
|
+
<Alert variant="info" class="mt-4 mb-4">
|
|
1219
|
+
<AlertTitle>No editable fields found</AlertTitle>
|
|
1220
|
+
<AlertDescription class="text-sm">
|
|
1221
|
+
This block does not have any editable fields defined.
|
|
1222
|
+
</AlertDescription>
|
|
1223
|
+
</Alert>
|
|
1224
|
+
</div>
|
|
1225
|
+
<div :class="modelValue.synced ? 'h-[calc(100vh-160px)]' : 'h-[calc(100vh-130px)]'" class="p-6 space-y-4 overflow-y-auto">
|
|
1226
|
+
<template v-for="entry in editableMetaEntries" :key="entry.field">
|
|
1227
|
+
<div v-if="entry.meta.type === 'array'">
|
|
1228
|
+
<div v-if="!entry.meta?.api && !entry.meta?.collection">
|
|
1229
|
+
<div v-if="entry.meta?.schema">
|
|
1230
|
+
<Card v-if="!state.reload" class="mb-4 bg-white shadow-sm border border-gray-200 p-4">
|
|
1231
|
+
<CardHeader class="p-0 mb-2">
|
|
1232
|
+
<div class="relative flex items-center bg-secondary p-2 justify-between sticky top-0 z-10 bg-primary rounded">
|
|
1233
|
+
<span class="text-lg font-semibold whitespace-nowrap pr-1"> {{ genTitleFromField(entry) }}</span>
|
|
1234
|
+
<div class="flex w-full items-center">
|
|
1235
|
+
<div class="w-full border-t border-gray-300 dark:border-white/15" aria-hidden="true" />
|
|
1236
|
+
<edge-shad-button variant="text" class="hover:text-primary/50 text-xs h-[26px] text-primary" @click="state.editMode = !state.editMode">
|
|
1237
|
+
<Popover>
|
|
1238
|
+
<PopoverTrigger as-child>
|
|
1239
|
+
<edge-shad-button
|
|
1240
|
+
variant="text"
|
|
1241
|
+
type="submit"
|
|
1242
|
+
class="bg-secondary hover:text-primary/50 text-xs h-[26px] text-primary"
|
|
1243
|
+
>
|
|
1244
|
+
<Plus class="w-4 h-4" />
|
|
1245
|
+
</edge-shad-button>
|
|
1246
|
+
</PopoverTrigger>
|
|
1247
|
+
<PopoverContent class="!w-80 mr-20">
|
|
1248
|
+
<Card class="border-none shadow-none p-4">
|
|
1249
|
+
<template v-for="schemaItem in entry.meta.schema" :key="schemaItem.field">
|
|
837
1250
|
<edge-cms-block-input
|
|
838
|
-
v-model="
|
|
1251
|
+
v-model="state.arrayItems[entry.field][schemaItem.field]"
|
|
839
1252
|
:type="schemaItem.type"
|
|
1253
|
+
:field="schemaItem.field"
|
|
840
1254
|
:schema="schemaItem"
|
|
841
|
-
:field="`${schemaItem.field}-${index}-entry`"
|
|
842
1255
|
:label="genTitleFromField(schemaItem)"
|
|
843
1256
|
/>
|
|
844
|
-
</
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
</div>
|
|
1257
|
+
</template>
|
|
1258
|
+
<CardFooter class="mt-2 flex justify-end">
|
|
1259
|
+
<edge-shad-button
|
|
1260
|
+
class="bg-secondary hover:text-white text-xs h-[26px] text-primary"
|
|
1261
|
+
@click="addToArray(entry.field)"
|
|
1262
|
+
>
|
|
1263
|
+
Add Entry
|
|
1264
|
+
</edge-shad-button>
|
|
1265
|
+
</CardFooter>
|
|
1266
|
+
</Card>
|
|
1267
|
+
</PopoverContent>
|
|
1268
|
+
</Popover>
|
|
1269
|
+
</edge-shad-button>
|
|
858
1270
|
</div>
|
|
859
1271
|
</div>
|
|
860
|
-
</
|
|
861
|
-
|
|
862
|
-
|
|
1272
|
+
</CardHeader>
|
|
1273
|
+
<draggable
|
|
1274
|
+
v-if="state.draft?.[entry.field] && state.draft[entry.field].length > 0"
|
|
1275
|
+
v-model="state.draft[entry.field]"
|
|
1276
|
+
handle=".handle"
|
|
1277
|
+
item-key="index"
|
|
1278
|
+
>
|
|
1279
|
+
<template #item="{ element, index }">
|
|
1280
|
+
<div :key="index" class="">
|
|
1281
|
+
<div class="flex gap-2 w-full items-center w-full border-1 border-dotted py-1 mb-1">
|
|
1282
|
+
<div class="text-left px-2">
|
|
1283
|
+
<Grip class="handle pointer" />
|
|
1284
|
+
</div>
|
|
1285
|
+
<div class="px-2 py-2 w-[98%] flex gap-1">
|
|
1286
|
+
<template v-for="schemaItem in entry.meta.schema" :key="schemaItem.field">
|
|
1287
|
+
<Popover>
|
|
1288
|
+
<PopoverTrigger as-child>
|
|
1289
|
+
<Alert class="w-[200px] text-xs py-1 px-2 cursor-pointer hover:bg-primary hover:text-white">
|
|
1290
|
+
<AlertTitle> {{ genTitleFromField(schemaItem) }}</AlertTitle>
|
|
1291
|
+
<AlertDescription class="text-sm truncate max-w-[200px]">
|
|
1292
|
+
{{ element[schemaItem.field] }}
|
|
1293
|
+
</AlertDescription>
|
|
1294
|
+
</Alert>
|
|
1295
|
+
</PopoverTrigger>
|
|
1296
|
+
<PopoverContent class="!w-80 mr-20">
|
|
1297
|
+
<Card class="border-none shadow-none p-4">
|
|
1298
|
+
<edge-cms-block-input
|
|
1299
|
+
v-model="element[schemaItem.field]"
|
|
1300
|
+
:type="schemaItem.type"
|
|
1301
|
+
:schema="schemaItem"
|
|
1302
|
+
:field="`${schemaItem.field}-${index}-entry`"
|
|
1303
|
+
:label="genTitleFromField(schemaItem)"
|
|
1304
|
+
/>
|
|
1305
|
+
</Card>
|
|
1306
|
+
</PopoverContent>
|
|
1307
|
+
</Popover>
|
|
1308
|
+
</template>
|
|
1309
|
+
</div>
|
|
1310
|
+
<div class="pr-2">
|
|
1311
|
+
<edge-shad-button
|
|
1312
|
+
variant="destructive"
|
|
1313
|
+
size="icon"
|
|
1314
|
+
@click="state.draft[entry.field].splice(index, 1)"
|
|
1315
|
+
>
|
|
1316
|
+
<Trash class="h-4 w-4" />
|
|
1317
|
+
</edge-shad-button>
|
|
1318
|
+
</div>
|
|
1319
|
+
</div>
|
|
1320
|
+
</div>
|
|
1321
|
+
</template>
|
|
1322
|
+
</draggable>
|
|
1323
|
+
</Card>
|
|
1324
|
+
</div>
|
|
1325
|
+
<edge-cms-block-input
|
|
1326
|
+
v-else
|
|
1327
|
+
v-model="state.draft[entry.field]"
|
|
1328
|
+
:type="entry.meta.type"
|
|
1329
|
+
:field="entry.field"
|
|
1330
|
+
:label="genTitleFromField(entry)"
|
|
1331
|
+
/>
|
|
863
1332
|
</div>
|
|
864
|
-
<
|
|
865
|
-
v-
|
|
1333
|
+
<div v-else>
|
|
1334
|
+
<template v-if="entry.meta?.queryOptions">
|
|
1335
|
+
<div v-for="option in entry.meta.queryOptions" :key="option.field" class="mb-2">
|
|
1336
|
+
<edge-shad-select-tags
|
|
1337
|
+
v-if="entry.meta?.collection?.path === 'posts' && option.field === 'tags'"
|
|
1338
|
+
v-model="state.meta[entry.field].queryItems[option.field]"
|
|
1339
|
+
:items="getTagsFromPosts"
|
|
1340
|
+
:label="`${genTitleFromField(option)}`"
|
|
1341
|
+
:name="option.field"
|
|
1342
|
+
:placeholder="`Select ${genTitleFromField(option)}`"
|
|
1343
|
+
/>
|
|
1344
|
+
<edge-cms-options-select
|
|
1345
|
+
v-else-if="entry.meta?.collection?.path !== 'post'"
|
|
1346
|
+
v-model="state.meta[entry.field].queryItems[option.field]"
|
|
1347
|
+
:option="option"
|
|
1348
|
+
:label="genTitleFromField(option)"
|
|
1349
|
+
:multiple="option?.multiple || false"
|
|
1350
|
+
/>
|
|
1351
|
+
</div>
|
|
1352
|
+
</template>
|
|
1353
|
+
<edge-shad-number
|
|
1354
|
+
v-if="entry.meta?.collection?.path !== 'post' && !isLimitOne(entry.field)"
|
|
1355
|
+
v-model="state.meta[entry.field].limit"
|
|
1356
|
+
name="limit"
|
|
1357
|
+
label="Limit"
|
|
1358
|
+
/>
|
|
1359
|
+
</div>
|
|
1360
|
+
</div>
|
|
1361
|
+
<div v-else-if="entry.meta?.type === 'image'" class="w-full">
|
|
1362
|
+
<div class="mb-2 text-sm font-medium text-foreground">
|
|
1363
|
+
{{ genTitleFromField(entry) }}
|
|
1364
|
+
</div>
|
|
1365
|
+
<div class="relative py-2 rounded-md flex items-center justify-center" :class="previewBackgroundClass(state.draft[entry.field])">
|
|
1366
|
+
<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">
|
|
1367
|
+
<Dialog v-model:open="state.imageOpenByField[entry.field]">
|
|
1368
|
+
<DialogTrigger as-child>
|
|
1369
|
+
<edge-shad-button variant="outline" class="bg-white text-black hover:bg-gray-200">
|
|
1370
|
+
<ImagePlus class="h-5 w-5 mr-2" />
|
|
1371
|
+
Select Image
|
|
1372
|
+
</edge-shad-button>
|
|
1373
|
+
</DialogTrigger>
|
|
1374
|
+
<DialogContent class="w-full max-w-[1200px] max-h-[80vh] overflow-y-auto">
|
|
1375
|
+
<DialogHeader>
|
|
1376
|
+
<DialogTitle>Select Image</DialogTitle>
|
|
1377
|
+
<DialogDescription />
|
|
1378
|
+
</DialogHeader>
|
|
1379
|
+
<edge-cms-media-manager
|
|
1380
|
+
v-if="entry.meta?.tags && entry.meta.tags.length > 0"
|
|
1381
|
+
:site="props.siteId"
|
|
1382
|
+
:select-mode="true"
|
|
1383
|
+
:default-tags="entry.meta.tags"
|
|
1384
|
+
@select="(url) => { state.draft[entry.field] = url; state.imageOpenByField[entry.field] = false }"
|
|
1385
|
+
/>
|
|
1386
|
+
<edge-cms-media-manager
|
|
1387
|
+
v-else
|
|
1388
|
+
:site="props.siteId"
|
|
1389
|
+
:select-mode="true"
|
|
1390
|
+
@select="(url) => { state.draft[entry.field] = url; state.imageOpenByField[entry.field] = false }"
|
|
1391
|
+
/>
|
|
1392
|
+
</DialogContent>
|
|
1393
|
+
</Dialog>
|
|
1394
|
+
</div>
|
|
1395
|
+
<img
|
|
1396
|
+
v-if="state.draft[entry.field]"
|
|
1397
|
+
:src="state.draft[entry.field]"
|
|
1398
|
+
class="max-h-40 max-w-full h-auto w-auto object-contain"
|
|
1399
|
+
>
|
|
1400
|
+
</div>
|
|
1401
|
+
</div>
|
|
1402
|
+
<div v-else-if="entry.meta?.option">
|
|
1403
|
+
<edge-cms-options-select
|
|
866
1404
|
v-model="state.draft[entry.field]"
|
|
867
|
-
:
|
|
868
|
-
:field="entry.field"
|
|
1405
|
+
:option="entry.meta.option"
|
|
869
1406
|
:label="genTitleFromField(entry)"
|
|
870
1407
|
/>
|
|
871
1408
|
</div>
|
|
872
1409
|
<div v-else>
|
|
873
|
-
<
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
:items="getTagsFromPosts"
|
|
879
|
-
:label="`${genTitleFromField(option)}`"
|
|
880
|
-
:name="option.field"
|
|
881
|
-
:placeholder="`Select ${genTitleFromField(option)}`"
|
|
882
|
-
/>
|
|
883
|
-
<edge-cms-options-select
|
|
884
|
-
v-else-if="entry.meta?.collection?.path !== 'post'"
|
|
885
|
-
v-model="state.meta[entry.field].queryItems[option.field]"
|
|
886
|
-
:option="option"
|
|
887
|
-
:label="genTitleFromField(option)"
|
|
888
|
-
:multiple="option?.multiple || false"
|
|
889
|
-
/>
|
|
890
|
-
</div>
|
|
891
|
-
</template>
|
|
892
|
-
<edge-shad-number
|
|
893
|
-
v-if="entry.meta?.collection?.path !== 'post' && !isLimitOne(entry.field)"
|
|
894
|
-
v-model="state.meta[entry.field].limit"
|
|
895
|
-
name="limit"
|
|
896
|
-
label="Limit"
|
|
1410
|
+
<edge-cms-block-input
|
|
1411
|
+
v-model="state.draft[entry.field]"
|
|
1412
|
+
:type="entry.meta.type"
|
|
1413
|
+
:field="entry.field"
|
|
1414
|
+
:label="genTitleFromField(entry)"
|
|
897
1415
|
/>
|
|
898
1416
|
</div>
|
|
899
|
-
</
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
<
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
<edge-shad-button variant="outline" class="bg-white text-black hover:bg-gray-200">
|
|
909
|
-
<ImagePlus class="h-5 w-5 mr-2" />
|
|
910
|
-
Select Image
|
|
911
|
-
</edge-shad-button>
|
|
912
|
-
</DialogTrigger>
|
|
913
|
-
<DialogContent class="w-full max-w-[1200px] max-h-[80vh] overflow-y-auto">
|
|
914
|
-
<DialogHeader>
|
|
915
|
-
<DialogTitle>Select Image</DialogTitle>
|
|
916
|
-
<DialogDescription />
|
|
917
|
-
</DialogHeader>
|
|
918
|
-
<edge-cms-media-manager
|
|
919
|
-
v-if="entry.meta?.tags && entry.meta.tags.length > 0"
|
|
920
|
-
:site="props.siteId"
|
|
921
|
-
:select-mode="true"
|
|
922
|
-
:default-tags="entry.meta.tags"
|
|
923
|
-
@select="(url) => { state.draft[entry.field] = url; state.imageOpenByField[entry.field] = false }"
|
|
924
|
-
/>
|
|
925
|
-
<edge-cms-media-manager
|
|
926
|
-
v-else
|
|
927
|
-
:site="props.siteId"
|
|
928
|
-
:select-mode="true"
|
|
929
|
-
@select="(url) => { state.draft[entry.field] = url; state.imageOpenByField[entry.field] = false }"
|
|
930
|
-
/>
|
|
931
|
-
</DialogContent>
|
|
932
|
-
</Dialog>
|
|
1417
|
+
</template>
|
|
1418
|
+
</div>
|
|
1419
|
+
|
|
1420
|
+
<div class="sticky bottom-0 bg-background px-6 pb-4 pt-2">
|
|
1421
|
+
<Alert v-if="state.validationErrors.length" variant="destructive" class="mb-3">
|
|
1422
|
+
<AlertTitle>Fix the highlighted fields</AlertTitle>
|
|
1423
|
+
<AlertDescription class="text-sm">
|
|
1424
|
+
<div v-for="(error, index) in state.validationErrors" :key="`${error}-${index}`">
|
|
1425
|
+
{{ error }}
|
|
933
1426
|
</div>
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
</
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
1427
|
+
</AlertDescription>
|
|
1428
|
+
</Alert>
|
|
1429
|
+
<SheetFooter class="flex justify-between">
|
|
1430
|
+
<edge-shad-button variant="destructive" class="text-white" @click="state.open = false">
|
|
1431
|
+
Cancel
|
|
1432
|
+
</edge-shad-button>
|
|
1433
|
+
<edge-shad-button class=" bg-slate-800 hover:bg-slate-400 w-full" @click="save">
|
|
1434
|
+
Save changes
|
|
1435
|
+
</edge-shad-button>
|
|
1436
|
+
</SheetFooter>
|
|
1437
|
+
</div>
|
|
1438
|
+
</edge-shad-form>
|
|
1439
|
+
|
|
1440
|
+
<edge-shad-dialog v-model="state.aiDialogOpen">
|
|
1441
|
+
<DialogContent class="max-w-[640px]">
|
|
1442
|
+
<DialogHeader>
|
|
1443
|
+
<DialogTitle>Generate with AI</DialogTitle>
|
|
1444
|
+
<DialogDescription>
|
|
1445
|
+
Choose which fields the AI should fill and add any optional instructions.
|
|
1446
|
+
</DialogDescription>
|
|
1447
|
+
</DialogHeader>
|
|
1448
|
+
<div class="space-y-4">
|
|
1449
|
+
<edge-shad-textarea
|
|
1450
|
+
v-model="state.aiInstructions"
|
|
1451
|
+
name="aiInstructions"
|
|
1452
|
+
label="Instructions (Optional)"
|
|
1453
|
+
placeholder="Share tone, audience, and any details the AI should include."
|
|
954
1454
|
/>
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
<AlertDescription class="text-sm">
|
|
963
|
-
<div v-for="(error, index) in state.validationErrors" :key="`${error}-${index}`">
|
|
964
|
-
{{ error }}
|
|
965
|
-
</div>
|
|
966
|
-
</AlertDescription>
|
|
967
|
-
</Alert>
|
|
968
|
-
<SheetFooter class="flex justify-between">
|
|
969
|
-
<edge-shad-button variant="destructive" class="text-white" @click="state.open = false">
|
|
970
|
-
Cancel
|
|
971
|
-
</edge-shad-button>
|
|
972
|
-
<edge-shad-button class=" bg-slate-800 hover:bg-slate-400 w-full" @click="save">
|
|
973
|
-
Save changes
|
|
974
|
-
</edge-shad-button>
|
|
975
|
-
</SheetFooter>
|
|
976
|
-
</div>
|
|
977
|
-
</edge-shad-form>
|
|
978
|
-
|
|
979
|
-
<edge-shad-dialog v-model="state.aiDialogOpen">
|
|
980
|
-
<DialogContent class="max-w-[640px]">
|
|
981
|
-
<DialogHeader>
|
|
982
|
-
<DialogTitle>Generate with AI</DialogTitle>
|
|
983
|
-
<DialogDescription>
|
|
984
|
-
Choose which fields the AI should fill and add any optional instructions.
|
|
985
|
-
</DialogDescription>
|
|
986
|
-
</DialogHeader>
|
|
987
|
-
<div class="space-y-4">
|
|
988
|
-
<edge-shad-textarea
|
|
989
|
-
v-model="state.aiInstructions"
|
|
990
|
-
name="aiInstructions"
|
|
991
|
-
label="Instructions (Optional)"
|
|
992
|
-
placeholder="Share tone, audience, and any details the AI should include."
|
|
993
|
-
/>
|
|
994
|
-
<div class="space-y-2">
|
|
995
|
-
<div class="flex items-center justify-between text-xs font-semibold uppercase tracking-wide text-muted-foreground">
|
|
996
|
-
<span>Fields</span>
|
|
997
|
-
<span>{{ selectedAiFieldIds.length }} selected</span>
|
|
998
|
-
</div>
|
|
999
|
-
<edge-shad-checkbox v-model="allAiFieldsSelected" name="aiSelectAll">
|
|
1000
|
-
Select all fields
|
|
1001
|
-
</edge-shad-checkbox>
|
|
1002
|
-
<div v-if="aiFieldOptions.length" class="grid gap-2 md:grid-cols-2">
|
|
1003
|
-
<edge-shad-checkbox
|
|
1004
|
-
v-for="option in aiFieldOptions"
|
|
1005
|
-
:key="option.id"
|
|
1006
|
-
v-model="state.aiSelectedFields[option.id]"
|
|
1007
|
-
:name="`ai-field-${option.id}`"
|
|
1008
|
-
>
|
|
1009
|
-
{{ option.label }}
|
|
1010
|
-
<span class="ml-2 text-xs text-muted-foreground">({{ option.type }})</span>
|
|
1455
|
+
<div class="space-y-2">
|
|
1456
|
+
<div class="flex items-center justify-between text-xs font-semibold uppercase tracking-wide text-muted-foreground">
|
|
1457
|
+
<span>Fields</span>
|
|
1458
|
+
<span>{{ selectedAiFieldIds.length }} selected</span>
|
|
1459
|
+
</div>
|
|
1460
|
+
<edge-shad-checkbox v-model="allAiFieldsSelected" name="aiSelectAll">
|
|
1461
|
+
Select all fields
|
|
1011
1462
|
</edge-shad-checkbox>
|
|
1463
|
+
<div v-if="aiFieldOptions.length" class="grid gap-2 md:grid-cols-2">
|
|
1464
|
+
<edge-shad-checkbox
|
|
1465
|
+
v-for="option in aiFieldOptions"
|
|
1466
|
+
:key="option.id"
|
|
1467
|
+
v-model="state.aiSelectedFields[option.id]"
|
|
1468
|
+
:name="`ai-field-${option.id}`"
|
|
1469
|
+
>
|
|
1470
|
+
{{ option.label }}
|
|
1471
|
+
<span class="ml-2 text-xs text-muted-foreground">({{ option.type }})</span>
|
|
1472
|
+
</edge-shad-checkbox>
|
|
1473
|
+
</div>
|
|
1474
|
+
<Alert v-else variant="info">
|
|
1475
|
+
<AlertTitle>No editable fields</AlertTitle>
|
|
1476
|
+
<AlertDescription class="text-sm">
|
|
1477
|
+
Add editable fields to this block to enable AI generation.
|
|
1478
|
+
</AlertDescription>
|
|
1479
|
+
</Alert>
|
|
1012
1480
|
</div>
|
|
1013
|
-
<Alert v-
|
|
1014
|
-
<AlertTitle>
|
|
1481
|
+
<Alert v-if="state.aiError" variant="destructive">
|
|
1482
|
+
<AlertTitle>AI generation failed</AlertTitle>
|
|
1015
1483
|
<AlertDescription class="text-sm">
|
|
1016
|
-
|
|
1484
|
+
{{ state.aiError }}
|
|
1017
1485
|
</AlertDescription>
|
|
1018
1486
|
</Alert>
|
|
1019
1487
|
</div>
|
|
1020
|
-
<
|
|
1021
|
-
<
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
<Loader2 v-if="state.aiGenerating" class="w-4 h-4 mr-2 animate-spin" />
|
|
1038
|
-
Generate
|
|
1039
|
-
</edge-shad-button>
|
|
1040
|
-
</DialogFooter>
|
|
1041
|
-
</DialogContent>
|
|
1042
|
-
</edge-shad-dialog>
|
|
1488
|
+
<DialogFooter class="pt-4 flex justify-between">
|
|
1489
|
+
<edge-shad-button type="button" variant="destructive" class="text-white" @click="closeAiDialog">
|
|
1490
|
+
Cancel
|
|
1491
|
+
</edge-shad-button>
|
|
1492
|
+
<edge-shad-button
|
|
1493
|
+
type="button"
|
|
1494
|
+
class="w-full"
|
|
1495
|
+
:disabled="state.aiGenerating || !selectedAiFieldIds.length"
|
|
1496
|
+
@click="generateWithAi"
|
|
1497
|
+
>
|
|
1498
|
+
<Loader2 v-if="state.aiGenerating" class="w-4 h-4 mr-2 animate-spin" />
|
|
1499
|
+
Generate
|
|
1500
|
+
</edge-shad-button>
|
|
1501
|
+
</DialogFooter>
|
|
1502
|
+
</DialogContent>
|
|
1503
|
+
</edge-shad-dialog>
|
|
1504
|
+
</template>
|
|
1043
1505
|
</edge-cms-block-sheet-content>
|
|
1044
1506
|
</Sheet>
|
|
1045
1507
|
</div>
|