@edgedev/create-edge-app 1.2.33 → 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 +136 -0
- package/edge/components/cms/block.vue +977 -305
- package/edge/components/cms/blockApi.vue +3 -3
- package/edge/components/cms/blockEditor.vue +688 -86
- package/edge/components/cms/blockPicker.vue +31 -5
- package/edge/components/cms/blockRender.vue +3 -3
- package/edge/components/cms/blocksManager.vue +790 -82
- package/edge/components/cms/codeEditor.vue +15 -6
- package/edge/components/cms/fontUpload.vue +318 -2
- package/edge/components/cms/htmlContent.vue +825 -93
- 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 +96 -34
- package/edge/components/cms/page.vue +902 -58
- package/edge/components/cms/posts.vue +13 -4
- package/edge/components/cms/site.vue +638 -87
- package/edge/components/cms/siteSettingsForm.vue +19 -9
- package/edge/components/cms/sitesManager.vue +5 -4
- package/edge/components/cms/themeDefaultMenu.vue +20 -2
- package/edge/components/cms/themeEditor.vue +196 -162
- package/edge/components/editor.vue +5 -1
- package/edge/composables/global.ts +37 -5
- package/edge/composables/siteSettingsTemplate.js +2 -0
- package/edge/composables/useCmsNewDocs.js +100 -0
- package/edge/composables/useEdgeCmsDialogPositionFix.js +19 -0
- package/edge/routes/cms/dashboard/blocks/[block].vue +5 -0
- package/edge/routes/cms/dashboard/blocks/index.vue +12 -1
- package/edge/routes/cms/dashboard/media/index.vue +5 -0
- package/edge/routes/cms/dashboard/sites/[site]/[[page]].vue +4 -0
- package/edge/routes/cms/dashboard/sites/[site].vue +4 -0
- package/edge/routes/cms/dashboard/sites/index.vue +4 -0
- package/edge/routes/cms/dashboard/templates/[page].vue +4 -0
- package/edge/routes/cms/dashboard/templates/index.vue +4 -0
- package/edge/routes/cms/dashboard/themes/[theme].vue +5 -0
- package/edge/routes/cms/dashboard/themes/index.vue +330 -1
- 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/firebase.json +4 -0
- package/nuxt.config.ts +1 -1
- package/package.json +2 -2
|
@@ -26,33 +26,140 @@ const props = defineProps({
|
|
|
26
26
|
type: String,
|
|
27
27
|
default: 'auto',
|
|
28
28
|
},
|
|
29
|
+
allowDelete: {
|
|
30
|
+
type: Boolean,
|
|
31
|
+
default: true,
|
|
32
|
+
},
|
|
33
|
+
containFixed: {
|
|
34
|
+
type: Boolean,
|
|
35
|
+
default: false,
|
|
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
|
+
},
|
|
29
49
|
})
|
|
30
50
|
const emit = defineEmits(['update:modelValue', 'delete'])
|
|
31
51
|
const edgeFirebase = inject('edgeFirebase')
|
|
52
|
+
|
|
53
|
+
function normalizeConfigLiteral(str) {
|
|
54
|
+
return str
|
|
55
|
+
.replace(/(\{|,)\s*([A-Za-z_][\w-]*)\s*:/g, '$1"$2":')
|
|
56
|
+
.replace(/'/g, '"')
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function safeParseTagConfig(raw) {
|
|
60
|
+
try {
|
|
61
|
+
return JSON.parse(normalizeConfigLiteral(raw))
|
|
62
|
+
}
|
|
63
|
+
catch {
|
|
64
|
+
return null
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function findMatchingBrace(str, startIdx) {
|
|
69
|
+
let depth = 0
|
|
70
|
+
let inString = false
|
|
71
|
+
let quote = null
|
|
72
|
+
let escape = false
|
|
73
|
+
|
|
74
|
+
for (let i = startIdx; i < str.length; i++) {
|
|
75
|
+
const ch = str[i]
|
|
76
|
+
if (inString) {
|
|
77
|
+
if (escape) {
|
|
78
|
+
escape = false
|
|
79
|
+
continue
|
|
80
|
+
}
|
|
81
|
+
if (ch === '\\') {
|
|
82
|
+
escape = true
|
|
83
|
+
continue
|
|
84
|
+
}
|
|
85
|
+
if (ch === quote) {
|
|
86
|
+
inString = false
|
|
87
|
+
quote = null
|
|
88
|
+
}
|
|
89
|
+
continue
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
if (ch === '"' || ch === '\'') {
|
|
93
|
+
inString = true
|
|
94
|
+
quote = ch
|
|
95
|
+
continue
|
|
96
|
+
}
|
|
97
|
+
if (ch === '{')
|
|
98
|
+
depth++
|
|
99
|
+
else if (ch === '}') {
|
|
100
|
+
depth--
|
|
101
|
+
if (depth === 0)
|
|
102
|
+
return i
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
return -1
|
|
107
|
+
}
|
|
108
|
+
|
|
32
109
|
function extractFieldsInOrder(template) {
|
|
33
110
|
if (!template || typeof template !== 'string')
|
|
34
111
|
return []
|
|
112
|
+
|
|
35
113
|
const fields = []
|
|
36
114
|
const seen = new Set()
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
115
|
+
|
|
116
|
+
const TAG_START_RE = /\{\{\{\#([A-Za-z0-9_-]+)\s*\{/g
|
|
117
|
+
TAG_START_RE.lastIndex = 0
|
|
118
|
+
|
|
119
|
+
for (;;) {
|
|
120
|
+
const m = TAG_START_RE.exec(template)
|
|
121
|
+
if (!m)
|
|
122
|
+
break
|
|
123
|
+
|
|
124
|
+
const configStart = TAG_START_RE.lastIndex - 1
|
|
125
|
+
if (configStart < 0 || template[configStart] !== '{')
|
|
126
|
+
continue
|
|
127
|
+
|
|
128
|
+
const configEnd = findMatchingBrace(template, configStart)
|
|
129
|
+
if (configEnd === -1)
|
|
130
|
+
continue
|
|
131
|
+
|
|
132
|
+
const rawCfg = template.slice(configStart, configEnd + 1)
|
|
133
|
+
const parsedCfg = safeParseTagConfig(rawCfg)
|
|
134
|
+
|
|
135
|
+
let field = typeof parsedCfg?.field === 'string'
|
|
136
|
+
? parsedCfg.field.trim()
|
|
137
|
+
: ''
|
|
138
|
+
|
|
139
|
+
if (!field) {
|
|
140
|
+
const fm = rawCfg.match(/["']?field["']?\s*:\s*["']([^"']+)["']/)
|
|
141
|
+
field = fm?.[1]?.trim() || ''
|
|
45
142
|
}
|
|
46
|
-
|
|
143
|
+
|
|
144
|
+
if (field && !seen.has(field)) {
|
|
145
|
+
fields.push(field)
|
|
146
|
+
seen.add(field)
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const closeTriple = template.indexOf('}}}', configEnd)
|
|
150
|
+
TAG_START_RE.lastIndex = closeTriple !== -1 ? closeTriple + 3 : configEnd + 1
|
|
47
151
|
}
|
|
152
|
+
|
|
48
153
|
return fields
|
|
49
154
|
}
|
|
50
155
|
|
|
51
156
|
const modelValue = useVModel(props, 'modelValue', emit)
|
|
52
157
|
const blockFormRef = ref(null)
|
|
158
|
+
const previewContentEditorRef = ref(null)
|
|
53
159
|
|
|
54
160
|
const state = reactive({
|
|
55
161
|
open: false,
|
|
162
|
+
editorMode: 'fields',
|
|
56
163
|
draft: {},
|
|
57
164
|
delete: false,
|
|
58
165
|
meta: {},
|
|
@@ -69,6 +176,91 @@ const state = reactive({
|
|
|
69
176
|
aiGenerating: false,
|
|
70
177
|
aiError: '',
|
|
71
178
|
validationErrors: [],
|
|
179
|
+
blockContentDraft: '',
|
|
180
|
+
blockContentDocId: '',
|
|
181
|
+
blockContentUpdating: false,
|
|
182
|
+
blockContentError: '',
|
|
183
|
+
})
|
|
184
|
+
|
|
185
|
+
const INTERACTIVE_CLICK_SELECTOR = [
|
|
186
|
+
'[data-cms-interactive]',
|
|
187
|
+
'.cms-block-interactive',
|
|
188
|
+
'.cms-nav-toggle',
|
|
189
|
+
'.cms-nav-overlay',
|
|
190
|
+
'.cms-nav-panel',
|
|
191
|
+
'.cms-nav-close',
|
|
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]',
|
|
201
|
+
].join(', ')
|
|
202
|
+
|
|
203
|
+
const hasFixedPositionInContent = computed(() => {
|
|
204
|
+
const content = String(modelValue.value?.content || '')
|
|
205
|
+
return /\bfixed\b/.test(content)
|
|
206
|
+
})
|
|
207
|
+
|
|
208
|
+
const normalizePreviewType = (value) => {
|
|
209
|
+
return value === 'dark' ? 'dark' : 'light'
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
const resolvedPreviewType = computed(() => normalizePreviewType(modelValue.value?.previewType))
|
|
213
|
+
const sourceBlockDocId = computed(() => {
|
|
214
|
+
const direct = String(modelValue.value?.blockId || '').trim()
|
|
215
|
+
if (direct)
|
|
216
|
+
return direct
|
|
217
|
+
return String(props.blockId || '').trim()
|
|
218
|
+
})
|
|
219
|
+
|
|
220
|
+
const inheritedPreviewType = computed(() => {
|
|
221
|
+
const explicit = modelValue.value?.previewType
|
|
222
|
+
if (explicit === 'light' || explicit === 'dark')
|
|
223
|
+
return explicit
|
|
224
|
+
const docId = sourceBlockDocId.value
|
|
225
|
+
if (!docId)
|
|
226
|
+
return null
|
|
227
|
+
const blockDoc = edgeFirebase.data?.[`${edgeGlobal.edgeState.organizationDocPath}/blocks`]?.[docId]
|
|
228
|
+
const inherited = blockDoc?.previewType
|
|
229
|
+
return (inherited === 'light' || inherited === 'dark') ? inherited : null
|
|
230
|
+
})
|
|
231
|
+
|
|
232
|
+
const effectivePreviewType = computed(() => {
|
|
233
|
+
return normalizePreviewType(inheritedPreviewType.value ?? resolvedPreviewType.value)
|
|
234
|
+
})
|
|
235
|
+
|
|
236
|
+
const canOpenFieldEditor = computed(() => props.editMode)
|
|
237
|
+
const canOpenPreviewContentEditor = computed(() => !props.editMode && props.allowPreviewContentEdit)
|
|
238
|
+
const canOpenEditor = computed(() => canOpenFieldEditor.value || canOpenPreviewContentEditor.value)
|
|
239
|
+
|
|
240
|
+
const shouldContainFixedPreview = computed(() => {
|
|
241
|
+
return (props.editMode || props.containFixed) && hasFixedPositionInContent.value
|
|
242
|
+
})
|
|
243
|
+
|
|
244
|
+
const shouldDisableInteractivePreview = computed(() => {
|
|
245
|
+
return props.editMode && props.disableInteractivePreviewInEdit
|
|
246
|
+
})
|
|
247
|
+
|
|
248
|
+
const blockWrapperClass = computed(() => ({
|
|
249
|
+
'overflow-visible': shouldContainFixedPreview.value,
|
|
250
|
+
'min-h-[88px]': props.editMode && shouldContainFixedPreview.value && shouldDisableInteractivePreview.value,
|
|
251
|
+
'min-h-[calc(100vh-360px)]': props.editMode && shouldContainFixedPreview.value && !shouldDisableInteractivePreview.value,
|
|
252
|
+
'z-30': shouldContainFixedPreview.value,
|
|
253
|
+
'bg-white text-black': props.editMode && effectivePreviewType.value === 'light',
|
|
254
|
+
'bg-neutral-950 text-neutral-50': props.editMode && effectivePreviewType.value === 'dark',
|
|
255
|
+
'cms-nav-edit-static': shouldDisableInteractivePreview.value,
|
|
256
|
+
}))
|
|
257
|
+
|
|
258
|
+
const blockWrapperStyle = computed(() => {
|
|
259
|
+
if (!shouldContainFixedPreview.value || !props.editMode)
|
|
260
|
+
return null
|
|
261
|
+
return {
|
|
262
|
+
transform: 'translateZ(0)',
|
|
263
|
+
}
|
|
72
264
|
})
|
|
73
265
|
|
|
74
266
|
const isLightName = (value) => {
|
|
@@ -79,6 +271,94 @@ const isLightName = (value) => {
|
|
|
79
271
|
|
|
80
272
|
const previewBackgroundClass = value => (isLightName(value) ? 'bg-neutral-900/90' : 'bg-neutral-100')
|
|
81
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
|
+
|
|
82
362
|
const ensureQueryItemsDefaults = (meta) => {
|
|
83
363
|
Object.keys(meta || {}).forEach((key) => {
|
|
84
364
|
const cfg = meta[key]
|
|
@@ -147,8 +427,241 @@ const resetArrayItems = (field, metaSource = null) => {
|
|
|
147
427
|
}
|
|
148
428
|
}
|
|
149
429
|
|
|
150
|
-
const
|
|
151
|
-
|
|
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
|
+
|
|
647
|
+
const openEditor = async (event) => {
|
|
648
|
+
if (!canOpenEditor.value)
|
|
649
|
+
return
|
|
650
|
+
const target = event?.target
|
|
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))
|
|
152
665
|
return
|
|
153
666
|
const blockData = edgeFirebase.data[`${edgeGlobal.edgeState.organizationDocPath}/blocks`]?.[modelValue.value.blockId]
|
|
154
667
|
const templateMeta = blockData?.meta || modelValue.value?.meta || {}
|
|
@@ -188,10 +701,16 @@ const openEditor = async () => {
|
|
|
188
701
|
}
|
|
189
702
|
modelValue.value.blockUpdatedAt = new Date().toISOString()
|
|
190
703
|
state.validationErrors = []
|
|
704
|
+
state.editorMode = 'fields'
|
|
191
705
|
state.open = true
|
|
192
706
|
state.afterLoad = true
|
|
193
707
|
}
|
|
194
708
|
|
|
709
|
+
const isLimitOne = (field) => {
|
|
710
|
+
const limit = Number(state.meta?.[field]?.limit)
|
|
711
|
+
return Number.isFinite(limit) && limit === 1
|
|
712
|
+
}
|
|
713
|
+
|
|
195
714
|
const normalizeValidationNumber = (value) => {
|
|
196
715
|
if (value === null || value === undefined || value === '')
|
|
197
716
|
return null
|
|
@@ -264,6 +783,31 @@ const orderedMeta = computed(() => {
|
|
|
264
783
|
return out
|
|
265
784
|
})
|
|
266
785
|
|
|
786
|
+
const hasEditableArrayControls = (entry) => {
|
|
787
|
+
if (!entry?.meta)
|
|
788
|
+
return false
|
|
789
|
+
|
|
790
|
+
// Manual arrays are editable through the schema/list UI.
|
|
791
|
+
if (!entry.meta?.api && !entry.meta?.collection)
|
|
792
|
+
return true
|
|
793
|
+
|
|
794
|
+
const collectionPath = entry.meta?.collection?.path
|
|
795
|
+
const supportsQueryControls = collectionPath !== 'post'
|
|
796
|
+
const queryOptions = Array.isArray(entry.meta?.queryOptions) ? entry.meta.queryOptions : []
|
|
797
|
+
const hasQueryOptions = supportsQueryControls && queryOptions.length > 0
|
|
798
|
+
const hasLimitControl = supportsQueryControls && !isLimitOne(entry.field)
|
|
799
|
+
|
|
800
|
+
return hasQueryOptions || hasLimitControl
|
|
801
|
+
}
|
|
802
|
+
|
|
803
|
+
const editableMetaEntries = computed(() => {
|
|
804
|
+
return orderedMeta.value.filter((entry) => {
|
|
805
|
+
if (entry?.meta?.type === 'array')
|
|
806
|
+
return hasEditableArrayControls(entry)
|
|
807
|
+
return true
|
|
808
|
+
})
|
|
809
|
+
})
|
|
810
|
+
|
|
267
811
|
const genTitleFromField = (field) => {
|
|
268
812
|
if (field?.title)
|
|
269
813
|
return field.title
|
|
@@ -324,7 +868,7 @@ const save = () => {
|
|
|
324
868
|
}
|
|
325
869
|
|
|
326
870
|
const aiFieldOptions = computed(() => {
|
|
327
|
-
return
|
|
871
|
+
return editableMetaEntries.value
|
|
328
872
|
.map(entry => ({
|
|
329
873
|
id: entry.field,
|
|
330
874
|
label: genTitleFromField(entry),
|
|
@@ -479,27 +1023,33 @@ const getTagsFromPosts = computed(() => {
|
|
|
479
1023
|
<template>
|
|
480
1024
|
<div>
|
|
481
1025
|
<div
|
|
482
|
-
:class="{ 'cursor-pointer':
|
|
483
|
-
|
|
484
|
-
|
|
1026
|
+
:class="[{ 'cursor-pointer': canOpenEditor }, blockWrapperClass]"
|
|
1027
|
+
:style="blockWrapperStyle"
|
|
1028
|
+
class="relative group"
|
|
1029
|
+
@click.capture="openEditor($event)"
|
|
485
1030
|
>
|
|
486
1031
|
<!-- Content -->
|
|
487
|
-
<
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
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>
|
|
496
1043
|
<!-- Darken overlay on hover -->
|
|
497
|
-
<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]" />
|
|
498
1045
|
|
|
499
1046
|
<!-- Hover controls -->
|
|
500
|
-
<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
|
+
>
|
|
501
1051
|
<!-- Delete button top right -->
|
|
502
|
-
<div 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">
|
|
503
1053
|
<edge-shad-button
|
|
504
1054
|
variant="destructive"
|
|
505
1055
|
size="icon"
|
|
@@ -538,313 +1088,435 @@ const getTagsFromPosts = computed(() => {
|
|
|
538
1088
|
</edge-shad-dialog>
|
|
539
1089
|
|
|
540
1090
|
<Sheet v-model:open="state.open">
|
|
541
|
-
<edge-cms-block-sheet-content
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
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>
|
|
549
1107
|
</div>
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
class="h-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
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>
|
|
568
1169
|
<AlertDescription class="text-sm">
|
|
569
|
-
|
|
1170
|
+
{{ state.blockContentError }}
|
|
570
1171
|
</AlertDescription>
|
|
571
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>
|
|
572
1191
|
</div>
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
<Grip class="handle pointer" />
|
|
632
|
-
</div>
|
|
633
|
-
<div class="px-2 py-2 w-[98%] flex gap-1">
|
|
634
|
-
<template v-for="schemaItem in entry.meta.schema" :key="schemaItem.field">
|
|
635
|
-
<Popover>
|
|
636
|
-
<PopoverTrigger as-child>
|
|
637
|
-
<Alert class="w-[200px] text-xs py-1 px-2 cursor-pointer hover:bg-primary hover:text-white">
|
|
638
|
-
<AlertTitle> {{ genTitleFromField(schemaItem) }}</AlertTitle>
|
|
639
|
-
<AlertDescription class="text-sm truncate max-w-[200px]">
|
|
640
|
-
{{ element[schemaItem.field] }}
|
|
641
|
-
</AlertDescription>
|
|
642
|
-
</Alert>
|
|
643
|
-
</PopoverTrigger>
|
|
644
|
-
<PopoverContent class="!w-80 mr-20">
|
|
645
|
-
<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">
|
|
646
1250
|
<edge-cms-block-input
|
|
647
|
-
v-model="
|
|
1251
|
+
v-model="state.arrayItems[entry.field][schemaItem.field]"
|
|
648
1252
|
:type="schemaItem.type"
|
|
1253
|
+
:field="schemaItem.field"
|
|
649
1254
|
:schema="schemaItem"
|
|
650
|
-
:field="`${schemaItem.field}-${index}-entry`"
|
|
651
1255
|
:label="genTitleFromField(schemaItem)"
|
|
652
1256
|
/>
|
|
653
|
-
</
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
</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>
|
|
667
1270
|
</div>
|
|
668
1271
|
</div>
|
|
669
|
-
</
|
|
670
|
-
|
|
671
|
-
|
|
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
|
+
/>
|
|
1332
|
+
</div>
|
|
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
|
+
/>
|
|
672
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
|
|
1404
|
+
v-model="state.draft[entry.field]"
|
|
1405
|
+
:option="entry.meta.option"
|
|
1406
|
+
:label="genTitleFromField(entry)"
|
|
1407
|
+
/>
|
|
1408
|
+
</div>
|
|
1409
|
+
<div v-else>
|
|
673
1410
|
<edge-cms-block-input
|
|
674
|
-
v-else
|
|
675
1411
|
v-model="state.draft[entry.field]"
|
|
676
1412
|
:type="entry.meta.type"
|
|
677
1413
|
:field="entry.field"
|
|
678
1414
|
:label="genTitleFromField(entry)"
|
|
679
1415
|
/>
|
|
680
1416
|
</div>
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
:placeholder="`Select ${genTitleFromField(option)}`"
|
|
691
|
-
/>
|
|
692
|
-
<edge-cms-options-select
|
|
693
|
-
v-else-if="entry.meta?.collection?.path !== 'post'"
|
|
694
|
-
v-model="state.meta[entry.field].queryItems[option.field]"
|
|
695
|
-
:option="option"
|
|
696
|
-
:label="genTitleFromField(option)"
|
|
697
|
-
:multiple="option?.multiple || false"
|
|
698
|
-
/>
|
|
699
|
-
</div>
|
|
700
|
-
</template>
|
|
701
|
-
<edge-shad-number v-if="entry.meta?.collection?.path !== 'post'" v-model="state.meta[entry.field].limit" name="limit" label="Limit" />
|
|
702
|
-
</div>
|
|
703
|
-
</div>
|
|
704
|
-
<div v-else-if="entry.meta?.type === 'image'" class="w-full">
|
|
705
|
-
<div class="mb-2 text-sm font-medium text-foreground">
|
|
706
|
-
{{ genTitleFromField(entry) }}
|
|
707
|
-
</div>
|
|
708
|
-
<div class="relative py-2 rounded-md flex items-center justify-center" :class="previewBackgroundClass(state.draft[entry.field])">
|
|
709
|
-
<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">
|
|
710
|
-
<Dialog v-model:open="state.imageOpenByField[entry.field]">
|
|
711
|
-
<DialogTrigger as-child>
|
|
712
|
-
<edge-shad-button variant="outline" class="bg-white text-black hover:bg-gray-200">
|
|
713
|
-
<ImagePlus class="h-5 w-5 mr-2" />
|
|
714
|
-
Select Image
|
|
715
|
-
</edge-shad-button>
|
|
716
|
-
</DialogTrigger>
|
|
717
|
-
<DialogContent class="w-full max-w-[1200px] max-h-[80vh] overflow-y-auto">
|
|
718
|
-
<DialogHeader>
|
|
719
|
-
<DialogTitle>Select Image</DialogTitle>
|
|
720
|
-
<DialogDescription />
|
|
721
|
-
</DialogHeader>
|
|
722
|
-
<edge-cms-media-manager
|
|
723
|
-
v-if="entry.meta?.tags && entry.meta.tags.length > 0"
|
|
724
|
-
:site="props.siteId"
|
|
725
|
-
:select-mode="true"
|
|
726
|
-
:default-tags="entry.meta.tags"
|
|
727
|
-
@select="(url) => { state.draft[entry.field] = url; state.imageOpenByField[entry.field] = false }"
|
|
728
|
-
/>
|
|
729
|
-
<edge-cms-media-manager
|
|
730
|
-
v-else
|
|
731
|
-
:site="props.siteId"
|
|
732
|
-
:select-mode="true"
|
|
733
|
-
@select="(url) => { state.draft[entry.field] = url; state.imageOpenByField[entry.field] = false }"
|
|
734
|
-
/>
|
|
735
|
-
</DialogContent>
|
|
736
|
-
</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 }}
|
|
737
1426
|
</div>
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
</
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
/>
|
|
751
|
-
</div>
|
|
752
|
-
<div v-else>
|
|
753
|
-
<edge-cms-block-input
|
|
754
|
-
v-model="state.draft[entry.field]"
|
|
755
|
-
:type="entry.meta.type"
|
|
756
|
-
:field="entry.field"
|
|
757
|
-
:label="genTitleFromField(entry)"
|
|
758
|
-
/>
|
|
759
|
-
</div>
|
|
760
|
-
</template>
|
|
761
|
-
</div>
|
|
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>
|
|
762
1439
|
|
|
763
|
-
<
|
|
764
|
-
<
|
|
765
|
-
<
|
|
766
|
-
|
|
767
|
-
<
|
|
768
|
-
|
|
769
|
-
</
|
|
770
|
-
</
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
<DialogHeader>
|
|
786
|
-
<DialogTitle>Generate with AI</DialogTitle>
|
|
787
|
-
<DialogDescription>
|
|
788
|
-
Choose which fields the AI should fill and add any optional instructions.
|
|
789
|
-
</DialogDescription>
|
|
790
|
-
</DialogHeader>
|
|
791
|
-
<div class="space-y-4">
|
|
792
|
-
<edge-shad-textarea
|
|
793
|
-
v-model="state.aiInstructions"
|
|
794
|
-
name="aiInstructions"
|
|
795
|
-
label="Instructions (Optional)"
|
|
796
|
-
placeholder="Share tone, audience, and any details the AI should include."
|
|
797
|
-
/>
|
|
798
|
-
<div class="space-y-2">
|
|
799
|
-
<div class="flex items-center justify-between text-xs font-semibold uppercase tracking-wide text-muted-foreground">
|
|
800
|
-
<span>Fields</span>
|
|
801
|
-
<span>{{ selectedAiFieldIds.length }} selected</span>
|
|
802
|
-
</div>
|
|
803
|
-
<edge-shad-checkbox v-model="allAiFieldsSelected" name="aiSelectAll">
|
|
804
|
-
Select all fields
|
|
805
|
-
</edge-shad-checkbox>
|
|
806
|
-
<div v-if="aiFieldOptions.length" class="grid gap-2 md:grid-cols-2">
|
|
807
|
-
<edge-shad-checkbox
|
|
808
|
-
v-for="option in aiFieldOptions"
|
|
809
|
-
:key="option.id"
|
|
810
|
-
v-model="state.aiSelectedFields[option.id]"
|
|
811
|
-
:name="`ai-field-${option.id}`"
|
|
812
|
-
>
|
|
813
|
-
{{ option.label }}
|
|
814
|
-
<span class="ml-2 text-xs text-muted-foreground">({{ option.type }})</span>
|
|
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."
|
|
1454
|
+
/>
|
|
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
|
|
815
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>
|
|
816
1480
|
</div>
|
|
817
|
-
<Alert v-
|
|
818
|
-
<AlertTitle>
|
|
1481
|
+
<Alert v-if="state.aiError" variant="destructive">
|
|
1482
|
+
<AlertTitle>AI generation failed</AlertTitle>
|
|
819
1483
|
<AlertDescription class="text-sm">
|
|
820
|
-
|
|
1484
|
+
{{ state.aiError }}
|
|
821
1485
|
</AlertDescription>
|
|
822
1486
|
</Alert>
|
|
823
1487
|
</div>
|
|
824
|
-
<
|
|
825
|
-
<
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
<Loader2 v-if="state.aiGenerating" class="w-4 h-4 mr-2 animate-spin" />
|
|
842
|
-
Generate
|
|
843
|
-
</edge-shad-button>
|
|
844
|
-
</DialogFooter>
|
|
845
|
-
</DialogContent>
|
|
846
|
-
</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>
|
|
847
1505
|
</edge-cms-block-sheet-content>
|
|
848
1506
|
</Sheet>
|
|
849
1507
|
</div>
|
|
850
1508
|
</template>
|
|
1509
|
+
|
|
1510
|
+
<style scoped>
|
|
1511
|
+
.cms-nav-edit-static :deep([data-cms-nav-root] .cms-nav-toggle),
|
|
1512
|
+
.cms-nav-edit-static :deep([data-cms-nav-root] .cms-nav-close),
|
|
1513
|
+
.cms-nav-edit-static :deep([data-cms-nav-root] .cms-nav-link) {
|
|
1514
|
+
pointer-events: none !important;
|
|
1515
|
+
}
|
|
1516
|
+
|
|
1517
|
+
.cms-nav-edit-static :deep([data-cms-nav-root] .cms-nav-overlay),
|
|
1518
|
+
.cms-nav-edit-static :deep([data-cms-nav-root] .cms-nav-panel) {
|
|
1519
|
+
display: none !important;
|
|
1520
|
+
pointer-events: none !important;
|
|
1521
|
+
}
|
|
1522
|
+
</style>
|