@edgedev/create-edge-app 1.1.28 → 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 +29 -16
- 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 +131 -19
- package/edge/components/cms/optionsSelect.vue +20 -3
- package/edge/components/cms/page.vue +9 -0
- package/edge/components/cms/site.vue +31 -2
- package/edge/components/cms/siteSettingsForm.vue +7 -0
- 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 +496 -261
- package/edge/components/shad/number.vue +2 -2
- package/edge/composables/global.ts +4 -1
- package/edge/composables/structuredDataTemplates.js +6 -6
- 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
|
|
@@ -122,11 +122,13 @@ const sanitizeQueryItems = (meta) => {
|
|
|
122
122
|
return cleaned
|
|
123
123
|
}
|
|
124
124
|
|
|
125
|
-
const resetArrayItems = (field) => {
|
|
125
|
+
const resetArrayItems = (field, metaSource = null) => {
|
|
126
|
+
const meta = metaSource || modelValue.value?.meta || {}
|
|
127
|
+
const fieldMeta = meta?.[field]
|
|
126
128
|
if (!state.arrayItems?.[field]) {
|
|
127
129
|
state.arrayItems[field] = {}
|
|
128
130
|
}
|
|
129
|
-
for (const schemaItem of
|
|
131
|
+
for (const schemaItem of (fieldMeta?.schema || [])) {
|
|
130
132
|
if (schemaItem.type === 'text') {
|
|
131
133
|
state.arrayItems[field][schemaItem.field] = ''
|
|
132
134
|
}
|
|
@@ -148,25 +150,35 @@ const resetArrayItems = (field) => {
|
|
|
148
150
|
const openEditor = async () => {
|
|
149
151
|
if (!props.editMode)
|
|
150
152
|
return
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
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
|
|
156
167
|
}
|
|
157
168
|
}
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
if (blockData?.meta) {
|
|
164
|
-
for (const key of Object.keys(blockData.meta)) {
|
|
165
|
-
if (!(key in state.metaUpdate)) {
|
|
166
|
-
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)
|
|
167
174
|
}
|
|
168
175
|
}
|
|
169
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) || {}
|
|
170
182
|
if (blockData?.values) {
|
|
171
183
|
for (const key of Object.keys(blockData.values)) {
|
|
172
184
|
if (!(key in state.draft)) {
|
|
@@ -682,6 +694,7 @@ const getTagsFromPosts = computed(() => {
|
|
|
682
694
|
v-model="state.meta[entry.field].queryItems[option.field]"
|
|
683
695
|
:option="option"
|
|
684
696
|
:label="genTitleFromField(option)"
|
|
697
|
+
:multiple="option?.multiple || false"
|
|
685
698
|
/>
|
|
686
699
|
</div>
|
|
687
700
|
</template>
|
|
@@ -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
|
},
|
|
@@ -526,6 +571,9 @@ const buildPagePayloadFromTemplate = (templateDoc, slug) => {
|
|
|
526
571
|
metaTitle: '',
|
|
527
572
|
metaDescription: '',
|
|
528
573
|
structuredData,
|
|
574
|
+
postMetaTitle: '',
|
|
575
|
+
postMetaDescription: '',
|
|
576
|
+
postStructuredData: '',
|
|
529
577
|
doc_created_at: timestamp,
|
|
530
578
|
last_updated: timestamp,
|
|
531
579
|
}
|
|
@@ -797,6 +845,10 @@ const showPageSettings = (page) => {
|
|
|
797
845
|
state.pageSettings = true
|
|
798
846
|
}
|
|
799
847
|
|
|
848
|
+
const handlePageWorkingDoc = (doc) => {
|
|
849
|
+
ensurePostSeoDefaults(doc)
|
|
850
|
+
}
|
|
851
|
+
|
|
800
852
|
const formErrors = (error) => {
|
|
801
853
|
console.log('Form errors:', error)
|
|
802
854
|
console.log(Object.values(error))
|
|
@@ -1214,6 +1266,7 @@ const theme = computed(() => {
|
|
|
1214
1266
|
:save-function-override="onSubmit"
|
|
1215
1267
|
card-content-class="px-0"
|
|
1216
1268
|
@error="formErrors"
|
|
1269
|
+
@working-doc="handlePageWorkingDoc"
|
|
1217
1270
|
>
|
|
1218
1271
|
<template #main="slotProps">
|
|
1219
1272
|
<div class="p-6 space-y-4 h-[calc(100vh-142px)] overflow-y-auto">
|
|
@@ -1253,24 +1306,83 @@ const theme = computed(() => {
|
|
|
1253
1306
|
<CardDescription>Meta tags for the page.</CardDescription>
|
|
1254
1307
|
</CardHeader>
|
|
1255
1308
|
<CardContent class="pt-0">
|
|
1256
|
-
<
|
|
1257
|
-
v-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1309
|
+
<Tabs class="w-full" default-value="list">
|
|
1310
|
+
<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">
|
|
1311
|
+
<TabsTrigger
|
|
1312
|
+
value="list"
|
|
1313
|
+
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"
|
|
1314
|
+
>
|
|
1315
|
+
Index Page
|
|
1316
|
+
</TabsTrigger>
|
|
1317
|
+
<TabsTrigger
|
|
1318
|
+
value="post"
|
|
1319
|
+
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"
|
|
1320
|
+
>
|
|
1321
|
+
Detail Page
|
|
1322
|
+
</TabsTrigger>
|
|
1323
|
+
</TabsList>
|
|
1324
|
+
<TabsContent value="list" class="mt-4 space-y-4">
|
|
1325
|
+
<edge-shad-input
|
|
1326
|
+
v-model="slotProps.workingDoc.metaTitle"
|
|
1327
|
+
label="Meta Title"
|
|
1328
|
+
name="metaTitle"
|
|
1329
|
+
/>
|
|
1330
|
+
<edge-shad-textarea
|
|
1331
|
+
v-model="slotProps.workingDoc.metaDescription"
|
|
1332
|
+
label="Meta Description"
|
|
1333
|
+
name="metaDescription"
|
|
1334
|
+
/>
|
|
1335
|
+
<div class="rounded-md border border-border/60 bg-muted/30 px-3 py-2 text-xs text-muted-foreground">
|
|
1336
|
+
CMS tokens in double curly braces are replaced on the front end.
|
|
1337
|
+
Example: <span v-pre class="font-semibold text-foreground">"{{cms-site}}"</span> for the site URL,
|
|
1338
|
+
<span v-pre class="font-semibold text-foreground">"{{cms-url}}"</span> for the page URL, and
|
|
1339
|
+
<span v-pre class="font-semibold text-foreground">"{{cms-logo}}"</span> for the logo URL. Keep the tokens intact.
|
|
1340
|
+
</div>
|
|
1341
|
+
<edge-cms-code-editor
|
|
1342
|
+
v-model="slotProps.workingDoc.structuredData"
|
|
1343
|
+
title="Structured Data (JSON-LD)"
|
|
1344
|
+
language="json"
|
|
1345
|
+
name="structuredData"
|
|
1346
|
+
validate-json
|
|
1347
|
+
height="300px"
|
|
1348
|
+
class="mb-4 w-full"
|
|
1349
|
+
/>
|
|
1350
|
+
</TabsContent>
|
|
1351
|
+
<TabsContent value="post" class="mt-4 space-y-4">
|
|
1352
|
+
<div class="rounded-md border border-border/60 bg-muted/30 px-3 py-2 text-xs text-muted-foreground">
|
|
1353
|
+
You can use template keys in double curly braces to pull data from the detail record.
|
|
1354
|
+
Example: <span v-pre class="font-semibold text-foreground">"{{name}}"</span> will be replaced with the record’s name.
|
|
1355
|
+
Dot notation is supported for nested objects, e.g. <span v-pre class="font-semibold text-foreground">"{{data.name}}"</span>.
|
|
1356
|
+
These keys work in the Title, Description, and Structured Data fields.
|
|
1357
|
+
</div>
|
|
1358
|
+
<edge-shad-input
|
|
1359
|
+
v-model="slotProps.workingDoc.postMetaTitle"
|
|
1360
|
+
label="Meta Title"
|
|
1361
|
+
name="postMetaTitle"
|
|
1362
|
+
/>
|
|
1363
|
+
<edge-shad-textarea
|
|
1364
|
+
v-model="slotProps.workingDoc.postMetaDescription"
|
|
1365
|
+
label="Meta Description"
|
|
1366
|
+
name="postMetaDescription"
|
|
1367
|
+
/>
|
|
1368
|
+
|
|
1369
|
+
<div class="rounded-md border border-border/60 bg-muted/30 px-3 py-2 text-xs text-muted-foreground">
|
|
1370
|
+
CMS tokens in double curly braces are replaced on the front end.
|
|
1371
|
+
Example: <span v-pre class="font-semibold text-foreground">"{{cms-site}}"</span> for the site URL,
|
|
1372
|
+
<span v-pre class="font-semibold text-foreground">"{{cms-url}}"</span> for the page URL, and
|
|
1373
|
+
<span v-pre class="font-semibold text-foreground">"{{cms-logo}}"</span> for the logo URL. Keep the tokens intact.
|
|
1374
|
+
</div>
|
|
1375
|
+
<edge-cms-code-editor
|
|
1376
|
+
v-model="slotProps.workingDoc.postStructuredData"
|
|
1377
|
+
title="Structured Data (JSON-LD)"
|
|
1378
|
+
language="json"
|
|
1379
|
+
name="postStructuredData"
|
|
1380
|
+
validate-json
|
|
1381
|
+
height="300px"
|
|
1382
|
+
class="mb-4 w-full"
|
|
1383
|
+
/>
|
|
1384
|
+
</TabsContent>
|
|
1385
|
+
</Tabs>
|
|
1274
1386
|
</CardContent>
|
|
1275
1387
|
</Card>
|
|
1276
1388
|
</div>
|
|
@@ -1278,7 +1390,7 @@ const theme = computed(() => {
|
|
|
1278
1390
|
<edge-shad-button variant="destructive" class="text-white" @click="state.pageSettings = false">
|
|
1279
1391
|
Cancel
|
|
1280
1392
|
</edge-shad-button>
|
|
1281
|
-
<edge-shad-button :disabled="slotProps.submitting" type="submit" class=" bg-slate-800 hover:bg-slate-400 w-full">
|
|
1393
|
+
<edge-shad-button :disabled="slotProps.submitting || hasStructuredDataErrors(slotProps.workingDoc)" type="submit" class=" bg-slate-800 hover:bg-slate-400 w-full">
|
|
1282
1394
|
<Loader2 v-if="slotProps.submitting" class=" h-4 w-4 animate-spin" />
|
|
1283
1395
|
Update
|
|
1284
1396
|
</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
|
})
|
|
@@ -33,6 +33,22 @@ const normalizeForCompare = (value) => {
|
|
|
33
33
|
|
|
34
34
|
const stableSerialize = value => JSON.stringify(normalizeForCompare(value))
|
|
35
35
|
const areEqualNormalized = (a, b) => stableSerialize(a) === stableSerialize(b)
|
|
36
|
+
const isJsonInvalid = (value) => {
|
|
37
|
+
if (value === null || value === undefined)
|
|
38
|
+
return false
|
|
39
|
+
if (typeof value === 'object')
|
|
40
|
+
return false
|
|
41
|
+
const text = String(value).trim()
|
|
42
|
+
if (!text)
|
|
43
|
+
return false
|
|
44
|
+
try {
|
|
45
|
+
JSON.parse(text)
|
|
46
|
+
return false
|
|
47
|
+
}
|
|
48
|
+
catch {
|
|
49
|
+
return true
|
|
50
|
+
}
|
|
51
|
+
}
|
|
36
52
|
|
|
37
53
|
const isTemplateSite = computed(() => props.site === 'templates')
|
|
38
54
|
const router = useRouter()
|
|
@@ -339,8 +355,9 @@ const themeOptionsMap = computed(() => {
|
|
|
339
355
|
const orgUsers = computed(() => edgeFirebase.state?.users || {})
|
|
340
356
|
const userOptions = computed(() => {
|
|
341
357
|
return Object.entries(orgUsers.value || {})
|
|
358
|
+
.filter(([, user]) => Boolean(user?.userId))
|
|
342
359
|
.map(([id, user]) => ({
|
|
343
|
-
value: user?.userId
|
|
360
|
+
value: user?.userId,
|
|
344
361
|
label: user?.meta?.name || user?.userId || id,
|
|
345
362
|
}))
|
|
346
363
|
.sort((a, b) => a.label.localeCompare(b.label))
|
|
@@ -859,6 +876,9 @@ const isPublishedPageDiff = (pageId) => {
|
|
|
859
876
|
metaTitle: publishedPage.metaTitle,
|
|
860
877
|
metaDescription: publishedPage.metaDescription,
|
|
861
878
|
structuredData: publishedPage.structuredData,
|
|
879
|
+
postMetaTitle: publishedPage.postMetaTitle,
|
|
880
|
+
postMetaDescription: publishedPage.postMetaDescription,
|
|
881
|
+
postStructuredData: publishedPage.postStructuredData,
|
|
862
882
|
},
|
|
863
883
|
{
|
|
864
884
|
content: draftPage.content,
|
|
@@ -868,6 +888,9 @@ const isPublishedPageDiff = (pageId) => {
|
|
|
868
888
|
metaTitle: draftPage.metaTitle,
|
|
869
889
|
metaDescription: draftPage.metaDescription,
|
|
870
890
|
structuredData: draftPage.structuredData,
|
|
891
|
+
postMetaTitle: draftPage.postMetaTitle,
|
|
892
|
+
postMetaDescription: draftPage.postMetaDescription,
|
|
893
|
+
postStructuredData: draftPage.postStructuredData,
|
|
871
894
|
},
|
|
872
895
|
)
|
|
873
896
|
}
|
|
@@ -1069,6 +1092,9 @@ const isAnyPagesDiff = computed(() => {
|
|
|
1069
1092
|
metaTitle: pageData.metaTitle,
|
|
1070
1093
|
metaDescription: pageData.metaDescription,
|
|
1071
1094
|
structuredData: pageData.structuredData,
|
|
1095
|
+
postMetaTitle: pageData.postMetaTitle,
|
|
1096
|
+
postMetaDescription: pageData.postMetaDescription,
|
|
1097
|
+
postStructuredData: pageData.postStructuredData,
|
|
1072
1098
|
},
|
|
1073
1099
|
{
|
|
1074
1100
|
content: publishedPage.content,
|
|
@@ -1078,6 +1104,9 @@ const isAnyPagesDiff = computed(() => {
|
|
|
1078
1104
|
metaTitle: publishedPage.metaTitle,
|
|
1079
1105
|
metaDescription: publishedPage.metaDescription,
|
|
1080
1106
|
structuredData: publishedPage.structuredData,
|
|
1107
|
+
postMetaTitle: publishedPage.postMetaTitle,
|
|
1108
|
+
postMetaDescription: publishedPage.postMetaDescription,
|
|
1109
|
+
postStructuredData: publishedPage.postStructuredData,
|
|
1081
1110
|
},
|
|
1082
1111
|
)) {
|
|
1083
1112
|
return true
|
|
@@ -1596,7 +1625,7 @@ const pageSettingsUpdated = async (pageData) => {
|
|
|
1596
1625
|
<edge-shad-button variant="destructive" class="text-white" @click="state.siteSettings = false">
|
|
1597
1626
|
Cancel
|
|
1598
1627
|
</edge-shad-button>
|
|
1599
|
-
<edge-shad-button :disabled="slotProps.submitting" type="submit" class=" bg-slate-800 hover:bg-slate-400 w-full">
|
|
1628
|
+
<edge-shad-button :disabled="slotProps.submitting || isJsonInvalid(slotProps.workingDoc?.structuredData)" type="submit" class=" bg-slate-800 hover:bg-slate-400 w-full">
|
|
1600
1629
|
<Loader2 v-if="slotProps.submitting" class=" h-4 w-4 animate-spin" />
|
|
1601
1630
|
Update
|
|
1602
1631
|
</edge-shad-button>
|
|
@@ -534,11 +534,18 @@ onMounted(async () => {
|
|
|
534
534
|
label="Meta Description"
|
|
535
535
|
name="metaDescription"
|
|
536
536
|
/>
|
|
537
|
+
<div class="rounded-md border border-border/60 bg-muted/30 px-3 py-2 text-xs text-muted-foreground">
|
|
538
|
+
CMS tokens in double curly braces are replaced on the front end.
|
|
539
|
+
Example: <span v-pre class="font-semibold text-foreground">"{{cms-site}}"</span> for the site URL,
|
|
540
|
+
<span v-pre class="font-semibold text-foreground">"{{cms-url}}"</span> for the page URL, and
|
|
541
|
+
<span v-pre class="font-semibold text-foreground">"{{cms-logo}}"</span> for the logo URL. Keep the tokens intact.
|
|
542
|
+
</div>
|
|
537
543
|
<edge-cms-code-editor
|
|
538
544
|
v-model="props.settings.structuredData"
|
|
539
545
|
title="Structured Data (JSON-LD)"
|
|
540
546
|
language="json"
|
|
541
547
|
name="structuredData"
|
|
548
|
+
validate-json
|
|
542
549
|
height="300px"
|
|
543
550
|
class="mb-4 w-full"
|
|
544
551
|
/>
|