@edgedev/create-edge-app 1.1.28 → 1.2.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/agents.md +2 -0
- package/bin/cli.js +13 -5
- package/deploy-services.sh +237 -0
- package/deploy.sh +88 -1
- package/edge/components/auth/register.vue +51 -0
- package/edge/components/cms/block.vue +29 -16
- package/edge/components/cms/blockEditor.vue +748 -7
- package/edge/components/cms/codeEditor.vue +24 -2
- package/edge/components/cms/htmlContent.vue +10 -2
- package/edge/components/cms/mediaManager.vue +19 -3
- package/edge/components/cms/menu.vue +231 -34
- package/edge/components/cms/optionsSelect.vue +20 -3
- package/edge/components/cms/page.vue +9 -0
- package/edge/components/cms/site.vue +114 -5
- package/edge/components/cms/siteSettingsForm.vue +7 -0
- package/edge/components/cms/themeEditor.vue +9 -3
- package/edge/components/dashboard.vue +22 -3
- package/edge/components/imagePicker.vue +126 -0
- package/edge/components/myAccount.vue +1 -0
- package/edge/components/myProfile.vue +345 -61
- package/edge/components/organizationMembers.vue +569 -261
- package/edge/components/shad/combobox.vue +2 -2
- package/edge/components/shad/number.vue +2 -2
- package/edge/composables/global.ts +5 -2
- package/edge/composables/structuredDataTemplates.js +6 -6
- package/firebase_init.sh +63 -2
- package/package.json +1 -1
- package/services/.deploy.shared.env.example +12 -0
|
@@ -56,12 +56,34 @@ const props = defineProps({
|
|
|
56
56
|
type: String,
|
|
57
57
|
default: '',
|
|
58
58
|
},
|
|
59
|
+
validateJson: {
|
|
60
|
+
type: Boolean,
|
|
61
|
+
default: false,
|
|
62
|
+
},
|
|
59
63
|
})
|
|
60
64
|
|
|
61
65
|
const emit = defineEmits(['update:modelValue', 'lineClick'])
|
|
62
66
|
const localModelValue = ref(null)
|
|
63
67
|
const edgeFirebase = inject('edgeFirebase')
|
|
64
68
|
const expectsJsonObject = ref(false)
|
|
69
|
+
const jsonValidationError = computed(() => {
|
|
70
|
+
if (!props.validateJson || props.language !== 'json')
|
|
71
|
+
return ''
|
|
72
|
+
const raw = localModelValue.value
|
|
73
|
+
if (raw === null || raw === undefined)
|
|
74
|
+
return ''
|
|
75
|
+
const text = String(raw).trim()
|
|
76
|
+
if (!text)
|
|
77
|
+
return ''
|
|
78
|
+
try {
|
|
79
|
+
JSON.parse(text)
|
|
80
|
+
return ''
|
|
81
|
+
}
|
|
82
|
+
catch (error) {
|
|
83
|
+
return `Invalid JSON: ${error.message}`
|
|
84
|
+
}
|
|
85
|
+
})
|
|
86
|
+
const displayError = computed(() => props.error || jsonValidationError.value)
|
|
65
87
|
|
|
66
88
|
const editorOptions = {
|
|
67
89
|
mode: 'htmlmixed',
|
|
@@ -379,11 +401,11 @@ onBeforeUnmount(() => {
|
|
|
379
401
|
</template>
|
|
380
402
|
<template #center>
|
|
381
403
|
<div class="w-full px-2">
|
|
382
|
-
<Alert v-if="
|
|
404
|
+
<Alert v-if="displayError" variant="destructive" class="rounded-[6px] py-1">
|
|
383
405
|
<TriangleAlert class="h-4 w-4" />
|
|
384
406
|
<AlertTitle>Error</AlertTitle>
|
|
385
407
|
<AlertDescription>
|
|
386
|
-
{{
|
|
408
|
+
{{ displayError }}
|
|
387
409
|
</AlertDescription>
|
|
388
410
|
</Alert>
|
|
389
411
|
</div>
|
|
@@ -699,10 +699,18 @@ function rewriteAllClasses(scopeEl, theme, isolated = true, viewportMode = 'auto
|
|
|
699
699
|
scopeEl.querySelectorAll('[class]').forEach((el) => {
|
|
700
700
|
let base = el.dataset.viewportBaseClass
|
|
701
701
|
if (typeof base !== 'string') {
|
|
702
|
-
|
|
702
|
+
if (typeof el.className === 'string') {
|
|
703
|
+
base = el.className
|
|
704
|
+
}
|
|
705
|
+
else if (el.className && typeof el.className.baseVal === 'string') {
|
|
706
|
+
base = el.className.baseVal
|
|
707
|
+
}
|
|
708
|
+
else {
|
|
709
|
+
base = el.getAttribute('class') || ''
|
|
710
|
+
}
|
|
703
711
|
el.dataset.viewportBaseClass = base
|
|
704
712
|
}
|
|
705
|
-
const orig = base || ''
|
|
713
|
+
const orig = typeof base === 'string' ? base : String(base || '')
|
|
706
714
|
if (!orig.trim())
|
|
707
715
|
return
|
|
708
716
|
const origTokens = orig.split(/\s+/).filter(Boolean)
|
|
@@ -8,6 +8,11 @@ const props = defineProps({
|
|
|
8
8
|
required: false,
|
|
9
9
|
default: 'all',
|
|
10
10
|
},
|
|
11
|
+
includeCmsAll: {
|
|
12
|
+
type: Boolean,
|
|
13
|
+
required: false,
|
|
14
|
+
default: true,
|
|
15
|
+
},
|
|
11
16
|
selectMode: {
|
|
12
17
|
type: Boolean,
|
|
13
18
|
required: false,
|
|
@@ -137,6 +142,7 @@ onBeforeMount(() => {
|
|
|
137
142
|
console.log('Default tags prop:', props.defaultTags)
|
|
138
143
|
if (props.defaultTags && Array.isArray(props.defaultTags) && props.defaultTags.length > 0) {
|
|
139
144
|
state.filterTags = [...props.defaultTags]
|
|
145
|
+
state.tags = [...props.defaultTags]
|
|
140
146
|
}
|
|
141
147
|
})
|
|
142
148
|
|
|
@@ -166,6 +172,12 @@ const isLightName = (name) => {
|
|
|
166
172
|
}
|
|
167
173
|
|
|
168
174
|
const previewBackgroundClass = computed(() => (isLightName(state.workingDoc?.name) ? 'bg-neutral-900/90' : 'bg-neutral-100'))
|
|
175
|
+
|
|
176
|
+
const siteQueryValue = computed(() => {
|
|
177
|
+
if (!props.site)
|
|
178
|
+
return []
|
|
179
|
+
return props.includeCmsAll ? ['all', props.site] : [props.site]
|
|
180
|
+
})
|
|
169
181
|
</script>
|
|
170
182
|
|
|
171
183
|
<template>
|
|
@@ -228,8 +240,12 @@ const previewBackgroundClass = computed(() => (isLightName(state.workingDoc?.nam
|
|
|
228
240
|
v-if="state.tags.length === 0"
|
|
229
241
|
class="pointer-events-auto absolute inset-0 z-20 rounded-[20px] border border-dashed border-border/70 bg-background/85 dark:bg-background/80 backdrop-blur-sm flex flex-col items-center justify-center text-center px-6 text-foreground"
|
|
230
242
|
>
|
|
231
|
-
<div class="text-lg font-semibold">
|
|
232
|
-
|
|
243
|
+
<div class="text-lg font-semibold">
|
|
244
|
+
Tags are required
|
|
245
|
+
</div>
|
|
246
|
+
<div class="text-sm text-muted-foreground">
|
|
247
|
+
Add tags above to enable upload
|
|
248
|
+
</div>
|
|
233
249
|
</div>
|
|
234
250
|
</div>
|
|
235
251
|
</SheetContent>
|
|
@@ -239,7 +255,7 @@ const previewBackgroundClass = computed(() => (isLightName(state.workingDoc?.nam
|
|
|
239
255
|
sort-field="uploadTime"
|
|
240
256
|
query-field="meta.cmssite"
|
|
241
257
|
:filters="filters"
|
|
242
|
-
:query-value="
|
|
258
|
+
:query-value="siteQueryValue"
|
|
243
259
|
query-operator="array-contains-any"
|
|
244
260
|
header-class=""
|
|
245
261
|
sort-direction="desc" class="w-full flex-1 border-none shadow-none bg-background"
|
|
@@ -75,6 +75,42 @@ const normalizeForCompare = (value) => {
|
|
|
75
75
|
|
|
76
76
|
const stableSerialize = value => JSON.stringify(normalizeForCompare(value))
|
|
77
77
|
const areEqualNormalized = (a, b) => stableSerialize(a) === stableSerialize(b)
|
|
78
|
+
const isBlankString = value => String(value || '').trim() === ''
|
|
79
|
+
const isJsonInvalid = (value) => {
|
|
80
|
+
if (value === null || value === undefined)
|
|
81
|
+
return false
|
|
82
|
+
if (typeof value === 'object')
|
|
83
|
+
return false
|
|
84
|
+
const text = String(value).trim()
|
|
85
|
+
if (!text)
|
|
86
|
+
return false
|
|
87
|
+
try {
|
|
88
|
+
JSON.parse(text)
|
|
89
|
+
return false
|
|
90
|
+
}
|
|
91
|
+
catch {
|
|
92
|
+
return true
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
const hasStructuredDataErrors = (doc) => {
|
|
96
|
+
if (!doc)
|
|
97
|
+
return false
|
|
98
|
+
if (isJsonInvalid(doc.structuredData))
|
|
99
|
+
return true
|
|
100
|
+
if (doc.post && isJsonInvalid(doc.postStructuredData))
|
|
101
|
+
return true
|
|
102
|
+
return false
|
|
103
|
+
}
|
|
104
|
+
const ensurePostSeoDefaults = (doc) => {
|
|
105
|
+
if (!doc?.post)
|
|
106
|
+
return
|
|
107
|
+
if (isBlankString(doc.postMetaTitle))
|
|
108
|
+
doc.postMetaTitle = doc.metaTitle || ''
|
|
109
|
+
if (isBlankString(doc.postMetaDescription))
|
|
110
|
+
doc.postMetaDescription = doc.metaDescription || ''
|
|
111
|
+
if (isBlankString(doc.postStructuredData))
|
|
112
|
+
doc.postStructuredData = doc.structuredData || buildPageStructuredData()
|
|
113
|
+
}
|
|
78
114
|
|
|
79
115
|
const orderedMenus = computed(() => {
|
|
80
116
|
const menuEntries = Object.entries(modelValue.value || {}).map(([name, menu], originalIndex) => ({
|
|
@@ -117,6 +153,9 @@ const isPublishedPageDiff = (pageId) => {
|
|
|
117
153
|
metaTitle: publishedPage.metaTitle,
|
|
118
154
|
metaDescription: publishedPage.metaDescription,
|
|
119
155
|
structuredData: publishedPage.structuredData,
|
|
156
|
+
postMetaTitle: publishedPage.postMetaTitle,
|
|
157
|
+
postMetaDescription: publishedPage.postMetaDescription,
|
|
158
|
+
postStructuredData: publishedPage.postStructuredData,
|
|
120
159
|
},
|
|
121
160
|
{
|
|
122
161
|
content: draftPage.content,
|
|
@@ -126,6 +165,9 @@ const isPublishedPageDiff = (pageId) => {
|
|
|
126
165
|
metaTitle: draftPage.metaTitle,
|
|
127
166
|
metaDescription: draftPage.metaDescription,
|
|
128
167
|
structuredData: draftPage.structuredData,
|
|
168
|
+
postMetaTitle: draftPage.postMetaTitle,
|
|
169
|
+
postMetaDescription: draftPage.postMetaDescription,
|
|
170
|
+
postStructuredData: draftPage.postStructuredData,
|
|
129
171
|
},
|
|
130
172
|
)
|
|
131
173
|
}
|
|
@@ -172,6 +214,9 @@ const state = reactive({
|
|
|
172
214
|
metaTitle: { value: '' },
|
|
173
215
|
metaDescription: { value: '' },
|
|
174
216
|
structuredData: { value: buildPageStructuredData() },
|
|
217
|
+
postMetaTitle: { value: '' },
|
|
218
|
+
postMetaDescription: { value: '' },
|
|
219
|
+
postStructuredData: { value: '' },
|
|
175
220
|
tags: { value: [] },
|
|
176
221
|
allowedThemes: { value: [] },
|
|
177
222
|
},
|
|
@@ -337,12 +382,82 @@ const resolveBlockForPreview = (block) => {
|
|
|
337
382
|
return null
|
|
338
383
|
}
|
|
339
384
|
|
|
340
|
-
const
|
|
385
|
+
const normalizePreviewColumns = (row) => {
|
|
386
|
+
if (!Array.isArray(row?.columns) || !row.columns.length)
|
|
387
|
+
return []
|
|
388
|
+
return row.columns.map((column, idx) => ({
|
|
389
|
+
id: column?.id || `${row?.id || 'row'}-col-${idx}`,
|
|
390
|
+
span: Number(column?.span) || null,
|
|
391
|
+
blocks: Array.isArray(column?.blocks) ? column.blocks.filter(Boolean) : [],
|
|
392
|
+
}))
|
|
393
|
+
}
|
|
341
394
|
|
|
342
|
-
const
|
|
343
|
-
|
|
395
|
+
const templatePreviewRows = (template) => {
|
|
396
|
+
const structureRows = Array.isArray(template?.structure) ? template.structure : []
|
|
397
|
+
if (structureRows.length) {
|
|
398
|
+
return structureRows
|
|
399
|
+
.map((row, rowIndex) => ({
|
|
400
|
+
id: row?.id || `${template?.docId || 'template'}-row-${rowIndex}`,
|
|
401
|
+
columns: normalizePreviewColumns(row),
|
|
402
|
+
}))
|
|
403
|
+
.filter(row => row.columns.length > 0)
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
const legacyBlocks = Array.isArray(template?.content) ? template.content.filter(Boolean) : []
|
|
407
|
+
if (!legacyBlocks.length)
|
|
344
408
|
return []
|
|
345
|
-
return
|
|
409
|
+
return [{
|
|
410
|
+
id: `${template?.docId || 'template'}-legacy-row`,
|
|
411
|
+
columns: [{
|
|
412
|
+
id: `${template?.docId || 'template'}-legacy-col`,
|
|
413
|
+
span: null,
|
|
414
|
+
blocks: legacyBlocks,
|
|
415
|
+
}],
|
|
416
|
+
}]
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
const templateHasPreview = template => templatePreviewRows(template).length > 0
|
|
420
|
+
|
|
421
|
+
const resolveTemplateBlockSource = (template, blockRef) => {
|
|
422
|
+
if (!blockRef)
|
|
423
|
+
return null
|
|
424
|
+
if (typeof blockRef === 'object')
|
|
425
|
+
return blockRef
|
|
426
|
+
const lookupId = String(blockRef).trim()
|
|
427
|
+
if (!lookupId)
|
|
428
|
+
return null
|
|
429
|
+
const templateBlocks = Array.isArray(template?.content) ? template.content : []
|
|
430
|
+
return templateBlocks.find(block => block?.id === lookupId || block?.blockId === lookupId) || null
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
const resolveTemplateBlockForPreview = (template, blockRef) => {
|
|
434
|
+
const source = resolveTemplateBlockSource(template, blockRef)
|
|
435
|
+
return resolveBlockForPreview(source)
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
const hasPreviewSpans = row => (row?.columns || []).some(column => Number.isFinite(Number(column?.span)))
|
|
439
|
+
|
|
440
|
+
const previewGridClass = (row) => {
|
|
441
|
+
if (hasPreviewSpans(row))
|
|
442
|
+
return 'grid grid-cols-1 sm:grid-cols-6 gap-4'
|
|
443
|
+
const count = row?.columns?.length || 1
|
|
444
|
+
const map = {
|
|
445
|
+
1: 'grid grid-cols-1 gap-4',
|
|
446
|
+
2: 'grid grid-cols-1 sm:grid-cols-2 gap-4',
|
|
447
|
+
3: 'grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4',
|
|
448
|
+
4: 'grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4',
|
|
449
|
+
5: 'grid grid-cols-1 sm:grid-cols-3 lg:grid-cols-5 gap-4',
|
|
450
|
+
6: 'grid grid-cols-1 sm:grid-cols-3 lg:grid-cols-6 gap-4',
|
|
451
|
+
}
|
|
452
|
+
return map[count] || map[1]
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
const previewColumnStyle = (column) => {
|
|
456
|
+
const span = Number(column?.span)
|
|
457
|
+
if (!Number.isFinite(span))
|
|
458
|
+
return {}
|
|
459
|
+
const safeSpan = Math.min(Math.max(span, 1), 6)
|
|
460
|
+
return { gridColumn: `span ${safeSpan} / span ${safeSpan}` }
|
|
346
461
|
}
|
|
347
462
|
|
|
348
463
|
const renameFolderOrPageShow = (item) => {
|
|
@@ -526,6 +641,9 @@ const buildPagePayloadFromTemplate = (templateDoc, slug) => {
|
|
|
526
641
|
metaTitle: '',
|
|
527
642
|
metaDescription: '',
|
|
528
643
|
structuredData,
|
|
644
|
+
postMetaTitle: '',
|
|
645
|
+
postMetaDescription: '',
|
|
646
|
+
postStructuredData: '',
|
|
529
647
|
doc_created_at: timestamp,
|
|
530
648
|
last_updated: timestamp,
|
|
531
649
|
}
|
|
@@ -797,6 +915,10 @@ const showPageSettings = (page) => {
|
|
|
797
915
|
state.pageSettings = true
|
|
798
916
|
}
|
|
799
917
|
|
|
918
|
+
const handlePageWorkingDoc = (doc) => {
|
|
919
|
+
ensurePostSeoDefaults(doc)
|
|
920
|
+
}
|
|
921
|
+
|
|
800
922
|
const formErrors = (error) => {
|
|
801
923
|
console.log('Form errors:', error)
|
|
802
924
|
console.log(Object.values(error))
|
|
@@ -1103,19 +1225,34 @@ const theme = computed(() => {
|
|
|
1103
1225
|
Blank page
|
|
1104
1226
|
</div>
|
|
1105
1227
|
</template>
|
|
1106
|
-
<template v-else-if="
|
|
1228
|
+
<template v-else-if="templateHasPreview(template)">
|
|
1107
1229
|
<div
|
|
1108
|
-
v-for="(
|
|
1109
|
-
:key="`${template.docId}-
|
|
1230
|
+
v-for="(row, rowIndex) in templatePreviewRows(template)"
|
|
1231
|
+
:key="`${template.docId}-row-${row.id || rowIndex}`"
|
|
1232
|
+
class="w-full"
|
|
1110
1233
|
>
|
|
1111
|
-
<
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1234
|
+
<div :class="previewGridClass(row)">
|
|
1235
|
+
<div
|
|
1236
|
+
v-for="(column, colIndex) in row.columns"
|
|
1237
|
+
:key="`${template.docId}-row-${row.id || rowIndex}-col-${column.id || colIndex}`"
|
|
1238
|
+
class="min-w-0"
|
|
1239
|
+
:style="previewColumnStyle(column)"
|
|
1240
|
+
>
|
|
1241
|
+
<div
|
|
1242
|
+
v-for="(blockRef, blockIdx) in column.blocks || []"
|
|
1243
|
+
:key="`${template.docId}-row-${row.id || rowIndex}-col-${column.id || colIndex}-block-${blockIdx}`"
|
|
1244
|
+
>
|
|
1245
|
+
<edge-cms-block-api
|
|
1246
|
+
v-if="resolveTemplateBlockForPreview(template, blockRef)"
|
|
1247
|
+
:content="resolveTemplateBlockForPreview(template, blockRef).content"
|
|
1248
|
+
:values="resolveTemplateBlockForPreview(template, blockRef).values"
|
|
1249
|
+
:meta="resolveTemplateBlockForPreview(template, blockRef).meta"
|
|
1250
|
+
:theme="theme"
|
|
1251
|
+
:isolated="true"
|
|
1252
|
+
/>
|
|
1253
|
+
</div>
|
|
1254
|
+
</div>
|
|
1255
|
+
</div>
|
|
1119
1256
|
</div>
|
|
1120
1257
|
</template>
|
|
1121
1258
|
<template v-else>
|
|
@@ -1214,6 +1351,7 @@ const theme = computed(() => {
|
|
|
1214
1351
|
:save-function-override="onSubmit"
|
|
1215
1352
|
card-content-class="px-0"
|
|
1216
1353
|
@error="formErrors"
|
|
1354
|
+
@working-doc="handlePageWorkingDoc"
|
|
1217
1355
|
>
|
|
1218
1356
|
<template #main="slotProps">
|
|
1219
1357
|
<div class="p-6 space-y-4 h-[calc(100vh-142px)] overflow-y-auto">
|
|
@@ -1253,24 +1391,83 @@ const theme = computed(() => {
|
|
|
1253
1391
|
<CardDescription>Meta tags for the page.</CardDescription>
|
|
1254
1392
|
</CardHeader>
|
|
1255
1393
|
<CardContent class="pt-0">
|
|
1256
|
-
<
|
|
1257
|
-
v-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1394
|
+
<Tabs class="w-full" default-value="list">
|
|
1395
|
+
<TabsList v-if="slotProps.workingDoc?.post" class="w-full grid grid-cols-2 gap-1 rounded-lg border border-border/60 bg-muted/30 p-1">
|
|
1396
|
+
<TabsTrigger
|
|
1397
|
+
value="list"
|
|
1398
|
+
class="text-xs font-semibold uppercase tracking-wide transition-all data-[state=active]:bg-slate-900 data-[state=active]:text-white data-[state=active]:shadow-sm"
|
|
1399
|
+
>
|
|
1400
|
+
Index Page
|
|
1401
|
+
</TabsTrigger>
|
|
1402
|
+
<TabsTrigger
|
|
1403
|
+
value="post"
|
|
1404
|
+
class="text-xs font-semibold uppercase tracking-wide transition-all data-[state=active]:bg-slate-900 data-[state=active]:text-white data-[state=active]:shadow-sm"
|
|
1405
|
+
>
|
|
1406
|
+
Detail Page
|
|
1407
|
+
</TabsTrigger>
|
|
1408
|
+
</TabsList>
|
|
1409
|
+
<TabsContent value="list" class="mt-4 space-y-4">
|
|
1410
|
+
<edge-shad-input
|
|
1411
|
+
v-model="slotProps.workingDoc.metaTitle"
|
|
1412
|
+
label="Meta Title"
|
|
1413
|
+
name="metaTitle"
|
|
1414
|
+
/>
|
|
1415
|
+
<edge-shad-textarea
|
|
1416
|
+
v-model="slotProps.workingDoc.metaDescription"
|
|
1417
|
+
label="Meta Description"
|
|
1418
|
+
name="metaDescription"
|
|
1419
|
+
/>
|
|
1420
|
+
<div class="rounded-md border border-border/60 bg-muted/30 px-3 py-2 text-xs text-muted-foreground">
|
|
1421
|
+
CMS tokens in double curly braces are replaced on the front end.
|
|
1422
|
+
Example: <span v-pre class="font-semibold text-foreground">"{{cms-site}}"</span> for the site URL,
|
|
1423
|
+
<span v-pre class="font-semibold text-foreground">"{{cms-url}}"</span> for the page URL, and
|
|
1424
|
+
<span v-pre class="font-semibold text-foreground">"{{cms-logo}}"</span> for the logo URL. Keep the tokens intact.
|
|
1425
|
+
</div>
|
|
1426
|
+
<edge-cms-code-editor
|
|
1427
|
+
v-model="slotProps.workingDoc.structuredData"
|
|
1428
|
+
title="Structured Data (JSON-LD)"
|
|
1429
|
+
language="json"
|
|
1430
|
+
name="structuredData"
|
|
1431
|
+
validate-json
|
|
1432
|
+
height="300px"
|
|
1433
|
+
class="mb-4 w-full"
|
|
1434
|
+
/>
|
|
1435
|
+
</TabsContent>
|
|
1436
|
+
<TabsContent value="post" class="mt-4 space-y-4">
|
|
1437
|
+
<div class="rounded-md border border-border/60 bg-muted/30 px-3 py-2 text-xs text-muted-foreground">
|
|
1438
|
+
You can use template keys in double curly braces to pull data from the detail record.
|
|
1439
|
+
Example: <span v-pre class="font-semibold text-foreground">"{{name}}"</span> will be replaced with the record’s name.
|
|
1440
|
+
Dot notation is supported for nested objects, e.g. <span v-pre class="font-semibold text-foreground">"{{data.name}}"</span>.
|
|
1441
|
+
These keys work in the Title, Description, and Structured Data fields.
|
|
1442
|
+
</div>
|
|
1443
|
+
<edge-shad-input
|
|
1444
|
+
v-model="slotProps.workingDoc.postMetaTitle"
|
|
1445
|
+
label="Meta Title"
|
|
1446
|
+
name="postMetaTitle"
|
|
1447
|
+
/>
|
|
1448
|
+
<edge-shad-textarea
|
|
1449
|
+
v-model="slotProps.workingDoc.postMetaDescription"
|
|
1450
|
+
label="Meta Description"
|
|
1451
|
+
name="postMetaDescription"
|
|
1452
|
+
/>
|
|
1453
|
+
|
|
1454
|
+
<div class="rounded-md border border-border/60 bg-muted/30 px-3 py-2 text-xs text-muted-foreground">
|
|
1455
|
+
CMS tokens in double curly braces are replaced on the front end.
|
|
1456
|
+
Example: <span v-pre class="font-semibold text-foreground">"{{cms-site}}"</span> for the site URL,
|
|
1457
|
+
<span v-pre class="font-semibold text-foreground">"{{cms-url}}"</span> for the page URL, and
|
|
1458
|
+
<span v-pre class="font-semibold text-foreground">"{{cms-logo}}"</span> for the logo URL. Keep the tokens intact.
|
|
1459
|
+
</div>
|
|
1460
|
+
<edge-cms-code-editor
|
|
1461
|
+
v-model="slotProps.workingDoc.postStructuredData"
|
|
1462
|
+
title="Structured Data (JSON-LD)"
|
|
1463
|
+
language="json"
|
|
1464
|
+
name="postStructuredData"
|
|
1465
|
+
validate-json
|
|
1466
|
+
height="300px"
|
|
1467
|
+
class="mb-4 w-full"
|
|
1468
|
+
/>
|
|
1469
|
+
</TabsContent>
|
|
1470
|
+
</Tabs>
|
|
1274
1471
|
</CardContent>
|
|
1275
1472
|
</Card>
|
|
1276
1473
|
</div>
|
|
@@ -1278,7 +1475,7 @@ const theme = computed(() => {
|
|
|
1278
1475
|
<edge-shad-button variant="destructive" class="text-white" @click="state.pageSettings = false">
|
|
1279
1476
|
Cancel
|
|
1280
1477
|
</edge-shad-button>
|
|
1281
|
-
<edge-shad-button :disabled="slotProps.submitting" type="submit" class=" bg-slate-800 hover:bg-slate-400 w-full">
|
|
1478
|
+
<edge-shad-button :disabled="slotProps.submitting || hasStructuredDataErrors(slotProps.workingDoc)" type="submit" class=" bg-slate-800 hover:bg-slate-400 w-full">
|
|
1282
1479
|
<Loader2 v-if="slotProps.submitting" class=" h-4 w-4 animate-spin" />
|
|
1283
1480
|
Update
|
|
1284
1481
|
</edge-shad-button>
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
import { useVModel } from '@vueuse/core'
|
|
3
3
|
const props = defineProps({
|
|
4
4
|
modelValue: {
|
|
5
|
-
type: [String, Boolean, Number, null],
|
|
5
|
+
type: [String, Boolean, Number, Array, null],
|
|
6
6
|
required: false,
|
|
7
7
|
default: null,
|
|
8
8
|
},
|
|
@@ -14,6 +14,10 @@ const props = defineProps({
|
|
|
14
14
|
type: String,
|
|
15
15
|
required: false,
|
|
16
16
|
},
|
|
17
|
+
multiple: {
|
|
18
|
+
type: Boolean,
|
|
19
|
+
default: false,
|
|
20
|
+
},
|
|
17
21
|
})
|
|
18
22
|
const emits = defineEmits(['update:modelValue'])
|
|
19
23
|
const edgeFirebase = inject('edgeFirebase')
|
|
@@ -91,17 +95,30 @@ onBeforeMount(async () => {
|
|
|
91
95
|
})
|
|
92
96
|
.filter(Boolean) // remove nulls
|
|
93
97
|
}
|
|
94
|
-
|
|
98
|
+
if (!props.multiple) {
|
|
99
|
+
staticOption.options.unshift({ title: '(none)', name: NONE_VALUE })
|
|
100
|
+
}
|
|
95
101
|
state.loading = false
|
|
96
102
|
})
|
|
97
103
|
</script>
|
|
98
104
|
|
|
99
105
|
<template>
|
|
100
106
|
<edge-shad-select
|
|
101
|
-
v-if="!state.loading && staticOption.options.length > 0"
|
|
107
|
+
v-if="!state.loading && staticOption.options.length > 0 && !props.multiple"
|
|
102
108
|
v-model="selectValue"
|
|
103
109
|
:label="props.label"
|
|
104
110
|
:name="props.option.field"
|
|
105
111
|
:items="staticOption.options"
|
|
106
112
|
/>
|
|
113
|
+
<edge-shad-select-tags
|
|
114
|
+
v-else-if="!state.loading && staticOption.options.length > 0 && props.multiple"
|
|
115
|
+
:model-value="Array.isArray(modelValue) ? modelValue : []"
|
|
116
|
+
:label="props.label"
|
|
117
|
+
:name="props.option.field"
|
|
118
|
+
:items="staticOption.options"
|
|
119
|
+
item-title="title"
|
|
120
|
+
item-value="name"
|
|
121
|
+
:allow-additions="false"
|
|
122
|
+
@update:model-value="value => (modelValue = Array.isArray(value) ? value : [])"
|
|
123
|
+
/>
|
|
107
124
|
</template>
|
|
@@ -881,6 +881,9 @@ const isPublishedPageDiff = (pageId) => {
|
|
|
881
881
|
metaTitle: publishedPage.metaTitle,
|
|
882
882
|
metaDescription: publishedPage.metaDescription,
|
|
883
883
|
structuredData: publishedPage.structuredData,
|
|
884
|
+
postMetaTitle: publishedPage.postMetaTitle,
|
|
885
|
+
postMetaDescription: publishedPage.postMetaDescription,
|
|
886
|
+
postStructuredData: publishedPage.postStructuredData,
|
|
884
887
|
},
|
|
885
888
|
{
|
|
886
889
|
content: draftPage.content,
|
|
@@ -890,6 +893,9 @@ const isPublishedPageDiff = (pageId) => {
|
|
|
890
893
|
metaTitle: draftPage.metaTitle,
|
|
891
894
|
metaDescription: draftPage.metaDescription,
|
|
892
895
|
structuredData: draftPage.structuredData,
|
|
896
|
+
postMetaTitle: draftPage.postMetaTitle,
|
|
897
|
+
postMetaDescription: draftPage.postMetaDescription,
|
|
898
|
+
postStructuredData: draftPage.postStructuredData,
|
|
893
899
|
},
|
|
894
900
|
)
|
|
895
901
|
}
|
|
@@ -1087,6 +1093,9 @@ const unpublishedChangeDetails = computed(() => {
|
|
|
1087
1093
|
compareField('metaTitle', 'Meta title', val => summarizeChangeValue(val, true))
|
|
1088
1094
|
compareField('metaDescription', 'Meta description', val => summarizeChangeValue(val, true))
|
|
1089
1095
|
compareField('structuredData', 'Structured data', val => summarizeChangeValue(val, true))
|
|
1096
|
+
compareField('postMetaTitle', 'Detail meta title', val => summarizeChangeValue(val, true))
|
|
1097
|
+
compareField('postMetaDescription', 'Detail meta description', val => summarizeChangeValue(val, true))
|
|
1098
|
+
compareField('postStructuredData', 'Detail structured data', val => summarizeChangeValue(val, true))
|
|
1090
1099
|
|
|
1091
1100
|
return changes
|
|
1092
1101
|
})
|