@edgedev/create-edge-app 1.2.33 → 1.2.35
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/README.md +1 -0
- package/agents.md +95 -2
- package/deploy.sh +136 -0
- package/edge/components/cms/block.vue +977 -305
- package/edge/components/cms/blockApi.vue +3 -3
- package/edge/components/cms/blockEditor.vue +688 -86
- package/edge/components/cms/blockPicker.vue +31 -5
- package/edge/components/cms/blockRender.vue +3 -3
- package/edge/components/cms/blocksManager.vue +790 -82
- package/edge/components/cms/codeEditor.vue +15 -6
- package/edge/components/cms/fontUpload.vue +318 -2
- package/edge/components/cms/htmlContent.vue +825 -93
- package/edge/components/cms/init_blocks/contact_us.html +55 -47
- package/edge/components/cms/init_blocks/newsletter.html +56 -96
- package/edge/components/cms/menu.vue +96 -34
- package/edge/components/cms/page.vue +902 -58
- package/edge/components/cms/posts.vue +13 -4
- package/edge/components/cms/site.vue +638 -87
- package/edge/components/cms/siteSettingsForm.vue +19 -9
- package/edge/components/cms/sitesManager.vue +5 -4
- package/edge/components/cms/themeDefaultMenu.vue +20 -2
- package/edge/components/cms/themeEditor.vue +196 -162
- package/edge/components/editor.vue +5 -1
- package/edge/composables/global.ts +37 -5
- package/edge/composables/siteSettingsTemplate.js +2 -0
- package/edge/composables/useCmsNewDocs.js +100 -0
- package/edge/composables/useEdgeCmsDialogPositionFix.js +19 -0
- package/edge/routes/cms/dashboard/blocks/[block].vue +5 -0
- package/edge/routes/cms/dashboard/blocks/index.vue +12 -1
- package/edge/routes/cms/dashboard/media/index.vue +5 -0
- package/edge/routes/cms/dashboard/sites/[site]/[[page]].vue +4 -0
- package/edge/routes/cms/dashboard/sites/[site].vue +4 -0
- package/edge/routes/cms/dashboard/sites/index.vue +4 -0
- package/edge/routes/cms/dashboard/templates/[page].vue +4 -0
- package/edge/routes/cms/dashboard/templates/index.vue +4 -0
- package/edge/routes/cms/dashboard/themes/[theme].vue +5 -0
- package/edge/routes/cms/dashboard/themes/index.vue +330 -1
- package/edge-pull.sh +16 -2
- package/edge-push.sh +9 -1
- package/edge-remote.sh +20 -0
- package/edge-status.sh +9 -5
- package/edge-update-all.sh +127 -0
- package/firebase.json +4 -0
- package/nuxt.config.ts +1 -1
- package/package.json +2 -2
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
<script setup>
|
|
2
|
-
import { AlertTriangle, ArrowDown, ArrowUp, Maximize2, Monitor, Smartphone, Sparkles, Tablet, UploadCloud } from 'lucide-vue-next'
|
|
2
|
+
import { AlertTriangle, ArrowDown, ArrowUp, Download, 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,7 +20,18 @@ const props = defineProps({
|
|
|
20
20
|
const emit = defineEmits(['head'])
|
|
21
21
|
|
|
22
22
|
const edgeFirebase = inject('edgeFirebase')
|
|
23
|
+
const router = useRouter()
|
|
23
24
|
const { buildPageStructuredData } = useStructuredDataTemplates()
|
|
25
|
+
const cmsMultiOrg = useState('cmsMultiOrg', () => true)
|
|
26
|
+
const isAdmin = computed(() => edgeGlobal.isAdminGlobal(edgeFirebase).value)
|
|
27
|
+
const isDevModeEnabled = computed(() => process.dev || Boolean(edgeGlobal.edgeState.devOverride))
|
|
28
|
+
const canOpenPreviewBlockContentEditor = computed(() => {
|
|
29
|
+
if (!isAdmin.value)
|
|
30
|
+
return false
|
|
31
|
+
if (cmsMultiOrg.value)
|
|
32
|
+
return true
|
|
33
|
+
return isDevModeEnabled.value
|
|
34
|
+
})
|
|
24
35
|
|
|
25
36
|
const state = reactive({
|
|
26
37
|
newDocs: {
|
|
@@ -41,7 +52,15 @@ const state = reactive({
|
|
|
41
52
|
workingDoc: {},
|
|
42
53
|
seoAiLoading: false,
|
|
43
54
|
seoAiError: '',
|
|
55
|
+
importingJson: false,
|
|
56
|
+
importDocIdDialogOpen: false,
|
|
57
|
+
importDocIdValue: '',
|
|
58
|
+
importConflictDialogOpen: false,
|
|
59
|
+
importConflictDocId: '',
|
|
60
|
+
importErrorDialogOpen: false,
|
|
61
|
+
importErrorMessage: '',
|
|
44
62
|
previewViewport: 'full',
|
|
63
|
+
previewPageView: 'list',
|
|
45
64
|
newRowLayout: '6',
|
|
46
65
|
newPostRowLayout: '6',
|
|
47
66
|
rowSettings: {
|
|
@@ -68,6 +87,10 @@ const state = reactive({
|
|
|
68
87
|
},
|
|
69
88
|
})
|
|
70
89
|
|
|
90
|
+
const pageImportInputRef = ref(null)
|
|
91
|
+
const pageImportDocIdResolver = ref(null)
|
|
92
|
+
const pageImportConflictResolver = ref(null)
|
|
93
|
+
|
|
71
94
|
const schemas = {
|
|
72
95
|
pages: toTypedSchema(z.object({
|
|
73
96
|
name: z.string({
|
|
@@ -97,10 +120,28 @@ const previewViewportStyle = computed(() => {
|
|
|
97
120
|
}
|
|
98
121
|
})
|
|
99
122
|
|
|
123
|
+
const previewViewportContainStyle = computed(() => {
|
|
124
|
+
return {
|
|
125
|
+
...(previewViewportStyle.value || {}),
|
|
126
|
+
}
|
|
127
|
+
})
|
|
128
|
+
|
|
100
129
|
const setPreviewViewport = (viewportId) => {
|
|
101
130
|
state.previewViewport = viewportId
|
|
102
131
|
}
|
|
103
132
|
|
|
133
|
+
const hasPostView = (workingDoc) => {
|
|
134
|
+
if (!workingDoc || typeof workingDoc !== 'object')
|
|
135
|
+
return false
|
|
136
|
+
return Boolean(workingDoc.post)
|
|
137
|
+
|| (Array.isArray(workingDoc.postContent) && workingDoc.postContent.length > 0)
|
|
138
|
+
|| (Array.isArray(workingDoc.postStructure) && workingDoc.postStructure.length > 0)
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
const setPreviewPageView = (view) => {
|
|
142
|
+
state.previewPageView = view === 'post' ? 'post' : 'list'
|
|
143
|
+
}
|
|
144
|
+
|
|
104
145
|
const previewViewportMode = computed(() => {
|
|
105
146
|
if (state.previewViewport === 'full')
|
|
106
147
|
return 'auto'
|
|
@@ -108,6 +149,13 @@ const previewViewportMode = computed(() => {
|
|
|
108
149
|
})
|
|
109
150
|
|
|
110
151
|
const isMobilePreview = computed(() => previewViewportMode.value === 'mobile')
|
|
152
|
+
const pagePreviewRenderKey = computed(() => {
|
|
153
|
+
const siteKey = String(props.site || '')
|
|
154
|
+
const pageKey = String(props.page || '')
|
|
155
|
+
const themeKey = String(effectiveThemeId.value || selectedThemeId.value || 'no-theme')
|
|
156
|
+
const modeKey = state.editMode ? 'edit' : 'preview'
|
|
157
|
+
return `${siteKey}:${pageKey}:${themeKey}:${modeKey}`
|
|
158
|
+
})
|
|
111
159
|
|
|
112
160
|
const GRID_CLASSES = {
|
|
113
161
|
1: 'grid grid-cols-1 gap-4',
|
|
@@ -135,6 +183,14 @@ const ROW_GAP_OPTIONS = [
|
|
|
135
183
|
{ name: '8', title: 'X-Large' },
|
|
136
184
|
]
|
|
137
185
|
|
|
186
|
+
const ROW_GAP_CLASS_MAP = {
|
|
187
|
+
0: 'gap-0 sm:gap-0',
|
|
188
|
+
2: 'gap-0 sm:gap-2',
|
|
189
|
+
4: 'gap-0 sm:gap-4',
|
|
190
|
+
6: 'gap-0 sm:gap-6',
|
|
191
|
+
8: 'gap-0 sm:gap-8',
|
|
192
|
+
}
|
|
193
|
+
|
|
138
194
|
const ROW_MOBILE_STACK_OPTIONS = [
|
|
139
195
|
{ name: 'normal', title: 'Left first' },
|
|
140
196
|
{ name: 'reverse', title: 'Right first' },
|
|
@@ -440,17 +496,51 @@ const blockPick = (block, index, slotProps, post = false) => {
|
|
|
440
496
|
}
|
|
441
497
|
|
|
442
498
|
const applyCollectionUniqueKeys = (workingDoc) => {
|
|
499
|
+
const hasTemplateToken = (value) => {
|
|
500
|
+
if (typeof value === 'string')
|
|
501
|
+
return value.includes('{orgId}') || value.includes('{siteId}')
|
|
502
|
+
if (Array.isArray(value))
|
|
503
|
+
return value.some(entry => hasTemplateToken(entry))
|
|
504
|
+
if (value && typeof value === 'object')
|
|
505
|
+
return Object.values(value).some(entry => hasTemplateToken(entry))
|
|
506
|
+
return false
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
const resolveTokens = (value) => {
|
|
510
|
+
if (typeof value === 'string') {
|
|
511
|
+
let resolved = value
|
|
512
|
+
const orgId = edgeGlobal.edgeState.currentOrganization || ''
|
|
513
|
+
const siteId = props.site || ''
|
|
514
|
+
if (resolved.includes('{orgId}') && orgId)
|
|
515
|
+
resolved = resolved.replaceAll('{orgId}', orgId)
|
|
516
|
+
if (resolved.includes('{siteId}') && siteId)
|
|
517
|
+
resolved = resolved.replaceAll('{siteId}', siteId)
|
|
518
|
+
return resolved
|
|
519
|
+
}
|
|
520
|
+
if (Array.isArray(value))
|
|
521
|
+
return value.map(entry => resolveTokens(entry))
|
|
522
|
+
if (value && typeof value === 'object') {
|
|
523
|
+
const out = {}
|
|
524
|
+
Object.keys(value).forEach((key) => {
|
|
525
|
+
out[key] = resolveTokens(value[key])
|
|
526
|
+
})
|
|
527
|
+
return out
|
|
528
|
+
}
|
|
529
|
+
return value
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
const isEmptyQueryItem = (value) => {
|
|
533
|
+
if (value === undefined || value === null || value === '')
|
|
534
|
+
return true
|
|
535
|
+
if (Array.isArray(value))
|
|
536
|
+
return value.length === 0
|
|
537
|
+
return false
|
|
538
|
+
}
|
|
539
|
+
|
|
443
540
|
const resolveUniqueKey = (template) => {
|
|
444
541
|
if (!template || typeof template !== 'string')
|
|
445
542
|
return ''
|
|
446
|
-
|
|
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
|
|
543
|
+
return resolveTokens(template)
|
|
454
544
|
}
|
|
455
545
|
|
|
456
546
|
const applyToBlocks = (blocks) => {
|
|
@@ -462,6 +552,27 @@ const applyCollectionUniqueKeys = (workingDoc) => {
|
|
|
462
552
|
return
|
|
463
553
|
Object.keys(meta).forEach((fieldKey) => {
|
|
464
554
|
const cfg = meta[fieldKey]
|
|
555
|
+
if (!cfg || typeof cfg !== 'object')
|
|
556
|
+
return
|
|
557
|
+
|
|
558
|
+
// Materialize tokenized collection.query filters (e.g. {siteId}) into queryItems
|
|
559
|
+
// so frontend hydration receives concrete runtime filter selections.
|
|
560
|
+
if (Array.isArray(cfg?.collection?.query)) {
|
|
561
|
+
if (!cfg.queryItems || typeof cfg.queryItems !== 'object')
|
|
562
|
+
cfg.queryItems = {}
|
|
563
|
+
for (const queryFilter of cfg.collection.query) {
|
|
564
|
+
const queryField = queryFilter?.field
|
|
565
|
+
if (!queryField || typeof queryField !== 'string')
|
|
566
|
+
continue
|
|
567
|
+
const rawValue = queryFilter?.value
|
|
568
|
+
if (!hasTemplateToken(rawValue))
|
|
569
|
+
continue
|
|
570
|
+
if (!isEmptyQueryItem(cfg.queryItems[queryField]))
|
|
571
|
+
continue
|
|
572
|
+
cfg.queryItems[queryField] = resolveTokens(rawValue)
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
|
|
465
576
|
if (!cfg?.collection?.uniqueKey)
|
|
466
577
|
return
|
|
467
578
|
const resolved = resolveUniqueKey(cfg.collection.uniqueKey)
|
|
@@ -493,11 +604,48 @@ onMounted(() => {
|
|
|
493
604
|
}
|
|
494
605
|
})
|
|
495
606
|
|
|
607
|
+
const previewSnapshotsBootstrapping = ref(false)
|
|
608
|
+
|
|
609
|
+
const ensurePreviewSnapshots = async () => {
|
|
610
|
+
const orgId = String(edgeGlobal.edgeState.currentOrganization || '').trim()
|
|
611
|
+
if (!orgId)
|
|
612
|
+
return
|
|
613
|
+
|
|
614
|
+
if (previewSnapshotsBootstrapping.value)
|
|
615
|
+
return
|
|
616
|
+
previewSnapshotsBootstrapping.value = true
|
|
617
|
+
|
|
618
|
+
const themesPath = `organizations/${orgId}/themes`
|
|
619
|
+
const sitesPath = `organizations/${orgId}/sites`
|
|
620
|
+
|
|
621
|
+
// Non-blocking bootstrap: never hold page render on snapshot latency.
|
|
622
|
+
try {
|
|
623
|
+
if (!edgeFirebase.data?.[themesPath]) {
|
|
624
|
+
await edgeFirebase.startSnapshot(themesPath)
|
|
625
|
+
}
|
|
626
|
+
if (!edgeFirebase.data?.[sitesPath]) {
|
|
627
|
+
await edgeFirebase.startSnapshot(sitesPath)
|
|
628
|
+
}
|
|
629
|
+
}
|
|
630
|
+
catch (error) {
|
|
631
|
+
console.error('Failed to start page preview snapshots', error)
|
|
632
|
+
}
|
|
633
|
+
finally {
|
|
634
|
+
previewSnapshotsBootstrapping.value = false
|
|
635
|
+
}
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
onBeforeMount(() => {
|
|
639
|
+
ensurePreviewSnapshots()
|
|
640
|
+
})
|
|
641
|
+
|
|
496
642
|
const editorDocUpdates = (workingDoc) => {
|
|
497
643
|
ensureStructureDefaults(workingDoc, false)
|
|
498
644
|
if (workingDoc?.post || (Array.isArray(workingDoc?.postContent) && workingDoc.postContent.length > 0) || Array.isArray(workingDoc?.postStructure))
|
|
499
645
|
ensureStructureDefaults(workingDoc, true)
|
|
500
646
|
applyCollectionUniqueKeys(workingDoc)
|
|
647
|
+
if (!hasPostView(workingDoc) && state.previewPageView === 'post')
|
|
648
|
+
state.previewPageView = 'list'
|
|
501
649
|
const blockIds = (workingDoc.content || []).map(block => block.blockId).filter(id => id)
|
|
502
650
|
const postBlockIds = workingDoc.postContent ? workingDoc.postContent.map(block => block.blockId).filter(id => id) : []
|
|
503
651
|
blockIds.push(...postBlockIds)
|
|
@@ -528,19 +676,204 @@ const selectedThemeId = computed(() => {
|
|
|
528
676
|
return edgeFirebase.data?.[`${edgeGlobal.edgeState.organizationDocPath}/sites`]?.[props.site]?.theme || ''
|
|
529
677
|
})
|
|
530
678
|
|
|
531
|
-
const
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
679
|
+
const themePreviewCache = useState('edge-cms-page-theme-preview-cache', () => ({}))
|
|
680
|
+
const themeCacheKey = computed(() => {
|
|
681
|
+
const orgId = String(edgeGlobal.edgeState.currentOrganization || 'no-org').trim() || 'no-org'
|
|
682
|
+
const siteKey = props.isTemplateSite ? 'templates' : String(props.site || 'no-site').trim() || 'no-site'
|
|
683
|
+
return `${orgId}:${siteKey}`
|
|
684
|
+
})
|
|
685
|
+
|
|
686
|
+
const hydrateThemeCache = () => {
|
|
687
|
+
const cache = themePreviewCache.value?.[themeCacheKey.value] || {}
|
|
688
|
+
return {
|
|
689
|
+
themeId: typeof cache?.themeId === 'string' ? cache.themeId : '',
|
|
690
|
+
theme: cache?.theme && typeof cache.theme === 'object' ? cache.theme : null,
|
|
691
|
+
head: cache?.head && typeof cache.head === 'object' ? cache.head : {},
|
|
692
|
+
}
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
const writeThemeCache = (patch = {}) => {
|
|
696
|
+
const current = themePreviewCache.value?.[themeCacheKey.value] || {}
|
|
697
|
+
themePreviewCache.value = {
|
|
698
|
+
...(themePreviewCache.value || {}),
|
|
699
|
+
[themeCacheKey.value]: {
|
|
700
|
+
...current,
|
|
701
|
+
...patch,
|
|
702
|
+
},
|
|
703
|
+
}
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
const initialThemeCache = hydrateThemeCache()
|
|
707
|
+
const lastStableThemeId = ref(initialThemeCache.themeId)
|
|
708
|
+
const lastResolvedTheme = ref(initialThemeCache.theme)
|
|
709
|
+
const lastResolvedHead = ref(initialThemeCache.head)
|
|
710
|
+
|
|
711
|
+
const parseThemeDoc = (themeDoc) => {
|
|
712
|
+
const themeContents = themeDoc?.theme || null
|
|
536
713
|
if (!themeContents)
|
|
537
714
|
return null
|
|
715
|
+
const extraCSS = typeof themeDoc?.extraCSS === 'string' ? themeDoc.extraCSS : ''
|
|
538
716
|
try {
|
|
539
|
-
|
|
717
|
+
const parsed = typeof themeContents === 'string' ? JSON.parse(themeContents) : themeContents
|
|
718
|
+
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed))
|
|
719
|
+
return null
|
|
720
|
+
return { ...parsed, extraCSS }
|
|
540
721
|
}
|
|
541
|
-
catch
|
|
722
|
+
catch {
|
|
723
|
+
return null
|
|
724
|
+
}
|
|
725
|
+
}
|
|
726
|
+
|
|
727
|
+
const parseHeadDoc = (themeDoc) => {
|
|
728
|
+
try {
|
|
729
|
+
const parsed = JSON.parse(themeDoc?.headJSON || '{}')
|
|
730
|
+
if (parsed && typeof parsed === 'object' && !Array.isArray(parsed))
|
|
731
|
+
return parsed
|
|
732
|
+
}
|
|
733
|
+
catch {}
|
|
734
|
+
return {}
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
const applyResolvedTheme = (themeDoc, themeId = '') => {
|
|
738
|
+
const normalizedThemeId = String(themeId || themeDoc?.docId || '').trim()
|
|
739
|
+
if (normalizedThemeId)
|
|
740
|
+
lastStableThemeId.value = normalizedThemeId
|
|
741
|
+
|
|
742
|
+
const parsedTheme = parseThemeDoc(themeDoc)
|
|
743
|
+
if (parsedTheme && typeof parsedTheme === 'object') {
|
|
744
|
+
lastResolvedTheme.value = parsedTheme
|
|
745
|
+
writeThemeCache({ theme: parsedTheme })
|
|
746
|
+
}
|
|
747
|
+
|
|
748
|
+
const parsedHead = parseHeadDoc(themeDoc)
|
|
749
|
+
if (parsedHead && typeof parsedHead === 'object') {
|
|
750
|
+
lastResolvedHead.value = parsedHead
|
|
751
|
+
writeThemeCache({ head: parsedHead })
|
|
752
|
+
}
|
|
753
|
+
|
|
754
|
+
if (normalizedThemeId)
|
|
755
|
+
writeThemeCache({ themeId: normalizedThemeId })
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
const themeFallbackLoading = ref(false)
|
|
759
|
+
const loadSiteThemeFallback = async () => {
|
|
760
|
+
if (themeFallbackLoading.value)
|
|
761
|
+
return
|
|
762
|
+
|
|
763
|
+
const orgPath = String(edgeGlobal.edgeState.organizationDocPath || '').trim()
|
|
764
|
+
if (!orgPath)
|
|
765
|
+
return
|
|
766
|
+
|
|
767
|
+
const selectedId = String(selectedThemeId.value || '').trim()
|
|
768
|
+
if (props.isTemplateSite) {
|
|
769
|
+
if (!selectedId)
|
|
770
|
+
return
|
|
771
|
+
const fromSnapshot = edgeFirebase.data?.[`${orgPath}/themes`]?.[selectedId] || null
|
|
772
|
+
if (fromSnapshot)
|
|
773
|
+
applyResolvedTheme(fromSnapshot, selectedId)
|
|
774
|
+
return
|
|
775
|
+
}
|
|
776
|
+
|
|
777
|
+
const siteId = String(props.site || '').trim()
|
|
778
|
+
if (!siteId || siteId === 'new')
|
|
779
|
+
return
|
|
780
|
+
|
|
781
|
+
themeFallbackLoading.value = true
|
|
782
|
+
try {
|
|
783
|
+
let themeId = selectedId
|
|
784
|
+
if (!themeId) {
|
|
785
|
+
const siteDoc = await edgeFirebase.getDocData(`${orgPath}/sites`, siteId)
|
|
786
|
+
themeId = String(siteDoc?.theme || '').trim()
|
|
787
|
+
}
|
|
788
|
+
if (!themeId)
|
|
789
|
+
return
|
|
790
|
+
|
|
791
|
+
writeThemeCache({ themeId })
|
|
792
|
+
lastStableThemeId.value = themeId
|
|
793
|
+
|
|
794
|
+
const fromSnapshot = edgeFirebase.data?.[`${orgPath}/themes`]?.[themeId] || null
|
|
795
|
+
if (fromSnapshot) {
|
|
796
|
+
applyResolvedTheme(fromSnapshot, themeId)
|
|
797
|
+
if (lastResolvedTheme.value)
|
|
798
|
+
return
|
|
799
|
+
}
|
|
800
|
+
|
|
801
|
+
const themeDoc = await edgeFirebase.getDocData(`${orgPath}/themes`, themeId)
|
|
802
|
+
if (themeDoc)
|
|
803
|
+
applyResolvedTheme(themeDoc, themeId)
|
|
804
|
+
}
|
|
805
|
+
catch (error) {
|
|
806
|
+
console.error('Failed to load fallback theme for page preview', error)
|
|
807
|
+
}
|
|
808
|
+
finally {
|
|
809
|
+
themeFallbackLoading.value = false
|
|
810
|
+
}
|
|
811
|
+
}
|
|
812
|
+
|
|
813
|
+
watch(
|
|
814
|
+
() => edgeGlobal.edgeState.currentOrganization,
|
|
815
|
+
() => {
|
|
816
|
+
ensurePreviewSnapshots()
|
|
817
|
+
loadSiteThemeFallback()
|
|
818
|
+
},
|
|
819
|
+
{ immediate: true },
|
|
820
|
+
)
|
|
821
|
+
|
|
822
|
+
watch(
|
|
823
|
+
() => [props.site, props.page, props.isTemplateSite],
|
|
824
|
+
() => {
|
|
825
|
+
loadSiteThemeFallback()
|
|
826
|
+
},
|
|
827
|
+
{ immediate: true },
|
|
828
|
+
)
|
|
829
|
+
|
|
830
|
+
watch(
|
|
831
|
+
themeCacheKey,
|
|
832
|
+
() => {
|
|
833
|
+
const hydrated = hydrateThemeCache()
|
|
834
|
+
if (hydrated.themeId)
|
|
835
|
+
lastStableThemeId.value = hydrated.themeId
|
|
836
|
+
if (hydrated.theme && typeof hydrated.theme === 'object')
|
|
837
|
+
lastResolvedTheme.value = hydrated.theme
|
|
838
|
+
if (hydrated.head && typeof hydrated.head === 'object')
|
|
839
|
+
lastResolvedHead.value = hydrated.head
|
|
840
|
+
},
|
|
841
|
+
{ immediate: true },
|
|
842
|
+
)
|
|
843
|
+
|
|
844
|
+
watch(selectedThemeId, (themeId) => {
|
|
845
|
+
const normalized = String(themeId || '').trim()
|
|
846
|
+
if (normalized) {
|
|
847
|
+
lastStableThemeId.value = normalized
|
|
848
|
+
writeThemeCache({ themeId: normalized })
|
|
849
|
+
}
|
|
850
|
+
loadSiteThemeFallback()
|
|
851
|
+
}, { immediate: true })
|
|
852
|
+
|
|
853
|
+
const effectiveThemeId = computed(() => {
|
|
854
|
+
const normalized = String(selectedThemeId.value || '').trim()
|
|
855
|
+
if (normalized)
|
|
856
|
+
return normalized
|
|
857
|
+
return lastStableThemeId.value
|
|
858
|
+
})
|
|
859
|
+
|
|
860
|
+
const parsedTheme = computed(() => {
|
|
861
|
+
const themeId = effectiveThemeId.value
|
|
862
|
+
if (!themeId)
|
|
542
863
|
return null
|
|
864
|
+
const themeDoc = edgeFirebase.data?.[`${edgeGlobal.edgeState.organizationDocPath}/themes`]?.[themeId] || null
|
|
865
|
+
return parseThemeDoc(themeDoc)
|
|
866
|
+
})
|
|
867
|
+
|
|
868
|
+
watch(parsedTheme, (nextTheme) => {
|
|
869
|
+
if (nextTheme && typeof nextTheme === 'object') {
|
|
870
|
+
lastResolvedTheme.value = nextTheme
|
|
871
|
+
writeThemeCache({ theme: nextTheme })
|
|
543
872
|
}
|
|
873
|
+
}, { immediate: true, deep: true })
|
|
874
|
+
|
|
875
|
+
const theme = computed(() => {
|
|
876
|
+
return parsedTheme.value || lastResolvedTheme.value || null
|
|
544
877
|
})
|
|
545
878
|
|
|
546
879
|
const themeColorMap = computed(() => {
|
|
@@ -612,11 +945,7 @@ const rowUsesSpans = row => (row?.columns || []).some(col => Number.isFinite(col
|
|
|
612
945
|
|
|
613
946
|
const rowGapClass = (row) => {
|
|
614
947
|
const gap = Number(row?.gap)
|
|
615
|
-
|
|
616
|
-
const safeGap = allowed.has(gap) ? gap : 4
|
|
617
|
-
if (safeGap === 0)
|
|
618
|
-
return 'gap-0'
|
|
619
|
-
return ['gap-0', `sm:gap-${safeGap}`].join(' ')
|
|
948
|
+
return ROW_GAP_CLASS_MAP[gap] || ROW_GAP_CLASS_MAP[4]
|
|
620
949
|
}
|
|
621
950
|
|
|
622
951
|
const rowGridClass = (row) => {
|
|
@@ -847,14 +1176,20 @@ const addRowAt = (workingDoc, layoutValue = '6', insertIndex = 0, isPost = false
|
|
|
847
1176
|
}
|
|
848
1177
|
|
|
849
1178
|
const headObject = computed(() => {
|
|
850
|
-
const themeId =
|
|
1179
|
+
const themeId = effectiveThemeId.value
|
|
851
1180
|
if (!themeId)
|
|
852
|
-
return {}
|
|
1181
|
+
return lastResolvedHead.value || {}
|
|
853
1182
|
try {
|
|
854
|
-
|
|
1183
|
+
const parsedHead = parseHeadDoc(edgeFirebase.data?.[`${edgeGlobal.edgeState.organizationDocPath}/themes`]?.[themeId] || null)
|
|
1184
|
+
if (parsedHead && typeof parsedHead === 'object') {
|
|
1185
|
+
lastResolvedHead.value = parsedHead
|
|
1186
|
+
writeThemeCache({ head: parsedHead })
|
|
1187
|
+
return parsedHead
|
|
1188
|
+
}
|
|
1189
|
+
return lastResolvedHead.value || {}
|
|
855
1190
|
}
|
|
856
1191
|
catch (e) {
|
|
857
|
-
return {}
|
|
1192
|
+
return lastResolvedHead.value || {}
|
|
858
1193
|
}
|
|
859
1194
|
})
|
|
860
1195
|
|
|
@@ -918,6 +1253,403 @@ const currentPage = computed(() => {
|
|
|
918
1253
|
return edgeFirebase.data?.[`${edgeGlobal.edgeState.organizationDocPath}/sites/${props.site}/pages`]?.[props.page] || null
|
|
919
1254
|
})
|
|
920
1255
|
|
|
1256
|
+
const pagesCollectionPath = computed(() => `${edgeGlobal.edgeState.organizationDocPath}/sites/${props.site}/pages`)
|
|
1257
|
+
const pagesCollection = computed(() => edgeFirebase.data?.[pagesCollectionPath.value] || {})
|
|
1258
|
+
const pageEditorBasePath = computed(() => (props.isTemplateSite ? '/app/dashboard/templates' : `/app/dashboard/sites/${props.site}`))
|
|
1259
|
+
const INVALID_PAGE_IMPORT_MESSAGE = 'Invalid file. Please import a valid page file.'
|
|
1260
|
+
|
|
1261
|
+
const downloadJsonFile = (payload, filename) => {
|
|
1262
|
+
if (typeof window === 'undefined')
|
|
1263
|
+
return
|
|
1264
|
+
const blob = new Blob([JSON.stringify(payload, null, 2)], { type: 'application/json' })
|
|
1265
|
+
const objectUrl = URL.createObjectURL(blob)
|
|
1266
|
+
const anchor = document.createElement('a')
|
|
1267
|
+
anchor.href = objectUrl
|
|
1268
|
+
anchor.download = filename
|
|
1269
|
+
document.body.appendChild(anchor)
|
|
1270
|
+
anchor.click()
|
|
1271
|
+
anchor.remove()
|
|
1272
|
+
URL.revokeObjectURL(objectUrl)
|
|
1273
|
+
}
|
|
1274
|
+
|
|
1275
|
+
const readTextFile = file => new Promise((resolve, reject) => {
|
|
1276
|
+
if (typeof FileReader === 'undefined') {
|
|
1277
|
+
reject(new Error('File import is only available in the browser.'))
|
|
1278
|
+
return
|
|
1279
|
+
}
|
|
1280
|
+
const reader = new FileReader()
|
|
1281
|
+
reader.onload = () => resolve(String(reader.result || ''))
|
|
1282
|
+
reader.onerror = () => reject(new Error('Could not read the selected file.'))
|
|
1283
|
+
reader.readAsText(file)
|
|
1284
|
+
})
|
|
1285
|
+
|
|
1286
|
+
const normalizeImportedDoc = (payload, fallbackDocId = '') => {
|
|
1287
|
+
if (!payload || typeof payload !== 'object' || Array.isArray(payload))
|
|
1288
|
+
throw new Error(INVALID_PAGE_IMPORT_MESSAGE)
|
|
1289
|
+
|
|
1290
|
+
if (payload.document && typeof payload.document === 'object' && !Array.isArray(payload.document)) {
|
|
1291
|
+
const normalized = { ...payload.document }
|
|
1292
|
+
if (!normalized.docId && payload.docId)
|
|
1293
|
+
normalized.docId = payload.docId
|
|
1294
|
+
if (!normalized.docId && fallbackDocId)
|
|
1295
|
+
normalized.docId = fallbackDocId
|
|
1296
|
+
return normalized
|
|
1297
|
+
}
|
|
1298
|
+
|
|
1299
|
+
const normalized = { ...payload }
|
|
1300
|
+
if (!normalized.docId && fallbackDocId)
|
|
1301
|
+
normalized.docId = fallbackDocId
|
|
1302
|
+
return normalized
|
|
1303
|
+
}
|
|
1304
|
+
|
|
1305
|
+
const isPlainObject = value => !!value && typeof value === 'object' && !Array.isArray(value)
|
|
1306
|
+
|
|
1307
|
+
const cloneSchemaValue = (value) => {
|
|
1308
|
+
if (isPlainObject(value) || Array.isArray(value))
|
|
1309
|
+
return edgeGlobal.dupObject(value)
|
|
1310
|
+
return value
|
|
1311
|
+
}
|
|
1312
|
+
|
|
1313
|
+
const getDocDefaultsFromSchema = (schema = {}) => {
|
|
1314
|
+
const defaults = {}
|
|
1315
|
+
for (const [key, schemaEntry] of Object.entries(schema || {})) {
|
|
1316
|
+
const hasValueProp = isPlainObject(schemaEntry) && Object.prototype.hasOwnProperty.call(schemaEntry, 'value')
|
|
1317
|
+
const baseValue = hasValueProp ? schemaEntry.value : schemaEntry
|
|
1318
|
+
defaults[key] = cloneSchemaValue(baseValue)
|
|
1319
|
+
}
|
|
1320
|
+
return defaults
|
|
1321
|
+
}
|
|
1322
|
+
|
|
1323
|
+
const getPageDocDefaults = () => getDocDefaultsFromSchema(state.newDocs?.pages || {})
|
|
1324
|
+
|
|
1325
|
+
const isBlankString = value => String(value || '').trim() === ''
|
|
1326
|
+
|
|
1327
|
+
const applyImportedPageSeoDefaults = (doc) => {
|
|
1328
|
+
if (!isPlainObject(doc))
|
|
1329
|
+
return doc
|
|
1330
|
+
|
|
1331
|
+
if (isBlankString(doc.structuredData))
|
|
1332
|
+
doc.structuredData = buildPageStructuredData()
|
|
1333
|
+
|
|
1334
|
+
if (doc.post && isBlankString(doc.postStructuredData))
|
|
1335
|
+
doc.postStructuredData = doc.structuredData || buildPageStructuredData()
|
|
1336
|
+
|
|
1337
|
+
return doc
|
|
1338
|
+
}
|
|
1339
|
+
|
|
1340
|
+
const validateImportedPageDoc = (doc) => {
|
|
1341
|
+
if (!isPlainObject(doc))
|
|
1342
|
+
throw new Error(INVALID_PAGE_IMPORT_MESSAGE)
|
|
1343
|
+
|
|
1344
|
+
const requiredKeys = Object.keys(state.newDocs?.pages || {})
|
|
1345
|
+
const missing = requiredKeys.filter(key => !Object.prototype.hasOwnProperty.call(doc, key))
|
|
1346
|
+
if (missing.length)
|
|
1347
|
+
throw new Error(INVALID_PAGE_IMPORT_MESSAGE)
|
|
1348
|
+
|
|
1349
|
+
return doc
|
|
1350
|
+
}
|
|
1351
|
+
|
|
1352
|
+
const normalizeMenusForImport = (menus) => {
|
|
1353
|
+
const normalized = isPlainObject(menus) ? edgeGlobal.dupObject(menus) : {}
|
|
1354
|
+
if (!Array.isArray(normalized['Site Root']))
|
|
1355
|
+
normalized['Site Root'] = []
|
|
1356
|
+
if (!Array.isArray(normalized['Not In Menu']))
|
|
1357
|
+
normalized['Not In Menu'] = []
|
|
1358
|
+
return normalized
|
|
1359
|
+
}
|
|
1360
|
+
|
|
1361
|
+
const walkMenuEntries = (items, callback) => {
|
|
1362
|
+
if (!Array.isArray(items))
|
|
1363
|
+
return
|
|
1364
|
+
for (const entry of items) {
|
|
1365
|
+
if (!entry || typeof entry !== 'object')
|
|
1366
|
+
continue
|
|
1367
|
+
callback(entry)
|
|
1368
|
+
if (isPlainObject(entry.item)) {
|
|
1369
|
+
for (const nested of Object.values(entry.item)) {
|
|
1370
|
+
if (Array.isArray(nested))
|
|
1371
|
+
walkMenuEntries(nested, callback)
|
|
1372
|
+
}
|
|
1373
|
+
}
|
|
1374
|
+
}
|
|
1375
|
+
}
|
|
1376
|
+
|
|
1377
|
+
const menuIncludesDocId = (menus, docId) => {
|
|
1378
|
+
let found = false
|
|
1379
|
+
const checkEntry = (entry) => {
|
|
1380
|
+
if (found)
|
|
1381
|
+
return
|
|
1382
|
+
if (typeof entry?.item === 'string' && entry.item === docId)
|
|
1383
|
+
found = true
|
|
1384
|
+
}
|
|
1385
|
+
for (const menuItems of Object.values(menus || {})) {
|
|
1386
|
+
walkMenuEntries(menuItems, checkEntry)
|
|
1387
|
+
if (found)
|
|
1388
|
+
return true
|
|
1389
|
+
}
|
|
1390
|
+
return false
|
|
1391
|
+
}
|
|
1392
|
+
|
|
1393
|
+
const collectMenuPageNames = (menus) => {
|
|
1394
|
+
const names = new Set()
|
|
1395
|
+
const collectEntry = (entry) => {
|
|
1396
|
+
if (typeof entry?.item !== 'string')
|
|
1397
|
+
return
|
|
1398
|
+
const name = String(entry?.name || '').trim()
|
|
1399
|
+
if (name)
|
|
1400
|
+
names.add(name)
|
|
1401
|
+
}
|
|
1402
|
+
for (const menuItems of Object.values(menus || {}))
|
|
1403
|
+
walkMenuEntries(menuItems, collectEntry)
|
|
1404
|
+
return names
|
|
1405
|
+
}
|
|
1406
|
+
|
|
1407
|
+
const slugifyMenuPageName = (value) => {
|
|
1408
|
+
return String(value || '')
|
|
1409
|
+
.trim()
|
|
1410
|
+
.toLowerCase()
|
|
1411
|
+
.replace(/[^a-z0-9]+/g, '-')
|
|
1412
|
+
.replace(/(^-|-$)+/g, '') || 'page'
|
|
1413
|
+
}
|
|
1414
|
+
const titleFromSlug = (slug) => {
|
|
1415
|
+
if (!slug)
|
|
1416
|
+
return ''
|
|
1417
|
+
return String(slug).replace(/-/g, ' ').replace(/\b\w/g, l => l.toUpperCase())
|
|
1418
|
+
}
|
|
1419
|
+
|
|
1420
|
+
const makeUniqueMenuPageName = (value, existingNames = new Set()) => {
|
|
1421
|
+
const base = slugifyMenuPageName(value)
|
|
1422
|
+
let candidate = base
|
|
1423
|
+
let suffix = 2
|
|
1424
|
+
while (existingNames.has(candidate)) {
|
|
1425
|
+
candidate = `${base}-${suffix}`
|
|
1426
|
+
suffix += 1
|
|
1427
|
+
}
|
|
1428
|
+
return candidate
|
|
1429
|
+
}
|
|
1430
|
+
|
|
1431
|
+
const addImportedPageToSiteMenu = async (docId, pageName = '') => {
|
|
1432
|
+
const nextDocId = String(docId || '').trim()
|
|
1433
|
+
if (!nextDocId)
|
|
1434
|
+
return
|
|
1435
|
+
const siteId = String(props.site || '').trim()
|
|
1436
|
+
if (!siteId)
|
|
1437
|
+
return
|
|
1438
|
+
|
|
1439
|
+
const sitesCollectionPath = `${edgeGlobal.edgeState.organizationDocPath}/sites`
|
|
1440
|
+
const siteDoc = edgeFirebase.data?.[sitesCollectionPath]?.[siteId] || {}
|
|
1441
|
+
const menus = normalizeMenusForImport(siteDoc?.menus)
|
|
1442
|
+
if (menuIncludesDocId(menus, nextDocId))
|
|
1443
|
+
return
|
|
1444
|
+
|
|
1445
|
+
const existingNames = collectMenuPageNames(menus)
|
|
1446
|
+
const menuName = makeUniqueMenuPageName(pageName || nextDocId, existingNames)
|
|
1447
|
+
const menuTitle = String(pageName || '').trim() || titleFromSlug(menuName)
|
|
1448
|
+
menus['Site Root'].push({ name: menuName, menuTitle, item: nextDocId })
|
|
1449
|
+
|
|
1450
|
+
const results = await edgeFirebase.changeDoc(sitesCollectionPath, siteId, { menus })
|
|
1451
|
+
if (results?.success === false)
|
|
1452
|
+
throw new Error('Could not save updated site menu.')
|
|
1453
|
+
}
|
|
1454
|
+
|
|
1455
|
+
const makeRandomPageDocId = (docsMap = {}) => {
|
|
1456
|
+
let nextDocId = String(edgeGlobal.generateShortId() || '').trim()
|
|
1457
|
+
while (!nextDocId || docsMap[nextDocId])
|
|
1458
|
+
nextDocId = String(edgeGlobal.generateShortId() || '').trim()
|
|
1459
|
+
return nextDocId
|
|
1460
|
+
}
|
|
1461
|
+
|
|
1462
|
+
const makeImportedPageNameForNew = (baseName, docsMap = {}) => {
|
|
1463
|
+
const normalizedBase = String(baseName || '').trim() || 'page'
|
|
1464
|
+
const existingNames = new Set(
|
|
1465
|
+
Object.values(docsMap || {})
|
|
1466
|
+
.map(doc => String(doc?.name || '').trim().toLowerCase())
|
|
1467
|
+
.filter(Boolean),
|
|
1468
|
+
)
|
|
1469
|
+
|
|
1470
|
+
let suffix = 1
|
|
1471
|
+
let candidate = `${normalizedBase}-${suffix}`
|
|
1472
|
+
while (existingNames.has(candidate.toLowerCase())) {
|
|
1473
|
+
suffix += 1
|
|
1474
|
+
candidate = `${normalizedBase}-${suffix}`
|
|
1475
|
+
}
|
|
1476
|
+
return candidate
|
|
1477
|
+
}
|
|
1478
|
+
|
|
1479
|
+
const requestPageImportDocId = (initialValue = '') => {
|
|
1480
|
+
state.importDocIdValue = String(initialValue || '')
|
|
1481
|
+
state.importDocIdDialogOpen = true
|
|
1482
|
+
return new Promise((resolve) => {
|
|
1483
|
+
pageImportDocIdResolver.value = resolve
|
|
1484
|
+
})
|
|
1485
|
+
}
|
|
1486
|
+
|
|
1487
|
+
const resolvePageImportDocId = (value = '') => {
|
|
1488
|
+
const resolver = pageImportDocIdResolver.value
|
|
1489
|
+
pageImportDocIdResolver.value = null
|
|
1490
|
+
state.importDocIdDialogOpen = false
|
|
1491
|
+
if (resolver)
|
|
1492
|
+
resolver(String(value || '').trim())
|
|
1493
|
+
}
|
|
1494
|
+
|
|
1495
|
+
const requestPageImportConflict = (docId) => {
|
|
1496
|
+
state.importConflictDocId = String(docId || '')
|
|
1497
|
+
state.importConflictDialogOpen = true
|
|
1498
|
+
return new Promise((resolve) => {
|
|
1499
|
+
pageImportConflictResolver.value = resolve
|
|
1500
|
+
})
|
|
1501
|
+
}
|
|
1502
|
+
|
|
1503
|
+
const resolvePageImportConflict = (action = 'cancel') => {
|
|
1504
|
+
const resolver = pageImportConflictResolver.value
|
|
1505
|
+
pageImportConflictResolver.value = null
|
|
1506
|
+
state.importConflictDialogOpen = false
|
|
1507
|
+
if (resolver)
|
|
1508
|
+
resolver(action)
|
|
1509
|
+
}
|
|
1510
|
+
|
|
1511
|
+
watch(() => state.importDocIdDialogOpen, (open) => {
|
|
1512
|
+
if (!open && pageImportDocIdResolver.value) {
|
|
1513
|
+
const resolver = pageImportDocIdResolver.value
|
|
1514
|
+
pageImportDocIdResolver.value = null
|
|
1515
|
+
resolver('')
|
|
1516
|
+
}
|
|
1517
|
+
})
|
|
1518
|
+
|
|
1519
|
+
watch(() => state.importConflictDialogOpen, (open) => {
|
|
1520
|
+
if (!open && pageImportConflictResolver.value) {
|
|
1521
|
+
const resolver = pageImportConflictResolver.value
|
|
1522
|
+
pageImportConflictResolver.value = null
|
|
1523
|
+
resolver('cancel')
|
|
1524
|
+
}
|
|
1525
|
+
})
|
|
1526
|
+
|
|
1527
|
+
const getImportDocId = async (incomingDoc, fallbackDocId = '') => {
|
|
1528
|
+
let nextDocId = String(incomingDoc?.docId || '').trim()
|
|
1529
|
+
if (!nextDocId)
|
|
1530
|
+
nextDocId = await requestPageImportDocId(fallbackDocId)
|
|
1531
|
+
if (!nextDocId)
|
|
1532
|
+
throw new Error('Import canceled. A docId is required.')
|
|
1533
|
+
if (nextDocId.includes('/'))
|
|
1534
|
+
throw new Error('docId cannot include "/".')
|
|
1535
|
+
return nextDocId
|
|
1536
|
+
}
|
|
1537
|
+
|
|
1538
|
+
const notifySuccess = (message) => {
|
|
1539
|
+
edgeFirebase?.toast?.success?.(message)
|
|
1540
|
+
}
|
|
1541
|
+
|
|
1542
|
+
const notifyError = (message) => {
|
|
1543
|
+
edgeFirebase?.toast?.error?.(message)
|
|
1544
|
+
}
|
|
1545
|
+
|
|
1546
|
+
const openImportErrorDialog = (message) => {
|
|
1547
|
+
state.importErrorMessage = String(message || 'Failed to import page JSON.')
|
|
1548
|
+
state.importErrorDialogOpen = true
|
|
1549
|
+
}
|
|
1550
|
+
|
|
1551
|
+
const exportCurrentPage = () => {
|
|
1552
|
+
const doc = currentPage.value
|
|
1553
|
+
if (!doc || !props.page || props.page === 'new') {
|
|
1554
|
+
notifyError('Save this page before exporting.')
|
|
1555
|
+
return
|
|
1556
|
+
}
|
|
1557
|
+
const docId = String(doc.docId || props.page).trim()
|
|
1558
|
+
const exportPayload = { ...getPageDocDefaults(), ...doc, docId }
|
|
1559
|
+
downloadJsonFile(exportPayload, `page-${docId}.json`)
|
|
1560
|
+
notifySuccess(`Exported page "${docId}".`)
|
|
1561
|
+
}
|
|
1562
|
+
|
|
1563
|
+
const triggerPageImport = () => {
|
|
1564
|
+
pageImportInputRef.value?.click()
|
|
1565
|
+
}
|
|
1566
|
+
|
|
1567
|
+
const importSinglePageFile = async (file, existingPages = {}, fallbackDocId = '') => {
|
|
1568
|
+
const fileText = await readTextFile(file)
|
|
1569
|
+
const parsed = JSON.parse(fileText)
|
|
1570
|
+
const importedDoc = applyImportedPageSeoDefaults(validateImportedPageDoc(normalizeImportedDoc(parsed, fallbackDocId)))
|
|
1571
|
+
const incomingDocId = await getImportDocId(importedDoc, fallbackDocId)
|
|
1572
|
+
let targetDocId = incomingDocId
|
|
1573
|
+
let importDecision = 'create'
|
|
1574
|
+
|
|
1575
|
+
if (existingPages[targetDocId]) {
|
|
1576
|
+
const decision = await requestPageImportConflict(targetDocId)
|
|
1577
|
+
if (decision === 'cancel')
|
|
1578
|
+
return ''
|
|
1579
|
+
if (decision === 'new') {
|
|
1580
|
+
targetDocId = makeRandomPageDocId(existingPages)
|
|
1581
|
+
importedDoc.name = makeImportedPageNameForNew(importedDoc.name || incomingDocId, existingPages)
|
|
1582
|
+
importDecision = 'new'
|
|
1583
|
+
}
|
|
1584
|
+
else {
|
|
1585
|
+
importDecision = 'overwrite'
|
|
1586
|
+
}
|
|
1587
|
+
}
|
|
1588
|
+
|
|
1589
|
+
const isCreatingNewPage = !existingPages[targetDocId]
|
|
1590
|
+
const payload = { ...getPageDocDefaults(), ...importedDoc, docId: targetDocId }
|
|
1591
|
+
await edgeFirebase.storeDoc(pagesCollectionPath.value, payload, targetDocId)
|
|
1592
|
+
existingPages[targetDocId] = payload
|
|
1593
|
+
|
|
1594
|
+
if (isCreatingNewPage) {
|
|
1595
|
+
try {
|
|
1596
|
+
await addImportedPageToSiteMenu(targetDocId, importedDoc.name)
|
|
1597
|
+
}
|
|
1598
|
+
catch (menuError) {
|
|
1599
|
+
console.error('Imported page but failed to update site menu', menuError)
|
|
1600
|
+
openImportErrorDialog('Imported page, but could not add it to Site Menu automatically.')
|
|
1601
|
+
}
|
|
1602
|
+
}
|
|
1603
|
+
|
|
1604
|
+
if (importDecision === 'overwrite')
|
|
1605
|
+
notifySuccess(`Overwrote page "${targetDocId}".`)
|
|
1606
|
+
else if (importDecision === 'new')
|
|
1607
|
+
notifySuccess(`Imported page as new "${targetDocId}".`)
|
|
1608
|
+
else
|
|
1609
|
+
notifySuccess(`Imported page "${targetDocId}".`)
|
|
1610
|
+
|
|
1611
|
+
return targetDocId
|
|
1612
|
+
}
|
|
1613
|
+
|
|
1614
|
+
const handlePageImport = async (event) => {
|
|
1615
|
+
const input = event?.target
|
|
1616
|
+
const files = Array.from(input?.files || [])
|
|
1617
|
+
if (!files.length)
|
|
1618
|
+
return
|
|
1619
|
+
|
|
1620
|
+
state.importingJson = true
|
|
1621
|
+
const fallbackDocId = props.page !== 'new' ? props.page : ''
|
|
1622
|
+
const existingPages = { ...(pagesCollection.value || {}) }
|
|
1623
|
+
let lastImportedDocId = ''
|
|
1624
|
+
try {
|
|
1625
|
+
for (const file of files) {
|
|
1626
|
+
try {
|
|
1627
|
+
const importedDocId = await importSinglePageFile(file, existingPages, fallbackDocId)
|
|
1628
|
+
if (importedDocId)
|
|
1629
|
+
lastImportedDocId = importedDocId
|
|
1630
|
+
}
|
|
1631
|
+
catch (error) {
|
|
1632
|
+
console.error('Failed to import page JSON', error)
|
|
1633
|
+
const message = error?.message || 'Failed to import page JSON.'
|
|
1634
|
+
if (/^Import canceled\./i.test(message))
|
|
1635
|
+
continue
|
|
1636
|
+
if (error instanceof SyntaxError || message === INVALID_PAGE_IMPORT_MESSAGE)
|
|
1637
|
+
openImportErrorDialog(INVALID_PAGE_IMPORT_MESSAGE)
|
|
1638
|
+
else
|
|
1639
|
+
openImportErrorDialog(message)
|
|
1640
|
+
}
|
|
1641
|
+
}
|
|
1642
|
+
|
|
1643
|
+
if (files.length === 1 && lastImportedDocId && lastImportedDocId !== props.page)
|
|
1644
|
+
await router.push(`${pageEditorBasePath.value}/${lastImportedDocId}`)
|
|
1645
|
+
}
|
|
1646
|
+
finally {
|
|
1647
|
+
state.importingJson = false
|
|
1648
|
+
if (input)
|
|
1649
|
+
input.value = ''
|
|
1650
|
+
}
|
|
1651
|
+
}
|
|
1652
|
+
|
|
921
1653
|
watch (currentPage, (newPage) => {
|
|
922
1654
|
state.workingDoc.last_updated = newPage?.last_updated
|
|
923
1655
|
state.workingDoc.metaTitle = newPage?.metaTitle
|
|
@@ -1132,7 +1864,7 @@ const hasUnsavedChanges = (changes) => {
|
|
|
1132
1864
|
:doc-id="page"
|
|
1133
1865
|
:schema="schemas.pages"
|
|
1134
1866
|
:new-doc-schema="state.newDocs.pages"
|
|
1135
|
-
class="w-full mx-auto flex-1 bg-transparent flex flex-col border-none shadow-none pt-0 px-0"
|
|
1867
|
+
class="w-full mx-auto flex-1 bg-transparent flex flex-col border-none shadow-none pt-0 px-0" :class="[!state.editMode ? 'cms-page-preview-mode' : '']"
|
|
1136
1868
|
:show-footer="false"
|
|
1137
1869
|
:save-redirect-override="`/app/dashboard/sites/${site}`"
|
|
1138
1870
|
:no-close-after-save="true"
|
|
@@ -1141,7 +1873,7 @@ const hasUnsavedChanges = (changes) => {
|
|
|
1141
1873
|
@unsaved-changes="hasUnsavedChanges"
|
|
1142
1874
|
>
|
|
1143
1875
|
<template #header="slotProps">
|
|
1144
|
-
<div class="relative flex items-center
|
|
1876
|
+
<div class="relative flex items-center p-2 justify-between top-0 z-50 bg-gray-100 rounded h-[50px]">
|
|
1145
1877
|
<span class="text-lg font-semibold whitespace-nowrap pr-1">{{ pageName }}</span>
|
|
1146
1878
|
|
|
1147
1879
|
<div class="flex w-full items-center">
|
|
@@ -1188,22 +1920,62 @@ const hasUnsavedChanges = (changes) => {
|
|
|
1188
1920
|
</div>
|
|
1189
1921
|
<div class="w-full border-t border-border" aria-hidden="true" />
|
|
1190
1922
|
|
|
1191
|
-
<div class="flex items-center gap-
|
|
1192
|
-
<span class="text-[11px] uppercase tracking-wide text-muted-foreground">Viewport</span>
|
|
1923
|
+
<div class="flex items-center gap-2 px-3">
|
|
1193
1924
|
<edge-shad-button
|
|
1194
|
-
v-for="option in previewViewportOptions"
|
|
1195
|
-
:key="option.id"
|
|
1196
1925
|
type="button"
|
|
1197
|
-
variant="ghost"
|
|
1198
1926
|
size="icon"
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1927
|
+
variant="outline"
|
|
1928
|
+
class="h-8 w-8"
|
|
1929
|
+
:disabled="!currentPage || !props.page || props.page === 'new'"
|
|
1930
|
+
title="Export Page"
|
|
1931
|
+
aria-label="Export Page"
|
|
1932
|
+
@click="exportCurrentPage"
|
|
1202
1933
|
>
|
|
1203
|
-
<
|
|
1934
|
+
<Download class="w-3.5 h-3.5" />
|
|
1204
1935
|
</edge-shad-button>
|
|
1205
1936
|
</div>
|
|
1206
1937
|
|
|
1938
|
+
<div class="flex flex-col items-center gap-1 px-2">
|
|
1939
|
+
<div class="flex items-center gap-1">
|
|
1940
|
+
<edge-shad-button
|
|
1941
|
+
v-for="option in previewViewportOptions"
|
|
1942
|
+
:key="option.id"
|
|
1943
|
+
type="button"
|
|
1944
|
+
variant="ghost"
|
|
1945
|
+
size="icon"
|
|
1946
|
+
class="h-[26px] w-[26px] text-xs gap-1 border transition-colors"
|
|
1947
|
+
:class="state.previewViewport === option.id ? 'bg-primary text-primary-foreground border-primary shadow-sm' : 'bg-muted text-foreground border-border hover:bg-muted/80'"
|
|
1948
|
+
@click="setPreviewViewport(option.id)"
|
|
1949
|
+
>
|
|
1950
|
+
<component :is="option.icon" class="w-3.5 h-3.5" />
|
|
1951
|
+
</edge-shad-button>
|
|
1952
|
+
</div>
|
|
1953
|
+
<span class="text-[10px] leading-tight text-muted-foreground">Viewport</span>
|
|
1954
|
+
</div>
|
|
1955
|
+
<div v-if="hasPostView(slotProps.workingDoc)" class="flex flex-col items-center gap-1 px-2">
|
|
1956
|
+
<div class="flex items-center gap-1">
|
|
1957
|
+
<edge-shad-button
|
|
1958
|
+
type="button"
|
|
1959
|
+
variant="ghost"
|
|
1960
|
+
class="h-[26px] px-2 text-xs border transition-colors"
|
|
1961
|
+
:class="state.previewPageView === 'list' ? 'bg-primary text-primary-foreground border-primary shadow-sm' : 'bg-muted text-foreground border-border hover:bg-muted/80'"
|
|
1962
|
+
@click="setPreviewPageView('list')"
|
|
1963
|
+
>
|
|
1964
|
+
Index
|
|
1965
|
+
</edge-shad-button>
|
|
1966
|
+
<edge-shad-button
|
|
1967
|
+
type="button"
|
|
1968
|
+
variant="ghost"
|
|
1969
|
+
class="h-[26px] px-2 text-xs border transition-colors"
|
|
1970
|
+
:class="state.previewPageView === 'post' ? 'bg-primary text-primary-foreground border-primary shadow-sm' : 'bg-muted text-foreground border-border hover:bg-muted/80'"
|
|
1971
|
+
@click="setPreviewPageView('post')"
|
|
1972
|
+
>
|
|
1973
|
+
Detail
|
|
1974
|
+
</edge-shad-button>
|
|
1975
|
+
</div>
|
|
1976
|
+
<span class="text-[10px] leading-tight text-muted-foreground">View</span>
|
|
1977
|
+
</div>
|
|
1978
|
+
|
|
1207
1979
|
<edge-shad-button variant="text" class="hover:text-primary/50 text-xs h-[26px] text-primary" @click="state.editMode = !state.editMode">
|
|
1208
1980
|
<template v-if="state.editMode">
|
|
1209
1981
|
<Eye class="w-4 h-4" />
|
|
@@ -1247,7 +2019,7 @@ const hasUnsavedChanges = (changes) => {
|
|
|
1247
2019
|
</div>
|
|
1248
2020
|
</template>
|
|
1249
2021
|
<template #success-alert>
|
|
1250
|
-
<div v-if="!props.isTemplateSite" class="mt-2 flex flex-wrap items-center gap-2">
|
|
2022
|
+
<div v-if="state.editMode && !props.isTemplateSite" class="mt-2 flex flex-wrap items-center gap-2">
|
|
1251
2023
|
<edge-shad-button
|
|
1252
2024
|
variant="outline"
|
|
1253
2025
|
class="text-xs h-[28px] gap-1"
|
|
@@ -1264,22 +2036,15 @@ const hasUnsavedChanges = (changes) => {
|
|
|
1264
2036
|
</div>
|
|
1265
2037
|
</template>
|
|
1266
2038
|
<template #main="slotProps">
|
|
1267
|
-
<Tabs class="w-full"
|
|
1268
|
-
<
|
|
1269
|
-
<TabsTrigger value="list">
|
|
1270
|
-
Index Page
|
|
1271
|
-
</TabsTrigger>
|
|
1272
|
-
<TabsTrigger value="post">
|
|
1273
|
-
Detail Page
|
|
1274
|
-
</TabsTrigger>
|
|
1275
|
-
</TabsList>
|
|
1276
|
-
<TabsContent value="list">
|
|
1277
|
-
<Separator class="my-4" />
|
|
2039
|
+
<Tabs class="w-full" :model-value="hasPostView(slotProps.workingDoc) ? state.previewPageView : 'list'">
|
|
2040
|
+
<TabsContent value="list" class="mt-0">
|
|
1278
2041
|
<div
|
|
1279
|
-
:key="
|
|
1280
|
-
|
|
1281
|
-
:
|
|
1282
|
-
|
|
2042
|
+
:key="`${pagePreviewRenderKey}:list`"
|
|
2043
|
+
data-cms-preview-surface="page"
|
|
2044
|
+
:data-cms-preview-mode="state.editMode ? 'edit' : 'preview'"
|
|
2045
|
+
class="w-full h-[calc(100vh-180px)] mt-2 overflow-y-auto mx-auto bg-card border border-border shadow-sm md:shadow-md p-0 space-y-6"
|
|
2046
|
+
:class="[{ 'transition-all duration-300': !state.editMode }, state.editMode ? 'rounded-lg' : 'rounded-none']"
|
|
2047
|
+
:style="previewViewportContainStyle"
|
|
1283
2048
|
>
|
|
1284
2049
|
<edge-button-divider v-if="state.editMode" class="my-2">
|
|
1285
2050
|
<Popover v-model:open="state.addRowPopoverOpen.listTop">
|
|
@@ -1419,9 +2184,13 @@ const hasUnsavedChanges = (changes) => {
|
|
|
1419
2184
|
<div :key="blockId" class="relative group">
|
|
1420
2185
|
<edge-cms-block
|
|
1421
2186
|
v-if="blockIndex(slotProps.workingDoc, blockId, false) !== -1"
|
|
2187
|
+
:key="`${pagePreviewRenderKey}:${blockId}:${effectiveThemeId}:list`"
|
|
1422
2188
|
v-model="slotProps.workingDoc.content[blockIndex(slotProps.workingDoc, blockId, false)]"
|
|
1423
2189
|
:site-id="props.site"
|
|
1424
2190
|
:edit-mode="state.editMode"
|
|
2191
|
+
:override-clicks-in-edit-mode="state.editMode"
|
|
2192
|
+
:allow-preview-content-edit="!state.editMode && canOpenPreviewBlockContentEditor"
|
|
2193
|
+
:contain-fixed="state.editMode"
|
|
1425
2194
|
:viewport-mode="previewViewportMode"
|
|
1426
2195
|
:block-id="blockId"
|
|
1427
2196
|
:theme="theme"
|
|
@@ -1524,13 +2293,14 @@ const hasUnsavedChanges = (changes) => {
|
|
|
1524
2293
|
</edge-button-divider>
|
|
1525
2294
|
</div>
|
|
1526
2295
|
</TabsContent>
|
|
1527
|
-
<TabsContent value="post">
|
|
1528
|
-
<Separator class="my-4" />
|
|
2296
|
+
<TabsContent v-if="hasPostView(slotProps.workingDoc)" value="post" class="mt-0">
|
|
1529
2297
|
<div
|
|
1530
|
-
:key="`${
|
|
1531
|
-
|
|
1532
|
-
:
|
|
1533
|
-
|
|
2298
|
+
:key="`${pagePreviewRenderKey}:post`"
|
|
2299
|
+
data-cms-preview-surface="page"
|
|
2300
|
+
:data-cms-preview-mode="state.editMode ? 'edit' : 'preview'"
|
|
2301
|
+
class="w-full h-[calc(100vh-180px)] mt-2 overflow-y-auto mx-auto bg-card border border-border shadow-sm md:shadow-md p-0 space-y-6"
|
|
2302
|
+
:class="[{ 'transition-all duration-300': !state.editMode }, state.editMode ? 'rounded-lg' : 'rounded-none']"
|
|
2303
|
+
:style="previewViewportContainStyle"
|
|
1534
2304
|
>
|
|
1535
2305
|
<edge-button-divider v-if="state.editMode" class="my-2">
|
|
1536
2306
|
<Popover v-model:open="state.addRowPopoverOpen.postTop">
|
|
@@ -1670,8 +2440,12 @@ const hasUnsavedChanges = (changes) => {
|
|
|
1670
2440
|
<div :key="blockId" class="relative group">
|
|
1671
2441
|
<edge-cms-block
|
|
1672
2442
|
v-if="blockIndex(slotProps.workingDoc, blockId, true) !== -1"
|
|
2443
|
+
:key="`${pagePreviewRenderKey}:${blockId}:${effectiveThemeId}:post`"
|
|
1673
2444
|
v-model="slotProps.workingDoc.postContent[blockIndex(slotProps.workingDoc, blockId, true)]"
|
|
1674
2445
|
:edit-mode="state.editMode"
|
|
2446
|
+
:override-clicks-in-edit-mode="state.editMode"
|
|
2447
|
+
:allow-preview-content-edit="!state.editMode && canOpenPreviewBlockContentEditor"
|
|
2448
|
+
:contain-fixed="state.editMode"
|
|
1675
2449
|
:viewport-mode="previewViewportMode"
|
|
1676
2450
|
:block-id="blockId"
|
|
1677
2451
|
:theme="theme"
|
|
@@ -1846,6 +2620,72 @@ const hasUnsavedChanges = (changes) => {
|
|
|
1846
2620
|
</Sheet>
|
|
1847
2621
|
</template>
|
|
1848
2622
|
</edge-editor>
|
|
2623
|
+
<edge-shad-dialog v-model="state.importDocIdDialogOpen">
|
|
2624
|
+
<DialogContent class="pt-8">
|
|
2625
|
+
<DialogHeader>
|
|
2626
|
+
<DialogTitle class="text-left">
|
|
2627
|
+
Enter Page Doc ID
|
|
2628
|
+
</DialogTitle>
|
|
2629
|
+
<DialogDescription>
|
|
2630
|
+
This JSON file does not include a <code>docId</code>. Enter the doc ID you want to import into this site.
|
|
2631
|
+
</DialogDescription>
|
|
2632
|
+
</DialogHeader>
|
|
2633
|
+
<edge-shad-input
|
|
2634
|
+
v-model="state.importDocIdValue"
|
|
2635
|
+
name="page-import-doc-id"
|
|
2636
|
+
label="Doc ID"
|
|
2637
|
+
placeholder="example-page-id"
|
|
2638
|
+
/>
|
|
2639
|
+
<DialogFooter class="pt-2 flex justify-between">
|
|
2640
|
+
<edge-shad-button variant="outline" @click="resolvePageImportDocId('')">
|
|
2641
|
+
Cancel
|
|
2642
|
+
</edge-shad-button>
|
|
2643
|
+
<edge-shad-button @click="resolvePageImportDocId(state.importDocIdValue)">
|
|
2644
|
+
Continue
|
|
2645
|
+
</edge-shad-button>
|
|
2646
|
+
</DialogFooter>
|
|
2647
|
+
</DialogContent>
|
|
2648
|
+
</edge-shad-dialog>
|
|
2649
|
+
<edge-shad-dialog v-model="state.importConflictDialogOpen">
|
|
2650
|
+
<DialogContent class="pt-8">
|
|
2651
|
+
<DialogHeader>
|
|
2652
|
+
<DialogTitle class="text-left">
|
|
2653
|
+
Page Already Exists
|
|
2654
|
+
</DialogTitle>
|
|
2655
|
+
<DialogDescription>
|
|
2656
|
+
<code>{{ state.importConflictDocId }}</code> already exists in this site. Choose to overwrite it or import as a new page.
|
|
2657
|
+
</DialogDescription>
|
|
2658
|
+
</DialogHeader>
|
|
2659
|
+
<DialogFooter class="pt-2 flex justify-between">
|
|
2660
|
+
<edge-shad-button variant="outline" @click="resolvePageImportConflict('cancel')">
|
|
2661
|
+
Cancel
|
|
2662
|
+
</edge-shad-button>
|
|
2663
|
+
<edge-shad-button variant="outline" @click="resolvePageImportConflict('new')">
|
|
2664
|
+
Add As New
|
|
2665
|
+
</edge-shad-button>
|
|
2666
|
+
<edge-shad-button @click="resolvePageImportConflict('overwrite')">
|
|
2667
|
+
Overwrite
|
|
2668
|
+
</edge-shad-button>
|
|
2669
|
+
</DialogFooter>
|
|
2670
|
+
</DialogContent>
|
|
2671
|
+
</edge-shad-dialog>
|
|
2672
|
+
<edge-shad-dialog v-model="state.importErrorDialogOpen">
|
|
2673
|
+
<DialogContent class="pt-8">
|
|
2674
|
+
<DialogHeader>
|
|
2675
|
+
<DialogTitle class="text-left">
|
|
2676
|
+
Import Failed
|
|
2677
|
+
</DialogTitle>
|
|
2678
|
+
<DialogDescription class="text-left">
|
|
2679
|
+
{{ state.importErrorMessage }}
|
|
2680
|
+
</DialogDescription>
|
|
2681
|
+
</DialogHeader>
|
|
2682
|
+
<DialogFooter class="pt-2">
|
|
2683
|
+
<edge-shad-button @click="state.importErrorDialogOpen = false">
|
|
2684
|
+
Close
|
|
2685
|
+
</edge-shad-button>
|
|
2686
|
+
</DialogFooter>
|
|
2687
|
+
</DialogContent>
|
|
2688
|
+
</edge-shad-dialog>
|
|
1849
2689
|
<edge-shad-dialog v-model="state.showUnpublishedChangesDialog">
|
|
1850
2690
|
<DialogContent class="max-w-2xl">
|
|
1851
2691
|
<DialogHeader>
|
|
@@ -1924,4 +2764,8 @@ const hasUnsavedChanges = (changes) => {
|
|
|
1924
2764
|
.block-drag-handle:active {
|
|
1925
2765
|
cursor: grabbing;
|
|
1926
2766
|
}
|
|
2767
|
+
|
|
2768
|
+
.cms-page-preview-mode :deep(.border-emerald-200.bg-emerald-50) {
|
|
2769
|
+
display: none !important;
|
|
2770
|
+
}
|
|
1927
2771
|
</style>
|