@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
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
<script setup lang="js">
|
|
2
2
|
import { useVModel } from '@vueuse/core'
|
|
3
|
-
import { File, FileCheck, FileCog, FileDown, FileMinus2, FilePen, FilePlus2, FileUp, FileWarning, FileX, Folder, FolderMinus, FolderOpen, FolderPen, FolderPlus } from 'lucide-vue-next'
|
|
3
|
+
import { File, FileCheck, FileCog, FileDown, FileMinus2, FilePen, FilePlus2, FileUp, FileWarning, FileX, Folder, FolderMinus, FolderOpen, FolderPen, FolderPlus, Link } from 'lucide-vue-next'
|
|
4
4
|
import { toTypedSchema } from '@vee-validate/zod'
|
|
5
5
|
import * as z from 'zod'
|
|
6
|
+
import { useStructuredDataTemplates } from '@/edge/composables/structuredDataTemplates'
|
|
6
7
|
|
|
7
8
|
const props = defineProps({
|
|
8
9
|
prevModelValue: {
|
|
@@ -50,6 +51,15 @@ const router = useRouter()
|
|
|
50
51
|
const modelValue = useVModel(props, 'modelValue', emit)
|
|
51
52
|
const route = useRoute()
|
|
52
53
|
const edgeFirebase = inject('edgeFirebase')
|
|
54
|
+
const { buildPageStructuredData } = useStructuredDataTemplates()
|
|
55
|
+
|
|
56
|
+
const isExternalLinkEntry = entry => entry?.item && typeof entry.item === 'object' && entry.item.type === 'external'
|
|
57
|
+
const isPageEntry = entry => typeof entry?.item === 'string'
|
|
58
|
+
const isRenameDisabled = entry => isPageEntry(entry) && !!entry?.disableRename
|
|
59
|
+
const isDeleteDisabled = entry => isPageEntry(entry) && !!entry?.disableDelete
|
|
60
|
+
const isLinkUrlSpecial = url => /^tel:|^mailto:/i.test(String(url || '').trim())
|
|
61
|
+
const linkTarget = url => (isLinkUrlSpecial(url) ? null : '_blank')
|
|
62
|
+
const linkRel = url => (isLinkUrlSpecial(url) ? null : 'noopener noreferrer')
|
|
53
63
|
|
|
54
64
|
const normalizeForCompare = (value) => {
|
|
55
65
|
if (Array.isArray(value))
|
|
@@ -65,6 +75,42 @@ const normalizeForCompare = (value) => {
|
|
|
65
75
|
|
|
66
76
|
const stableSerialize = value => JSON.stringify(normalizeForCompare(value))
|
|
67
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
|
+
}
|
|
68
114
|
|
|
69
115
|
const orderedMenus = computed(() => {
|
|
70
116
|
const menuEntries = Object.entries(modelValue.value || {}).map(([name, menu], originalIndex) => ({
|
|
@@ -107,6 +153,9 @@ const isPublishedPageDiff = (pageId) => {
|
|
|
107
153
|
metaTitle: publishedPage.metaTitle,
|
|
108
154
|
metaDescription: publishedPage.metaDescription,
|
|
109
155
|
structuredData: publishedPage.structuredData,
|
|
156
|
+
postMetaTitle: publishedPage.postMetaTitle,
|
|
157
|
+
postMetaDescription: publishedPage.postMetaDescription,
|
|
158
|
+
postStructuredData: publishedPage.postStructuredData,
|
|
110
159
|
},
|
|
111
160
|
{
|
|
112
161
|
content: draftPage.content,
|
|
@@ -116,6 +165,9 @@ const isPublishedPageDiff = (pageId) => {
|
|
|
116
165
|
metaTitle: draftPage.metaTitle,
|
|
117
166
|
metaDescription: draftPage.metaDescription,
|
|
118
167
|
structuredData: draftPage.structuredData,
|
|
168
|
+
postMetaTitle: draftPage.postMetaTitle,
|
|
169
|
+
postMetaDescription: draftPage.postMetaDescription,
|
|
170
|
+
postStructuredData: draftPage.postStructuredData,
|
|
119
171
|
},
|
|
120
172
|
)
|
|
121
173
|
}
|
|
@@ -142,17 +194,29 @@ const state = reactive({
|
|
|
142
194
|
newPageName: '',
|
|
143
195
|
indexPath: '',
|
|
144
196
|
addMenu: false,
|
|
197
|
+
addLinkDialog: false,
|
|
145
198
|
deletePage: {},
|
|
146
199
|
renameItem: {},
|
|
147
200
|
renameFolderOrPageDialog: false,
|
|
148
201
|
deletePageDialog: false,
|
|
149
202
|
pageSettings: false,
|
|
150
203
|
pageData: {},
|
|
204
|
+
linkDialogMode: 'add',
|
|
205
|
+
linkName: '',
|
|
206
|
+
linkUrl: '',
|
|
207
|
+
linkTargetMenu: '',
|
|
208
|
+
linkTargetIndex: -1,
|
|
151
209
|
newDocs: {
|
|
152
210
|
pages: {
|
|
153
211
|
name: { value: '' },
|
|
154
212
|
content: { value: [] },
|
|
155
213
|
blockIds: { value: [] },
|
|
214
|
+
metaTitle: { value: '' },
|
|
215
|
+
metaDescription: { value: '' },
|
|
216
|
+
structuredData: { value: buildPageStructuredData() },
|
|
217
|
+
postMetaTitle: { value: '' },
|
|
218
|
+
postMetaDescription: { value: '' },
|
|
219
|
+
postStructuredData: { value: '' },
|
|
156
220
|
tags: { value: [] },
|
|
157
221
|
allowedThemes: { value: [] },
|
|
158
222
|
},
|
|
@@ -182,6 +246,8 @@ const templateTagItems = computed(() => {
|
|
|
182
246
|
return [{ name: 'Quick Picks', title: 'Quick Picks' }, ...tagList]
|
|
183
247
|
})
|
|
184
248
|
|
|
249
|
+
const BLANK_TEMPLATE_ID = 'blank'
|
|
250
|
+
|
|
185
251
|
const resetAddPageDialogState = () => {
|
|
186
252
|
state.newPageName = ''
|
|
187
253
|
state.templateFilter = 'quick-picks'
|
|
@@ -194,6 +260,21 @@ watch(() => state.addPageDialog, (open) => {
|
|
|
194
260
|
resetAddPageDialogState()
|
|
195
261
|
})
|
|
196
262
|
|
|
263
|
+
const resetLinkDialogState = () => {
|
|
264
|
+
state.linkDialogMode = 'add'
|
|
265
|
+
state.linkName = ''
|
|
266
|
+
state.linkUrl = ''
|
|
267
|
+
state.linkTargetMenu = ''
|
|
268
|
+
state.linkTargetIndex = -1
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
watch(() => state.addLinkDialog, (open) => {
|
|
272
|
+
if (!open)
|
|
273
|
+
resetLinkDialogState()
|
|
274
|
+
})
|
|
275
|
+
|
|
276
|
+
const TEMPLATE_COLLECTION_PATH = computed(() => `${edgeGlobal.edgeState.organizationDocPath}/sites/templates/pages`)
|
|
277
|
+
|
|
197
278
|
onMounted(async () => {
|
|
198
279
|
if (!edgeGlobal.edgeState.organizationDocPath)
|
|
199
280
|
return
|
|
@@ -202,8 +283,6 @@ onMounted(async () => {
|
|
|
202
283
|
await edgeFirebase.startSnapshot(path)
|
|
203
284
|
})
|
|
204
285
|
|
|
205
|
-
const TEMPLATE_COLLECTION_PATH = computed(() => `${edgeGlobal.edgeState.organizationDocPath}/sites/templates/pages`)
|
|
206
|
-
|
|
207
286
|
const templatePagesCollection = computed(() => {
|
|
208
287
|
return edgeFirebase.data?.[TEMPLATE_COLLECTION_PATH.value] || {}
|
|
209
288
|
})
|
|
@@ -265,8 +344,6 @@ watch(filteredTemplates, (templates) => {
|
|
|
265
344
|
state.selectedTemplateId = BLANK_TEMPLATE_ID
|
|
266
345
|
})
|
|
267
346
|
|
|
268
|
-
const BLANK_TEMPLATE_ID = 'blank'
|
|
269
|
-
|
|
270
347
|
const blankTemplateTile = {
|
|
271
348
|
docId: BLANK_TEMPLATE_ID,
|
|
272
349
|
name: 'Blank Page',
|
|
@@ -314,6 +391,8 @@ const templatePreviewBlocks = (template) => {
|
|
|
314
391
|
}
|
|
315
392
|
|
|
316
393
|
const renameFolderOrPageShow = (item) => {
|
|
394
|
+
if (isRenameDisabled(item))
|
|
395
|
+
return
|
|
317
396
|
// Work on a copy so edits in the dialog do not mutate the live menu entry.
|
|
318
397
|
state.renameItem = edgeGlobal.dupObject(item || {})
|
|
319
398
|
state.renameItem.previousName = item?.name
|
|
@@ -328,6 +407,8 @@ const addPageShow = (menuName, isMenu = false) => {
|
|
|
328
407
|
}
|
|
329
408
|
|
|
330
409
|
const deletePageShow = (page) => {
|
|
410
|
+
if (isDeleteDisabled(page))
|
|
411
|
+
return
|
|
331
412
|
state.deletePage = page
|
|
332
413
|
state.deletePageDialog = true
|
|
333
414
|
}
|
|
@@ -343,6 +424,9 @@ const collectRootLevelSlugs = (excludeName = '') => {
|
|
|
343
424
|
if (entry.name && entry.name !== excludeName)
|
|
344
425
|
slugs.add(entry.name)
|
|
345
426
|
}
|
|
427
|
+
else if (isExternalLinkEntry(entry)) {
|
|
428
|
+
continue
|
|
429
|
+
}
|
|
346
430
|
// Top-level folder at "/<folder>/*"
|
|
347
431
|
else if (entry && typeof entry.item === 'object') {
|
|
348
432
|
const key = Object.keys(entry.item)[0]
|
|
@@ -362,6 +446,9 @@ const collectRootLevelSlugs = (excludeName = '') => {
|
|
|
362
446
|
if (entry.name && entry.name !== excludeName)
|
|
363
447
|
slugs.add(entry.name)
|
|
364
448
|
}
|
|
449
|
+
else if (isExternalLinkEntry(entry)) {
|
|
450
|
+
continue
|
|
451
|
+
}
|
|
365
452
|
// Top-level folder at "/<folder>/*"
|
|
366
453
|
else if (entry && typeof entry.item === 'object') {
|
|
367
454
|
const key = Object.keys(entry.item)[0]
|
|
@@ -380,6 +467,9 @@ const collectRootLevelSlugs = (excludeName = '') => {
|
|
|
380
467
|
if (entry.name && entry.name !== excludeName)
|
|
381
468
|
slugs.add(entry.name)
|
|
382
469
|
}
|
|
470
|
+
else if (isExternalLinkEntry(entry)) {
|
|
471
|
+
continue
|
|
472
|
+
}
|
|
383
473
|
// Top-level folder at "/<folder>/*"
|
|
384
474
|
else if (entry && typeof entry.item === 'object') {
|
|
385
475
|
const key = Object.keys(entry.item)[0]
|
|
@@ -471,6 +561,8 @@ const hydrateSyncedBlocksFromSite = (blocks = []) => {
|
|
|
471
561
|
|
|
472
562
|
const buildPagePayloadFromTemplate = (templateDoc, slug) => {
|
|
473
563
|
const timestamp = Date.now()
|
|
564
|
+
const templateStructuredData = typeof templateDoc?.structuredData === 'string' ? templateDoc.structuredData.trim() : ''
|
|
565
|
+
const structuredData = templateDoc ? (templateStructuredData || buildPageStructuredData()) : buildPageStructuredData()
|
|
474
566
|
const basePayload = {
|
|
475
567
|
name: slug,
|
|
476
568
|
content: [],
|
|
@@ -478,7 +570,10 @@ const buildPagePayloadFromTemplate = (templateDoc, slug) => {
|
|
|
478
570
|
blockIds: [],
|
|
479
571
|
metaTitle: '',
|
|
480
572
|
metaDescription: '',
|
|
481
|
-
structuredData
|
|
573
|
+
structuredData,
|
|
574
|
+
postMetaTitle: '',
|
|
575
|
+
postMetaDescription: '',
|
|
576
|
+
postStructuredData: '',
|
|
482
577
|
doc_created_at: timestamp,
|
|
483
578
|
last_updated: timestamp,
|
|
484
579
|
}
|
|
@@ -492,10 +587,35 @@ const buildPagePayloadFromTemplate = (templateDoc, slug) => {
|
|
|
492
587
|
copy.content = Array.isArray(copy.content) ? hydrateSyncedBlocksFromSite(copy.content) : []
|
|
493
588
|
copy.postContent = Array.isArray(copy.postContent) ? hydrateSyncedBlocksFromSite(copy.postContent) : []
|
|
494
589
|
copy.blockIds = deriveBlockIds(copy)
|
|
590
|
+
if (!String(copy.structuredData || '').trim())
|
|
591
|
+
copy.structuredData = structuredData
|
|
495
592
|
return { ...basePayload, ...copy }
|
|
496
593
|
}
|
|
497
594
|
|
|
498
595
|
const renameFolderOrPageAction = async () => {
|
|
596
|
+
if (isRenameDisabled(state.renameItem)) {
|
|
597
|
+
state.renameFolderOrPageDialog = false
|
|
598
|
+
state.renameItem = {}
|
|
599
|
+
return
|
|
600
|
+
}
|
|
601
|
+
if (isExternalLinkEntry(state.renameItem)) {
|
|
602
|
+
const nextName = state.renameItem.name?.trim() || ''
|
|
603
|
+
if (!nextName) {
|
|
604
|
+
state.renameFolderOrPageDialog = false
|
|
605
|
+
state.renameItem = {}
|
|
606
|
+
return
|
|
607
|
+
}
|
|
608
|
+
const menuName = state.renameItem.menuName
|
|
609
|
+
const index = state.renameItem.index
|
|
610
|
+
if (menuName && Number.isInteger(index) && Array.isArray(modelValue.value[menuName])) {
|
|
611
|
+
const target = modelValue.value[menuName][index]
|
|
612
|
+
if (target)
|
|
613
|
+
target.name = nextName
|
|
614
|
+
}
|
|
615
|
+
state.renameFolderOrPageDialog = false
|
|
616
|
+
state.renameItem = {}
|
|
617
|
+
return
|
|
618
|
+
}
|
|
499
619
|
const newSlug = slugGenerator(state.renameItem.name, state.renameItem.previousName || '')
|
|
500
620
|
|
|
501
621
|
if (state.renameItem.name === state.renameItem.previousName) {
|
|
@@ -582,7 +702,70 @@ const addPageAction = async () => {
|
|
|
582
702
|
|
|
583
703
|
state.addPageDialog = false
|
|
584
704
|
}
|
|
705
|
+
|
|
706
|
+
const addLinkShow = (menuName) => {
|
|
707
|
+
state.linkDialogMode = 'add'
|
|
708
|
+
state.linkTargetMenu = menuName
|
|
709
|
+
state.addLinkDialog = true
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
const editLinkShow = (menuName, index, entry) => {
|
|
713
|
+
state.linkDialogMode = 'edit'
|
|
714
|
+
state.linkTargetMenu = menuName
|
|
715
|
+
state.linkTargetIndex = index
|
|
716
|
+
state.linkName = entry?.name || ''
|
|
717
|
+
state.linkUrl = entry?.item?.url || ''
|
|
718
|
+
state.addLinkDialog = true
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
const linkDialogSchema = toTypedSchema(z.object({
|
|
722
|
+
name: z.string({
|
|
723
|
+
required_error: 'Name is required',
|
|
724
|
+
}).min(1, { message: 'Name is required' }),
|
|
725
|
+
url: z.string({
|
|
726
|
+
required_error: 'URL is required',
|
|
727
|
+
}).min(1, { message: 'URL is required' }),
|
|
728
|
+
}))
|
|
729
|
+
|
|
730
|
+
const submitLinkDialog = () => {
|
|
731
|
+
const name = state.linkName?.trim() || ''
|
|
732
|
+
const url = state.linkUrl?.trim() || ''
|
|
733
|
+
if (!name || !url)
|
|
734
|
+
return
|
|
735
|
+
const payload = { name, item: { type: 'external', url } }
|
|
736
|
+
if (!Array.isArray(modelValue.value[state.linkTargetMenu]))
|
|
737
|
+
modelValue.value[state.linkTargetMenu] = []
|
|
738
|
+
if (state.linkDialogMode === 'edit') {
|
|
739
|
+
const target = modelValue.value[state.linkTargetMenu]?.[state.linkTargetIndex]
|
|
740
|
+
if (target) {
|
|
741
|
+
target.name = name
|
|
742
|
+
target.item = { type: 'external', url }
|
|
743
|
+
}
|
|
744
|
+
else {
|
|
745
|
+
modelValue.value[state.linkTargetMenu].push(payload)
|
|
746
|
+
}
|
|
747
|
+
}
|
|
748
|
+
else {
|
|
749
|
+
modelValue.value[state.linkTargetMenu].push(payload)
|
|
750
|
+
}
|
|
751
|
+
state.addLinkDialog = false
|
|
752
|
+
}
|
|
585
753
|
const deletePageAction = async () => {
|
|
754
|
+
if (isDeleteDisabled(state.deletePage)) {
|
|
755
|
+
state.deletePageDialog = false
|
|
756
|
+
state.deletePage = {}
|
|
757
|
+
return
|
|
758
|
+
}
|
|
759
|
+
if (isExternalLinkEntry(state.deletePage)) {
|
|
760
|
+
const menuName = state.deletePage.menuName
|
|
761
|
+
const index = state.deletePage.index
|
|
762
|
+
if (menuName && Number.isInteger(index) && Array.isArray(modelValue.value[menuName])) {
|
|
763
|
+
modelValue.value[menuName].splice(index, 1)
|
|
764
|
+
}
|
|
765
|
+
state.deletePageDialog = false
|
|
766
|
+
state.deletePage = {}
|
|
767
|
+
return
|
|
768
|
+
}
|
|
586
769
|
if (state.deletePage.item === '') {
|
|
587
770
|
// deleting a folder
|
|
588
771
|
delete modelValue.value[state.deletePage.name]
|
|
@@ -662,6 +845,10 @@ const showPageSettings = (page) => {
|
|
|
662
845
|
state.pageSettings = true
|
|
663
846
|
}
|
|
664
847
|
|
|
848
|
+
const handlePageWorkingDoc = (doc) => {
|
|
849
|
+
ensurePostSeoDefaults(doc)
|
|
850
|
+
}
|
|
851
|
+
|
|
665
852
|
const formErrors = (error) => {
|
|
666
853
|
console.log('Form errors:', error)
|
|
667
854
|
console.log(Object.values(error))
|
|
@@ -701,11 +888,11 @@ const theme = computed(() => {
|
|
|
701
888
|
|
|
702
889
|
<template>
|
|
703
890
|
<SidebarMenuItem v-for="({ menu, name: menuName }) in orderedMenus" :key="menuName">
|
|
704
|
-
<SidebarMenuButton class="!px-0 hover:!bg-transparent">
|
|
891
|
+
<SidebarMenuButton class="group !px-0 hover:!bg-transparent">
|
|
705
892
|
<FolderOpen
|
|
706
|
-
class="mr-2"
|
|
893
|
+
class="mr-2 group-hover:text-black"
|
|
707
894
|
/>
|
|
708
|
-
<span v-if="!props.isTemplateSite">{{ menuName === 'Site Root' ? 'Site Menu' : menuName }}</span>
|
|
895
|
+
<span v-if="!props.isTemplateSite" class="hover:text-black !text-black">{{ menuName === 'Site Root' ? 'Site Menu' : menuName }}</span>
|
|
709
896
|
<SidebarGroupAction class="absolute right-2 top-0 hover:!bg-transparent">
|
|
710
897
|
<DropdownMenu>
|
|
711
898
|
<DropdownMenuTrigger as-child>
|
|
@@ -725,6 +912,10 @@ const theme = computed(() => {
|
|
|
725
912
|
<FilePlus2 />
|
|
726
913
|
<span>New Page</span>
|
|
727
914
|
</DropdownMenuItem>
|
|
915
|
+
<DropdownMenuItem v-if="!props.isTemplateSite" @click="addLinkShow(menuName)">
|
|
916
|
+
<Link />
|
|
917
|
+
<span>New Link</span>
|
|
918
|
+
</DropdownMenuItem>
|
|
728
919
|
<DropdownMenuItem v-if="!props.prevMenu && !props.isTemplateSite" @click="addPageShow(menuName, true)">
|
|
729
920
|
<FolderPlus />
|
|
730
921
|
<span>New Folder</span>
|
|
@@ -754,7 +945,7 @@ const theme = computed(() => {
|
|
|
754
945
|
<template #item="{ element, index }">
|
|
755
946
|
<div class="handle list-group-item">
|
|
756
947
|
<edge-cms-menu
|
|
757
|
-
v-if="typeof element.item === 'object'"
|
|
948
|
+
v-if="typeof element.item === 'object' && !isExternalLinkEntry(element)"
|
|
758
949
|
v-model="modelValue[menuName][index].item"
|
|
759
950
|
:prev-menu="menuName"
|
|
760
951
|
:prev-model-value="modelValue"
|
|
@@ -764,13 +955,33 @@ const theme = computed(() => {
|
|
|
764
955
|
:is-template-site="props.isTemplateSite"
|
|
765
956
|
/>
|
|
766
957
|
<SidebarMenuSubItem v-else class="relative">
|
|
767
|
-
<SidebarMenuSubButton
|
|
768
|
-
|
|
958
|
+
<SidebarMenuSubButton
|
|
959
|
+
:class="{ 'text-gray-400': element.item === '' }"
|
|
960
|
+
as-child
|
|
961
|
+
:is-active="!isExternalLinkEntry(element) && element.item === props.page"
|
|
962
|
+
>
|
|
963
|
+
<NuxtLink
|
|
964
|
+
v-if="!isExternalLinkEntry(element)"
|
|
965
|
+
:disabled="element.item === ''"
|
|
966
|
+
:class="{ '!text-red-500': element.name === 'Deleting...' }"
|
|
967
|
+
class="text-xs"
|
|
968
|
+
:to="`${pageRouteBase}/${element.item}`"
|
|
969
|
+
>
|
|
769
970
|
<Loader2 v-if="element.item === '' || element.name === 'Deleting...'" :class="{ '!text-red-500': element.name === 'Deleting...' }" class="w-4 h-4 animate-spin" />
|
|
770
971
|
<FileWarning v-else-if="isPublishedPageDiff(element.item) && !props.isTemplateSite" class="!text-yellow-600" />
|
|
771
972
|
<FileCheck v-else class="text-xs !text-green-700 font-normal" />
|
|
772
973
|
<span>{{ element.name }}</span>
|
|
773
974
|
</NuxtLink>
|
|
975
|
+
<a
|
|
976
|
+
v-else
|
|
977
|
+
:href="element.item?.url || '#'"
|
|
978
|
+
class="text-xs inline-flex items-center gap-2"
|
|
979
|
+
:target="linkTarget(element.item?.url)"
|
|
980
|
+
:rel="linkRel(element.item?.url)"
|
|
981
|
+
>
|
|
982
|
+
<Link class="w-4 h-4 text-muted-foreground" />
|
|
983
|
+
<span>{{ element.name }}</span>
|
|
984
|
+
</a>
|
|
774
985
|
</SidebarMenuSubButton>
|
|
775
986
|
<div class="absolute right-0 -top-0.5">
|
|
776
987
|
<DropdownMenu>
|
|
@@ -787,34 +998,51 @@ const theme = computed(() => {
|
|
|
787
998
|
<File class="w-5 h-5" /> {{ ROOT_MENUS.includes(menuName) ? '' : menuName }}/{{ element.name }}
|
|
788
999
|
</DropdownMenuLabel>
|
|
789
1000
|
<DropdownMenuSeparator />
|
|
790
|
-
<
|
|
791
|
-
<
|
|
792
|
-
|
|
793
|
-
<
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
<
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
<
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
<
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
<
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
<
|
|
816
|
-
|
|
817
|
-
|
|
1001
|
+
<template v-if="!isExternalLinkEntry(element)">
|
|
1002
|
+
<DropdownMenuItem :disabled="edgeGlobal.edgeState.cmsPageWithUnsavedChanges === element.item" @click="showPageSettings(element)">
|
|
1003
|
+
<FileCog />
|
|
1004
|
+
<div class="flex flex-col">
|
|
1005
|
+
<span>Settings</span>
|
|
1006
|
+
<span v-if="edgeGlobal.edgeState.cmsPageWithUnsavedChanges === element.item" class="text-xs text-red-500">(Unsaved Changes)</span>
|
|
1007
|
+
</div>
|
|
1008
|
+
</DropdownMenuItem>
|
|
1009
|
+
<DropdownMenuItem v-if="!props.isTemplateSite && isPublishedPageDiff(element.item)" @click="publishPage(element.item)">
|
|
1010
|
+
<FileUp />
|
|
1011
|
+
Publish
|
|
1012
|
+
</DropdownMenuItem>
|
|
1013
|
+
<DropdownMenuItem :disabled="isRenameDisabled(element)" @click="renameFolderOrPageShow({ ...element, menuName, index })">
|
|
1014
|
+
<FilePen />
|
|
1015
|
+
<span>Rename</span>
|
|
1016
|
+
</DropdownMenuItem>
|
|
1017
|
+
<DropdownMenuSeparator />
|
|
1018
|
+
<DropdownMenuItem v-if="!props.isTemplateSite && isPublishedPageDiff(element.item) && isPublished(element.item)" @click="discardPageChanges(element.item)">
|
|
1019
|
+
<FileX />
|
|
1020
|
+
Discard Changes
|
|
1021
|
+
</DropdownMenuItem>
|
|
1022
|
+
<DropdownMenuItem v-if="!props.isTemplateSite && isPublished(element.item)" @click="unPublishPage(element.item)">
|
|
1023
|
+
<FileDown />
|
|
1024
|
+
Unpublish
|
|
1025
|
+
</DropdownMenuItem>
|
|
1026
|
+
<DropdownMenuItem class="text-destructive" :disabled="isDeleteDisabled(element)" @click="deletePageShow({ ...element, menuName, index })">
|
|
1027
|
+
<FileMinus2 />
|
|
1028
|
+
<span>Delete</span>
|
|
1029
|
+
</DropdownMenuItem>
|
|
1030
|
+
</template>
|
|
1031
|
+
<template v-else>
|
|
1032
|
+
<DropdownMenuItem @click="editLinkShow(menuName, index, element)">
|
|
1033
|
+
<Link />
|
|
1034
|
+
<span>Edit Link</span>
|
|
1035
|
+
</DropdownMenuItem>
|
|
1036
|
+
<DropdownMenuItem @click="renameFolderOrPageShow({ ...element, menuName, index })">
|
|
1037
|
+
<FilePen />
|
|
1038
|
+
<span>Rename</span>
|
|
1039
|
+
</DropdownMenuItem>
|
|
1040
|
+
<DropdownMenuSeparator />
|
|
1041
|
+
<DropdownMenuItem class="text-destructive" @click="deletePageShow({ ...element, menuName, index })">
|
|
1042
|
+
<FileMinus2 />
|
|
1043
|
+
<span>Delete</span>
|
|
1044
|
+
</DropdownMenuItem>
|
|
1045
|
+
</template>
|
|
818
1046
|
</DropdownMenuContent>
|
|
819
1047
|
</DropdownMenu>
|
|
820
1048
|
</div>
|
|
@@ -831,6 +1059,7 @@ const theme = computed(() => {
|
|
|
831
1059
|
<DialogHeader>
|
|
832
1060
|
<DialogTitle class="text-left">
|
|
833
1061
|
<span v-if="state.deletePage.item === ''">Delete Folder "{{ state.deletePage.name }}"</span>
|
|
1062
|
+
<span v-else-if="isExternalLinkEntry(state.deletePage)">Delete Link "{{ state.deletePage.name }}"</span>
|
|
834
1063
|
<span v-else>Delete Page "{{ state.deletePage.name }}"</span>
|
|
835
1064
|
</DialogTitle>
|
|
836
1065
|
<DialogDescription />
|
|
@@ -847,7 +1076,9 @@ const theme = computed(() => {
|
|
|
847
1076
|
<edge-shad-button
|
|
848
1077
|
variant="destructive" class="text-white w-full" @click="deletePageAction()"
|
|
849
1078
|
>
|
|
850
|
-
Delete
|
|
1079
|
+
<span v-if="state.deletePage.item === ''">Delete Folder</span>
|
|
1080
|
+
<span v-else-if="isExternalLinkEntry(state.deletePage)">Delete Link</span>
|
|
1081
|
+
<span v-else>Delete Page</span>
|
|
851
1082
|
</edge-shad-button>
|
|
852
1083
|
</DialogFooter>
|
|
853
1084
|
</DialogContent>
|
|
@@ -962,6 +1193,32 @@ const theme = computed(() => {
|
|
|
962
1193
|
</edge-shad-form>
|
|
963
1194
|
</DialogContent>
|
|
964
1195
|
</edge-shad-dialog>
|
|
1196
|
+
<edge-shad-dialog v-model="state.addLinkDialog">
|
|
1197
|
+
<DialogContent class="pt-10">
|
|
1198
|
+
<edge-shad-form :schema="linkDialogSchema" @submit="submitLinkDialog">
|
|
1199
|
+
<DialogHeader>
|
|
1200
|
+
<DialogTitle class="text-left">
|
|
1201
|
+
<span v-if="state.linkDialogMode === 'edit'">Edit Link</span>
|
|
1202
|
+
<span v-else>Add link to "{{ state.linkTargetMenu }}"</span>
|
|
1203
|
+
</DialogTitle>
|
|
1204
|
+
<DialogDescription />
|
|
1205
|
+
</DialogHeader>
|
|
1206
|
+
<div class="space-y-4">
|
|
1207
|
+
<edge-shad-input v-model="state.linkName" name="name" label="Label" placeholder="Link label" />
|
|
1208
|
+
<edge-shad-input v-model="state.linkUrl" name="url" label="URL" placeholder="https://example.com or tel:123-456-7890" />
|
|
1209
|
+
</div>
|
|
1210
|
+
<DialogFooter class="pt-2 flex justify-between">
|
|
1211
|
+
<edge-shad-button type="button" variant="destructive" @click="state.addLinkDialog = false">
|
|
1212
|
+
Cancel
|
|
1213
|
+
</edge-shad-button>
|
|
1214
|
+
<edge-shad-button type="submit" class="text-white bg-slate-800 hover:bg-slate-400 w-full">
|
|
1215
|
+
<span v-if="state.linkDialogMode === 'edit'">Update Link</span>
|
|
1216
|
+
<span v-else>Add Link</span>
|
|
1217
|
+
</edge-shad-button>
|
|
1218
|
+
</DialogFooter>
|
|
1219
|
+
</edge-shad-form>
|
|
1220
|
+
</DialogContent>
|
|
1221
|
+
</edge-shad-dialog>
|
|
965
1222
|
<edge-shad-dialog
|
|
966
1223
|
v-model="state.renameFolderOrPageDialog"
|
|
967
1224
|
>
|
|
@@ -975,6 +1232,12 @@ const theme = computed(() => {
|
|
|
975
1232
|
<DialogDescription />
|
|
976
1233
|
</DialogHeader>
|
|
977
1234
|
<edge-shad-input v-model="state.renameItem.name" name="name" placeholder="New Name" />
|
|
1235
|
+
<p
|
|
1236
|
+
v-if="state.renameItem.item !== ''"
|
|
1237
|
+
class="rounded-md border border-red-200 bg-red-50 px-3 py-2 text-xs font-medium text-red-700"
|
|
1238
|
+
>
|
|
1239
|
+
Renaming a page changes its URL. If search engines already indexed the old URL, rankings may drop temporarily.
|
|
1240
|
+
</p>
|
|
978
1241
|
<DialogFooter class="pt-2 flex justify-between">
|
|
979
1242
|
<edge-shad-button variant="destructive" @click="state.renameFolderOrPageDialog = false">
|
|
980
1243
|
Cancel
|
|
@@ -1003,6 +1266,7 @@ const theme = computed(() => {
|
|
|
1003
1266
|
:save-function-override="onSubmit"
|
|
1004
1267
|
card-content-class="px-0"
|
|
1005
1268
|
@error="formErrors"
|
|
1269
|
+
@working-doc="handlePageWorkingDoc"
|
|
1006
1270
|
>
|
|
1007
1271
|
<template #main="slotProps">
|
|
1008
1272
|
<div class="p-6 space-y-4 h-[calc(100vh-142px)] overflow-y-auto">
|
|
@@ -1024,7 +1288,7 @@ const theme = computed(() => {
|
|
|
1024
1288
|
:allow-additions="true"
|
|
1025
1289
|
/>
|
|
1026
1290
|
<edge-shad-select-tags
|
|
1027
|
-
v-if="props.themeOptions.length"
|
|
1291
|
+
v-if="props.isTemplateSite && props.themeOptions.length"
|
|
1028
1292
|
:model-value="Array.isArray(slotProps.workingDoc.allowedThemes) ? slotProps.workingDoc.allowedThemes : []"
|
|
1029
1293
|
name="allowedThemes"
|
|
1030
1294
|
label="Allowed Themes"
|
|
@@ -1042,24 +1306,83 @@ const theme = computed(() => {
|
|
|
1042
1306
|
<CardDescription>Meta tags for the page.</CardDescription>
|
|
1043
1307
|
</CardHeader>
|
|
1044
1308
|
<CardContent class="pt-0">
|
|
1045
|
-
<
|
|
1046
|
-
v-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
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>
|
|
1063
1386
|
</CardContent>
|
|
1064
1387
|
</Card>
|
|
1065
1388
|
</div>
|
|
@@ -1067,7 +1390,7 @@ const theme = computed(() => {
|
|
|
1067
1390
|
<edge-shad-button variant="destructive" class="text-white" @click="state.pageSettings = false">
|
|
1068
1391
|
Cancel
|
|
1069
1392
|
</edge-shad-button>
|
|
1070
|
-
<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">
|
|
1071
1394
|
<Loader2 v-if="slotProps.submitting" class=" h-4 w-4 animate-spin" />
|
|
1072
1395
|
Update
|
|
1073
1396
|
</edge-shad-button>
|