@edgedev/create-edge-app 1.2.29 → 1.2.31
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/.env +1 -1
- package/.env.dev +1 -1
- package/{pages/app/dashboard/blocks/index.vue → edge/components/cms/blocksManager.vue} +9 -24
- package/edge/components/cms/site.vue +5 -0
- package/edge/components/cms/siteSettingsForm.vue +331 -49
- package/edge/components/cms/sitesManager.vue +110 -0
- package/edge/components/editor.vue +100 -35
- package/edge/composables/siteSettingsTemplate.js +2 -0
- package/edge/routes/cms/dashboard/blocks/index.vue +21 -0
- package/edge/routes/cms/dashboard/sites/index.vue +13 -0
- package/edge/routes/cms/nuxtHooks.js +52 -0
- package/edge/routes/cms/routes.js +56 -0
- package/nuxt.config.ts +19 -2
- package/package.json +1 -1
- package/pages/app/dashboard/sites/index.vue +0 -114
- /package/{pages/app → edge/routes/cms}/dashboard/blocks/[block].vue +0 -0
- /package/{pages/app → edge/routes/cms}/dashboard/media/index.vue +0 -0
- /package/{pages/app → edge/routes/cms}/dashboard/sites/[site]/[[page]].vue +0 -0
- /package/{pages/app → edge/routes/cms}/dashboard/sites/[site].vue +0 -0
- /package/{pages/app → edge/routes/cms}/dashboard/templates/[page].vue +0 -0
- /package/{pages/app/dashboard/templates.vue → edge/routes/cms/dashboard/templates/index.vue} +0 -0
- /package/{pages/app → edge/routes/cms}/dashboard/themes/[theme].vue +0 -0
- /package/{pages/app → edge/routes/cms}/dashboard/themes/index.vue +0 -0
package/.env
CHANGED
package/.env.dev
CHANGED
|
@@ -7,15 +7,7 @@ const state = reactive({
|
|
|
7
7
|
themesFilter: [],
|
|
8
8
|
})
|
|
9
9
|
|
|
10
|
-
const
|
|
11
|
-
loaded: [],
|
|
12
|
-
})
|
|
13
|
-
|
|
14
|
-
definePageMeta({
|
|
15
|
-
middleware: 'auth',
|
|
16
|
-
})
|
|
17
|
-
|
|
18
|
-
const rawInitBlockFiles = import.meta.glob('../../../../edge/components/cms/init_blocks/*.html', {
|
|
10
|
+
const rawInitBlockFiles = import.meta.glob('./init_blocks/*.html', {
|
|
19
11
|
as: 'raw',
|
|
20
12
|
eager: true,
|
|
21
13
|
})
|
|
@@ -74,14 +66,6 @@ const seedInitialBlocks = async () => {
|
|
|
74
66
|
return created
|
|
75
67
|
}
|
|
76
68
|
|
|
77
|
-
onBeforeMount(async () => {
|
|
78
|
-
restoreFilters()
|
|
79
|
-
if (!edgeFirebase.data?.[`organizations/${edgeGlobal.edgeState.currentOrganization}/themes`]) {
|
|
80
|
-
await edgeFirebase.startSnapshot(`organizations/${edgeGlobal.edgeState.currentOrganization}/themes`)
|
|
81
|
-
}
|
|
82
|
-
state.mounted = true
|
|
83
|
-
})
|
|
84
|
-
|
|
85
69
|
const getThemeFromId = (themeId) => {
|
|
86
70
|
const theme = edgeFirebase.data[`organizations/${edgeGlobal.edgeState.currentOrganization}/themes`]?.[themeId]
|
|
87
71
|
console.log('getThemeFromId', themeId, theme.name)
|
|
@@ -93,13 +77,6 @@ const loadingRender = (content) => {
|
|
|
93
77
|
return safeContent.replaceAll('{{loading}}', '').replaceAll('{{loaded}}', 'hidden')
|
|
94
78
|
}
|
|
95
79
|
|
|
96
|
-
const markPreviewLoaded = (isLoading, id) => {
|
|
97
|
-
if (!isLoading && !previewState.loaded.includes(id))
|
|
98
|
-
previewState.loaded.push(id)
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
const hasPreviewLoaded = id => previewState.loaded.includes(id)
|
|
102
|
-
|
|
103
80
|
const FILTER_STORAGE_KEY = 'edge.blocks.filters'
|
|
104
81
|
|
|
105
82
|
const restoreFilters = () => {
|
|
@@ -136,6 +113,14 @@ watch(
|
|
|
136
113
|
{ deep: true },
|
|
137
114
|
)
|
|
138
115
|
|
|
116
|
+
onBeforeMount(async () => {
|
|
117
|
+
restoreFilters()
|
|
118
|
+
if (!edgeFirebase.data?.[`organizations/${edgeGlobal.edgeState.currentOrganization}/themes`]) {
|
|
119
|
+
await edgeFirebase.startSnapshot(`organizations/${edgeGlobal.edgeState.currentOrganization}/themes`)
|
|
120
|
+
}
|
|
121
|
+
state.mounted = true
|
|
122
|
+
})
|
|
123
|
+
|
|
139
124
|
const tagOptions = computed(() => {
|
|
140
125
|
const tagsSet = new Set()
|
|
141
126
|
const blocks = edgeFirebase.data?.[`${edgeGlobal.edgeState.organizationDocPath}/blocks`] || {}
|
|
@@ -103,6 +103,7 @@ const schemas = {
|
|
|
103
103
|
message: 'At least one domain is required',
|
|
104
104
|
path: ['domains', 0],
|
|
105
105
|
}),
|
|
106
|
+
forwardApex: z.boolean().optional(),
|
|
106
107
|
contactEmail: z.string().optional(),
|
|
107
108
|
contactPhone: z.string().optional(),
|
|
108
109
|
theme: z.string({
|
|
@@ -783,6 +784,7 @@ const isSiteDiff = computed(() => {
|
|
|
783
784
|
brandLogoLight: publishedSite.brandLogoLight,
|
|
784
785
|
favicon: publishedSite.favicon,
|
|
785
786
|
menuPosition: publishedSite.menuPosition,
|
|
787
|
+
forwardApex: publishedSite.forwardApex,
|
|
786
788
|
contactEmail: publishedSite.contactEmail,
|
|
787
789
|
contactPhone: publishedSite.contactPhone,
|
|
788
790
|
metaTitle: publishedSite.metaTitle,
|
|
@@ -810,6 +812,7 @@ const isSiteDiff = computed(() => {
|
|
|
810
812
|
brandLogoLight: siteData.value.brandLogoLight,
|
|
811
813
|
favicon: siteData.value.favicon,
|
|
812
814
|
menuPosition: siteData.value.menuPosition,
|
|
815
|
+
forwardApex: siteData.value.forwardApex,
|
|
813
816
|
contactEmail: siteData.value.contactEmail,
|
|
814
817
|
contactPhone: siteData.value.contactPhone,
|
|
815
818
|
metaTitle: siteData.value.metaTitle,
|
|
@@ -851,6 +854,7 @@ const discardSiteSettings = async () => {
|
|
|
851
854
|
brandLogoLight: publishedSite.brandLogoLight || '',
|
|
852
855
|
favicon: publishedSite.favicon || '',
|
|
853
856
|
menuPosition: publishedSite.menuPosition || '',
|
|
857
|
+
forwardApex: publishedSite.forwardApex === false ? false : true,
|
|
854
858
|
contactEmail: publishedSite.contactEmail || '',
|
|
855
859
|
contactPhone: publishedSite.contactPhone || '',
|
|
856
860
|
metaTitle: publishedSite.metaTitle || '',
|
|
@@ -1699,6 +1703,7 @@ const pageSettingsUpdated = async (pageData) => {
|
|
|
1699
1703
|
:enable-media-picker="true"
|
|
1700
1704
|
:site-id="props.site"
|
|
1701
1705
|
:domain-error="domainError"
|
|
1706
|
+
:settings-open="state.siteSettings"
|
|
1702
1707
|
/>
|
|
1703
1708
|
</div>
|
|
1704
1709
|
<SheetFooter class="pt-2 flex justify-between">
|
|
@@ -42,6 +42,10 @@ const props = defineProps({
|
|
|
42
42
|
type: String,
|
|
43
43
|
default: '',
|
|
44
44
|
},
|
|
45
|
+
settingsOpen: {
|
|
46
|
+
type: Boolean,
|
|
47
|
+
default: true,
|
|
48
|
+
},
|
|
45
49
|
})
|
|
46
50
|
|
|
47
51
|
const edgeFirebase = inject('edgeFirebase')
|
|
@@ -101,41 +105,266 @@ const menuPositionOptions = [
|
|
|
101
105
|
|
|
102
106
|
const domainError = computed(() => String(props.domainError || '').trim())
|
|
103
107
|
const serverPagesProject = ref('')
|
|
108
|
+
const domainRegistry = ref({})
|
|
109
|
+
const loadingDomainRegistry = ref(false)
|
|
110
|
+
const hasLoadedDomainRegistry = ref(false)
|
|
104
111
|
const pagesProject = computed(() => String(serverPagesProject.value || '').trim())
|
|
105
112
|
const pagesDomain = computed(() => (pagesProject.value ? `${pagesProject.value}.pages.dev` : '(CLOUDFLARE_PAGES_PROJECT).pages.dev'))
|
|
113
|
+
const forwardApexEnabled = computed({
|
|
114
|
+
get: () => props.settings?.forwardApex !== false,
|
|
115
|
+
set: (value) => {
|
|
116
|
+
props.settings.forwardApex = !!value
|
|
117
|
+
},
|
|
118
|
+
})
|
|
119
|
+
|
|
120
|
+
const normalizeDomain = (value) => {
|
|
121
|
+
if (!value)
|
|
122
|
+
return ''
|
|
123
|
+
let normalized = String(value).trim().toLowerCase()
|
|
124
|
+
if (!normalized)
|
|
125
|
+
return ''
|
|
126
|
+
if (normalized.includes('://')) {
|
|
127
|
+
try {
|
|
128
|
+
normalized = new URL(normalized).host
|
|
129
|
+
}
|
|
130
|
+
catch {
|
|
131
|
+
normalized = normalized.split('://').pop() || normalized
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
normalized = normalized.split('/')[0] || ''
|
|
135
|
+
if (normalized.includes(':') && !normalized.startsWith('[')) {
|
|
136
|
+
normalized = normalized.split(':')[0] || ''
|
|
137
|
+
}
|
|
138
|
+
return normalized.replace(/\.+$/g, '')
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
const isIpv4Address = (value) => {
|
|
142
|
+
const parts = String(value || '').split('.')
|
|
143
|
+
if (parts.length !== 4)
|
|
144
|
+
return false
|
|
145
|
+
return parts.every((part) => {
|
|
146
|
+
if (!/^\d{1,3}$/.test(part))
|
|
147
|
+
return false
|
|
148
|
+
const num = Number(part)
|
|
149
|
+
return num >= 0 && num <= 255
|
|
150
|
+
})
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
const isIpv6Address = (value) => {
|
|
154
|
+
const normalized = String(value || '').toLowerCase()
|
|
155
|
+
if (!normalized.includes(':'))
|
|
156
|
+
return false
|
|
157
|
+
return /^[0-9a-f:]+$/.test(normalized)
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
const isIpAddress = value => isIpv4Address(value) || isIpv6Address(value)
|
|
161
|
+
|
|
162
|
+
const shouldDisplayDomainDnsRecords = (domain) => {
|
|
163
|
+
if (!domain)
|
|
164
|
+
return false
|
|
165
|
+
if (domain.includes('localhost'))
|
|
166
|
+
return false
|
|
167
|
+
if (isIpAddress(domain))
|
|
168
|
+
return false
|
|
169
|
+
if (domain.endsWith('.dev'))
|
|
170
|
+
return false
|
|
171
|
+
return true
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
const getCloudflareApexDomain = (domain) => {
|
|
175
|
+
if (!domain)
|
|
176
|
+
return ''
|
|
177
|
+
if (domain.startsWith('www.'))
|
|
178
|
+
return domain.slice(4)
|
|
179
|
+
return domain
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
const getCloudflarePagesDomain = (domain) => {
|
|
183
|
+
if (!domain)
|
|
184
|
+
return ''
|
|
185
|
+
if (domain.startsWith('www.'))
|
|
186
|
+
return domain
|
|
187
|
+
return `www.${domain}`
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
const normalizedDomains = computed(() => {
|
|
191
|
+
const values = Array.isArray(props.settings?.domains) ? props.settings.domains : []
|
|
192
|
+
return Array.from(new Set(values.map(normalizeDomain).filter(Boolean)))
|
|
193
|
+
})
|
|
194
|
+
|
|
195
|
+
const dnsEligibleDomains = computed(() => normalizedDomains.value.filter(shouldDisplayDomainDnsRecords))
|
|
196
|
+
|
|
197
|
+
const organizationId = computed(() => String(edgeGlobal?.edgeState?.currentOrganization || '').trim())
|
|
198
|
+
const shouldShowDomainRegistryLoading = computed(() => {
|
|
199
|
+
if (!normalizedDomains.value.length)
|
|
200
|
+
return false
|
|
201
|
+
return loadingDomainRegistry.value || !hasLoadedDomainRegistry.value
|
|
202
|
+
})
|
|
203
|
+
const DOMAIN_REGISTRY_POLL_MS = 2500
|
|
204
|
+
let domainRegistryPollTimer = null
|
|
205
|
+
|
|
206
|
+
const buildFallbackDomainEntry = (domain) => {
|
|
207
|
+
const apexDomain = getCloudflareApexDomain(domain)
|
|
208
|
+
const wwwDomain = getCloudflarePagesDomain(apexDomain)
|
|
209
|
+
const target = pagesDomain.value
|
|
210
|
+
const hasTarget = !!String(target || '').trim()
|
|
211
|
+
return {
|
|
212
|
+
domain,
|
|
213
|
+
apexDomain,
|
|
214
|
+
wwwDomain,
|
|
215
|
+
apexAttempted: false,
|
|
216
|
+
apexAdded: false,
|
|
217
|
+
apexError: '',
|
|
218
|
+
dnsGuidance: 'Add the www CNAME record. Apex is unavailable; forward apex to www.',
|
|
219
|
+
dnsRecords: {
|
|
220
|
+
target,
|
|
221
|
+
www: {
|
|
222
|
+
type: 'CNAME',
|
|
223
|
+
name: 'www',
|
|
224
|
+
host: wwwDomain,
|
|
225
|
+
value: target,
|
|
226
|
+
enabled: hasTarget,
|
|
227
|
+
},
|
|
228
|
+
apex: {
|
|
229
|
+
type: 'CNAME',
|
|
230
|
+
name: '@',
|
|
231
|
+
host: apexDomain,
|
|
232
|
+
value: target,
|
|
233
|
+
enabled: false,
|
|
234
|
+
},
|
|
235
|
+
},
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
const domainDnsEntries = computed(() => {
|
|
240
|
+
return dnsEligibleDomains.value.map((domain) => {
|
|
241
|
+
const fallback = buildFallbackDomainEntry(domain)
|
|
242
|
+
const value = domainRegistry.value?.[domain] || {}
|
|
243
|
+
const dnsRecords = {
|
|
244
|
+
...fallback.dnsRecords,
|
|
245
|
+
...(value.dnsRecords || {}),
|
|
246
|
+
www: {
|
|
247
|
+
...fallback.dnsRecords.www,
|
|
248
|
+
...(value?.dnsRecords?.www || {}),
|
|
249
|
+
},
|
|
250
|
+
apex: {
|
|
251
|
+
...fallback.dnsRecords.apex,
|
|
252
|
+
...(value?.dnsRecords?.apex || {}),
|
|
253
|
+
},
|
|
254
|
+
}
|
|
255
|
+
const apexAdded = value?.apexAdded === true
|
|
256
|
+
return {
|
|
257
|
+
...fallback,
|
|
258
|
+
...value,
|
|
259
|
+
dnsRecords,
|
|
260
|
+
apexAdded,
|
|
261
|
+
apexAttempted: value?.apexAttempted === true,
|
|
262
|
+
apexError: String(value?.apexError || '').trim(),
|
|
263
|
+
dnsGuidance: String(value?.dnsGuidance || '').trim()
|
|
264
|
+
|| (apexAdded ? '' : fallback.dnsGuidance),
|
|
265
|
+
}
|
|
266
|
+
})
|
|
267
|
+
})
|
|
106
268
|
|
|
107
|
-
|
|
269
|
+
const fetchDomainRegistry = async (options = {}) => {
|
|
270
|
+
const { background = false } = options
|
|
108
271
|
if (!edgeFirebase?.runFunction)
|
|
109
272
|
return
|
|
273
|
+
const domains = normalizedDomains.value
|
|
274
|
+
if (!background)
|
|
275
|
+
loadingDomainRegistry.value = true
|
|
110
276
|
try {
|
|
111
|
-
const response = await edgeFirebase.runFunction('cms-getCloudflarePagesProject', {
|
|
277
|
+
const response = await edgeFirebase.runFunction('cms-getCloudflarePagesProject', {
|
|
278
|
+
orgId: organizationId.value,
|
|
279
|
+
siteId: props.siteId || '',
|
|
280
|
+
domains,
|
|
281
|
+
})
|
|
112
282
|
serverPagesProject.value = String(response?.data?.project || '').trim()
|
|
283
|
+
const nextRegistry = response?.data?.domainRegistry
|
|
284
|
+
if (domains.length && nextRegistry && typeof nextRegistry === 'object')
|
|
285
|
+
domainRegistry.value = nextRegistry
|
|
286
|
+
else
|
|
287
|
+
domainRegistry.value = {}
|
|
288
|
+
hasLoadedDomainRegistry.value = true
|
|
113
289
|
}
|
|
114
290
|
catch {
|
|
115
291
|
serverPagesProject.value = ''
|
|
292
|
+
domainRegistry.value = {}
|
|
293
|
+
hasLoadedDomainRegistry.value = true
|
|
116
294
|
}
|
|
295
|
+
finally {
|
|
296
|
+
if (!background)
|
|
297
|
+
loadingDomainRegistry.value = false
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
const stopDomainRegistryPolling = () => {
|
|
302
|
+
if (domainRegistryPollTimer) {
|
|
303
|
+
clearInterval(domainRegistryPollTimer)
|
|
304
|
+
domainRegistryPollTimer = null
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
const startDomainRegistryPolling = () => {
|
|
309
|
+
stopDomainRegistryPolling()
|
|
310
|
+
if (!props.settingsOpen)
|
|
311
|
+
return
|
|
312
|
+
if (!normalizedDomains.value.length)
|
|
313
|
+
return
|
|
314
|
+
domainRegistryPollTimer = setInterval(() => {
|
|
315
|
+
fetchDomainRegistry({ background: true }).catch(() => {})
|
|
316
|
+
}, DOMAIN_REGISTRY_POLL_MS)
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
watch(() => `${props.siteId}:${organizationId.value}:${normalizedDomains.value.join('|')}`, async () => {
|
|
320
|
+
hasLoadedDomainRegistry.value = false
|
|
321
|
+
await fetchDomainRegistry()
|
|
322
|
+
startDomainRegistryPolling()
|
|
323
|
+
})
|
|
324
|
+
|
|
325
|
+
watch(() => props.settingsOpen, async (open) => {
|
|
326
|
+
if (open) {
|
|
327
|
+
hasLoadedDomainRegistry.value = false
|
|
328
|
+
await fetchDomainRegistry()
|
|
329
|
+
startDomainRegistryPolling()
|
|
330
|
+
}
|
|
331
|
+
else {
|
|
332
|
+
stopDomainRegistryPolling()
|
|
333
|
+
}
|
|
334
|
+
}, { immediate: true })
|
|
335
|
+
|
|
336
|
+
onBeforeUnmount(() => {
|
|
337
|
+
stopDomainRegistryPolling()
|
|
117
338
|
})
|
|
339
|
+
|
|
340
|
+
watch(() => props.settings?.forwardApex, (value) => {
|
|
341
|
+
if (value === undefined)
|
|
342
|
+
props.settings.forwardApex = true
|
|
343
|
+
}, { immediate: true })
|
|
118
344
|
</script>
|
|
119
345
|
|
|
120
346
|
<template>
|
|
121
347
|
<Tabs class="w-full" default-value="general">
|
|
122
|
-
<TabsList class="w-full
|
|
123
|
-
<TabsTrigger value="general" class="text-
|
|
348
|
+
<TabsList class="w-full mt-3 bg-secondary rounded-sm grid grid-cols-2 md:grid-cols-4 xl:grid-cols-7 gap-1">
|
|
349
|
+
<TabsTrigger value="general" class="w-full text-black data-[state=active]:bg-black data-[state=active]:text-white">
|
|
124
350
|
General
|
|
125
351
|
</TabsTrigger>
|
|
126
|
-
<TabsTrigger value="
|
|
352
|
+
<TabsTrigger value="domains" class="w-full text-black data-[state=active]:bg-black data-[state=active]:text-white">
|
|
353
|
+
Domains
|
|
354
|
+
</TabsTrigger>
|
|
355
|
+
<TabsTrigger value="appearance" class="w-full text-black data-[state=active]:bg-black data-[state=active]:text-white">
|
|
127
356
|
Appearance
|
|
128
357
|
</TabsTrigger>
|
|
129
|
-
<TabsTrigger value="branding" class="text-
|
|
358
|
+
<TabsTrigger value="branding" class="w-full text-black data-[state=active]:bg-black data-[state=active]:text-white">
|
|
130
359
|
Branding
|
|
131
360
|
</TabsTrigger>
|
|
132
|
-
<TabsTrigger value="seo" class="text-
|
|
361
|
+
<TabsTrigger value="seo" class="w-full text-black data-[state=active]:bg-black data-[state=active]:text-white">
|
|
133
362
|
SEO
|
|
134
363
|
</TabsTrigger>
|
|
135
|
-
<TabsTrigger value="tracking" class="text-
|
|
364
|
+
<TabsTrigger value="tracking" class="w-full text-black data-[state=active]:bg-black data-[state=active]:text-white">
|
|
136
365
|
Tracking Pixels
|
|
137
366
|
</TabsTrigger>
|
|
138
|
-
<TabsTrigger value="social" class="text-
|
|
367
|
+
<TabsTrigger value="social" class="w-full text-black data-[state=active]:bg-black data-[state=active]:text-white">
|
|
139
368
|
Social Media
|
|
140
369
|
</TabsTrigger>
|
|
141
370
|
</TabsList>
|
|
@@ -147,46 +376,6 @@ onMounted(async () => {
|
|
|
147
376
|
placeholder="Enter name"
|
|
148
377
|
class="w-full"
|
|
149
378
|
/>
|
|
150
|
-
<edge-shad-tags
|
|
151
|
-
v-model="props.settings.domains"
|
|
152
|
-
name="domains"
|
|
153
|
-
label="Domains"
|
|
154
|
-
placeholder="Add or remove domains"
|
|
155
|
-
class="w-full"
|
|
156
|
-
/>
|
|
157
|
-
<Alert v-if="domainError" variant="destructive">
|
|
158
|
-
<CircleAlert class="h-4 w-4" />
|
|
159
|
-
<AlertTitle>Domain error</AlertTitle>
|
|
160
|
-
<AlertDescription class="text-sm">
|
|
161
|
-
{{ domainError }}
|
|
162
|
-
</AlertDescription>
|
|
163
|
-
</Alert>
|
|
164
|
-
<div class="rounded-lg border border-border/60 bg-muted/40 p-4 space-y-3">
|
|
165
|
-
<div class="flex items-center justify-between">
|
|
166
|
-
<div class="text-sm font-semibold text-foreground">
|
|
167
|
-
Domain DNS records
|
|
168
|
-
</div>
|
|
169
|
-
<div class="text-xs text-muted-foreground">
|
|
170
|
-
Target: <span class="font-mono">{{ pagesDomain }}</span>
|
|
171
|
-
</div>
|
|
172
|
-
</div>
|
|
173
|
-
<p class="text-sm text-muted-foreground">
|
|
174
|
-
Add these records at your DNS provider.
|
|
175
|
-
</p>
|
|
176
|
-
<div class="space-y-2 text-sm">
|
|
177
|
-
<div class="grid grid-cols-[70px_1fr] gap-3">
|
|
178
|
-
<div class="text-muted-foreground">CNAME</div>
|
|
179
|
-
<div class="font-mono">www → {{ pagesDomain }}</div>
|
|
180
|
-
</div>
|
|
181
|
-
<div class="grid grid-cols-[70px_1fr] gap-3">
|
|
182
|
-
<div class="text-muted-foreground">CNAME</div>
|
|
183
|
-
<div class="font-mono">@ → {{ pagesDomain }}</div>
|
|
184
|
-
</div>
|
|
185
|
-
</div>
|
|
186
|
-
<p class="text-xs text-muted-foreground">
|
|
187
|
-
Then forward the root/apex (TLD) domain to <span class="font-mono">www</span> with a 301 redirect.
|
|
188
|
-
</p>
|
|
189
|
-
</div>
|
|
190
379
|
<edge-shad-input
|
|
191
380
|
v-model="props.settings.contactEmail"
|
|
192
381
|
name="contactEmail"
|
|
@@ -219,6 +408,99 @@ onMounted(async () => {
|
|
|
219
408
|
No organization users available for this site.
|
|
220
409
|
</p>
|
|
221
410
|
</TabsContent>
|
|
411
|
+
<TabsContent value="domains" class="pt-4 space-y-4">
|
|
412
|
+
<edge-shad-tags
|
|
413
|
+
v-model="props.settings.domains"
|
|
414
|
+
name="domains"
|
|
415
|
+
label="Domains"
|
|
416
|
+
placeholder="Add or remove domains"
|
|
417
|
+
class="w-full"
|
|
418
|
+
/>
|
|
419
|
+
<div class="rounded-lg border border-border/60 bg-muted/40 p-4 space-y-3">
|
|
420
|
+
<edge-shad-switch
|
|
421
|
+
v-model="forwardApexEnabled"
|
|
422
|
+
name="forwardApex"
|
|
423
|
+
label="Forward Apex (non-www) domains to www"
|
|
424
|
+
class="w-full"
|
|
425
|
+
>
|
|
426
|
+
Use a single canonical host (www) to avoid duplicate URLs and consolidate SEO signals.
|
|
427
|
+
</edge-shad-switch>
|
|
428
|
+
<p class="text-xs text-amber-700 dark:text-amber-300">
|
|
429
|
+
Warning: if your DNS provider already redirects www to non-www, this can create a redirect loop.
|
|
430
|
+
</p>
|
|
431
|
+
</div>
|
|
432
|
+
<Alert v-if="domainError" variant="destructive">
|
|
433
|
+
<CircleAlert class="h-4 w-4" />
|
|
434
|
+
<AlertTitle>Domain error</AlertTitle>
|
|
435
|
+
<AlertDescription class="text-sm">
|
|
436
|
+
{{ domainError }}
|
|
437
|
+
</AlertDescription>
|
|
438
|
+
</Alert>
|
|
439
|
+
<div class="rounded-lg border border-border/60 bg-muted/40 p-4 space-y-3">
|
|
440
|
+
<div class="flex items-center justify-between">
|
|
441
|
+
<div class="text-sm font-semibold text-foreground">
|
|
442
|
+
Domain DNS records
|
|
443
|
+
</div>
|
|
444
|
+
<div class="text-xs text-muted-foreground">
|
|
445
|
+
Target: <span class="font-mono">{{ pagesDomain }}</span>
|
|
446
|
+
</div>
|
|
447
|
+
</div>
|
|
448
|
+
<p class="text-sm text-muted-foreground">
|
|
449
|
+
Records are listed for each domain.
|
|
450
|
+
</p>
|
|
451
|
+
<p v-if="shouldShowDomainRegistryLoading" class="text-xs text-muted-foreground">
|
|
452
|
+
Waiting for latest domain sync results...
|
|
453
|
+
</p>
|
|
454
|
+
<div v-if="!domainDnsEntries.length" class="text-sm text-muted-foreground italic">
|
|
455
|
+
Add at least one valid domain to see DNS records.
|
|
456
|
+
</div>
|
|
457
|
+
<div
|
|
458
|
+
v-for="entry in domainDnsEntries"
|
|
459
|
+
:key="entry.domain"
|
|
460
|
+
class="rounded-md border border-border/50 bg-background/70 p-3 space-y-3"
|
|
461
|
+
>
|
|
462
|
+
<div class="flex items-center justify-between gap-3">
|
|
463
|
+
<div class="text-sm font-semibold text-foreground font-mono">
|
|
464
|
+
{{ entry.domain }}
|
|
465
|
+
</div>
|
|
466
|
+
<div class="text-xs">
|
|
467
|
+
<span
|
|
468
|
+
v-if="entry.apexAdded"
|
|
469
|
+
class="rounded bg-emerald-500/15 text-emerald-700 dark:text-emerald-300 px-2 py-1"
|
|
470
|
+
>
|
|
471
|
+
Apex added
|
|
472
|
+
</span>
|
|
473
|
+
<span
|
|
474
|
+
v-else
|
|
475
|
+
class="rounded bg-amber-500/15 text-amber-700 dark:text-amber-300 px-2 py-1"
|
|
476
|
+
>
|
|
477
|
+
Apex not added
|
|
478
|
+
</span>
|
|
479
|
+
</div>
|
|
480
|
+
</div>
|
|
481
|
+
<div class="space-y-2 text-sm">
|
|
482
|
+
<div class="grid grid-cols-[70px_1fr] gap-3">
|
|
483
|
+
<div class="text-muted-foreground">CNAME</div>
|
|
484
|
+
<div class="font-mono">
|
|
485
|
+
{{ entry?.dnsRecords?.www?.name || 'www' }} → {{ entry?.dnsRecords?.www?.value || pagesDomain }}
|
|
486
|
+
</div>
|
|
487
|
+
</div>
|
|
488
|
+
<div class="grid grid-cols-[70px_1fr] gap-3">
|
|
489
|
+
<div class="text-muted-foreground">CNAME</div>
|
|
490
|
+
<div class="font-mono">
|
|
491
|
+
{{ entry?.dnsRecords?.apex?.name || '@' }} → {{ entry?.dnsRecords?.apex?.value || pagesDomain }}
|
|
492
|
+
</div>
|
|
493
|
+
</div>
|
|
494
|
+
</div>
|
|
495
|
+
<p v-if="entry.apexError" class="text-xs text-amber-700 dark:text-amber-300">
|
|
496
|
+
{{ entry.apexError }}
|
|
497
|
+
</p>
|
|
498
|
+
<p v-if="!entry.apexAdded && entry.dnsGuidance" class="text-xs text-muted-foreground">
|
|
499
|
+
{{ entry.dnsGuidance }}
|
|
500
|
+
</p>
|
|
501
|
+
</div>
|
|
502
|
+
</div>
|
|
503
|
+
</TabsContent>
|
|
222
504
|
<TabsContent value="appearance" class="pt-4 space-y-4">
|
|
223
505
|
<edge-shad-select-tags
|
|
224
506
|
v-if="props.showThemeFields && props.isAdmin"
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
<script setup>
|
|
2
|
+
import { Loader2 } from 'lucide-vue-next'
|
|
3
|
+
|
|
4
|
+
const edgeFirebase = inject('edgeFirebase')
|
|
5
|
+
const isAiBusy = status => status === 'queued' || status === 'running'
|
|
6
|
+
|
|
7
|
+
const state = reactive({
|
|
8
|
+
filter: '',
|
|
9
|
+
})
|
|
10
|
+
|
|
11
|
+
const isAdmin = computed(() => {
|
|
12
|
+
return edgeGlobal.isAdminGlobal(edgeFirebase).value
|
|
13
|
+
})
|
|
14
|
+
|
|
15
|
+
const disableAddSiteForNonAdmin = true
|
|
16
|
+
|
|
17
|
+
const currentOrgRoleName = computed(() => {
|
|
18
|
+
return String(edgeGlobal.getRoleName(edgeFirebase?.user?.roles || [], edgeGlobal.edgeState.currentOrganization) || '').toLowerCase()
|
|
19
|
+
})
|
|
20
|
+
|
|
21
|
+
const canAddSite = computed(() => {
|
|
22
|
+
if (!disableAddSiteForNonAdmin)
|
|
23
|
+
return true
|
|
24
|
+
return currentOrgRoleName.value === 'admin'
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
const queryField = computed(() => {
|
|
28
|
+
if (!isAdmin.value) {
|
|
29
|
+
return 'users'
|
|
30
|
+
}
|
|
31
|
+
return ''
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
const queryValue = computed(() => {
|
|
35
|
+
if (!isAdmin.value) {
|
|
36
|
+
return [edgeFirebase?.user?.uid]
|
|
37
|
+
}
|
|
38
|
+
return ''
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
const queryOperator = computed(() => {
|
|
42
|
+
if (!isAdmin.value) {
|
|
43
|
+
return 'array-contains-any'
|
|
44
|
+
}
|
|
45
|
+
return ''
|
|
46
|
+
})
|
|
47
|
+
</script>
|
|
48
|
+
|
|
49
|
+
<template>
|
|
50
|
+
<edge-dashboard
|
|
51
|
+
:filter="state.filter"
|
|
52
|
+
:query-field="queryField"
|
|
53
|
+
:query-value="queryValue"
|
|
54
|
+
:query-operator="queryOperator"
|
|
55
|
+
collection="sites"
|
|
56
|
+
class="flex-1 pt-0"
|
|
57
|
+
>
|
|
58
|
+
<template #header-end>
|
|
59
|
+
<edge-shad-button
|
|
60
|
+
v-if="canAddSite"
|
|
61
|
+
class="uppercase bg-primary"
|
|
62
|
+
to="/app/dashboard/sites/new"
|
|
63
|
+
>
|
|
64
|
+
Add Site
|
|
65
|
+
</edge-shad-button>
|
|
66
|
+
<div v-else class="hidden" />
|
|
67
|
+
</template>
|
|
68
|
+
<template #list="slotProps">
|
|
69
|
+
<template v-for="item in slotProps.filtered" :key="item.docId">
|
|
70
|
+
<edge-shad-button
|
|
71
|
+
variant="text"
|
|
72
|
+
class="cursor-pointer w-full flex justify-between items-center py-2 gap-3"
|
|
73
|
+
:to="isAiBusy(item.aiBootstrapStatus) ? undefined : `/app/dashboard/sites/${item.docId}`"
|
|
74
|
+
:disabled="isAiBusy(item.aiBootstrapStatus)"
|
|
75
|
+
>
|
|
76
|
+
<div>
|
|
77
|
+
<Avatar class="cursor-pointer p-0 h-8 w-8 mr-2">
|
|
78
|
+
<FilePenLine class="h-5 w-5" />
|
|
79
|
+
</Avatar>
|
|
80
|
+
</div>
|
|
81
|
+
<div class="grow text-left">
|
|
82
|
+
<div class="text-lg">
|
|
83
|
+
{{ item.name }}
|
|
84
|
+
</div>
|
|
85
|
+
<div v-if="isAiBusy(item.aiBootstrapStatus)" class="flex items-center gap-2 text-xs text-muted-foreground">
|
|
86
|
+
<Loader2 class="h-3 w-3 animate-spin" />
|
|
87
|
+
<span>AI is preparing this site</span>
|
|
88
|
+
</div>
|
|
89
|
+
</div>
|
|
90
|
+
<div>
|
|
91
|
+
<edge-shad-button
|
|
92
|
+
size="icon"
|
|
93
|
+
class="bg-slate-600 h-7 w-7"
|
|
94
|
+
@click.stop="slotProps.deleteItem(item.docId)"
|
|
95
|
+
>
|
|
96
|
+
<Trash class="h-5 w-5" />
|
|
97
|
+
</edge-shad-button>
|
|
98
|
+
</div>
|
|
99
|
+
</edge-shad-button>
|
|
100
|
+
<Separator class="dark:bg-slate-600" />
|
|
101
|
+
</template>
|
|
102
|
+
<div
|
|
103
|
+
v-if="slotProps.filtered.length === 0 && !isAdmin && disableAddSiteForNonAdmin"
|
|
104
|
+
class="px-4 py-6 text-sm text-muted-foreground"
|
|
105
|
+
>
|
|
106
|
+
No sites are assigned to your account. Contact an organization admin to add a site for you.
|
|
107
|
+
</div>
|
|
108
|
+
</template>
|
|
109
|
+
</edge-dashboard>
|
|
110
|
+
</template>
|
|
@@ -96,6 +96,46 @@ const state = reactive({
|
|
|
96
96
|
const edgeFirebase = inject('edgeFirebase')
|
|
97
97
|
// const edgeGlobal = inject('edgeGlobal')
|
|
98
98
|
|
|
99
|
+
const normalizeObject = (value) => {
|
|
100
|
+
if (Array.isArray(value)) {
|
|
101
|
+
return value.map(item => normalizeObject(item))
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
if (value && typeof value === 'object') {
|
|
105
|
+
return Object.keys(value)
|
|
106
|
+
.sort()
|
|
107
|
+
.reduce((acc, key) => {
|
|
108
|
+
acc[key] = normalizeObject(value[key])
|
|
109
|
+
return acc
|
|
110
|
+
}, {})
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
return value
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const withSchemaDefaults = (doc = {}) => {
|
|
117
|
+
const normalizedDoc = edgeGlobal.dupObject(doc || {})
|
|
118
|
+
Object.keys(newDoc.value).forEach((field) => {
|
|
119
|
+
if (!edgeGlobal.objHas(normalizedDoc, field)) {
|
|
120
|
+
normalizedDoc[field] = newDoc.value[field]
|
|
121
|
+
}
|
|
122
|
+
})
|
|
123
|
+
return normalizedDoc
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const comparableDoc = (doc = {}) => {
|
|
127
|
+
const sourceDoc = (doc && typeof doc === 'object') ? doc : {}
|
|
128
|
+
const comparable = {}
|
|
129
|
+
Object.keys(props.newDocSchema || {}).forEach((field) => {
|
|
130
|
+
if (edgeGlobal.objHas(sourceDoc, field)) {
|
|
131
|
+
comparable[field] = sourceDoc[field]
|
|
132
|
+
return
|
|
133
|
+
}
|
|
134
|
+
comparable[field] = newDoc.value[field]
|
|
135
|
+
})
|
|
136
|
+
return normalizeObject(comparable)
|
|
137
|
+
}
|
|
138
|
+
|
|
99
139
|
const unsavedChanges = computed(() => {
|
|
100
140
|
if (props.docId === 'new') {
|
|
101
141
|
return false
|
|
@@ -107,9 +147,9 @@ const unsavedChanges = computed(() => {
|
|
|
107
147
|
return false
|
|
108
148
|
}
|
|
109
149
|
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
return JSON.stringify(
|
|
150
|
+
const workingComparable = comparableDoc(state.workingDoc)
|
|
151
|
+
const baselineComparable = comparableDoc(baselineDoc)
|
|
152
|
+
return JSON.stringify(workingComparable) !== JSON.stringify(baselineComparable)
|
|
113
153
|
})
|
|
114
154
|
|
|
115
155
|
onBeforeRouteLeave((to, from, next) => {
|
|
@@ -164,7 +204,7 @@ const discardChanges = async () => {
|
|
|
164
204
|
}
|
|
165
205
|
return
|
|
166
206
|
}
|
|
167
|
-
state.workingDoc =
|
|
207
|
+
state.workingDoc = withSchemaDefaults(state.collectionData[props.docId])
|
|
168
208
|
state.bypassUnsavedChanges = true
|
|
169
209
|
state.dialog = false
|
|
170
210
|
edgeGlobal.edgeState.changeTracker = {}
|
|
@@ -328,44 +368,63 @@ const initData = (newVal) => {
|
|
|
328
368
|
if (edgeGlobal.objHas(newVal, props.docId) === false) {
|
|
329
369
|
return
|
|
330
370
|
}
|
|
331
|
-
state.workingDoc =
|
|
332
|
-
Object.keys(newDoc.value).forEach((field) => {
|
|
333
|
-
if (!edgeGlobal.objHas(state.workingDoc, field)) {
|
|
334
|
-
state.workingDoc[field] = newDoc.value[field]
|
|
335
|
-
}
|
|
336
|
-
})
|
|
371
|
+
state.workingDoc = withSchemaDefaults(newVal[props.docId])
|
|
337
372
|
state.afterMount = true
|
|
338
373
|
}
|
|
339
374
|
else {
|
|
340
375
|
if (!state.afterMount) {
|
|
341
|
-
state.workingDoc =
|
|
376
|
+
state.workingDoc = withSchemaDefaults(newDoc.value)
|
|
342
377
|
}
|
|
343
378
|
state.afterMount = true
|
|
344
379
|
}
|
|
345
380
|
}
|
|
346
381
|
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
edgeGlobal.edgeState.changeTracker = {}
|
|
350
|
-
for (const field of Object.keys(props.newDocSchema)) {
|
|
382
|
+
const startCollectionSnapshots = async () => {
|
|
383
|
+
for (const field of Object.keys(props.newDocSchema || {})) {
|
|
351
384
|
if (props.newDocSchema[field].type === 'collection') {
|
|
352
385
|
await edgeFirebase.startSnapshot(`${edgeGlobal.edgeState.organizationDocPath}/${field}`)
|
|
353
386
|
}
|
|
354
387
|
}
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
if (!edgeFirebase.data?.[
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
state.collectionData
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
const setCollectionData = async () => {
|
|
391
|
+
const collectionPath = `${edgeGlobal.edgeState.organizationDocPath}/${props.collection}`
|
|
392
|
+
if (!edgeFirebase.data?.[collectionPath]) {
|
|
393
|
+
if (props.docId !== 'new') {
|
|
394
|
+
const docData = await edgeFirebase.getDocData(collectionPath, props.docId)
|
|
395
|
+
state.collectionData = {
|
|
396
|
+
...(state.collectionData || {}),
|
|
397
|
+
[props.docId]: docData,
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
else if (!state.collectionData || typeof state.collectionData !== 'object') {
|
|
401
|
+
state.collectionData = {}
|
|
402
|
+
}
|
|
403
|
+
return
|
|
368
404
|
}
|
|
405
|
+
state.collectionData = edgeFirebase.data[collectionPath]
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
const resetEditorState = () => {
|
|
409
|
+
state.bypassUnsavedChanges = false
|
|
410
|
+
state.bypassRoute = ''
|
|
411
|
+
state.afterMount = false
|
|
412
|
+
state.dialog = false
|
|
413
|
+
state.successMessage = ''
|
|
414
|
+
state.skipNextValidation = props.docId === 'new'
|
|
415
|
+
edgeGlobal.edgeState.changeTracker = {}
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
const refreshEditorData = async () => {
|
|
419
|
+
resetEditorState()
|
|
420
|
+
await startCollectionSnapshots()
|
|
421
|
+
await setCollectionData()
|
|
422
|
+
initData(state.collectionData)
|
|
423
|
+
emit('unsavedChanges', unsavedChanges.value)
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
onBeforeMount(async () => {
|
|
427
|
+
await refreshEditorData()
|
|
369
428
|
if (props.noCloseAfterSave) {
|
|
370
429
|
state.overrideClose = true
|
|
371
430
|
}
|
|
@@ -374,6 +433,17 @@ onBeforeMount(async () => {
|
|
|
374
433
|
watch(() => state.collectionData, (newVal) => {
|
|
375
434
|
initData(newVal)
|
|
376
435
|
})
|
|
436
|
+
|
|
437
|
+
watch(() => [props.collection, props.docId], async (newVal, oldVal) => {
|
|
438
|
+
if (!oldVal) {
|
|
439
|
+
return
|
|
440
|
+
}
|
|
441
|
+
if (newVal[0] === oldVal[0] && newVal[1] === oldVal[1]) {
|
|
442
|
+
return
|
|
443
|
+
}
|
|
444
|
+
await refreshEditorData()
|
|
445
|
+
})
|
|
446
|
+
|
|
377
447
|
onActivated(() => {
|
|
378
448
|
// console.log('activated')
|
|
379
449
|
state.bypassUnsavedChanges = false
|
|
@@ -384,17 +454,12 @@ onActivated(() => {
|
|
|
384
454
|
if (edgeGlobal.objHas(state.collectionData, props.docId) === false) {
|
|
385
455
|
return
|
|
386
456
|
}
|
|
387
|
-
state.workingDoc =
|
|
388
|
-
Object.keys(newDoc.value).forEach((field) => {
|
|
389
|
-
if (!edgeGlobal.objHas(state.workingDoc, field)) {
|
|
390
|
-
state.workingDoc[field] = newDoc.value[field]
|
|
391
|
-
}
|
|
392
|
-
})
|
|
457
|
+
state.workingDoc = withSchemaDefaults(state.collectionData[props.docId])
|
|
393
458
|
|
|
394
459
|
// console.log('state.workingDoc', state.workingDoc)
|
|
395
460
|
}
|
|
396
461
|
else {
|
|
397
|
-
state.workingDoc =
|
|
462
|
+
state.workingDoc = withSchemaDefaults(newDoc.value)
|
|
398
463
|
Object.entries(route.query).forEach(([key, value]) => {
|
|
399
464
|
// Check if the key exists in state.workingDoc, and if so, set the value
|
|
400
465
|
if (key in state.workingDoc) {
|
|
@@ -15,6 +15,7 @@ export const useSiteSettingsTemplate = () => {
|
|
|
15
15
|
favicon: '',
|
|
16
16
|
menuPosition: 'right',
|
|
17
17
|
domains: [],
|
|
18
|
+
forwardApex: true,
|
|
18
19
|
contactEmail: '',
|
|
19
20
|
contactPhone: '',
|
|
20
21
|
metaTitle: '',
|
|
@@ -49,6 +50,7 @@ export const useSiteSettingsTemplate = () => {
|
|
|
49
50
|
favicon: { bindings: { 'field-type': 'text', 'label': 'Favicon' }, cols: '12', value: defaults.favicon },
|
|
50
51
|
menuPosition: { bindings: { 'field-type': 'select', 'label': 'Menu Position', 'items': ['left', 'center', 'right'] }, cols: '12', value: defaults.menuPosition },
|
|
51
52
|
domains: { bindings: { 'field-type': 'tags', 'label': 'Domains', 'helper': 'Add or remove domains' }, cols: '12', value: defaults.domains },
|
|
53
|
+
forwardApex: { bindings: { 'field-type': 'boolean', 'label': 'Forward Apex (non-www) domains to www' }, cols: '12', value: defaults.forwardApex },
|
|
52
54
|
contactEmail: { bindings: { 'field-type': 'text', 'label': 'Contact Email' }, cols: '12', value: defaults.contactEmail },
|
|
53
55
|
contactPhone: { bindings: { 'field-type': 'text', 'label': 'Contact Phone' }, cols: '12', value: defaults.contactPhone },
|
|
54
56
|
metaTitle: { bindings: { 'field-type': 'text', 'label': 'Meta Title' }, cols: '12', value: defaults.metaTitle },
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
<script setup>
|
|
2
|
+
const state = reactive({
|
|
3
|
+
mounted: false,
|
|
4
|
+
})
|
|
5
|
+
|
|
6
|
+
definePageMeta({
|
|
7
|
+
middleware: 'auth',
|
|
8
|
+
})
|
|
9
|
+
|
|
10
|
+
onMounted(() => {
|
|
11
|
+
state.mounted = true
|
|
12
|
+
})
|
|
13
|
+
</script>
|
|
14
|
+
|
|
15
|
+
<template>
|
|
16
|
+
<div
|
|
17
|
+
v-if="edgeGlobal.edgeState.organizationDocPath && state.mounted"
|
|
18
|
+
>
|
|
19
|
+
<edge-cms-blocks-manager />
|
|
20
|
+
</div>
|
|
21
|
+
</template>
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { resolve as resolvePath } from 'node:path'
|
|
2
|
+
import { cmsRoutes } from './routes'
|
|
3
|
+
|
|
4
|
+
const resolveRouteFileFromCwd = (file) => {
|
|
5
|
+
const normalizedPath = String(file || '').replace(/^\.\//, '')
|
|
6
|
+
return resolvePath(process.cwd(), normalizedPath)
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export const createCmsNuxtHooks = (resolveFile) => {
|
|
10
|
+
const resolveRouteFile = typeof resolveFile === 'function'
|
|
11
|
+
? resolveFile
|
|
12
|
+
: resolveRouteFileFromCwd
|
|
13
|
+
|
|
14
|
+
const routeExists = (routeList, name) => {
|
|
15
|
+
for (const route of routeList) {
|
|
16
|
+
if (route.name === name)
|
|
17
|
+
return true
|
|
18
|
+
if (route.children?.length && routeExists(route.children, name))
|
|
19
|
+
return true
|
|
20
|
+
}
|
|
21
|
+
return false
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const resolveCmsRoute = (route, appRootRoute, isChild = false) => {
|
|
25
|
+
const shouldStripAppPrefix = Boolean(appRootRoute) && !isChild
|
|
26
|
+
const nextRoute = {
|
|
27
|
+
...route,
|
|
28
|
+
path: shouldStripAppPrefix
|
|
29
|
+
? route.path.replace(/^\/app\/?/, '')
|
|
30
|
+
: route.path,
|
|
31
|
+
file: resolveRouteFile(route.file),
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
if (route.children?.length)
|
|
35
|
+
nextRoute.children = route.children.map(child => resolveCmsRoute(child, appRootRoute, true))
|
|
36
|
+
|
|
37
|
+
return nextRoute
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
return {
|
|
41
|
+
'pages:extend': (pages) => {
|
|
42
|
+
const appRootRoute = pages.find(page => page.path === '/app')
|
|
43
|
+
const targetRoutes = appRootRoute?.children || pages
|
|
44
|
+
|
|
45
|
+
cmsRoutes.forEach((route) => {
|
|
46
|
+
const routeToAdd = resolveCmsRoute(route, appRootRoute)
|
|
47
|
+
if (!routeExists(targetRoutes, routeToAdd.name))
|
|
48
|
+
targetRoutes.push(routeToAdd)
|
|
49
|
+
})
|
|
50
|
+
},
|
|
51
|
+
}
|
|
52
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
export const cmsRoutes = [
|
|
2
|
+
{
|
|
3
|
+
name: 'cms-dashboard-sites-index',
|
|
4
|
+
path: '/app/dashboard/sites',
|
|
5
|
+
file: './edge/routes/cms/dashboard/sites/index.vue',
|
|
6
|
+
},
|
|
7
|
+
{
|
|
8
|
+
name: 'cms-dashboard-blocks-index',
|
|
9
|
+
path: '/app/dashboard/blocks',
|
|
10
|
+
file: './edge/routes/cms/dashboard/blocks/index.vue',
|
|
11
|
+
},
|
|
12
|
+
{
|
|
13
|
+
name: 'cms-dashboard-blocks-block',
|
|
14
|
+
path: '/app/dashboard/blocks/:block',
|
|
15
|
+
file: './edge/routes/cms/dashboard/blocks/[block].vue',
|
|
16
|
+
},
|
|
17
|
+
{
|
|
18
|
+
name: 'cms-dashboard-media-index',
|
|
19
|
+
path: '/app/dashboard/media',
|
|
20
|
+
file: './edge/routes/cms/dashboard/media/index.vue',
|
|
21
|
+
},
|
|
22
|
+
{
|
|
23
|
+
name: 'cms-dashboard-sites-site',
|
|
24
|
+
path: '/app/dashboard/sites/:site',
|
|
25
|
+
file: './edge/routes/cms/dashboard/sites/[site].vue',
|
|
26
|
+
children: [
|
|
27
|
+
{
|
|
28
|
+
name: 'cms-dashboard-sites-site-page',
|
|
29
|
+
path: ':page',
|
|
30
|
+
file: './edge/routes/cms/dashboard/sites/[site]/[[page]].vue',
|
|
31
|
+
},
|
|
32
|
+
],
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
name: 'cms-dashboard-templates-index',
|
|
36
|
+
path: '/app/dashboard/templates',
|
|
37
|
+
file: './edge/routes/cms/dashboard/templates/index.vue',
|
|
38
|
+
children: [
|
|
39
|
+
{
|
|
40
|
+
name: 'cms-dashboard-templates-page',
|
|
41
|
+
path: ':page',
|
|
42
|
+
file: './edge/routes/cms/dashboard/templates/[page].vue',
|
|
43
|
+
},
|
|
44
|
+
],
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
name: 'cms-dashboard-themes-index',
|
|
48
|
+
path: '/app/dashboard/themes',
|
|
49
|
+
file: './edge/routes/cms/dashboard/themes/index.vue',
|
|
50
|
+
},
|
|
51
|
+
{
|
|
52
|
+
name: 'cms-dashboard-themes-theme',
|
|
53
|
+
path: '/app/dashboard/themes/:theme',
|
|
54
|
+
file: './edge/routes/cms/dashboard/themes/[theme].vue',
|
|
55
|
+
},
|
|
56
|
+
]
|
package/nuxt.config.ts
CHANGED
|
@@ -1,3 +1,8 @@
|
|
|
1
|
+
import { createCmsNuxtHooks } from './edge/routes/cms/nuxtHooks'
|
|
2
|
+
|
|
3
|
+
const cmsRouteHooks = {}
|
|
4
|
+
Object.assign(cmsRouteHooks, createCmsNuxtHooks()) // Comment out this line to disable CMS routes.
|
|
5
|
+
|
|
1
6
|
// https://nuxt.com/docs/api/configuration/nuxt-config
|
|
2
7
|
export default defineNuxtConfig({
|
|
3
8
|
ssr: false,
|
|
@@ -16,6 +21,12 @@ export default defineNuxtConfig({
|
|
|
16
21
|
{ rel: 'apple-touch-icon', sizes: '180x180', href: '/favicon/apple-touch-icon.png' },
|
|
17
22
|
{ rel: 'manifest', href: '/favicon/site.webmanifest' },
|
|
18
23
|
],
|
|
24
|
+
meta: [
|
|
25
|
+
{
|
|
26
|
+
'http-equiv': 'Content-Security-Policy',
|
|
27
|
+
'content': 'font-src \'self\' https://files.edgemarketingdesign.com https://use.typekit.net https://p.typekit.net data:;',
|
|
28
|
+
},
|
|
29
|
+
],
|
|
19
30
|
},
|
|
20
31
|
},
|
|
21
32
|
modules: ['@vant/nuxt', '@nuxtjs/tailwindcss', '@nuxtjs/color-mode', 'shadcn-nuxt'],
|
|
@@ -40,9 +51,11 @@ export default defineNuxtConfig({
|
|
|
40
51
|
},
|
|
41
52
|
components: {
|
|
42
53
|
dirs: [
|
|
43
|
-
{ path: '~/components
|
|
54
|
+
{ path: '~/components' },
|
|
55
|
+
|
|
56
|
+
// Namespaced wrappers — keep them, but lower priority is fine
|
|
44
57
|
{ path: '~/edge/components', global: true, prefix: 'edge' },
|
|
45
|
-
'~/components',
|
|
58
|
+
{ path: '~/components/formSubtypes', global: true, prefix: 'edge-form-subtypes' },
|
|
46
59
|
],
|
|
47
60
|
},
|
|
48
61
|
vite: {
|
|
@@ -50,11 +63,15 @@ export default defineNuxtConfig({
|
|
|
50
63
|
'process.env.DEBUG': false,
|
|
51
64
|
},
|
|
52
65
|
server: {
|
|
66
|
+
watch: {
|
|
67
|
+
ignored: ['**/.nuxt/**', '**/.output/**', '**/dist/**', '**/node_modules/**'],
|
|
68
|
+
},
|
|
53
69
|
hmr: {
|
|
54
70
|
port: 3000, // Make sure this port matches your Nuxt server port
|
|
55
71
|
clientPort: 3000, // Ensure this matches your Nuxt server port as well
|
|
56
72
|
},
|
|
57
73
|
},
|
|
58
74
|
},
|
|
75
|
+
hooks: cmsRouteHooks,
|
|
59
76
|
devtools: { enabled: false },
|
|
60
77
|
})
|
package/package.json
CHANGED
|
@@ -1,114 +0,0 @@
|
|
|
1
|
-
<script setup>
|
|
2
|
-
import { toTypedSchema } from '@vee-validate/zod'
|
|
3
|
-
import * as z from 'zod'
|
|
4
|
-
import { Loader2 } from 'lucide-vue-next'
|
|
5
|
-
const route = useRoute()
|
|
6
|
-
// const edgeGlobal = inject('edgeGlobal')
|
|
7
|
-
|
|
8
|
-
const edgeFirebase = inject('edgeFirebase')
|
|
9
|
-
const isAiBusy = status => status === 'queued' || status === 'running'
|
|
10
|
-
|
|
11
|
-
const state = reactive({
|
|
12
|
-
filter: '',
|
|
13
|
-
newDocs: {
|
|
14
|
-
sites: {
|
|
15
|
-
name: { bindings: { 'field-type': 'text', 'label': 'Name', 'helper': 'Name' }, cols: '12', value: '' },
|
|
16
|
-
},
|
|
17
|
-
},
|
|
18
|
-
})
|
|
19
|
-
|
|
20
|
-
const schemas = {
|
|
21
|
-
sites: toTypedSchema(z.object({
|
|
22
|
-
name: z.string({
|
|
23
|
-
required_error: 'Name is required',
|
|
24
|
-
}).min(1, { message: 'Name is required' }),
|
|
25
|
-
})),
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
const collection = computed(() => {
|
|
29
|
-
if (route.params.collection) {
|
|
30
|
-
return route.params.collection
|
|
31
|
-
}
|
|
32
|
-
return ''
|
|
33
|
-
})
|
|
34
|
-
const docId = computed(() => {
|
|
35
|
-
if (route.params.docId) {
|
|
36
|
-
return route.params.docId
|
|
37
|
-
}
|
|
38
|
-
return ''
|
|
39
|
-
})
|
|
40
|
-
definePageMeta({
|
|
41
|
-
middleware: 'auth',
|
|
42
|
-
})
|
|
43
|
-
|
|
44
|
-
onBeforeMount(() => {
|
|
45
|
-
// edgeGlobal.showLeftPanel(true)
|
|
46
|
-
})
|
|
47
|
-
const isAdmin = computed(() => {
|
|
48
|
-
return edgeGlobal.isAdminGlobal(edgeFirebase).value
|
|
49
|
-
})
|
|
50
|
-
const queryField = computed(() => {
|
|
51
|
-
if (!isAdmin.value) {
|
|
52
|
-
return 'users'
|
|
53
|
-
}
|
|
54
|
-
return ''
|
|
55
|
-
})
|
|
56
|
-
|
|
57
|
-
const queryValue = computed(() => {
|
|
58
|
-
if (!isAdmin.value) {
|
|
59
|
-
return [edgeFirebase?.user?.uid]
|
|
60
|
-
}
|
|
61
|
-
return ''
|
|
62
|
-
})
|
|
63
|
-
|
|
64
|
-
const queryOperator = computed(() => {
|
|
65
|
-
if (!isAdmin.value) {
|
|
66
|
-
return 'array-contains-any'
|
|
67
|
-
}
|
|
68
|
-
return ''
|
|
69
|
-
})
|
|
70
|
-
</script>
|
|
71
|
-
|
|
72
|
-
<template>
|
|
73
|
-
<div
|
|
74
|
-
v-if="edgeGlobal.edgeState.organizationDocPath"
|
|
75
|
-
>
|
|
76
|
-
<edge-dashboard :load-first-if-one="!isAdmin" :filter="state.filter" :query-field="queryField" :query-value="queryValue" :query-operator="queryOperator" collection="sites" class="flex-1 pt-0">
|
|
77
|
-
<template #list="slotProps">
|
|
78
|
-
<template v-for="item in slotProps.filtered" :key="item.docId">
|
|
79
|
-
<edge-shad-button
|
|
80
|
-
variant="text"
|
|
81
|
-
class="cursor-pointer w-full flex justify-between items-center py-2 gap-3"
|
|
82
|
-
:to="isAiBusy(item.aiBootstrapStatus) ? undefined : `/app/dashboard/sites/${item.docId}`"
|
|
83
|
-
:disabled="isAiBusy(item.aiBootstrapStatus)"
|
|
84
|
-
>
|
|
85
|
-
<div>
|
|
86
|
-
<Avatar class="cursor-pointer p-0 h-8 w-8 mr-2">
|
|
87
|
-
<FilePenLine class="h-5 w-5" />
|
|
88
|
-
</Avatar>
|
|
89
|
-
</div>
|
|
90
|
-
<div class="grow text-left">
|
|
91
|
-
<div class="text-lg">
|
|
92
|
-
{{ item.name }}
|
|
93
|
-
</div>
|
|
94
|
-
<div v-if="isAiBusy(item.aiBootstrapStatus)" class="flex items-center gap-2 text-xs text-muted-foreground">
|
|
95
|
-
<Loader2 class="h-3 w-3 animate-spin" />
|
|
96
|
-
<span>AI is preparing this site</span>
|
|
97
|
-
</div>
|
|
98
|
-
</div>
|
|
99
|
-
<div>
|
|
100
|
-
<edge-shad-button
|
|
101
|
-
size="icon"
|
|
102
|
-
class="bg-slate-600 h-7 w-7"
|
|
103
|
-
@click.stop="slotProps.deleteItem(item.docId)"
|
|
104
|
-
>
|
|
105
|
-
<Trash class="h-5 w-5" />
|
|
106
|
-
</edge-shad-button>
|
|
107
|
-
</div>
|
|
108
|
-
</edge-shad-button>
|
|
109
|
-
<Separator class="dark:bg-slate-600" />
|
|
110
|
-
</template>
|
|
111
|
-
</template>
|
|
112
|
-
</edge-dashboard>
|
|
113
|
-
</div>
|
|
114
|
-
</template>
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
/package/{pages/app/dashboard/templates.vue → edge/routes/cms/dashboard/templates/index.vue}
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|