@edgedev/create-edge-app 1.1.27 → 1.1.29
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/edge/components/auth/register.vue +51 -0
- package/edge/components/cms/block.vue +363 -42
- package/edge/components/cms/blockEditor.vue +50 -3
- package/edge/components/cms/codeEditor.vue +39 -2
- package/edge/components/cms/htmlContent.vue +10 -2
- package/edge/components/cms/init_blocks/footer.html +111 -19
- package/edge/components/cms/init_blocks/image.html +8 -0
- package/edge/components/cms/init_blocks/post_content.html +3 -2
- package/edge/components/cms/init_blocks/post_title_header.html +8 -6
- package/edge/components/cms/init_blocks/posts_list.html +6 -5
- package/edge/components/cms/mediaCard.vue +13 -2
- package/edge/components/cms/mediaManager.vue +35 -5
- package/edge/components/cms/menu.vue +384 -61
- package/edge/components/cms/optionsSelect.vue +20 -3
- package/edge/components/cms/page.vue +160 -18
- package/edge/components/cms/site.vue +548 -374
- package/edge/components/cms/siteSettingsForm.vue +623 -0
- package/edge/components/cms/themeDefaultMenu.vue +258 -22
- package/edge/components/cms/themeEditor.vue +95 -11
- package/edge/components/editor.vue +1 -0
- package/edge/components/formSubtypes/myOrgs.vue +112 -1
- package/edge/components/imagePicker.vue +126 -0
- package/edge/components/myAccount.vue +1 -0
- package/edge/components/myProfile.vue +345 -61
- package/edge/components/orgSwitcher.vue +1 -1
- package/edge/components/organizationMembers.vue +620 -235
- package/edge/components/shad/html.vue +6 -0
- package/edge/components/shad/number.vue +2 -2
- package/edge/components/sideBar.vue +7 -4
- package/edge/components/sideBarContent.vue +1 -1
- package/edge/components/userMenu.vue +50 -14
- package/edge/composables/global.ts +4 -1
- package/edge/composables/siteSettingsTemplate.js +79 -0
- package/edge/composables/structuredDataTemplates.js +36 -0
- package/package.json +1 -1
|
@@ -77,6 +77,47 @@ const register = reactive({
|
|
|
77
77
|
requestedOrgId: '',
|
|
78
78
|
})
|
|
79
79
|
|
|
80
|
+
const resolveAuthEmail = () => {
|
|
81
|
+
return (
|
|
82
|
+
edgeFirebase?.user?.email
|
|
83
|
+
|| edgeFirebase?.user?.firebaseUser?.email
|
|
84
|
+
|| edgeFirebase?.user?.firebaseUser?.providerData?.find(p => p?.email)?.email
|
|
85
|
+
|| ''
|
|
86
|
+
)
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const waitForUserSnapshot = async (timeoutMs = 8000) => {
|
|
90
|
+
const findUser = () => {
|
|
91
|
+
const users = Object.values(edgeFirebase.state?.users || {})
|
|
92
|
+
const stagedDocId = edgeFirebase?.user?.stagedDocId
|
|
93
|
+
const uid = edgeFirebase?.user?.uid
|
|
94
|
+
return users.find(u => (stagedDocId && u?.docId === stagedDocId) || (uid && u?.userId === uid))
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
if (findUser())
|
|
98
|
+
return true
|
|
99
|
+
|
|
100
|
+
return await new Promise((resolve) => {
|
|
101
|
+
let timeoutId = null
|
|
102
|
+
const stop = watch(
|
|
103
|
+
() => edgeFirebase.state?.users,
|
|
104
|
+
() => {
|
|
105
|
+
if (findUser()) {
|
|
106
|
+
stop()
|
|
107
|
+
if (timeoutId)
|
|
108
|
+
clearTimeout(timeoutId)
|
|
109
|
+
resolve(true)
|
|
110
|
+
}
|
|
111
|
+
},
|
|
112
|
+
{ immediate: true, deep: true },
|
|
113
|
+
)
|
|
114
|
+
timeoutId = setTimeout(() => {
|
|
115
|
+
stop()
|
|
116
|
+
resolve(false)
|
|
117
|
+
}, timeoutMs)
|
|
118
|
+
})
|
|
119
|
+
}
|
|
120
|
+
|
|
80
121
|
const onSubmit = async () => {
|
|
81
122
|
state.registering = true
|
|
82
123
|
|
|
@@ -108,6 +149,9 @@ const onSubmit = async () => {
|
|
|
108
149
|
if (state.showRegistrationCode || !props.registrationCode) {
|
|
109
150
|
register.registrationCode = state.registrationCode
|
|
110
151
|
}
|
|
152
|
+
if (state.provider === 'email' && register.email) {
|
|
153
|
+
register.meta.email = register.email
|
|
154
|
+
}
|
|
111
155
|
const result = await edgeFirebase.registerUser(register, state.provider)
|
|
112
156
|
state.error.error = !result.success
|
|
113
157
|
if (result.message === `${props.requestedOrgIdLabel} already exists.`) {
|
|
@@ -118,6 +162,13 @@ const onSubmit = async () => {
|
|
|
118
162
|
result.message = `${orgLabel} already exists. Please choose another.`
|
|
119
163
|
}
|
|
120
164
|
state.error.message = result.message.code ? result.message.code : result.message
|
|
165
|
+
if (result.success) {
|
|
166
|
+
const authEmail = resolveAuthEmail()
|
|
167
|
+
if (authEmail && (!register.meta.email || register.meta.email !== authEmail)) {
|
|
168
|
+
await waitForUserSnapshot()
|
|
169
|
+
await edgeFirebase.setUserMeta({ email: authEmail })
|
|
170
|
+
}
|
|
171
|
+
}
|
|
121
172
|
}
|
|
122
173
|
|
|
123
174
|
state.registering = false
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
<script setup>
|
|
2
2
|
import { useVModel } from '@vueuse/core'
|
|
3
|
-
import { ImagePlus, Plus } from 'lucide-vue-next'
|
|
3
|
+
import { ImagePlus, Loader2, Plus, Sparkles } from 'lucide-vue-next'
|
|
4
4
|
const props = defineProps({
|
|
5
5
|
modelValue: {
|
|
6
6
|
type: Object,
|
|
@@ -49,6 +49,7 @@ function extractFieldsInOrder(template) {
|
|
|
49
49
|
}
|
|
50
50
|
|
|
51
51
|
const modelValue = useVModel(props, 'modelValue', emit)
|
|
52
|
+
const blockFormRef = ref(null)
|
|
52
53
|
|
|
53
54
|
const state = reactive({
|
|
54
55
|
open: false,
|
|
@@ -61,8 +62,23 @@ const state = reactive({
|
|
|
61
62
|
loading: true,
|
|
62
63
|
afterLoad: false,
|
|
63
64
|
imageOpen: false,
|
|
65
|
+
imageOpenByField: {},
|
|
66
|
+
aiDialogOpen: false,
|
|
67
|
+
aiInstructions: '',
|
|
68
|
+
aiSelectedFields: {},
|
|
69
|
+
aiGenerating: false,
|
|
70
|
+
aiError: '',
|
|
71
|
+
validationErrors: [],
|
|
64
72
|
})
|
|
65
73
|
|
|
74
|
+
const isLightName = (value) => {
|
|
75
|
+
if (!value)
|
|
76
|
+
return false
|
|
77
|
+
return String(value).toLowerCase().includes('light')
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const previewBackgroundClass = value => (isLightName(value) ? 'bg-neutral-900/90' : 'bg-neutral-100')
|
|
81
|
+
|
|
66
82
|
const ensureQueryItemsDefaults = (meta) => {
|
|
67
83
|
Object.keys(meta || {}).forEach((key) => {
|
|
68
84
|
const cfg = meta[key]
|
|
@@ -106,11 +122,13 @@ const sanitizeQueryItems = (meta) => {
|
|
|
106
122
|
return cleaned
|
|
107
123
|
}
|
|
108
124
|
|
|
109
|
-
const resetArrayItems = (field) => {
|
|
125
|
+
const resetArrayItems = (field, metaSource = null) => {
|
|
126
|
+
const meta = metaSource || modelValue.value?.meta || {}
|
|
127
|
+
const fieldMeta = meta?.[field]
|
|
110
128
|
if (!state.arrayItems?.[field]) {
|
|
111
129
|
state.arrayItems[field] = {}
|
|
112
130
|
}
|
|
113
|
-
for (const schemaItem of
|
|
131
|
+
for (const schemaItem of (fieldMeta?.schema || [])) {
|
|
114
132
|
if (schemaItem.type === 'text') {
|
|
115
133
|
state.arrayItems[field][schemaItem.field] = ''
|
|
116
134
|
}
|
|
@@ -132,25 +150,35 @@ const resetArrayItems = (field) => {
|
|
|
132
150
|
const openEditor = async () => {
|
|
133
151
|
if (!props.editMode)
|
|
134
152
|
return
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
153
|
+
const blockData = edgeFirebase.data[`${edgeGlobal.edgeState.organizationDocPath}/blocks`]?.[modelValue.value.blockId]
|
|
154
|
+
const templateMeta = blockData?.meta || modelValue.value?.meta || {}
|
|
155
|
+
const storedMeta = modelValue.value?.meta || {}
|
|
156
|
+
const mergedMeta = edgeGlobal.dupObject(templateMeta) || {}
|
|
157
|
+
|
|
158
|
+
for (const key of Object.keys(mergedMeta)) {
|
|
159
|
+
const storedField = storedMeta?.[key]
|
|
160
|
+
if (!storedField || typeof storedField !== 'object')
|
|
161
|
+
continue
|
|
162
|
+
if (storedField.queryItems && typeof storedField.queryItems === 'object') {
|
|
163
|
+
mergedMeta[key].queryItems = edgeGlobal.dupObject(storedField.queryItems)
|
|
164
|
+
}
|
|
165
|
+
if (storedField.limit !== undefined) {
|
|
166
|
+
mergedMeta[key].limit = storedField.limit
|
|
140
167
|
}
|
|
141
168
|
}
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
if (blockData?.meta) {
|
|
148
|
-
for (const key of Object.keys(blockData.meta)) {
|
|
149
|
-
if (!(key in state.metaUpdate)) {
|
|
150
|
-
state.metaUpdate[key] = blockData.meta[key]
|
|
169
|
+
|
|
170
|
+
for (const key of Object.keys(mergedMeta || {})) {
|
|
171
|
+
if (mergedMeta[key]?.type === 'array' && mergedMeta[key]?.schema) {
|
|
172
|
+
if (!mergedMeta[key]?.api) {
|
|
173
|
+
resetArrayItems(key, mergedMeta)
|
|
151
174
|
}
|
|
152
175
|
}
|
|
153
176
|
}
|
|
177
|
+
|
|
178
|
+
state.draft = JSON.parse(JSON.stringify(modelValue.value?.values || {}))
|
|
179
|
+
state.meta = JSON.parse(JSON.stringify(mergedMeta || {}))
|
|
180
|
+
ensureQueryItemsDefaults(state.meta)
|
|
181
|
+
state.metaUpdate = edgeGlobal.dupObject(mergedMeta) || {}
|
|
154
182
|
if (blockData?.values) {
|
|
155
183
|
for (const key of Object.keys(blockData.values)) {
|
|
156
184
|
if (!(key in state.draft)) {
|
|
@@ -159,19 +187,59 @@ const openEditor = async () => {
|
|
|
159
187
|
}
|
|
160
188
|
}
|
|
161
189
|
modelValue.value.blockUpdatedAt = new Date().toISOString()
|
|
190
|
+
state.validationErrors = []
|
|
162
191
|
state.open = true
|
|
163
192
|
state.afterLoad = true
|
|
164
193
|
}
|
|
165
194
|
|
|
166
|
-
const
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
195
|
+
const normalizeValidationNumber = (value) => {
|
|
196
|
+
if (value === null || value === undefined || value === '')
|
|
197
|
+
return null
|
|
198
|
+
const parsed = Number(value)
|
|
199
|
+
return Number.isNaN(parsed) ? null : parsed
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
const stringLength = (value) => {
|
|
203
|
+
if (value === null || value === undefined)
|
|
204
|
+
return 0
|
|
205
|
+
return String(value).trim().length
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
const validateValueAgainstRules = (value, rules, label, typeHint) => {
|
|
209
|
+
if (!rules || typeof rules !== 'object')
|
|
210
|
+
return []
|
|
211
|
+
|
|
212
|
+
const errors = []
|
|
213
|
+
if (rules.required) {
|
|
214
|
+
const isEmptyArray = Array.isArray(value) && value.length === 0
|
|
215
|
+
const isEmptyString = typeof value === 'string' && stringLength(value) === 0
|
|
216
|
+
if (value === null || value === undefined || isEmptyArray || isEmptyString) {
|
|
217
|
+
errors.push(`${label} is required.`)
|
|
218
|
+
return errors
|
|
219
|
+
}
|
|
171
220
|
}
|
|
172
|
-
|
|
173
|
-
|
|
221
|
+
|
|
222
|
+
if (typeHint === 'number') {
|
|
223
|
+
const numericValue = normalizeValidationNumber(value)
|
|
224
|
+
if (numericValue !== null) {
|
|
225
|
+
if (rules.min !== undefined && numericValue < rules.min)
|
|
226
|
+
errors.push(`${label} must be at least ${rules.min}.`)
|
|
227
|
+
if (rules.max !== undefined && numericValue > rules.max)
|
|
228
|
+
errors.push(`${label} must be ${rules.max} or less.`)
|
|
229
|
+
}
|
|
230
|
+
return errors
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
const length = Array.isArray(value) ? value.length : stringLength(value)
|
|
234
|
+
if (rules.min !== undefined && length < rules.min) {
|
|
235
|
+
errors.push(`${label} must be at least ${rules.min} ${Array.isArray(value) ? 'items' : 'characters'}.`)
|
|
236
|
+
}
|
|
237
|
+
if (rules.max !== undefined && length > rules.max) {
|
|
238
|
+
errors.push(`${label} must be ${rules.max} ${Array.isArray(value) ? 'items' : 'characters'} or less.`)
|
|
239
|
+
}
|
|
240
|
+
return errors
|
|
174
241
|
}
|
|
242
|
+
|
|
175
243
|
const orderedMeta = computed(() => {
|
|
176
244
|
const metaObj = state.metaUpdate || {}
|
|
177
245
|
const tpl = modelValue.value?.content || ''
|
|
@@ -209,6 +277,161 @@ const genTitleFromField = (field) => {
|
|
|
209
277
|
.replace(/([a-z])([A-Z])/g, '$1 $2')
|
|
210
278
|
.replace(/^./, str => str.toUpperCase())
|
|
211
279
|
}
|
|
280
|
+
|
|
281
|
+
const collectValidationErrors = () => {
|
|
282
|
+
const errors = []
|
|
283
|
+
for (const entry of orderedMeta.value) {
|
|
284
|
+
const label = genTitleFromField(entry)
|
|
285
|
+
const value = state.draft?.[entry.field]
|
|
286
|
+
|
|
287
|
+
if (entry.meta?.type === 'array' && !entry.meta?.api && !entry.meta?.collection) {
|
|
288
|
+
const itemCount = Array.isArray(value) ? value.length : 0
|
|
289
|
+
if (itemCount < 1) {
|
|
290
|
+
errors.push(`${label} requires at least one item.`)
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
if (Array.isArray(value) && entry.meta?.schema) {
|
|
294
|
+
value.forEach((item, index) => {
|
|
295
|
+
for (const schemaItem of entry.meta.schema) {
|
|
296
|
+
const itemLabel = `${label} ${index + 1} · ${genTitleFromField(schemaItem)}`
|
|
297
|
+
const itemValue = item?.[schemaItem.field]
|
|
298
|
+
errors.push(...validateValueAgainstRules(itemValue, schemaItem.validation, itemLabel, schemaItem.type))
|
|
299
|
+
}
|
|
300
|
+
})
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
const topLevelErrors = validateValueAgainstRules(value, entry.meta?.validation, label, entry.meta?.type)
|
|
305
|
+
errors.push(...topLevelErrors)
|
|
306
|
+
}
|
|
307
|
+
return errors
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
const save = () => {
|
|
311
|
+
const validationErrors = collectValidationErrors()
|
|
312
|
+
if (validationErrors.length) {
|
|
313
|
+
state.validationErrors = validationErrors
|
|
314
|
+
return
|
|
315
|
+
}
|
|
316
|
+
state.validationErrors = []
|
|
317
|
+
const updated = {
|
|
318
|
+
...modelValue.value,
|
|
319
|
+
values: JSON.parse(JSON.stringify(state.draft)),
|
|
320
|
+
meta: sanitizeQueryItems(state.meta),
|
|
321
|
+
}
|
|
322
|
+
modelValue.value = updated
|
|
323
|
+
state.open = false
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
const aiFieldOptions = computed(() => {
|
|
327
|
+
return orderedMeta.value
|
|
328
|
+
.map(entry => ({
|
|
329
|
+
id: entry.field,
|
|
330
|
+
label: genTitleFromField(entry),
|
|
331
|
+
type: entry.meta?.type || 'text',
|
|
332
|
+
}))
|
|
333
|
+
.filter(option => option.type !== 'image' && option.type !== 'color' && !/url/i.test(option.id) && !/color/i.test(option.id))
|
|
334
|
+
})
|
|
335
|
+
|
|
336
|
+
const selectedAiFieldIds = computed(() => {
|
|
337
|
+
return aiFieldOptions.value
|
|
338
|
+
.filter(option => state.aiSelectedFields?.[option.id])
|
|
339
|
+
.map(option => option.id)
|
|
340
|
+
})
|
|
341
|
+
|
|
342
|
+
const allAiFieldsSelected = computed({
|
|
343
|
+
get: () => {
|
|
344
|
+
if (!aiFieldOptions.value.length)
|
|
345
|
+
return false
|
|
346
|
+
return aiFieldOptions.value.every(option => state.aiSelectedFields?.[option.id])
|
|
347
|
+
},
|
|
348
|
+
set: (value) => {
|
|
349
|
+
const next = {}
|
|
350
|
+
aiFieldOptions.value.forEach((option) => {
|
|
351
|
+
next[option.id] = value
|
|
352
|
+
})
|
|
353
|
+
state.aiSelectedFields = next
|
|
354
|
+
},
|
|
355
|
+
})
|
|
356
|
+
|
|
357
|
+
const resetAiSelections = () => {
|
|
358
|
+
const next = {}
|
|
359
|
+
aiFieldOptions.value.forEach((option) => {
|
|
360
|
+
next[option.id] = true
|
|
361
|
+
})
|
|
362
|
+
state.aiSelectedFields = next
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
const openAiDialog = () => {
|
|
366
|
+
state.aiError = ''
|
|
367
|
+
state.aiInstructions = ''
|
|
368
|
+
resetAiSelections()
|
|
369
|
+
state.aiDialogOpen = true
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
const closeAiDialog = () => {
|
|
373
|
+
state.aiDialogOpen = false
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
const generateWithAi = async () => {
|
|
377
|
+
if (state.aiGenerating)
|
|
378
|
+
return
|
|
379
|
+
const selectedFields = selectedAiFieldIds.value
|
|
380
|
+
if (!selectedFields.length) {
|
|
381
|
+
state.aiError = 'Select at least one field for AI generation.'
|
|
382
|
+
return
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
state.aiGenerating = true
|
|
386
|
+
state.aiError = ''
|
|
387
|
+
|
|
388
|
+
try {
|
|
389
|
+
const fields = aiFieldOptions.value.filter(option => selectedFields.includes(option.id))
|
|
390
|
+
const currentValues = selectedFields.reduce((acc, field) => {
|
|
391
|
+
acc[field] = state.draft?.[field]
|
|
392
|
+
return acc
|
|
393
|
+
}, {})
|
|
394
|
+
const meta = selectedFields.reduce((acc, field) => {
|
|
395
|
+
acc[field] = state.meta?.[field]
|
|
396
|
+
return acc
|
|
397
|
+
}, {})
|
|
398
|
+
|
|
399
|
+
const response = await edgeFirebase.runFunction('cms-generateBlockFields', {
|
|
400
|
+
orgId: edgeGlobal.edgeState.currentOrganization,
|
|
401
|
+
uid: edgeFirebase?.user?.uid || '',
|
|
402
|
+
blockId: modelValue.value?.blockId || props.blockId,
|
|
403
|
+
blockName: modelValue.value?.name || '',
|
|
404
|
+
content: modelValue.value?.content || '',
|
|
405
|
+
instructions: state.aiInstructions || '',
|
|
406
|
+
fields,
|
|
407
|
+
currentValues,
|
|
408
|
+
meta,
|
|
409
|
+
})
|
|
410
|
+
|
|
411
|
+
const aiFields = response?.data?.fields || {}
|
|
412
|
+
Object.keys(aiFields).forEach((field) => {
|
|
413
|
+
if (selectedFields.includes(field)) {
|
|
414
|
+
state.draft[field] = aiFields[field]
|
|
415
|
+
blockFormRef.value?.setFieldValue?.(field, aiFields[field])
|
|
416
|
+
}
|
|
417
|
+
})
|
|
418
|
+
|
|
419
|
+
const missingFields = selectedFields.filter(field => !(field in aiFields))
|
|
420
|
+
if (missingFields.length) {
|
|
421
|
+
state.aiError = `AI skipped: ${missingFields.join(', ')}`
|
|
422
|
+
return
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
closeAiDialog()
|
|
426
|
+
}
|
|
427
|
+
catch (error) {
|
|
428
|
+
console.error('Failed to generate block fields with AI', error)
|
|
429
|
+
state.aiError = 'AI generation failed. Try again.'
|
|
430
|
+
}
|
|
431
|
+
finally {
|
|
432
|
+
state.aiGenerating = false
|
|
433
|
+
}
|
|
434
|
+
}
|
|
212
435
|
const addToArray = async (field) => {
|
|
213
436
|
state.reload = true
|
|
214
437
|
state.draft[field].push(JSON.parse(JSON.stringify(state.arrayItems[field])))
|
|
@@ -317,13 +540,28 @@ const getTagsFromPosts = computed(() => {
|
|
|
317
540
|
<Sheet v-model:open="state.open">
|
|
318
541
|
<edge-cms-block-sheet-content v-if="state.afterLoad" class="w-full md:w-1/2 max-w-none sm:max-w-none max-w-2xl">
|
|
319
542
|
<SheetHeader>
|
|
320
|
-
<
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
543
|
+
<div class="flex flex-col gap-3 pr-10 md:flex-row md:items-start md:justify-between">
|
|
544
|
+
<div class="min-w-0">
|
|
545
|
+
<SheetTitle>Edit Block</SheetTitle>
|
|
546
|
+
<SheetDescription v-if="modelValue.synced" class="text-sm text-red-500">
|
|
547
|
+
This is a synced block. Changes made here will be reflected across all instances of this block on your site.
|
|
548
|
+
</SheetDescription>
|
|
549
|
+
</div>
|
|
550
|
+
<edge-shad-button
|
|
551
|
+
type="button"
|
|
552
|
+
size="sm"
|
|
553
|
+
class="h-8 gap-2 md:self-start"
|
|
554
|
+
variant="outline"
|
|
555
|
+
:disabled="!aiFieldOptions.length"
|
|
556
|
+
@click="openAiDialog"
|
|
557
|
+
>
|
|
558
|
+
<Sparkles class="w-4 h-4" />
|
|
559
|
+
Generate with AI
|
|
560
|
+
</edge-shad-button>
|
|
561
|
+
</div>
|
|
324
562
|
</SheetHeader>
|
|
325
563
|
|
|
326
|
-
<edge-shad-form>
|
|
564
|
+
<edge-shad-form ref="blockFormRef">
|
|
327
565
|
<div v-if="orderedMeta.length === 0">
|
|
328
566
|
<Alert variant="info" class="mt-4 mb-4">
|
|
329
567
|
<AlertTitle>No editable fields found</AlertTitle>
|
|
@@ -456,6 +694,7 @@ const getTagsFromPosts = computed(() => {
|
|
|
456
694
|
v-model="state.meta[entry.field].queryItems[option.field]"
|
|
457
695
|
:option="option"
|
|
458
696
|
:label="genTitleFromField(option)"
|
|
697
|
+
:multiple="option?.multiple || false"
|
|
459
698
|
/>
|
|
460
699
|
</div>
|
|
461
700
|
</template>
|
|
@@ -463,9 +702,12 @@ const getTagsFromPosts = computed(() => {
|
|
|
463
702
|
</div>
|
|
464
703
|
</div>
|
|
465
704
|
<div v-else-if="entry.meta?.type === 'image'" class="w-full">
|
|
466
|
-
<div class="
|
|
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])">
|
|
467
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">
|
|
468
|
-
<Dialog v-model:open="state.
|
|
710
|
+
<Dialog v-model:open="state.imageOpenByField[entry.field]">
|
|
469
711
|
<DialogTrigger as-child>
|
|
470
712
|
<edge-shad-button variant="outline" class="bg-white text-black hover:bg-gray-200">
|
|
471
713
|
<ImagePlus class="h-5 w-5 mr-2" />
|
|
@@ -482,18 +724,22 @@ const getTagsFromPosts = computed(() => {
|
|
|
482
724
|
:site="props.siteId"
|
|
483
725
|
:select-mode="true"
|
|
484
726
|
:default-tags="entry.meta.tags"
|
|
485
|
-
@select="(url) => { state.draft[entry.field] = url; state.
|
|
727
|
+
@select="(url) => { state.draft[entry.field] = url; state.imageOpenByField[entry.field] = false }"
|
|
486
728
|
/>
|
|
487
729
|
<edge-cms-media-manager
|
|
488
730
|
v-else
|
|
489
731
|
:site="props.siteId"
|
|
490
732
|
:select-mode="true"
|
|
491
|
-
@select="(url) => { state.draft[entry.field] = url; state.
|
|
733
|
+
@select="(url) => { state.draft[entry.field] = url; state.imageOpenByField[entry.field] = false }"
|
|
492
734
|
/>
|
|
493
735
|
</DialogContent>
|
|
494
736
|
</Dialog>
|
|
495
737
|
</div>
|
|
496
|
-
<img
|
|
738
|
+
<img
|
|
739
|
+
v-if="state.draft[entry.field]"
|
|
740
|
+
:src="state.draft[entry.field]"
|
|
741
|
+
class="max-h-40 max-w-full h-auto w-auto object-contain"
|
|
742
|
+
>
|
|
497
743
|
</div>
|
|
498
744
|
</div>
|
|
499
745
|
<div v-else-if="entry.meta?.option">
|
|
@@ -514,15 +760,90 @@ const getTagsFromPosts = computed(() => {
|
|
|
514
760
|
</template>
|
|
515
761
|
</div>
|
|
516
762
|
|
|
517
|
-
<
|
|
518
|
-
<
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
763
|
+
<div class="sticky bottom-0 bg-background px-6 pb-4 pt-2">
|
|
764
|
+
<Alert v-if="state.validationErrors.length" variant="destructive" class="mb-3">
|
|
765
|
+
<AlertTitle>Fix the highlighted fields</AlertTitle>
|
|
766
|
+
<AlertDescription class="text-sm">
|
|
767
|
+
<div v-for="(error, index) in state.validationErrors" :key="`${error}-${index}`">
|
|
768
|
+
{{ error }}
|
|
769
|
+
</div>
|
|
770
|
+
</AlertDescription>
|
|
771
|
+
</Alert>
|
|
772
|
+
<SheetFooter class="flex justify-between">
|
|
773
|
+
<edge-shad-button variant="destructive" class="text-white" @click="state.open = false">
|
|
774
|
+
Cancel
|
|
775
|
+
</edge-shad-button>
|
|
776
|
+
<edge-shad-button class=" bg-slate-800 hover:bg-slate-400 w-full" @click="save">
|
|
777
|
+
Save changes
|
|
778
|
+
</edge-shad-button>
|
|
779
|
+
</SheetFooter>
|
|
780
|
+
</div>
|
|
525
781
|
</edge-shad-form>
|
|
782
|
+
|
|
783
|
+
<edge-shad-dialog v-model="state.aiDialogOpen">
|
|
784
|
+
<DialogContent class="max-w-[640px]">
|
|
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>
|
|
815
|
+
</edge-shad-checkbox>
|
|
816
|
+
</div>
|
|
817
|
+
<Alert v-else variant="info">
|
|
818
|
+
<AlertTitle>No editable fields</AlertTitle>
|
|
819
|
+
<AlertDescription class="text-sm">
|
|
820
|
+
Add editable fields to this block to enable AI generation.
|
|
821
|
+
</AlertDescription>
|
|
822
|
+
</Alert>
|
|
823
|
+
</div>
|
|
824
|
+
<Alert v-if="state.aiError" variant="destructive">
|
|
825
|
+
<AlertTitle>AI generation failed</AlertTitle>
|
|
826
|
+
<AlertDescription class="text-sm">
|
|
827
|
+
{{ state.aiError }}
|
|
828
|
+
</AlertDescription>
|
|
829
|
+
</Alert>
|
|
830
|
+
</div>
|
|
831
|
+
<DialogFooter class="pt-4 flex justify-between">
|
|
832
|
+
<edge-shad-button type="button" variant="destructive" class="text-white" @click="closeAiDialog">
|
|
833
|
+
Cancel
|
|
834
|
+
</edge-shad-button>
|
|
835
|
+
<edge-shad-button
|
|
836
|
+
type="button"
|
|
837
|
+
class="w-full"
|
|
838
|
+
:disabled="state.aiGenerating || !selectedAiFieldIds.length"
|
|
839
|
+
@click="generateWithAi"
|
|
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>
|
|
526
847
|
</edge-cms-block-sheet-content>
|
|
527
848
|
</Sheet>
|
|
528
849
|
</div>
|
|
@@ -38,6 +38,7 @@ const state = reactive({
|
|
|
38
38
|
initialBlocksSeeded: false,
|
|
39
39
|
seedingInitialBlocks: false,
|
|
40
40
|
previewViewport: 'full',
|
|
41
|
+
previewBlock: null,
|
|
41
42
|
})
|
|
42
43
|
|
|
43
44
|
const blockSchema = toTypedSchema(z.object({
|
|
@@ -95,6 +96,8 @@ const PLACEHOLDERS = {
|
|
|
95
96
|
|
|
96
97
|
const contentEditorRef = ref(null)
|
|
97
98
|
|
|
99
|
+
const ignorePreviewDelete = () => {}
|
|
100
|
+
|
|
98
101
|
const BLOCK_CONTENT_SNIPPETS = [
|
|
99
102
|
{
|
|
100
103
|
label: 'Text Field',
|
|
@@ -339,6 +342,8 @@ function handleEditorLineClick(payload, workingDoc) {
|
|
|
339
342
|
const tag = findTagAtOffset(workingDoc.content, offset)
|
|
340
343
|
if (!tag)
|
|
341
344
|
return
|
|
345
|
+
if (tag.type === 'if')
|
|
346
|
+
return
|
|
342
347
|
|
|
343
348
|
const parsedCfg = safeParseConfig(tag.rawCfg)
|
|
344
349
|
state.jsonEditorError = ''
|
|
@@ -416,6 +421,42 @@ function handleJsonEditorSave() {
|
|
|
416
421
|
closeJsonEditor()
|
|
417
422
|
}
|
|
418
423
|
|
|
424
|
+
const buildPreviewBlock = (workingDoc, parsed) => {
|
|
425
|
+
const content = workingDoc?.content || ''
|
|
426
|
+
const nextValues = {}
|
|
427
|
+
const previousValues = state.previewBlock?.values || {}
|
|
428
|
+
Object.keys(parsed.values || {}).forEach((field) => {
|
|
429
|
+
if (previousValues[field] !== undefined)
|
|
430
|
+
nextValues[field] = previousValues[field]
|
|
431
|
+
else
|
|
432
|
+
nextValues[field] = parsed.values[field]
|
|
433
|
+
})
|
|
434
|
+
|
|
435
|
+
const previousMeta = state.previewBlock?.meta || {}
|
|
436
|
+
const nextMeta = {}
|
|
437
|
+
Object.keys(parsed.meta || {}).forEach((field) => {
|
|
438
|
+
if (previousMeta[field]) {
|
|
439
|
+
nextMeta[field] = {
|
|
440
|
+
...previousMeta[field],
|
|
441
|
+
...parsed.meta[field],
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
else {
|
|
445
|
+
nextMeta[field] = parsed.meta[field]
|
|
446
|
+
}
|
|
447
|
+
})
|
|
448
|
+
|
|
449
|
+
return {
|
|
450
|
+
id: state.previewBlock?.id || 'preview',
|
|
451
|
+
blockId: props.blockId,
|
|
452
|
+
name: workingDoc?.name || state.previewBlock?.name || '',
|
|
453
|
+
content,
|
|
454
|
+
values: nextValues,
|
|
455
|
+
meta: nextMeta,
|
|
456
|
+
synced: !!workingDoc?.synced,
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
|
|
419
460
|
const theme = computed(() => {
|
|
420
461
|
const theme = edgeGlobal.edgeState.blockEditorTheme || ''
|
|
421
462
|
let themeContents = null
|
|
@@ -443,7 +484,9 @@ watch(headObject, (newHeadElements) => {
|
|
|
443
484
|
}, { immediate: true, deep: true })
|
|
444
485
|
|
|
445
486
|
const editorDocUpdates = (workingDoc) => {
|
|
446
|
-
|
|
487
|
+
const parsed = blockModel(workingDoc.content)
|
|
488
|
+
state.workingDoc = parsed
|
|
489
|
+
state.previewBlock = buildPreviewBlock(workingDoc, parsed)
|
|
447
490
|
console.log('Editor workingDoc update:', state.workingDoc)
|
|
448
491
|
}
|
|
449
492
|
|
|
@@ -670,11 +713,15 @@ const getTagsFromBlocks = computed(() => {
|
|
|
670
713
|
class="w-full mx-auto bg-card border border-border rounded-lg shadow-sm md:shadow-md"
|
|
671
714
|
:style="previewViewportStyle"
|
|
672
715
|
>
|
|
673
|
-
<edge-cms-block
|
|
716
|
+
<edge-cms-block
|
|
717
|
+
v-if="state.previewBlock"
|
|
718
|
+
v-model="state.previewBlock"
|
|
674
719
|
:site-id="edgeGlobal.edgeState.blockEditorSite"
|
|
675
720
|
:theme="theme"
|
|
676
|
-
:
|
|
721
|
+
:edit-mode="true"
|
|
677
722
|
:viewport-mode="previewViewportMode"
|
|
723
|
+
:block-id="state.previewBlock.id"
|
|
724
|
+
@delete="ignorePreviewDelete"
|
|
678
725
|
/>
|
|
679
726
|
</div>
|
|
680
727
|
</div>
|