@edgedev/create-edge-app 1.1.28 → 1.2.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.
@@ -14,6 +14,11 @@ const props = defineProps({
14
14
  required: false,
15
15
  default: '',
16
16
  },
17
+ disableAddSiteForNonAdmin: {
18
+ type: Boolean,
19
+ required: false,
20
+ default: false,
21
+ },
17
22
  })
18
23
  const edgeFirebase = inject('edgeFirebase')
19
24
  const { createDefaults: createSiteSettingsDefaults, createNewDocSchema: createSiteSettingsNewDocSchema } = useSiteSettingsTemplate()
@@ -33,6 +38,22 @@ const normalizeForCompare = (value) => {
33
38
 
34
39
  const stableSerialize = value => JSON.stringify(normalizeForCompare(value))
35
40
  const areEqualNormalized = (a, b) => stableSerialize(a) === stableSerialize(b)
41
+ const isJsonInvalid = (value) => {
42
+ if (value === null || value === undefined)
43
+ return false
44
+ if (typeof value === 'object')
45
+ return false
46
+ const text = String(value).trim()
47
+ if (!text)
48
+ return false
49
+ try {
50
+ JSON.parse(text)
51
+ return false
52
+ }
53
+ catch {
54
+ return true
55
+ }
56
+ }
36
57
 
37
58
  const isTemplateSite = computed(() => props.site === 'templates')
38
59
  const router = useRouter()
@@ -121,6 +142,17 @@ const schemas = {
121
142
  const isAdmin = computed(() => {
122
143
  return edgeGlobal.isAdminGlobal(edgeFirebase).value
123
144
  })
145
+ const currentOrgRoleName = computed(() => {
146
+ return String(edgeGlobal.getRoleName(edgeFirebase?.user?.roles || [], edgeGlobal.edgeState.currentOrganization) || '').toLowerCase()
147
+ })
148
+ const isOrgAdmin = computed(() => {
149
+ return currentOrgRoleName.value === 'admin'
150
+ })
151
+ const canCreateSite = computed(() => {
152
+ if (!props.disableAddSiteForNonAdmin)
153
+ return true
154
+ return isOrgAdmin.value
155
+ })
124
156
 
125
157
  const siteData = computed(() => {
126
158
  return edgeFirebase.data?.[`${edgeGlobal.edgeState.organizationDocPath}/sites`]?.[props.site] || {}
@@ -339,12 +371,72 @@ const themeOptionsMap = computed(() => {
339
371
  const orgUsers = computed(() => edgeFirebase.state?.users || {})
340
372
  const userOptions = computed(() => {
341
373
  return Object.entries(orgUsers.value || {})
374
+ .filter(([, user]) => Boolean(user?.userId))
342
375
  .map(([id, user]) => ({
343
- value: user?.userId || id,
376
+ value: user?.userId,
344
377
  label: user?.meta?.name || user?.userId || id,
345
378
  }))
346
379
  .sort((a, b) => a.label.localeCompare(b.label))
347
380
  })
381
+ const authUid = computed(() => String(edgeFirebase?.user?.uid || '').trim())
382
+ const currentOrgUser = computed(() => {
383
+ if (!authUid.value)
384
+ return null
385
+ const users = Object.values(orgUsers.value || {})
386
+ return users.find((user) => {
387
+ const userId = String(user?.userId || '').trim()
388
+ const docId = String(user?.docId || '').trim()
389
+ const uid = String(user?.uid || '').trim()
390
+ return userId === authUid.value || docId === authUid.value || uid === authUid.value
391
+ }) || null
392
+ })
393
+ const currentOrgUserId = computed(() => {
394
+ return String(
395
+ currentOrgUser.value?.userId
396
+ || currentOrgUser.value?.docId
397
+ || authUid.value
398
+ || '',
399
+ ).trim()
400
+ })
401
+ const currentUserOption = computed(() => {
402
+ if (!currentOrgUserId.value)
403
+ return null
404
+ return {
405
+ value: currentOrgUserId.value,
406
+ label: currentOrgUser.value?.meta?.name || currentOrgUser.value?.meta?.email || currentOrgUserId.value,
407
+ }
408
+ })
409
+ const shouldForceCurrentUserForNewSite = computed(() => !isAdmin.value && props.site === 'new')
410
+ const aiUserOptions = computed(() => {
411
+ if (!shouldForceCurrentUserForNewSite.value)
412
+ return userOptions.value
413
+ return currentUserOption.value ? [currentUserOption.value] : []
414
+ })
415
+ const normalizeUserIds = items => (Array.isArray(items) ? items : [])
416
+ .map(item => String(item || '').trim())
417
+ .filter(Boolean)
418
+ const getSiteUsersModel = (workingDoc) => {
419
+ if (!workingDoc || typeof workingDoc !== 'object')
420
+ return []
421
+ const users = normalizeUserIds(workingDoc?.users)
422
+ if (!shouldForceCurrentUserForNewSite.value)
423
+ return users
424
+ if (!currentOrgUserId.value)
425
+ return users
426
+ if (users.length === 1 && users[0] === currentOrgUserId.value)
427
+ return users
428
+ workingDoc.users = [currentOrgUserId.value]
429
+ return workingDoc.users
430
+ }
431
+ const updateSiteUsersModel = (workingDoc, value) => {
432
+ if (!workingDoc || typeof workingDoc !== 'object')
433
+ return
434
+ if (shouldForceCurrentUserForNewSite.value) {
435
+ workingDoc.users = currentOrgUserId.value ? [currentOrgUserId.value] : []
436
+ return
437
+ }
438
+ workingDoc.users = normalizeUserIds(value)
439
+ }
348
440
 
349
441
  const themeItemsForAllowed = (allowed, current) => {
350
442
  const base = themeOptions.value
@@ -859,6 +951,9 @@ const isPublishedPageDiff = (pageId) => {
859
951
  metaTitle: publishedPage.metaTitle,
860
952
  metaDescription: publishedPage.metaDescription,
861
953
  structuredData: publishedPage.structuredData,
954
+ postMetaTitle: publishedPage.postMetaTitle,
955
+ postMetaDescription: publishedPage.postMetaDescription,
956
+ postStructuredData: publishedPage.postStructuredData,
862
957
  },
863
958
  {
864
959
  content: draftPage.content,
@@ -868,6 +963,9 @@ const isPublishedPageDiff = (pageId) => {
868
963
  metaTitle: draftPage.metaTitle,
869
964
  metaDescription: draftPage.metaDescription,
870
965
  structuredData: draftPage.structuredData,
966
+ postMetaTitle: draftPage.postMetaTitle,
967
+ postMetaDescription: draftPage.postMetaDescription,
968
+ postStructuredData: draftPage.postStructuredData,
871
969
  },
872
970
  )
873
971
  }
@@ -1069,6 +1167,9 @@ const isAnyPagesDiff = computed(() => {
1069
1167
  metaTitle: pageData.metaTitle,
1070
1168
  metaDescription: pageData.metaDescription,
1071
1169
  structuredData: pageData.structuredData,
1170
+ postMetaTitle: pageData.postMetaTitle,
1171
+ postMetaDescription: pageData.postMetaDescription,
1172
+ postStructuredData: pageData.postStructuredData,
1072
1173
  },
1073
1174
  {
1074
1175
  content: publishedPage.content,
@@ -1078,6 +1179,9 @@ const isAnyPagesDiff = computed(() => {
1078
1179
  metaTitle: publishedPage.metaTitle,
1079
1180
  metaDescription: publishedPage.metaDescription,
1080
1181
  structuredData: publishedPage.structuredData,
1182
+ postMetaTitle: publishedPage.postMetaTitle,
1183
+ postMetaDescription: publishedPage.postMetaDescription,
1184
+ postStructuredData: publishedPage.postStructuredData,
1081
1185
  },
1082
1186
  )) {
1083
1187
  return true
@@ -1104,7 +1208,7 @@ const pageSettingsUpdated = async (pageData) => {
1104
1208
  v-if="edgeGlobal.edgeState.organizationDocPath"
1105
1209
  >
1106
1210
  <edge-editor
1107
- v-if="!props.page && props.site === 'new'"
1211
+ v-if="!props.page && props.site === 'new' && canCreateSite"
1108
1212
  collection="sites"
1109
1213
  :doc-id="props.site"
1110
1214
  :schema="schemas.sites"
@@ -1186,7 +1290,8 @@ const pageSettingsUpdated = async (pageData) => {
1186
1290
  />
1187
1291
  <edge-shad-select-tags
1188
1292
  v-if="Object.keys(orgUsers).length > 0"
1189
- v-model="slotProps.workingDoc.users" :disabled="!edgeGlobal.isAdminGlobal(edgeFirebase).value"
1293
+ :model-value="getSiteUsersModel(slotProps.workingDoc)"
1294
+ :disabled="shouldForceCurrentUserForNewSite || !edgeGlobal.isAdminGlobal(edgeFirebase).value"
1190
1295
  :items="userOptions"
1191
1296
  name="users"
1192
1297
  label="Users"
@@ -1195,6 +1300,7 @@ const pageSettingsUpdated = async (pageData) => {
1195
1300
  placeholder="Select users"
1196
1301
  class="w-full"
1197
1302
  :multiple="true"
1303
+ @update:model-value="value => updateSiteUsersModel(slotProps.workingDoc, value)"
1198
1304
  />
1199
1305
  <div class="rounded-lg border border-dashed border-slate-200 p-4 ">
1200
1306
  <div class="flex items-start justify-between gap-3">
@@ -1219,7 +1325,7 @@ const pageSettingsUpdated = async (pageData) => {
1219
1325
  label="User Data for AI to use to build initial site"
1220
1326
  placeholder="- select one -"
1221
1327
  class="w-full"
1222
- :items="userOptions"
1328
+ :items="aiUserOptions"
1223
1329
  item-title="label"
1224
1330
  item-value="value"
1225
1331
  @update:model-value="value => (slotProps.workingDoc.aiAgentUserId = value || '')"
@@ -1236,6 +1342,9 @@ const pageSettingsUpdated = async (pageData) => {
1236
1342
  </div>
1237
1343
  </template>
1238
1344
  </edge-editor>
1345
+ <div v-else-if="!props.page && props.site === 'new' && !canCreateSite" class="p-6 text-sm text-red-600">
1346
+ Only organization admins can create sites.
1347
+ </div>
1239
1348
  <div v-else class="flex flex-col h-[calc(100vh-58px)] overflow-hidden">
1240
1349
  <div class="grid grid-cols-[1fr_auto_1fr] items-center gap-3 px-4 py-2 border-b bg-secondary">
1241
1350
  <div class="flex items-center gap-3">
@@ -1596,7 +1705,7 @@ const pageSettingsUpdated = async (pageData) => {
1596
1705
  <edge-shad-button variant="destructive" class="text-white" @click="state.siteSettings = false">
1597
1706
  Cancel
1598
1707
  </edge-shad-button>
1599
- <edge-shad-button :disabled="slotProps.submitting" type="submit" class=" bg-slate-800 hover:bg-slate-400 w-full">
1708
+ <edge-shad-button :disabled="slotProps.submitting || isJsonInvalid(slotProps.workingDoc?.structuredData)" type="submit" class=" bg-slate-800 hover:bg-slate-400 w-full">
1600
1709
  <Loader2 v-if="slotProps.submitting" class=" h-4 w-4 animate-spin" />
1601
1710
  Update
1602
1711
  </edge-shad-button>
@@ -534,11 +534,18 @@ onMounted(async () => {
534
534
  label="Meta Description"
535
535
  name="metaDescription"
536
536
  />
537
+ <div class="rounded-md border border-border/60 bg-muted/30 px-3 py-2 text-xs text-muted-foreground">
538
+ CMS tokens in double curly braces are replaced on the front end.
539
+ Example: <span v-pre class="font-semibold text-foreground">"{{cms-site}}"</span> for the site URL,
540
+ <span v-pre class="font-semibold text-foreground">"{{cms-url}}"</span> for the page URL, and
541
+ <span v-pre class="font-semibold text-foreground">"{{cms-logo}}"</span> for the logo URL. Keep the tokens intact.
542
+ </div>
537
543
  <edge-cms-code-editor
538
544
  v-model="props.settings.structuredData"
539
545
  title="Structured Data (JSON-LD)"
540
546
  language="json"
541
547
  name="structuredData"
548
+ validate-json
542
549
  height="300px"
543
550
  class="mb-4 w-full"
544
551
  />
@@ -132,7 +132,10 @@ watch(headObject, (newHeadElements) => {
132
132
  }, { immediate: true, deep: true })
133
133
 
134
134
  const sites = computed(() => {
135
- return Object.values(edgeFirebase.data?.[`${edgeGlobal.edgeState.organizationDocPath}/sites`] || {})
135
+ const sitesMap = edgeFirebase.data?.[`${edgeGlobal.edgeState.organizationDocPath}/sites`] || {}
136
+ return Object.entries(sitesMap)
137
+ .map(([docId, data]) => ({ docId, ...(data || {}) }))
138
+ .filter(site => site.docId && site.docId !== 'templates')
136
139
  })
137
140
 
138
141
  const templatePages = computed(() => {
@@ -324,9 +327,12 @@ const templatePageOptions = computed(() => {
324
327
 
325
328
  watch (sites, async (newSites) => {
326
329
  state.loading = true
327
- if (!edgeGlobal.edgeState.blockEditorSite && newSites.length > 0) {
330
+ const selectedSite = String(edgeGlobal.edgeState.blockEditorSite || '').trim()
331
+ const hasSelectedSite = newSites.some(site => site.docId === selectedSite)
332
+ if ((!selectedSite || !hasSelectedSite) && newSites.length > 0)
328
333
  edgeGlobal.edgeState.blockEditorSite = newSites[0].docId
329
- }
334
+ else if (!newSites.length)
335
+ edgeGlobal.edgeState.blockEditorSite = ''
330
336
  await nextTick()
331
337
  state.loading = false
332
338
  }, { immediate: true, deep: true })
@@ -170,6 +170,20 @@ const getByPath = (obj, path) => {
170
170
  }, obj)
171
171
  }
172
172
 
173
+ const normalizeCollectionItems = (collectionMap = {}) => {
174
+ const items = Object.entries(collectionMap).map(([docId, data]) => {
175
+ const source = (data && typeof data === 'object') ? data : {}
176
+ return {
177
+ ...source,
178
+ docId: String(source.docId || docId || ''),
179
+ }
180
+ })
181
+ if (props.collection === 'sites') {
182
+ return items.filter(item => item.docId !== 'templates')
183
+ }
184
+ return items
185
+ }
186
+
173
187
  const snapShotQuery = computed(() => {
174
188
  if (state.queryField && state.queryValue) {
175
189
  // console.log('snapShotQuery', state.queryField, state.queryOperator, state.queryValue)
@@ -232,7 +246,10 @@ const allData = computed(() => {
232
246
  if (!edgeFirebase.data?.[`${edgeGlobal.edgeState.organizationDocPath}/${props.collection}`]) {
233
247
  return []
234
248
  }
235
- return Object.values(edgeFirebase.data[`${edgeGlobal.edgeState.organizationDocPath}/${props.collection}`])
249
+ return normalizeCollectionItems(edgeFirebase.data[`${edgeGlobal.edgeState.organizationDocPath}/${props.collection}`])
250
+ }
251
+ if (props.collection === 'sites') {
252
+ return state.paginatedResults.filter(item => String(item?.docId || '') !== 'templates')
236
253
  }
237
254
  return state.paginatedResults
238
255
  })
@@ -243,10 +260,12 @@ const filtered = computed(() => {
243
260
  if (!edgeFirebase.data?.[`${edgeGlobal.edgeState.organizationDocPath}/${props.collection}`]) {
244
261
  return []
245
262
  }
246
- allData = Object.values(edgeFirebase.data[`${edgeGlobal.edgeState.organizationDocPath}/${props.collection}`])
263
+ allData = normalizeCollectionItems(edgeFirebase.data[`${edgeGlobal.edgeState.organizationDocPath}/${props.collection}`])
247
264
  }
248
265
  else {
249
- allData = state.paginatedResults
266
+ allData = props.collection === 'sites'
267
+ ? state.paginatedResults.filter(item => String(item?.docId || '') !== 'templates')
268
+ : state.paginatedResults
250
269
  }
251
270
 
252
271
  const qRaw = filterText.value
@@ -0,0 +1,126 @@
1
+ <script setup>
2
+ import { useVModel } from '@vueuse/core'
3
+ import { ImagePlus, Trash2 } from 'lucide-vue-next'
4
+
5
+ const props = defineProps({
6
+ modelValue: {
7
+ type: String,
8
+ default: '',
9
+ },
10
+ label: {
11
+ type: String,
12
+ default: 'Photo',
13
+ },
14
+ dialogTitle: {
15
+ type: String,
16
+ default: 'Select Image',
17
+ },
18
+ site: {
19
+ type: String,
20
+ default: 'clearwater-hub-images',
21
+ },
22
+ defaultTags: {
23
+ type: Array,
24
+ default: () => [],
25
+ },
26
+ includeCmsAll: {
27
+ type: Boolean,
28
+ default: true,
29
+ },
30
+ heightClass: {
31
+ type: String,
32
+ default: 'h-[160px]',
33
+ },
34
+ showRemove: {
35
+ type: Boolean,
36
+ default: true,
37
+ },
38
+ disabled: {
39
+ type: Boolean,
40
+ default: false,
41
+ },
42
+ })
43
+
44
+ const emits = defineEmits(['update:modelValue'])
45
+
46
+ const state = reactive({
47
+ dialog: false,
48
+ })
49
+
50
+ const modelValue = useVModel(props, 'modelValue', emits, {
51
+ passive: false,
52
+ prop: 'modelValue',
53
+ })
54
+
55
+ const openDialog = () => {
56
+ if (props.disabled)
57
+ return
58
+ state.dialog = true
59
+ }
60
+
61
+ const closeDialog = () => {
62
+ state.dialog = false
63
+ }
64
+
65
+ const selectImage = (url) => {
66
+ modelValue.value = url || ''
67
+ closeDialog()
68
+ }
69
+
70
+ const clearImage = () => {
71
+ if (props.disabled)
72
+ return
73
+ modelValue.value = ''
74
+ }
75
+ </script>
76
+
77
+ <template>
78
+ <div class="rounded-lg border bg-background p-4 space-y-3">
79
+ <div class="flex flex-wrap items-center justify-between gap-2">
80
+ <div class="text-xs font-semibold uppercase tracking-wide text-muted-foreground">
81
+ {{ props.label }}
82
+ </div>
83
+ <div class="flex items-center gap-2">
84
+ <edge-shad-button size="sm" variant="outline" class="h-8 px-2" :disabled="props.disabled" @click="openDialog">
85
+ <ImagePlus class="h-4 w-4 mr-2" />
86
+ Select
87
+ </edge-shad-button>
88
+ <edge-shad-button
89
+ v-if="props.showRemove && modelValue"
90
+ size="icon"
91
+ variant="ghost"
92
+ class="h-8 w-8 text-destructive/80 hover:text-destructive"
93
+ :disabled="props.disabled"
94
+ @click="clearImage"
95
+ >
96
+ <Trash2 class="h-4 w-4" />
97
+ </edge-shad-button>
98
+ </div>
99
+ </div>
100
+ <div class="rounded-lg flex items-center justify-center overflow-hidden border bg-muted/20" :class="props.heightClass">
101
+ <img
102
+ v-if="modelValue"
103
+ :src="modelValue"
104
+ alt=""
105
+ class="max-h-full max-w-full h-auto w-auto object-contain"
106
+ >
107
+ <span v-else class="text-sm text-muted-foreground italic">No photo selected</span>
108
+ </div>
109
+ </div>
110
+
111
+ <Dialog v-model:open="state.dialog">
112
+ <DialogContent class="w-full max-w-[1200px] max-h-[80vh] overflow-y-auto">
113
+ <DialogHeader>
114
+ <DialogTitle>{{ props.dialogTitle }}</DialogTitle>
115
+ <DialogDescription />
116
+ </DialogHeader>
117
+ <edge-cms-media-manager
118
+ :site="props.site"
119
+ :default-tags="props.defaultTags"
120
+ :include-cms-all="props.includeCmsAll"
121
+ :select-mode="true"
122
+ @select="selectImage"
123
+ />
124
+ </DialogContent>
125
+ </Dialog>
126
+ </template>
@@ -34,6 +34,7 @@ const updateUser = async () => {
34
34
  state.userError = { success: state.userError.success, message: state.userError.message.replace('Firebase: ', '').replace(' (auth/invalid-email)', '') }
35
35
  if (state.userError.success) {
36
36
  state.userError = { success: true, message: 'A verification link has been sent to your new email address. Please click the link to complete the email change process.' }
37
+ await edgeFirebase.setUserMeta({ email: state.username })
37
38
  }
38
39
  edgeGlobal.edgeState.changeTracker = {}
39
40
  state.loaded = false