@edgedev/create-edge-app 1.2.32 → 1.2.34

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.
Files changed (33) hide show
  1. package/deploy.sh +77 -0
  2. package/edge/components/cms/block.vue +228 -18
  3. package/edge/components/cms/blockApi.vue +3 -3
  4. package/edge/components/cms/blockEditor.vue +374 -85
  5. package/edge/components/cms/blockPicker.vue +29 -3
  6. package/edge/components/cms/blockRender.vue +3 -3
  7. package/edge/components/cms/blocksManager.vue +755 -82
  8. package/edge/components/cms/codeEditor.vue +15 -6
  9. package/edge/components/cms/fontUpload.vue +318 -2
  10. package/edge/components/cms/htmlContent.vue +230 -89
  11. package/edge/components/cms/menu.vue +5 -4
  12. package/edge/components/cms/page.vue +750 -21
  13. package/edge/components/cms/site.vue +624 -84
  14. package/edge/components/cms/sitesManager.vue +5 -4
  15. package/edge/components/cms/themeEditor.vue +196 -162
  16. package/edge/components/editor.vue +5 -1
  17. package/edge/composables/global.ts +37 -5
  18. package/edge/composables/useCmsNewDocs.js +100 -0
  19. package/edge/composables/useEdgeCmsDialogPositionFix.js +19 -0
  20. package/edge/routes/cms/dashboard/blocks/[block].vue +5 -0
  21. package/edge/routes/cms/dashboard/blocks/index.vue +12 -1
  22. package/edge/routes/cms/dashboard/media/index.vue +5 -0
  23. package/edge/routes/cms/dashboard/sites/[site]/[[page]].vue +4 -0
  24. package/edge/routes/cms/dashboard/sites/[site].vue +4 -0
  25. package/edge/routes/cms/dashboard/sites/index.vue +4 -0
  26. package/edge/routes/cms/dashboard/templates/[page].vue +4 -0
  27. package/edge/routes/cms/dashboard/templates/index.vue +4 -0
  28. package/edge/routes/cms/dashboard/themes/[theme].vue +5 -0
  29. package/edge/routes/cms/dashboard/themes/index.vue +330 -1
  30. package/firebase.json +4 -0
  31. package/nuxt.config.ts +1 -1
  32. package/package.json +2 -2
  33. package/pages/app.vue +12 -12
@@ -1,7 +1,7 @@
1
1
  <script setup lang="js">
2
2
  import { toTypedSchema } from '@vee-validate/zod'
3
3
  import * as z from 'zod'
4
- import { ArrowLeft, CircleAlert, FileCheck, FilePenLine, FileStack, FolderCog, FolderDown, FolderUp, FolderX, Inbox, Loader2, Mail, MailOpen, MoreHorizontal } from 'lucide-vue-next'
4
+ import { ArrowLeft, CircleAlert, FileCheck, FilePenLine, FileStack, FolderCog, FolderDown, FolderUp, FolderX, Inbox, Loader2, Mail, MailOpen, MoreHorizontal, Upload } from 'lucide-vue-next'
5
5
  import { useStructuredDataTemplates } from '@/edge/composables/structuredDataTemplates'
6
6
 
7
7
  const props = defineProps({
@@ -67,6 +67,16 @@ const state = reactive({
67
67
  userFilter: 'all',
68
68
  newDocs: {
69
69
  sites: createSiteSettingsNewDocSchema(),
70
+ pages: {
71
+ name: { bindings: { 'field-type': 'text', 'label': 'Name', 'helper': 'Name' }, cols: '12', value: '' },
72
+ content: { value: [] },
73
+ postContent: { value: [] },
74
+ structure: { value: [] },
75
+ postStructure: { value: [] },
76
+ metaTitle: { value: '' },
77
+ metaDescription: { value: '' },
78
+ structuredData: { value: buildPageStructuredData() },
79
+ },
70
80
  },
71
81
  mounted: false,
72
82
  page: {},
@@ -81,8 +91,19 @@ const state = reactive({
81
91
  submissionFilter: '',
82
92
  selectedSubmissionId: '',
83
93
  publishSiteLoading: false,
94
+ importingPages: false,
95
+ importPageDocIdDialogOpen: false,
96
+ importPageDocIdValue: '',
97
+ importPageConflictDialogOpen: false,
98
+ importPageConflictDocId: '',
99
+ importPageErrorDialogOpen: false,
100
+ importPageErrorMessage: '',
84
101
  })
85
102
 
103
+ const pageImportInputRef = ref(null)
104
+ const pageImportDocIdResolver = ref(null)
105
+ const pageImportConflictResolver = ref(null)
106
+
86
107
  const pageInit = {
87
108
  name: '',
88
109
  content: [],
@@ -154,6 +175,44 @@ const canCreateSite = computed(() => {
154
175
  return true
155
176
  return isOrgAdmin.value
156
177
  })
178
+ const cmsMultiOrg = useState('cmsMultiOrg', () => false)
179
+ const canEditSiteSettings = computed(() => {
180
+ if (!cmsMultiOrg.value)
181
+ return true
182
+ return currentOrgRoleName.value === 'admin' || currentOrgRoleName.value === 'site admin'
183
+ })
184
+ const useMenuPublishLabels = computed(() => {
185
+ return cmsMultiOrg.value && !canEditSiteSettings.value
186
+ })
187
+ const cmsSiteTabs = useState('cmsSiteTabs', () => ({
188
+ pages: true,
189
+ posts: true,
190
+ inbox: true,
191
+ }))
192
+ const cmsTabAccess = computed(() => {
193
+ const normalized = {
194
+ pages: cmsSiteTabs.value?.pages !== false,
195
+ posts: cmsSiteTabs.value?.posts !== false,
196
+ inbox: cmsSiteTabs.value?.inbox !== false,
197
+ }
198
+ if (!normalized.pages && !normalized.posts && !normalized.inbox) {
199
+ normalized.inbox = true
200
+ }
201
+ return normalized
202
+ })
203
+ const canViewPagesTab = computed(() => cmsTabAccess.value.pages)
204
+ const canViewPostsTab = computed(() => cmsTabAccess.value.posts)
205
+ const canViewInboxTab = computed(() => cmsTabAccess.value.inbox)
206
+ const hidePublishStatusAndActions = computed(() => cmsMultiOrg.value && !canViewPagesTab.value)
207
+ const defaultViewMode = computed(() => {
208
+ if (canViewPagesTab.value)
209
+ return 'pages'
210
+ if (canViewPostsTab.value)
211
+ return 'posts'
212
+ if (canViewInboxTab.value)
213
+ return 'submissions'
214
+ return 'pages'
215
+ })
157
216
 
158
217
  const siteData = computed(() => {
159
218
  return edgeFirebase.data?.[`${edgeGlobal.edgeState.organizationDocPath}/sites`]?.[props.site] || {}
@@ -854,7 +913,7 @@ const discardSiteSettings = async () => {
854
913
  brandLogoLight: publishedSite.brandLogoLight || '',
855
914
  favicon: publishedSite.favicon || '',
856
915
  menuPosition: publishedSite.menuPosition || '',
857
- forwardApex: publishedSite.forwardApex === false ? false : true,
916
+ forwardApex: publishedSite.forwardApex !== false,
858
917
  contactEmail: publishedSite.contactEmail || '',
859
918
  contactPhone: publishedSite.contactPhone || '',
860
919
  metaTitle: publishedSite.metaTitle || '',
@@ -925,6 +984,326 @@ const pageList = computed(() => {
925
984
  .sort((a, b) => (b.lastUpdated ?? 0) - (a.lastUpdated ?? 0))
926
985
  })
927
986
 
987
+ const INVALID_PAGE_IMPORT_MESSAGE = 'Invalid file. Please import a valid page file.'
988
+ const pageImportCollectionPath = computed(() => `${edgeGlobal.edgeState.organizationDocPath}/sites/${props.site}/pages`)
989
+
990
+ const readTextFile = file => new Promise((resolve, reject) => {
991
+ if (typeof FileReader === 'undefined') {
992
+ reject(new Error('File import is only available in the browser.'))
993
+ return
994
+ }
995
+ const reader = new FileReader()
996
+ reader.onload = () => resolve(String(reader.result || ''))
997
+ reader.onerror = () => reject(new Error('Could not read the selected file.'))
998
+ reader.readAsText(file)
999
+ })
1000
+
1001
+ const isPlainObject = value => !!value && typeof value === 'object' && !Array.isArray(value)
1002
+
1003
+ const normalizeImportedPageDoc = (payload, fallbackDocId = '') => {
1004
+ if (!payload || typeof payload !== 'object' || Array.isArray(payload))
1005
+ throw new Error(INVALID_PAGE_IMPORT_MESSAGE)
1006
+
1007
+ if (payload.document && typeof payload.document === 'object' && !Array.isArray(payload.document)) {
1008
+ const normalized = { ...payload.document }
1009
+ if (!normalized.docId && payload.docId)
1010
+ normalized.docId = payload.docId
1011
+ if (!normalized.docId && fallbackDocId)
1012
+ normalized.docId = fallbackDocId
1013
+ return normalized
1014
+ }
1015
+
1016
+ const normalized = { ...payload }
1017
+ if (!normalized.docId && fallbackDocId)
1018
+ normalized.docId = fallbackDocId
1019
+ return normalized
1020
+ }
1021
+
1022
+ const cloneSchemaValue = (value) => {
1023
+ if (isPlainObject(value) || Array.isArray(value))
1024
+ return edgeGlobal.dupObject(value)
1025
+ return value
1026
+ }
1027
+
1028
+ const getDocDefaultsFromSchema = (schema = {}) => {
1029
+ const defaults = {}
1030
+ for (const [key, schemaEntry] of Object.entries(schema || {})) {
1031
+ const hasValueProp = isPlainObject(schemaEntry) && Object.prototype.hasOwnProperty.call(schemaEntry, 'value')
1032
+ const baseValue = hasValueProp ? schemaEntry.value : schemaEntry
1033
+ defaults[key] = cloneSchemaValue(baseValue)
1034
+ }
1035
+ return defaults
1036
+ }
1037
+
1038
+ const getPageDocDefaults = () => getDocDefaultsFromSchema(state.newDocs?.pages || {})
1039
+
1040
+ const isBlankString = value => String(value || '').trim() === ''
1041
+
1042
+ const applyImportedPageSeoDefaults = (doc) => {
1043
+ if (!isPlainObject(doc))
1044
+ return doc
1045
+
1046
+ if (isBlankString(doc.structuredData))
1047
+ doc.structuredData = buildPageStructuredData()
1048
+
1049
+ if (doc.post && isBlankString(doc.postStructuredData))
1050
+ doc.postStructuredData = doc.structuredData || buildPageStructuredData()
1051
+
1052
+ return doc
1053
+ }
1054
+
1055
+ const validateImportedPageDoc = (doc) => {
1056
+ if (!isPlainObject(doc))
1057
+ throw new Error(INVALID_PAGE_IMPORT_MESSAGE)
1058
+
1059
+ const requiredKeys = Object.keys(state.newDocs?.pages || {})
1060
+ const missing = requiredKeys.filter(key => !Object.prototype.hasOwnProperty.call(doc, key))
1061
+ if (missing.length)
1062
+ throw new Error(INVALID_PAGE_IMPORT_MESSAGE)
1063
+
1064
+ return doc
1065
+ }
1066
+
1067
+ const normalizeMenusForImport = (menus) => {
1068
+ const normalized = isPlainObject(menus) ? edgeGlobal.dupObject(menus) : {}
1069
+ if (!Array.isArray(normalized['Site Root']))
1070
+ normalized['Site Root'] = []
1071
+ if (!Array.isArray(normalized['Not In Menu']))
1072
+ normalized['Not In Menu'] = []
1073
+ return normalized
1074
+ }
1075
+
1076
+ const walkMenuEntries = (items, callback) => {
1077
+ if (!Array.isArray(items))
1078
+ return
1079
+ for (const entry of items) {
1080
+ if (!entry || typeof entry !== 'object')
1081
+ continue
1082
+ callback(entry)
1083
+ if (isPlainObject(entry.item)) {
1084
+ for (const nested of Object.values(entry.item)) {
1085
+ if (Array.isArray(nested))
1086
+ walkMenuEntries(nested, callback)
1087
+ }
1088
+ }
1089
+ }
1090
+ }
1091
+
1092
+ const menuIncludesDocId = (menus, docId) => {
1093
+ let found = false
1094
+ const checkEntry = (entry) => {
1095
+ if (found)
1096
+ return
1097
+ if (typeof entry?.item === 'string' && entry.item === docId)
1098
+ found = true
1099
+ }
1100
+ for (const menuItems of Object.values(menus || {})) {
1101
+ walkMenuEntries(menuItems, checkEntry)
1102
+ if (found)
1103
+ return true
1104
+ }
1105
+ return false
1106
+ }
1107
+
1108
+ const collectMenuPageNames = (menus) => {
1109
+ const names = new Set()
1110
+ const collectEntry = (entry) => {
1111
+ if (typeof entry?.item !== 'string')
1112
+ return
1113
+ const name = String(entry?.name || '').trim()
1114
+ if (name)
1115
+ names.add(name)
1116
+ }
1117
+ for (const menuItems of Object.values(menus || {}))
1118
+ walkMenuEntries(menuItems, collectEntry)
1119
+ return names
1120
+ }
1121
+
1122
+ const slugifyMenuPageName = (value) => {
1123
+ return String(value || '')
1124
+ .trim()
1125
+ .toLowerCase()
1126
+ .replace(/[^a-z0-9]+/g, '-')
1127
+ .replace(/(^-|-$)+/g, '') || 'page'
1128
+ }
1129
+
1130
+ const makeUniqueMenuPageName = (value, existingNames = new Set()) => {
1131
+ const base = slugifyMenuPageName(value)
1132
+ let candidate = base
1133
+ let suffix = 2
1134
+ while (existingNames.has(candidate)) {
1135
+ candidate = `${base}-${suffix}`
1136
+ suffix += 1
1137
+ }
1138
+ return candidate
1139
+ }
1140
+
1141
+ const addImportedPageToSiteMenu = (docId, pageName = '') => {
1142
+ if (isTemplateSite.value)
1143
+ return
1144
+
1145
+ const nextDocId = String(docId || '').trim()
1146
+ if (!nextDocId)
1147
+ return
1148
+
1149
+ const menus = normalizeMenusForImport(siteData.value?.menus || state.menus)
1150
+ if (menuIncludesDocId(menus, nextDocId)) {
1151
+ state.menus = menus
1152
+ return
1153
+ }
1154
+
1155
+ const existingNames = collectMenuPageNames(menus)
1156
+ const menuName = makeUniqueMenuPageName(pageName || nextDocId, existingNames)
1157
+ menus['Site Root'].push({ name: menuName, item: nextDocId })
1158
+ state.menus = menus
1159
+ }
1160
+
1161
+ const makeRandomPageDocId = (docsMap = {}) => {
1162
+ let nextDocId = String(edgeGlobal.generateShortId() || '').trim()
1163
+ while (!nextDocId || docsMap[nextDocId])
1164
+ nextDocId = String(edgeGlobal.generateShortId() || '').trim()
1165
+ return nextDocId
1166
+ }
1167
+
1168
+ const makeImportedPageNameForNew = (baseName, docsMap = {}) => {
1169
+ const normalizedBase = String(baseName || '').trim() || 'page'
1170
+ const existingNames = new Set(
1171
+ Object.values(docsMap || {})
1172
+ .map(doc => String(doc?.name || '').trim().toLowerCase())
1173
+ .filter(Boolean),
1174
+ )
1175
+
1176
+ let suffix = 1
1177
+ let candidate = `${normalizedBase}-${suffix}`
1178
+ while (existingNames.has(candidate.toLowerCase())) {
1179
+ suffix += 1
1180
+ candidate = `${normalizedBase}-${suffix}`
1181
+ }
1182
+ return candidate
1183
+ }
1184
+
1185
+ const requestPageImportDocId = (initialValue = '') => {
1186
+ state.importPageDocIdValue = String(initialValue || '')
1187
+ state.importPageDocIdDialogOpen = true
1188
+ return new Promise((resolve) => {
1189
+ pageImportDocIdResolver.value = resolve
1190
+ })
1191
+ }
1192
+
1193
+ const resolvePageImportDocId = (value = '') => {
1194
+ const resolver = pageImportDocIdResolver.value
1195
+ pageImportDocIdResolver.value = null
1196
+ state.importPageDocIdDialogOpen = false
1197
+ if (resolver)
1198
+ resolver(String(value || '').trim())
1199
+ }
1200
+
1201
+ const requestPageImportConflict = (docId) => {
1202
+ state.importPageConflictDocId = String(docId || '')
1203
+ state.importPageConflictDialogOpen = true
1204
+ return new Promise((resolve) => {
1205
+ pageImportConflictResolver.value = resolve
1206
+ })
1207
+ }
1208
+
1209
+ const resolvePageImportConflict = (action = 'cancel') => {
1210
+ const resolver = pageImportConflictResolver.value
1211
+ pageImportConflictResolver.value = null
1212
+ state.importPageConflictDialogOpen = false
1213
+ if (resolver)
1214
+ resolver(action)
1215
+ }
1216
+
1217
+ const getImportDocId = async (incomingDoc, fallbackDocId = '') => {
1218
+ let nextDocId = String(incomingDoc?.docId || '').trim()
1219
+ if (!nextDocId)
1220
+ nextDocId = await requestPageImportDocId(fallbackDocId)
1221
+ if (!nextDocId)
1222
+ throw new Error('Import canceled. A docId is required.')
1223
+ if (nextDocId.includes('/'))
1224
+ throw new Error('docId cannot include "/".')
1225
+ return nextDocId
1226
+ }
1227
+
1228
+ const openImportErrorDialog = (message) => {
1229
+ state.importPageErrorMessage = String(message || 'Failed to import page.')
1230
+ state.importPageErrorDialogOpen = true
1231
+ }
1232
+
1233
+ const triggerPageImport = () => {
1234
+ pageImportInputRef.value?.click()
1235
+ }
1236
+
1237
+ const importSinglePageFile = async (file, existingPages = {}, fallbackDocId = '') => {
1238
+ const fileText = await readTextFile(file)
1239
+ const parsed = JSON.parse(fileText)
1240
+ const importedDoc = applyImportedPageSeoDefaults(validateImportedPageDoc(normalizeImportedPageDoc(parsed, fallbackDocId)))
1241
+ const incomingDocId = await getImportDocId(importedDoc, fallbackDocId)
1242
+ let targetDocId = incomingDocId
1243
+ let importDecision = 'create'
1244
+
1245
+ if (existingPages[targetDocId]) {
1246
+ const decision = await requestPageImportConflict(targetDocId)
1247
+ if (decision === 'cancel')
1248
+ return
1249
+ if (decision === 'new') {
1250
+ targetDocId = makeRandomPageDocId(existingPages)
1251
+ importedDoc.name = makeImportedPageNameForNew(importedDoc.name || incomingDocId, existingPages)
1252
+ importDecision = 'new'
1253
+ }
1254
+ else {
1255
+ importDecision = 'overwrite'
1256
+ }
1257
+ }
1258
+
1259
+ const payload = { ...getPageDocDefaults(), ...importedDoc, docId: targetDocId }
1260
+ await edgeFirebase.storeDoc(pageImportCollectionPath.value, payload, targetDocId)
1261
+ existingPages[targetDocId] = payload
1262
+ addImportedPageToSiteMenu(targetDocId, payload.name)
1263
+
1264
+ if (importDecision === 'overwrite')
1265
+ edgeFirebase?.toast?.success?.(`Overwrote page "${targetDocId}".`)
1266
+ else if (importDecision === 'new')
1267
+ edgeFirebase?.toast?.success?.(`Imported page as new "${targetDocId}".`)
1268
+ else
1269
+ edgeFirebase?.toast?.success?.(`Imported page "${targetDocId}".`)
1270
+ }
1271
+
1272
+ const handlePageImport = async (event) => {
1273
+ const input = event?.target
1274
+ const files = Array.from(input?.files || [])
1275
+ if (!files.length)
1276
+ return
1277
+
1278
+ state.importingPages = true
1279
+ const existingPages = { ...(pages.value || {}) }
1280
+ try {
1281
+ if (!edgeFirebase.data?.[pageImportCollectionPath.value])
1282
+ await edgeFirebase.startSnapshot(pageImportCollectionPath.value)
1283
+
1284
+ for (const file of files) {
1285
+ try {
1286
+ await importSinglePageFile(file, existingPages, '')
1287
+ }
1288
+ catch (error) {
1289
+ console.error('Failed to import page file', error)
1290
+ const message = error?.message || 'Failed to import page file.'
1291
+ if (/^Import canceled\./i.test(message))
1292
+ continue
1293
+ if (error instanceof SyntaxError || message === INVALID_PAGE_IMPORT_MESSAGE)
1294
+ openImportErrorDialog(INVALID_PAGE_IMPORT_MESSAGE)
1295
+ else
1296
+ openImportErrorDialog(message)
1297
+ }
1298
+ }
1299
+ }
1300
+ finally {
1301
+ state.importingPages = false
1302
+ if (input)
1303
+ input.value = ''
1304
+ }
1305
+ }
1306
+
928
1307
  const formatTimestamp = (input) => {
929
1308
  if (!input)
930
1309
  return 'Not yet saved'
@@ -978,10 +1357,39 @@ const isPublishedPageDiff = (pageId) => {
978
1357
 
979
1358
  const pageStatusLabel = pageId => (isPublishedPageDiff(pageId) ? 'Draft' : 'Published')
980
1359
  const hasSelection = computed(() => Boolean(props.page) || Boolean(state.selectedPostId))
981
- const showSplitView = computed(() => isTemplateSite.value || state.viewMode === 'pages' || hasSelection.value)
982
- const isEditingPost = computed(() => state.viewMode === 'posts' && Boolean(state.selectedPostId))
1360
+ const showSplitView = computed(() => isTemplateSite.value || (canViewPagesTab.value && (state.viewMode === 'pages' || hasSelection.value)))
1361
+ const isEditingPost = computed(() => canViewPostsTab.value && state.viewMode === 'posts' && Boolean(state.selectedPostId))
1362
+
1363
+ const ensureValidViewMode = () => {
1364
+ let nextMode = state.viewMode
1365
+ if (nextMode === 'pages' && !canViewPagesTab.value)
1366
+ nextMode = defaultViewMode.value
1367
+ if (nextMode === 'posts' && !canViewPostsTab.value)
1368
+ nextMode = defaultViewMode.value
1369
+ if (nextMode === 'submissions' && !canViewInboxTab.value)
1370
+ nextMode = defaultViewMode.value
1371
+
1372
+ if (state.viewMode !== nextMode)
1373
+ state.viewMode = nextMode
1374
+
1375
+ if (state.viewMode !== 'posts') {
1376
+ state.selectedPostId = ''
1377
+ }
1378
+ if (state.viewMode !== 'submissions') {
1379
+ state.selectedSubmissionId = ''
1380
+ }
1381
+ if (props.page && state.viewMode !== 'pages') {
1382
+ router.replace(pageRouteBase.value)
1383
+ }
1384
+ }
983
1385
 
984
1386
  const setViewMode = (mode) => {
1387
+ if (mode === 'pages' && !canViewPagesTab.value)
1388
+ return
1389
+ if (mode === 'posts' && !canViewPostsTab.value)
1390
+ return
1391
+ if (mode === 'submissions' && !canViewInboxTab.value)
1392
+ return
985
1393
  if (state.viewMode === mode)
986
1394
  return
987
1395
  state.viewMode = mode
@@ -995,6 +1403,8 @@ const setViewMode = (mode) => {
995
1403
  const handlePostSelect = (postId) => {
996
1404
  if (!postId)
997
1405
  return
1406
+ if (!canViewPostsTab.value)
1407
+ return
998
1408
  state.selectedPostId = postId
999
1409
  state.viewMode = 'posts'
1000
1410
  if (props.page)
@@ -1005,6 +1415,22 @@ const clearPostSelection = () => {
1005
1415
  state.selectedPostId = ''
1006
1416
  }
1007
1417
 
1418
+ watch(() => state.importPageDocIdDialogOpen, (open) => {
1419
+ if (!open && pageImportDocIdResolver.value) {
1420
+ const resolver = pageImportDocIdResolver.value
1421
+ pageImportDocIdResolver.value = null
1422
+ resolver('')
1423
+ }
1424
+ })
1425
+
1426
+ watch(() => state.importPageConflictDialogOpen, (open) => {
1427
+ if (!open && pageImportConflictResolver.value) {
1428
+ const resolver = pageImportConflictResolver.value
1429
+ pageImportConflictResolver.value = null
1430
+ resolver('cancel')
1431
+ }
1432
+ })
1433
+
1008
1434
  watch (() => siteData.value, () => {
1009
1435
  if (isTemplateSite.value)
1010
1436
  return
@@ -1040,14 +1466,33 @@ watch(pages, (pagesCollection) => {
1040
1466
  watch(() => props.page, (next) => {
1041
1467
  if (next) {
1042
1468
  state.selectedPostId = ''
1043
- state.viewMode = 'pages'
1469
+ if (canViewPagesTab.value) {
1470
+ state.viewMode = 'pages'
1471
+ }
1472
+ else {
1473
+ state.viewMode = defaultViewMode.value
1474
+ if (props.page)
1475
+ router.replace(pageRouteBase.value)
1476
+ }
1044
1477
  return
1045
1478
  }
1046
- if (state.selectedPostId) {
1479
+ if (state.selectedPostId && canViewPostsTab.value) {
1047
1480
  state.viewMode = 'posts'
1481
+ return
1048
1482
  }
1483
+ ensureValidViewMode()
1049
1484
  })
1050
1485
 
1486
+ watch(cmsTabAccess, () => {
1487
+ ensureValidViewMode()
1488
+ }, { immediate: true, deep: true })
1489
+
1490
+ watch(canEditSiteSettings, (allowed) => {
1491
+ if (!allowed && state.siteSettings) {
1492
+ state.siteSettings = false
1493
+ }
1494
+ }, { immediate: true })
1495
+
1051
1496
  watch([isViewingSubmissions, sortedSubmissionIds], () => {
1052
1497
  if (!isViewingSubmissions.value)
1053
1498
  return
@@ -1293,7 +1738,7 @@ const pageSettingsUpdated = async (pageData) => {
1293
1738
  @update:model-value="value => (slotProps.workingDoc.theme = value || '')"
1294
1739
  />
1295
1740
  <edge-shad-select-tags
1296
- v-if="Object.keys(orgUsers).length > 0"
1741
+ v-if="!cmsMultiOrg && Object.keys(orgUsers).length > 0"
1297
1742
  :model-value="getSiteUsersModel(slotProps.workingDoc)"
1298
1743
  :disabled="shouldForceCurrentUserForNewSite || !edgeGlobal.isAdminGlobal(edgeFirebase).value"
1299
1744
  :items="userOptions"
@@ -1324,6 +1769,7 @@ const pageSettingsUpdated = async (pageData) => {
1324
1769
  </div>
1325
1770
  <div class="space-y-3">
1326
1771
  <edge-shad-select
1772
+ v-if="!cmsMultiOrg"
1327
1773
  :model-value="slotProps.workingDoc.aiAgentUserId || ''"
1328
1774
  name="aiAgentUserId"
1329
1775
  label="User Data for AI to use to build initial site"
@@ -1350,16 +1796,20 @@ const pageSettingsUpdated = async (pageData) => {
1350
1796
  Only organization admins can create sites.
1351
1797
  </div>
1352
1798
  <div v-else class="flex flex-col h-[calc(100vh-58px)] overflow-hidden">
1353
- <div class="grid grid-cols-[1fr_auto_1fr] items-center gap-3 px-4 py-2 border-b bg-secondary">
1799
+ <div
1800
+ class="grid grid-cols-[1fr_auto_1fr] items-center gap-3 px-4 py-2 border bg-secondary"
1801
+ :class="isTemplateSite ? 'min-h-[68px]' : ''"
1802
+ >
1354
1803
  <div class="flex items-center gap-3">
1355
1804
  <FileStack class="w-5 h-5" />
1356
- <span class="text-lg font-semibold">
1805
+ <span class="text-lg font-normal">
1357
1806
  {{ siteData.name || 'Templates' }}
1358
1807
  </span>
1359
1808
  </div>
1360
1809
  <div class="flex justify-center">
1361
- <div v-if="!isTemplateSite" class="flex items-center rounded-full border border-border bg-background p-1 shadow-sm">
1810
+ <div v-if="!isTemplateSite && (canViewPagesTab || canViewPostsTab || canViewInboxTab)" class="flex items-center rounded-full border border-border bg-background p-1 shadow-sm">
1362
1811
  <edge-shad-button
1812
+ v-if="canViewPagesTab"
1363
1813
  variant="ghost"
1364
1814
  size="sm"
1365
1815
  class="h-8 px-4 text-xs gap-2 rounded-full"
@@ -1370,6 +1820,7 @@ const pageSettingsUpdated = async (pageData) => {
1370
1820
  Pages
1371
1821
  </edge-shad-button>
1372
1822
  <edge-shad-button
1823
+ v-if="canViewPostsTab"
1373
1824
  variant="ghost"
1374
1825
  size="sm"
1375
1826
  class="h-8 px-4 text-xs gap-2 rounded-full"
@@ -1380,6 +1831,7 @@ const pageSettingsUpdated = async (pageData) => {
1380
1831
  Posts
1381
1832
  </edge-shad-button>
1382
1833
  <edge-shad-button
1834
+ v-if="canViewInboxTab"
1383
1835
  variant="ghost"
1384
1836
  size="sm"
1385
1837
  class="h-8 px-4 text-xs gap-2 rounded-full"
@@ -1397,76 +1849,98 @@ const pageSettingsUpdated = async (pageData) => {
1397
1849
  </edge-shad-button>
1398
1850
  </div>
1399
1851
  </div>
1400
- <div v-if="!isTemplateSite" class="flex items-center gap-3 justify-end">
1401
- <Transition name="fade" mode="out-in">
1402
- <div v-if="isSiteDiff || isAnyPagesDiff" key="unpublished" class="flex gap-2 items-center">
1403
- <div class="flex gap-1 items-center bg-yellow-100 text-xs py-1 px-3 text-yellow-800 rounded">
1404
- <CircleAlert class="!text-yellow-800 w-3 h-6" />
1852
+ <div class="flex items-center gap-3 justify-end">
1853
+ <input
1854
+ ref="pageImportInputRef"
1855
+ type="file"
1856
+ multiple
1857
+ accept=".json,application/json"
1858
+ class="hidden"
1859
+ @change="handlePageImport"
1860
+ >
1861
+ <edge-shad-button
1862
+ type="button"
1863
+ size="icon"
1864
+ variant="outline"
1865
+ class="h-9 w-9"
1866
+ :disabled="state.importingPages"
1867
+ title="Import Page"
1868
+ aria-label="Import Page"
1869
+ @click="triggerPageImport"
1870
+ >
1871
+ <Loader2 v-if="state.importingPages" class="h-3.5 w-3.5 animate-spin" />
1872
+ <Upload v-else class="h-3.5 w-3.5" />
1873
+ </edge-shad-button>
1874
+ <template v-if="!isTemplateSite && !hidePublishStatusAndActions">
1875
+ <Transition name="fade" mode="out-in">
1876
+ <div v-if="isSiteDiff || isAnyPagesDiff" key="unpublished" class="flex gap-2 items-center">
1877
+ <div class="flex gap-1 items-center bg-yellow-100 text-xs py-1 px-3 text-yellow-800 rounded">
1878
+ <CircleAlert class="!text-yellow-800 w-3 h-6" />
1879
+ <span class="font-medium text-[10px]">
1880
+ {{ isSiteDiff ? (useMenuPublishLabels ? 'Unpublished Menu' : 'Unpublished Settings') : 'Unpublished Pages' }}
1881
+ </span>
1882
+ </div>
1883
+ <edge-shad-button
1884
+ class="h-8 px-4 text-xs gap-2 bg-primary text-primary-foreground hover:bg-primary/90 shadow-sm"
1885
+ :disabled="state.publishSiteLoading"
1886
+ @click="publishSiteAndSettings"
1887
+ >
1888
+ <Loader2 v-if="state.publishSiteLoading" class="h-3.5 w-3.5 animate-spin" />
1889
+ <FolderUp v-else class="h-3.5 w-3.5" />
1890
+ Publish Site
1891
+ </edge-shad-button>
1892
+ </div>
1893
+ <div v-else key="published" class="flex gap-1 items-center bg-green-100 text-xs py-1 px-3 text-green-800 rounded">
1894
+ <FileCheck class="!text-green-800 w-3 h-6" />
1405
1895
  <span class="font-medium text-[10px]">
1406
- {{ isSiteDiff ? 'Unpublished Settings' : 'Unpublished Pages' }}
1896
+ {{ useMenuPublishLabels ? 'Menu Published' : 'Settings Published' }}
1407
1897
  </span>
1408
1898
  </div>
1409
- <edge-shad-button
1410
- class="h-8 px-4 text-xs gap-2 bg-primary text-primary-foreground hover:bg-primary/90 shadow-sm"
1411
- :disabled="state.publishSiteLoading"
1412
- @click="publishSiteAndSettings"
1413
- >
1414
- <Loader2 v-if="state.publishSiteLoading" class="h-3.5 w-3.5 animate-spin" />
1415
- <FolderUp v-else class="h-3.5 w-3.5" />
1416
- Publish Site
1417
- </edge-shad-button>
1418
- </div>
1419
- <div v-else key="published" class="flex gap-1 items-center bg-green-100 text-xs py-1 px-3 text-green-800 rounded">
1420
- <FileCheck class="!text-green-800 w-3 h-6" />
1421
- <span class="font-medium text-[10px]">
1422
- Settings Published
1423
- </span>
1424
- </div>
1425
- </Transition>
1426
- <DropdownMenu>
1427
- <DropdownMenuTrigger as-child>
1428
- <edge-shad-button variant="outline" size="icon" class="h-9 w-9">
1429
- <MoreHorizontal />
1430
- </edge-shad-button>
1431
- </DropdownMenuTrigger>
1432
- <DropdownMenuContent side="right" align="start">
1433
- <DropdownMenuLabel class="flex items-center gap-2">
1434
- <FileStack class="w-5 h-5" />{{ siteData.name || 'Templates' }}
1435
- </DropdownMenuLabel>
1436
-
1437
- <DropdownMenuSeparator v-if="isSiteDiff" />
1438
- <DropdownMenuLabel v-if="isSiteDiff" class="flex items-center gap-2">
1439
- Site Settings
1440
- </DropdownMenuLabel>
1441
-
1442
- <DropdownMenuItem v-if="isSiteDiff" class="pl-4 text-xs" @click="publishSiteSettings">
1443
- <FolderUp />
1444
- Publish
1445
- </DropdownMenuItem>
1446
- <DropdownMenuItem v-if="isSiteDiff && isSiteSettingPublished" class="pl-4 text-xs" @click="discardSiteSettings">
1447
- <FolderX />
1448
- Discard Changes
1449
- </DropdownMenuItem>
1450
- <DropdownMenuSeparator />
1451
- <DropdownMenuItem v-if="isAnyPagesDiff" @click="publishSite">
1452
- <FolderUp />
1453
- Publish All Pages
1454
- </DropdownMenuItem>
1455
- <DropdownMenuItem v-if="isSiteSettingPublished || isAnyPagesPublished" @click="unPublishSite">
1456
- <FolderDown />
1457
- Unpublish Site
1458
- </DropdownMenuItem>
1459
-
1460
- <DropdownMenuItem @click="state.siteSettings = true">
1461
- <FolderCog />
1462
- <span>Settings</span>
1463
- </DropdownMenuItem>
1464
- </DropdownMenuContent>
1465
- </DropdownMenu>
1899
+ </Transition>
1900
+ <DropdownMenu>
1901
+ <DropdownMenuTrigger as-child>
1902
+ <edge-shad-button variant="outline" size="icon" class="h-9 w-9">
1903
+ <MoreHorizontal />
1904
+ </edge-shad-button>
1905
+ </DropdownMenuTrigger>
1906
+ <DropdownMenuContent side="right" align="start">
1907
+ <DropdownMenuLabel class="flex items-center gap-2">
1908
+ <FileStack class="w-5 h-5" />{{ siteData.name || 'Templates' }}
1909
+ </DropdownMenuLabel>
1910
+
1911
+ <DropdownMenuSeparator v-if="isSiteDiff" />
1912
+ <DropdownMenuLabel v-if="isSiteDiff" class="flex items-center gap-2">
1913
+ Site Settings
1914
+ </DropdownMenuLabel>
1915
+
1916
+ <DropdownMenuItem v-if="isSiteDiff" class="pl-4 text-xs" @click="publishSiteSettings">
1917
+ <FolderUp />
1918
+ Publish
1919
+ </DropdownMenuItem>
1920
+ <DropdownMenuItem v-if="isSiteDiff && isSiteSettingPublished" class="pl-4 text-xs" @click="discardSiteSettings">
1921
+ <FolderX />
1922
+ Discard Changes
1923
+ </DropdownMenuItem>
1924
+ <DropdownMenuSeparator />
1925
+ <DropdownMenuItem v-if="isAnyPagesDiff" @click="publishSite">
1926
+ <FolderUp />
1927
+ Publish All Pages
1928
+ </DropdownMenuItem>
1929
+ <DropdownMenuItem v-if="isSiteSettingPublished || isAnyPagesPublished" @click="unPublishSite">
1930
+ <FolderDown />
1931
+ Unpublish Site
1932
+ </DropdownMenuItem>
1933
+
1934
+ <DropdownMenuItem v-if="canEditSiteSettings" @click="state.siteSettings = true">
1935
+ <FolderCog />
1936
+ <span>Settings</span>
1937
+ </DropdownMenuItem>
1938
+ </DropdownMenuContent>
1939
+ </DropdownMenu>
1940
+ </template>
1466
1941
  </div>
1467
- <div v-else />
1468
1942
  </div>
1469
- <div class="flex-1">
1943
+ <div class="flex-1 min-h-0">
1470
1944
  <Transition name="fade" mode="out-in">
1471
1945
  <div v-if="isViewingSubmissions" class="flex-1 overflow-y-auto p-6">
1472
1946
  <edge-dashboard
@@ -1616,12 +2090,12 @@ const pageSettingsUpdated = async (pageData) => {
1616
2090
  @update:selected-post-id="clearPostSelection"
1617
2091
  />
1618
2092
  </div>
1619
- <ResizablePanelGroup v-else-if="showSplitView" direction="horizontal" class="w-full h-full flex-1">
1620
- <ResizablePanel class="bg-sidebar text-sidebar-foreground" :default-size="16">
1621
- <SidebarGroup class="mt-0 pt-0">
1622
- <SidebarGroupContent>
1623
- <SidebarMenu>
1624
- <template v-if="isTemplateSite || state.viewMode === 'pages'">
2093
+ <ResizablePanelGroup v-else-if="showSplitView" direction="horizontal" class="w-full h-full flex-1 min-h-0">
2094
+ <ResizablePanel class="bg-primary-foreground text-black min-h-0 overflow-hidden" :default-size="16">
2095
+ <SidebarGroup class="mt-0 pt-0 h-full min-h-0">
2096
+ <SidebarGroupContent class="h-full min-h-0 overflow-y-auto">
2097
+ <SidebarMenu class="pb-4">
2098
+ <template v-if="isTemplateSite || (canViewPagesTab && state.viewMode === 'pages')">
1625
2099
  <edge-cms-menu
1626
2100
  v-if="state.menus"
1627
2101
  v-model="state.menus"
@@ -1645,7 +2119,7 @@ const pageSettingsUpdated = async (pageData) => {
1645
2119
  </SidebarGroupContent>
1646
2120
  </SidebarGroup>
1647
2121
  </ResizablePanel>
1648
- <ResizablePanel ref="mainPanel">
2122
+ <ResizablePanel ref="mainPanel" class="min-h-0">
1649
2123
  <Transition name="fade" mode="out-in">
1650
2124
  <div v-if="props.page && !state.updating" :key="props.page" class="max-h-[calc(100vh-100px)] overflow-y-auto w-full">
1651
2125
  <NuxtPage class="flex flex-col flex-1 px-0 mx-0 pt-0" />
@@ -1672,7 +2146,73 @@ const pageSettingsUpdated = async (pageData) => {
1672
2146
  </Transition>
1673
2147
  </div>
1674
2148
  </div>
1675
- <Sheet v-model:open="state.siteSettings">
2149
+ <edge-shad-dialog v-model="state.importPageDocIdDialogOpen">
2150
+ <DialogContent class="pt-8">
2151
+ <DialogHeader>
2152
+ <DialogTitle class="text-left">
2153
+ Enter Page Doc ID
2154
+ </DialogTitle>
2155
+ <DialogDescription>
2156
+ This file does not include a <code>docId</code>. Enter the doc ID you want to import into this site.
2157
+ </DialogDescription>
2158
+ </DialogHeader>
2159
+ <edge-shad-input
2160
+ v-model="state.importPageDocIdValue"
2161
+ name="site-page-import-doc-id"
2162
+ label="Doc ID"
2163
+ placeholder="example-page-id"
2164
+ />
2165
+ <DialogFooter class="pt-2 flex justify-between">
2166
+ <edge-shad-button variant="outline" @click="resolvePageImportDocId('')">
2167
+ Cancel
2168
+ </edge-shad-button>
2169
+ <edge-shad-button @click="resolvePageImportDocId(state.importPageDocIdValue)">
2170
+ Continue
2171
+ </edge-shad-button>
2172
+ </DialogFooter>
2173
+ </DialogContent>
2174
+ </edge-shad-dialog>
2175
+ <edge-shad-dialog v-model="state.importPageConflictDialogOpen">
2176
+ <DialogContent class="pt-8">
2177
+ <DialogHeader>
2178
+ <DialogTitle class="text-left">
2179
+ Page Already Exists
2180
+ </DialogTitle>
2181
+ <DialogDescription>
2182
+ <code>{{ state.importPageConflictDocId }}</code> already exists in this {{ isTemplateSite ? 'template library' : 'site' }}. Choose to overwrite it or import as a new page.
2183
+ </DialogDescription>
2184
+ </DialogHeader>
2185
+ <DialogFooter class="pt-2 flex justify-between">
2186
+ <edge-shad-button variant="outline" @click="resolvePageImportConflict('cancel')">
2187
+ Cancel
2188
+ </edge-shad-button>
2189
+ <edge-shad-button variant="outline" @click="resolvePageImportConflict('new')">
2190
+ Add As New
2191
+ </edge-shad-button>
2192
+ <edge-shad-button @click="resolvePageImportConflict('overwrite')">
2193
+ Overwrite
2194
+ </edge-shad-button>
2195
+ </DialogFooter>
2196
+ </DialogContent>
2197
+ </edge-shad-dialog>
2198
+ <edge-shad-dialog v-model="state.importPageErrorDialogOpen">
2199
+ <DialogContent class="pt-8">
2200
+ <DialogHeader>
2201
+ <DialogTitle class="text-left">
2202
+ Import Failed
2203
+ </DialogTitle>
2204
+ <DialogDescription class="text-left">
2205
+ {{ state.importPageErrorMessage }}
2206
+ </DialogDescription>
2207
+ </DialogHeader>
2208
+ <DialogFooter class="pt-2">
2209
+ <edge-shad-button @click="state.importPageErrorDialogOpen = false">
2210
+ Close
2211
+ </edge-shad-button>
2212
+ </DialogFooter>
2213
+ </DialogContent>
2214
+ </edge-shad-dialog>
2215
+ <Sheet v-if="canEditSiteSettings" v-model:open="state.siteSettings">
1676
2216
  <SheetContent side="left" class="w-full md:w-1/2 max-w-none sm:max-w-none max-w-2xl">
1677
2217
  <SheetHeader>
1678
2218
  <SheetTitle>{{ siteData.name || 'Site' }}</SheetTitle>
@@ -1697,7 +2237,7 @@ const pageSettingsUpdated = async (pageData) => {
1697
2237
  :theme-options="themeOptions"
1698
2238
  :user-options="userOptions"
1699
2239
  :has-users="Object.keys(orgUsers).length > 0"
1700
- :show-users="true"
2240
+ :show-users="!cmsMultiOrg"
1701
2241
  :show-theme-fields="true"
1702
2242
  :is-admin="isAdmin"
1703
2243
  :enable-media-picker="true"