@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
|
@@ -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>
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
<script setup>
|
|
2
|
-
import { AlertTriangle, ArrowDown, ArrowUp, Maximize2, Monitor, Smartphone, Tablet } from 'lucide-vue-next'
|
|
2
|
+
import { AlertTriangle, ArrowDown, ArrowUp, Maximize2, Monitor, Smartphone, Sparkles, Tablet, UploadCloud } from 'lucide-vue-next'
|
|
3
3
|
import { toTypedSchema } from '@vee-validate/zod'
|
|
4
4
|
import * as z from 'zod'
|
|
5
5
|
const props = defineProps({
|
|
@@ -20,6 +20,7 @@ const props = defineProps({
|
|
|
20
20
|
const emit = defineEmits(['head'])
|
|
21
21
|
|
|
22
22
|
const edgeFirebase = inject('edgeFirebase')
|
|
23
|
+
const { buildPageStructuredData } = useStructuredDataTemplates()
|
|
23
24
|
|
|
24
25
|
const state = reactive({
|
|
25
26
|
newDocs: {
|
|
@@ -29,11 +30,17 @@ const state = reactive({
|
|
|
29
30
|
postContent: { value: [] },
|
|
30
31
|
structure: { value: [] },
|
|
31
32
|
postStructure: { value: [] },
|
|
33
|
+
metaTitle: { value: '' },
|
|
34
|
+
metaDescription: { value: '' },
|
|
35
|
+
structuredData: { value: buildPageStructuredData() },
|
|
32
36
|
},
|
|
33
37
|
},
|
|
34
38
|
editMode: false,
|
|
35
39
|
showUnpublishedChangesDialog: false,
|
|
40
|
+
publishLoading: false,
|
|
36
41
|
workingDoc: {},
|
|
42
|
+
seoAiLoading: false,
|
|
43
|
+
seoAiError: '',
|
|
37
44
|
previewViewport: 'full',
|
|
38
45
|
newRowLayout: '6',
|
|
39
46
|
newPostRowLayout: '6',
|
|
@@ -200,6 +207,40 @@ const ensureBlocksArray = (workingDoc, key) => {
|
|
|
200
207
|
}
|
|
201
208
|
}
|
|
202
209
|
|
|
210
|
+
const applySeoAiResults = (payload) => {
|
|
211
|
+
if (!payload || typeof payload !== 'object')
|
|
212
|
+
return
|
|
213
|
+
if (payload.metaTitle)
|
|
214
|
+
state.workingDoc.metaTitle = payload.metaTitle
|
|
215
|
+
if (payload.metaDescription)
|
|
216
|
+
state.workingDoc.metaDescription = payload.metaDescription
|
|
217
|
+
if (payload.structuredData)
|
|
218
|
+
state.workingDoc.structuredData = payload.structuredData
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
const updateSeoWithAi = async () => {
|
|
222
|
+
if (!edgeFirebase?.user?.uid)
|
|
223
|
+
return
|
|
224
|
+
state.seoAiLoading = true
|
|
225
|
+
state.seoAiError = ''
|
|
226
|
+
try {
|
|
227
|
+
const results = await edgeFirebase.runFunction('cms-updateSeoFromAi', {
|
|
228
|
+
orgId: edgeGlobal.edgeState.currentOrganization,
|
|
229
|
+
siteId: props.site,
|
|
230
|
+
pageId: props.page,
|
|
231
|
+
uid: edgeFirebase.user.uid,
|
|
232
|
+
})
|
|
233
|
+
applySeoAiResults(results?.data || {})
|
|
234
|
+
}
|
|
235
|
+
catch (error) {
|
|
236
|
+
console.error('Failed to update SEO with AI', error)
|
|
237
|
+
state.seoAiError = 'Failed to update SEO. Try again.'
|
|
238
|
+
}
|
|
239
|
+
finally {
|
|
240
|
+
state.seoAiLoading = false
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
203
244
|
const createRow = (columns = 1) => {
|
|
204
245
|
const row = {
|
|
205
246
|
id: edgeGlobal.generateShortId(),
|
|
@@ -398,6 +439,54 @@ const blockPick = (block, index, slotProps, post = false) => {
|
|
|
398
439
|
}
|
|
399
440
|
}
|
|
400
441
|
|
|
442
|
+
const applyCollectionUniqueKeys = (workingDoc) => {
|
|
443
|
+
const resolveUniqueKey = (template) => {
|
|
444
|
+
if (!template || typeof template !== 'string')
|
|
445
|
+
return ''
|
|
446
|
+
let resolved = template
|
|
447
|
+
const orgId = edgeGlobal.edgeState.currentOrganization || ''
|
|
448
|
+
const siteId = props.site || ''
|
|
449
|
+
if (resolved.includes('{orgId}') && orgId)
|
|
450
|
+
resolved = resolved.replaceAll('{orgId}', orgId)
|
|
451
|
+
if (resolved.includes('{siteId}') && siteId)
|
|
452
|
+
resolved = resolved.replaceAll('{siteId}', siteId)
|
|
453
|
+
return resolved
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
const applyToBlocks = (blocks) => {
|
|
457
|
+
if (!Array.isArray(blocks))
|
|
458
|
+
return
|
|
459
|
+
blocks.forEach((block) => {
|
|
460
|
+
const meta = block?.meta
|
|
461
|
+
if (!meta || typeof meta !== 'object')
|
|
462
|
+
return
|
|
463
|
+
Object.keys(meta).forEach((fieldKey) => {
|
|
464
|
+
const cfg = meta[fieldKey]
|
|
465
|
+
if (!cfg?.collection?.uniqueKey)
|
|
466
|
+
return
|
|
467
|
+
const resolved = resolveUniqueKey(cfg.collection.uniqueKey)
|
|
468
|
+
if (!resolved)
|
|
469
|
+
return
|
|
470
|
+
if (cfg.queryItems && !Object.prototype.hasOwnProperty.call(cfg, 'uniqueKey')) {
|
|
471
|
+
const reordered = {}
|
|
472
|
+
Object.keys(cfg).forEach((key) => {
|
|
473
|
+
reordered[key] = cfg[key]
|
|
474
|
+
if (key === 'queryItems')
|
|
475
|
+
reordered.uniqueKey = resolved
|
|
476
|
+
})
|
|
477
|
+
block.meta[fieldKey] = reordered
|
|
478
|
+
}
|
|
479
|
+
else {
|
|
480
|
+
cfg.uniqueKey = resolved
|
|
481
|
+
}
|
|
482
|
+
})
|
|
483
|
+
})
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
applyToBlocks(workingDoc?.content)
|
|
487
|
+
applyToBlocks(workingDoc?.postContent)
|
|
488
|
+
}
|
|
489
|
+
|
|
401
490
|
onMounted(() => {
|
|
402
491
|
if (props.page === 'new') {
|
|
403
492
|
state.editMode = true
|
|
@@ -408,6 +497,7 @@ const editorDocUpdates = (workingDoc) => {
|
|
|
408
497
|
ensureStructureDefaults(workingDoc, false)
|
|
409
498
|
if (workingDoc?.post || (Array.isArray(workingDoc?.postContent) && workingDoc.postContent.length > 0) || Array.isArray(workingDoc?.postStructure))
|
|
410
499
|
ensureStructureDefaults(workingDoc, true)
|
|
500
|
+
applyCollectionUniqueKeys(workingDoc)
|
|
411
501
|
const blockIds = (workingDoc.content || []).map(block => block.blockId).filter(id => id)
|
|
412
502
|
const postBlockIds = workingDoc.postContent ? workingDoc.postContent.map(block => block.blockId).filter(id => id) : []
|
|
413
503
|
blockIds.push(...postBlockIds)
|
|
@@ -520,6 +610,15 @@ const layoutSpansFromString = (value, fallback = [6]) => {
|
|
|
520
610
|
|
|
521
611
|
const rowUsesSpans = row => (row?.columns || []).some(col => Number.isFinite(col?.span))
|
|
522
612
|
|
|
613
|
+
const rowGapClass = (row) => {
|
|
614
|
+
const gap = Number(row?.gap)
|
|
615
|
+
const allowed = new Set([0, 2, 4, 6, 8])
|
|
616
|
+
const safeGap = allowed.has(gap) ? gap : 4
|
|
617
|
+
if (safeGap === 0)
|
|
618
|
+
return 'gap-0'
|
|
619
|
+
return ['gap-0', `sm:gap-${safeGap}`].join(' ')
|
|
620
|
+
}
|
|
621
|
+
|
|
523
622
|
const rowGridClass = (row) => {
|
|
524
623
|
const base = isMobilePreview.value
|
|
525
624
|
? 'grid grid-cols-1'
|
|
@@ -542,15 +641,6 @@ const rowVerticalAlignClass = (row) => {
|
|
|
542
641
|
return map[row?.verticalAlign] || map.start
|
|
543
642
|
}
|
|
544
643
|
|
|
545
|
-
const rowGapClass = (row) => {
|
|
546
|
-
const gap = Number(row?.gap)
|
|
547
|
-
const allowed = new Set([0, 2, 4, 6, 8])
|
|
548
|
-
const safeGap = allowed.has(gap) ? gap : 4
|
|
549
|
-
if (safeGap === 0)
|
|
550
|
-
return 'gap-0'
|
|
551
|
-
return ['gap-0', `sm:gap-${safeGap}`].join(' ')
|
|
552
|
-
}
|
|
553
|
-
|
|
554
644
|
const rowGridStyle = (row) => {
|
|
555
645
|
if (isMobilePreview.value)
|
|
556
646
|
return {}
|
|
@@ -791,6 +881,9 @@ const isPublishedPageDiff = (pageId) => {
|
|
|
791
881
|
metaTitle: publishedPage.metaTitle,
|
|
792
882
|
metaDescription: publishedPage.metaDescription,
|
|
793
883
|
structuredData: publishedPage.structuredData,
|
|
884
|
+
postMetaTitle: publishedPage.postMetaTitle,
|
|
885
|
+
postMetaDescription: publishedPage.postMetaDescription,
|
|
886
|
+
postStructuredData: publishedPage.postStructuredData,
|
|
794
887
|
},
|
|
795
888
|
{
|
|
796
889
|
content: draftPage.content,
|
|
@@ -800,6 +893,9 @@ const isPublishedPageDiff = (pageId) => {
|
|
|
800
893
|
metaTitle: draftPage.metaTitle,
|
|
801
894
|
metaDescription: draftPage.metaDescription,
|
|
802
895
|
structuredData: draftPage.structuredData,
|
|
896
|
+
postMetaTitle: draftPage.postMetaTitle,
|
|
897
|
+
postMetaDescription: draftPage.postMetaDescription,
|
|
898
|
+
postStructuredData: draftPage.postStructuredData,
|
|
803
899
|
},
|
|
804
900
|
)
|
|
805
901
|
}
|
|
@@ -997,10 +1093,28 @@ const unpublishedChangeDetails = computed(() => {
|
|
|
997
1093
|
compareField('metaTitle', 'Meta title', val => summarizeChangeValue(val, true))
|
|
998
1094
|
compareField('metaDescription', 'Meta description', val => summarizeChangeValue(val, true))
|
|
999
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))
|
|
1000
1099
|
|
|
1001
1100
|
return changes
|
|
1002
1101
|
})
|
|
1003
1102
|
|
|
1103
|
+
const publishPage = async (pageId) => {
|
|
1104
|
+
if (state.publishLoading)
|
|
1105
|
+
return
|
|
1106
|
+
const pageData = edgeFirebase.data?.[`${edgeGlobal.edgeState.organizationDocPath}/sites/${props.site}/pages`] || {}
|
|
1107
|
+
if (!pageData[pageId])
|
|
1108
|
+
return
|
|
1109
|
+
state.publishLoading = true
|
|
1110
|
+
try {
|
|
1111
|
+
await edgeFirebase.storeDoc(`${edgeGlobal.edgeState.organizationDocPath}/sites/${props.site}/published`, pageData[pageId])
|
|
1112
|
+
}
|
|
1113
|
+
finally {
|
|
1114
|
+
state.publishLoading = false
|
|
1115
|
+
}
|
|
1116
|
+
}
|
|
1117
|
+
|
|
1004
1118
|
const hasUnsavedChanges = (changes) => {
|
|
1005
1119
|
console.log('Unsaved changes:', changes)
|
|
1006
1120
|
if (changes === true) {
|
|
@@ -1034,14 +1148,25 @@ const hasUnsavedChanges = (changes) => {
|
|
|
1034
1148
|
<div class="w-full border-t border-gray-300 dark:border-white/15" aria-hidden="true" />
|
|
1035
1149
|
<div v-if="!props.isTemplateSite" class="px-4 text-gray-600 dark:text-gray-300 whitespace-nowrap text-center flex flex-col items-center gap-1">
|
|
1036
1150
|
<template v-if="isPublishedPageDiff(page)">
|
|
1037
|
-
<
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1151
|
+
<div class="flex items-center gap-2">
|
|
1152
|
+
<edge-shad-button
|
|
1153
|
+
variant="outline"
|
|
1154
|
+
class="bg-yellow-100 text-yellow-800 border-yellow-300 hover:bg-yellow-100 hover:text-yellow-900 text-xs h-[32px] gap-1"
|
|
1155
|
+
@click="state.showUnpublishedChangesDialog = true"
|
|
1156
|
+
>
|
|
1157
|
+
<AlertTriangle class="w-4 h-4" />
|
|
1158
|
+
Unpublished Changes
|
|
1159
|
+
</edge-shad-button>
|
|
1160
|
+
<edge-shad-button
|
|
1161
|
+
class="bg-primary text-primary-foreground hover:bg-primary/90 text-xs h-[32px] gap-1 shadow-sm"
|
|
1162
|
+
:disabled="state.publishLoading"
|
|
1163
|
+
@click="publishPage(page)"
|
|
1164
|
+
>
|
|
1165
|
+
<Loader2 v-if="state.publishLoading" class="w-4 h-4 animate-spin" />
|
|
1166
|
+
<UploadCloud v-else class="w-4 h-4" />
|
|
1167
|
+
Publish
|
|
1168
|
+
</edge-shad-button>
|
|
1169
|
+
</div>
|
|
1045
1170
|
</template>
|
|
1046
1171
|
<template v-else>
|
|
1047
1172
|
<edge-chip class="bg-green-100 text-green-800">
|
|
@@ -1121,6 +1246,23 @@ const hasUnsavedChanges = (changes) => {
|
|
|
1121
1246
|
</div>
|
|
1122
1247
|
</div>
|
|
1123
1248
|
</template>
|
|
1249
|
+
<template #success-alert>
|
|
1250
|
+
<div v-if="!props.isTemplateSite" class="mt-2 flex flex-wrap items-center gap-2">
|
|
1251
|
+
<edge-shad-button
|
|
1252
|
+
variant="outline"
|
|
1253
|
+
class="text-xs h-[28px] gap-1"
|
|
1254
|
+
:disabled="state.seoAiLoading"
|
|
1255
|
+
@click="updateSeoWithAi"
|
|
1256
|
+
>
|
|
1257
|
+
<Loader2 v-if="state.seoAiLoading" class="w-3.5 h-3.5 animate-spin" />
|
|
1258
|
+
<Sparkles v-else class="w-3.5 h-3.5" />
|
|
1259
|
+
Update SEO with AI
|
|
1260
|
+
</edge-shad-button>
|
|
1261
|
+
<span v-if="state.seoAiError" class="text-xs text-destructive">
|
|
1262
|
+
{{ state.seoAiError }}
|
|
1263
|
+
</span>
|
|
1264
|
+
</div>
|
|
1265
|
+
</template>
|
|
1124
1266
|
<template #main="slotProps">
|
|
1125
1267
|
<Tabs class="w-full" default-value="list">
|
|
1126
1268
|
<TabsList v-if="slotProps.workingDoc?.post" class="w-full mt-3 bg-primary rounded-sm">
|