@edgedev/create-edge-app 1.1.26 → 1.1.28
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/edge/components/cms/block.vue +334 -26
- package/edge/components/cms/blockEditor.vue +50 -3
- package/edge/components/cms/codeEditor.vue +15 -0
- package/edge/components/cms/init_blocks/footer.html +111 -19
- package/edge/components/cms/init_blocks/image.html +8 -0
- package/edge/components/cms/init_blocks/post_content.html +3 -2
- package/edge/components/cms/init_blocks/post_title_header.html +8 -6
- package/edge/components/cms/init_blocks/posts_list.html +6 -5
- package/edge/components/cms/mediaCard.vue +13 -2
- package/edge/components/cms/mediaManager.vue +16 -2
- package/edge/components/cms/menu.vue +253 -42
- package/edge/components/cms/page.vue +151 -18
- package/edge/components/cms/site.vue +537 -215
- package/edge/components/cms/siteSettingsForm.vue +616 -0
- package/edge/components/cms/themeDefaultMenu.vue +258 -22
- package/edge/components/cms/themeEditor.vue +99 -12
- package/edge/components/editor.vue +1 -0
- package/edge/components/formSubtypes/myOrgs.vue +112 -1
- package/edge/components/orgSwitcher.vue +1 -1
- package/edge/components/organizationMembers.vue +171 -21
- package/edge/components/shad/html.vue +6 -0
- package/edge/components/sideBar.vue +7 -4
- package/edge/components/sideBarContent.vue +1 -1
- package/edge/components/userMenu.vue +50 -14
- package/edge/composables/siteSettingsTemplate.js +79 -0
- package/edge/composables/structuredDataTemplates.js +36 -0
- package/package.json +1 -1
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
<script setup lang="js">
|
|
2
2
|
import { 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'
|
|
5
|
+
import { useStructuredDataTemplates } from '@/edge/composables/structuredDataTemplates'
|
|
4
6
|
|
|
5
|
-
import { ArrowLeft, CircleAlert, FileCheck, FilePenLine, FileStack, FolderCog, FolderDown, FolderUp, FolderX, Loader2, MoreHorizontal } from 'lucide-vue-next'
|
|
6
7
|
const props = defineProps({
|
|
7
8
|
site: {
|
|
8
9
|
type: String,
|
|
@@ -15,6 +16,8 @@ const props = defineProps({
|
|
|
15
16
|
},
|
|
16
17
|
})
|
|
17
18
|
const edgeFirebase = inject('edgeFirebase')
|
|
19
|
+
const { createDefaults: createSiteSettingsDefaults, createNewDocSchema: createSiteSettingsNewDocSchema } = useSiteSettingsTemplate()
|
|
20
|
+
const { buildPageStructuredData } = useStructuredDataTemplates()
|
|
18
21
|
|
|
19
22
|
const normalizeForCompare = (value) => {
|
|
20
23
|
if (Array.isArray(value))
|
|
@@ -34,26 +37,15 @@ const areEqualNormalized = (a, b) => stableSerialize(a) === stableSerialize(b)
|
|
|
34
37
|
const isTemplateSite = computed(() => props.site === 'templates')
|
|
35
38
|
const router = useRouter()
|
|
36
39
|
|
|
40
|
+
const SUBMISSION_IGNORE_FIELDS = new Set(['orgId', 'siteId', 'pageId', 'blockId'])
|
|
41
|
+
const SUBMISSION_LABEL_KEYS = ['name', 'fullName', 'firstName', 'lastName', 'email', 'phone']
|
|
42
|
+
const SUBMISSION_MESSAGE_KEYS = ['message', 'comments', 'notes', 'inquiry', 'details']
|
|
43
|
+
|
|
37
44
|
const state = reactive({
|
|
38
45
|
filter: '',
|
|
39
46
|
userFilter: 'all',
|
|
40
47
|
newDocs: {
|
|
41
|
-
sites:
|
|
42
|
-
name: { bindings: { 'field-type': 'text', 'label': 'Name' }, cols: '12', value: '' },
|
|
43
|
-
theme: { bindings: { 'field-type': 'collection', 'label': 'Themes', 'collection-path': 'themes' }, cols: '12', value: '' },
|
|
44
|
-
allowedThemes: { bindings: { 'field-type': 'tags', 'label': 'Allowed Themes' }, cols: '12', value: [] },
|
|
45
|
-
logo: { bindings: { 'field-type': 'text', 'label': 'Logo' }, cols: '12', value: '' },
|
|
46
|
-
favicon: { bindings: { 'field-type': 'text', 'label': 'Favicon' }, cols: '12', value: '' },
|
|
47
|
-
menuPosition: { bindings: { 'field-type': 'select', 'label': 'Menu Position', 'items': ['left', 'center', 'right'] }, cols: '12', value: 'right' },
|
|
48
|
-
domains: { bindings: { 'field-type': 'tags', 'label': 'Domains', 'helper': 'Add or remove domains' }, cols: '12', value: [] },
|
|
49
|
-
contactEmail: { bindings: { 'field-type': 'text', 'label': 'Contact Email' }, cols: '12', value: '' },
|
|
50
|
-
metaTitle: { bindings: { 'field-type': 'text', 'label': 'Meta Title' }, cols: '12', value: '' },
|
|
51
|
-
metaDescription: { bindings: { 'field-type': 'textarea', 'label': 'Meta Description' }, cols: '12', value: '' },
|
|
52
|
-
structuredData: { bindings: { 'field-type': 'textarea', 'label': 'Structured Data (JSON-LD)' }, cols: '12', value: '' },
|
|
53
|
-
users: { bindings: { 'field-type': 'users', 'label': 'Users', 'hint': 'Choose users' }, cols: '12', value: [] },
|
|
54
|
-
aiAgentUserId: { bindings: { 'field-type': 'select', 'label': 'Agent Data for AI to use to build initial site' }, cols: '12', value: '' },
|
|
55
|
-
aiInstructions: { bindings: { 'field-type': 'textarea', 'label': 'Additional AI Instructions' }, cols: '12', value: '' },
|
|
56
|
-
},
|
|
48
|
+
sites: createSiteSettingsNewDocSchema(),
|
|
57
49
|
},
|
|
58
50
|
mounted: false,
|
|
59
51
|
page: {},
|
|
@@ -62,17 +54,21 @@ const state = reactive({
|
|
|
62
54
|
siteSettings: false,
|
|
63
55
|
hasError: false,
|
|
64
56
|
updating: false,
|
|
65
|
-
logoPickerOpen: false,
|
|
66
|
-
faviconPickerOpen: false,
|
|
67
57
|
aiSectionOpen: false,
|
|
68
58
|
selectedPostId: '',
|
|
69
59
|
viewMode: 'pages',
|
|
60
|
+
submissionFilter: '',
|
|
61
|
+
selectedSubmissionId: '',
|
|
62
|
+
publishSiteLoading: false,
|
|
70
63
|
})
|
|
71
64
|
|
|
72
65
|
const pageInit = {
|
|
73
66
|
name: '',
|
|
74
67
|
content: [],
|
|
75
68
|
blockIds: [],
|
|
69
|
+
metaTitle: '',
|
|
70
|
+
metaDescription: '',
|
|
71
|
+
structuredData: buildPageStructuredData(),
|
|
76
72
|
}
|
|
77
73
|
|
|
78
74
|
const schemas = {
|
|
@@ -87,16 +83,31 @@ const schemas = {
|
|
|
87
83
|
path: ['domains', 0],
|
|
88
84
|
}),
|
|
89
85
|
contactEmail: z.string().optional(),
|
|
86
|
+
contactPhone: z.string().optional(),
|
|
90
87
|
theme: z.string({
|
|
91
88
|
required_error: 'Theme is required',
|
|
92
89
|
}).min(1, { message: 'Theme is required' }),
|
|
93
90
|
allowedThemes: z.array(z.string()).optional(),
|
|
94
91
|
logo: z.string().optional(),
|
|
92
|
+
logoLight: z.string().optional(),
|
|
93
|
+
logoText: z.string().optional(),
|
|
94
|
+
logoType: z.enum(['image', 'text']).optional(),
|
|
95
|
+
brandLogoDark: z.string().optional(),
|
|
96
|
+
brandLogoLight: z.string().optional(),
|
|
95
97
|
favicon: z.string().optional(),
|
|
96
98
|
menuPosition: z.enum(['left', 'center', 'right']).optional(),
|
|
97
99
|
metaTitle: z.string().optional(),
|
|
98
100
|
metaDescription: z.string().optional(),
|
|
99
101
|
structuredData: z.string().optional(),
|
|
102
|
+
trackingFacebookPixel: z.string().optional(),
|
|
103
|
+
trackingGoogleAnalytics: z.string().optional(),
|
|
104
|
+
trackingAdroll: z.string().optional(),
|
|
105
|
+
socialFacebook: z.string().optional(),
|
|
106
|
+
socialInstagram: z.string().optional(),
|
|
107
|
+
socialTwitter: z.string().optional(),
|
|
108
|
+
socialLinkedIn: z.string().optional(),
|
|
109
|
+
socialYouTube: z.string().optional(),
|
|
110
|
+
socialTikTok: z.string().optional(),
|
|
100
111
|
aiAgentUserId: z.string().optional(),
|
|
101
112
|
aiInstructions: z.string().optional(),
|
|
102
113
|
})),
|
|
@@ -114,6 +125,185 @@ const isAdmin = computed(() => {
|
|
|
114
125
|
const siteData = computed(() => {
|
|
115
126
|
return edgeFirebase.data?.[`${edgeGlobal.edgeState.organizationDocPath}/sites`]?.[props.site] || {}
|
|
116
127
|
})
|
|
128
|
+
const publishedSiteSettings = computed(() => {
|
|
129
|
+
return edgeFirebase.data?.[`${edgeGlobal.edgeState.organizationDocPath}/published-site-settings`]?.[props.site] || {}
|
|
130
|
+
})
|
|
131
|
+
const domainError = computed(() => {
|
|
132
|
+
return String(publishedSiteSettings.value?.domainError || '').trim()
|
|
133
|
+
})
|
|
134
|
+
|
|
135
|
+
const submissionsCollection = computed(() => `sites/${props.site}/lead-actions`)
|
|
136
|
+
const isViewingSubmissions = computed(() => state.viewMode === 'submissions')
|
|
137
|
+
const submissionsMap = computed(() => {
|
|
138
|
+
return edgeFirebase.data?.[`${edgeGlobal.edgeState.organizationDocPath}/${submissionsCollection.value}`] || {}
|
|
139
|
+
})
|
|
140
|
+
const selectedSubmission = computed(() => {
|
|
141
|
+
return submissionsMap.value?.[state.selectedSubmissionId] || null
|
|
142
|
+
})
|
|
143
|
+
const unreadSubmissionsCount = computed(() => {
|
|
144
|
+
return Object.values(submissionsMap.value || {}).filter((item) => {
|
|
145
|
+
if (item?.action !== 'Contact Form')
|
|
146
|
+
return false
|
|
147
|
+
return !item.readAt
|
|
148
|
+
}).length
|
|
149
|
+
})
|
|
150
|
+
|
|
151
|
+
const formatSubmissionValue = (value) => {
|
|
152
|
+
if (value === undefined || value === null)
|
|
153
|
+
return ''
|
|
154
|
+
if (typeof value === 'string')
|
|
155
|
+
return value
|
|
156
|
+
if (typeof value === 'number' || typeof value === 'boolean')
|
|
157
|
+
return String(value)
|
|
158
|
+
try {
|
|
159
|
+
return JSON.stringify(value)
|
|
160
|
+
}
|
|
161
|
+
catch {
|
|
162
|
+
return String(value)
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
const collectSubmissionEntries = (data) => {
|
|
167
|
+
if (!data || typeof data !== 'object')
|
|
168
|
+
return []
|
|
169
|
+
const entries = []
|
|
170
|
+
const seen = new Set()
|
|
171
|
+
const addEntry = (key, value) => {
|
|
172
|
+
const normalizedKey = String(key || '').trim()
|
|
173
|
+
if (!normalizedKey)
|
|
174
|
+
return
|
|
175
|
+
const lowerKey = normalizedKey.toLowerCase()
|
|
176
|
+
if (SUBMISSION_IGNORE_FIELDS.has(normalizedKey) || SUBMISSION_IGNORE_FIELDS.has(lowerKey))
|
|
177
|
+
return
|
|
178
|
+
if (value === undefined || value === null || value === '')
|
|
179
|
+
return
|
|
180
|
+
if (seen.has(lowerKey))
|
|
181
|
+
return
|
|
182
|
+
entries.push({ key: normalizedKey, value })
|
|
183
|
+
seen.add(lowerKey)
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
const addArrayFields = (fields) => {
|
|
187
|
+
if (!Array.isArray(fields))
|
|
188
|
+
return
|
|
189
|
+
fields.forEach((field) => {
|
|
190
|
+
if (!field)
|
|
191
|
+
return
|
|
192
|
+
const name = field.field || field.name || field.fieldName || field.label || field.title
|
|
193
|
+
const value = field.value ?? field.fieldValue ?? field.val
|
|
194
|
+
addEntry(name, value)
|
|
195
|
+
})
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
addArrayFields(data.fields)
|
|
199
|
+
addArrayFields(data.formFields)
|
|
200
|
+
addArrayFields(data.formData)
|
|
201
|
+
|
|
202
|
+
if (data.fields && typeof data.fields === 'object' && !Array.isArray(data.fields)) {
|
|
203
|
+
Object.entries(data.fields).forEach(([key, value]) => addEntry(key, value))
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
Object.entries(data).forEach(([key, value]) => {
|
|
207
|
+
if (key === 'fields' || key === 'formFields' || key === 'formData')
|
|
208
|
+
return
|
|
209
|
+
addEntry(key, value)
|
|
210
|
+
})
|
|
211
|
+
|
|
212
|
+
return entries.sort((a, b) => String(a.key).localeCompare(String(b.key)))
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
const getSubmissionLabel = (data) => {
|
|
216
|
+
if (!data || typeof data !== 'object')
|
|
217
|
+
return 'Contact Form Submission'
|
|
218
|
+
const name = [data.firstName, data.lastName].filter(Boolean).join(' ').trim()
|
|
219
|
+
if (name)
|
|
220
|
+
return name
|
|
221
|
+
const direct = SUBMISSION_LABEL_KEYS.find(key => String(data[key] || '').trim().length)
|
|
222
|
+
if (direct)
|
|
223
|
+
return String(data[direct]).trim()
|
|
224
|
+
return 'Contact Form Submission'
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
const getSubmissionMessage = (data) => {
|
|
228
|
+
if (!data || typeof data !== 'object')
|
|
229
|
+
return ''
|
|
230
|
+
const direct = SUBMISSION_MESSAGE_KEYS.find(key => String(data[key] || '').trim().length)
|
|
231
|
+
if (direct)
|
|
232
|
+
return String(data[direct]).trim()
|
|
233
|
+
return ''
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
const formatSubmissionKey = (key) => {
|
|
237
|
+
return String(key || '')
|
|
238
|
+
.trim()
|
|
239
|
+
.replace(/_/g, ' ')
|
|
240
|
+
.replace(/([a-z])([A-Z])/g, '$1 $2')
|
|
241
|
+
.replace(/\s+/g, ' ')
|
|
242
|
+
.replace(/^./, str => str.toUpperCase())
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
const getSubmissionEntriesPreview = (data, limit = 6) => {
|
|
246
|
+
return collectSubmissionEntries(data).slice(0, limit)
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
const formatSubmissionTimestamp = (timestamp) => {
|
|
250
|
+
const date = timestamp?.toDate?.() || (timestamp ? new Date(timestamp) : null)
|
|
251
|
+
if (!date || Number.isNaN(date.getTime()))
|
|
252
|
+
return ''
|
|
253
|
+
return new Intl.DateTimeFormat('en-US', {
|
|
254
|
+
dateStyle: 'medium',
|
|
255
|
+
timeStyle: 'short',
|
|
256
|
+
}).format(date)
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
const isSubmissionUnread = item => item && item.action === 'Contact Form' && !item.readAt
|
|
260
|
+
|
|
261
|
+
const markSubmissionRead = async (docId) => {
|
|
262
|
+
const item = submissionsMap.value?.[docId]
|
|
263
|
+
if (!item || !isSubmissionUnread(item))
|
|
264
|
+
return
|
|
265
|
+
try {
|
|
266
|
+
await edgeFirebase.changeDoc(
|
|
267
|
+
`${edgeGlobal.edgeState.organizationDocPath}/${submissionsCollection.value}`,
|
|
268
|
+
docId,
|
|
269
|
+
{ readAt: new Date().toISOString() },
|
|
270
|
+
)
|
|
271
|
+
}
|
|
272
|
+
catch (error) {
|
|
273
|
+
console.error('Failed to mark submission as read', error)
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
const markSubmissionUnread = async (docId) => {
|
|
278
|
+
const item = submissionsMap.value?.[docId]
|
|
279
|
+
if (!item || isSubmissionUnread(item))
|
|
280
|
+
return
|
|
281
|
+
try {
|
|
282
|
+
await edgeFirebase.changeDoc(
|
|
283
|
+
`${edgeGlobal.edgeState.organizationDocPath}/${submissionsCollection.value}`,
|
|
284
|
+
docId,
|
|
285
|
+
{ readAt: null },
|
|
286
|
+
)
|
|
287
|
+
}
|
|
288
|
+
catch (error) {
|
|
289
|
+
console.error('Failed to mark submission as unread', error)
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
const getSubmissionSortTime = (item) => {
|
|
294
|
+
const date = item?.timestamp?.toDate?.() || (item?.timestamp ? new Date(item.timestamp) : null)
|
|
295
|
+
if (!date || Number.isNaN(date.getTime()))
|
|
296
|
+
return 0
|
|
297
|
+
return date.getTime()
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
const sortedSubmissionIds = computed(() => {
|
|
301
|
+
return Object.values(submissionsMap.value || {})
|
|
302
|
+
.filter(item => item?.docId)
|
|
303
|
+
.map(item => ({ id: item.docId, time: getSubmissionSortTime(item) }))
|
|
304
|
+
.sort((a, b) => b.time - a.time)
|
|
305
|
+
.map(item => item.id)
|
|
306
|
+
})
|
|
117
307
|
|
|
118
308
|
const themeCollection = computed(() => {
|
|
119
309
|
return edgeFirebase.data?.[`${edgeGlobal.edgeState.organizationDocPath}/themes`] || {}
|
|
@@ -184,6 +374,8 @@ const menuPositionOptions = [
|
|
|
184
374
|
{ value: 'right', label: 'Right' },
|
|
185
375
|
]
|
|
186
376
|
|
|
377
|
+
const isExternalLinkEntry = entry => entry?.item && typeof entry.item === 'object' && entry.item.type === 'external'
|
|
378
|
+
|
|
187
379
|
const TEMPLATE_PAGES_PATH = computed(() => `${edgeGlobal.edgeState.organizationDocPath}/sites/templates/pages`)
|
|
188
380
|
const seededSiteIds = new Set()
|
|
189
381
|
|
|
@@ -266,6 +458,7 @@ const deriveBlockIdsFromDoc = (doc = {}) => {
|
|
|
266
458
|
|
|
267
459
|
const buildPagePayloadFromTemplateDoc = (templateDoc, slug, displayName = '') => {
|
|
268
460
|
const timestamp = Date.now()
|
|
461
|
+
const templateStructuredData = typeof templateDoc?.structuredData === 'string' ? templateDoc.structuredData.trim() : ''
|
|
269
462
|
const payload = {
|
|
270
463
|
name: displayName?.trim()?.length ? displayName : titleFromSlug(slug),
|
|
271
464
|
slug,
|
|
@@ -277,7 +470,7 @@ const buildPagePayloadFromTemplateDoc = (templateDoc, slug, displayName = '') =>
|
|
|
277
470
|
blockIds: [],
|
|
278
471
|
metaTitle: templateDoc?.metaTitle || '',
|
|
279
472
|
metaDescription: templateDoc?.metaDescription || '',
|
|
280
|
-
structuredData:
|
|
473
|
+
structuredData: templateStructuredData || buildPageStructuredData(),
|
|
281
474
|
doc_created_at: timestamp,
|
|
282
475
|
last_updated: timestamp,
|
|
283
476
|
}
|
|
@@ -297,6 +490,8 @@ const buildMenusFromDefaultPages = (defaultPages = []) => {
|
|
|
297
490
|
menus['Site Root'].push({
|
|
298
491
|
name: slug,
|
|
299
492
|
item: entry.pageId,
|
|
493
|
+
disableRename: !!entry?.disableRename,
|
|
494
|
+
disableDelete: !!entry?.disableDelete,
|
|
300
495
|
})
|
|
301
496
|
}
|
|
302
497
|
return menus
|
|
@@ -310,6 +505,37 @@ const deriveThemeMenus = (themeDoc = {}) => {
|
|
|
310
505
|
return null
|
|
311
506
|
}
|
|
312
507
|
|
|
508
|
+
const shouldApplyThemeSetting = (currentValue, baseValue) => {
|
|
509
|
+
if (currentValue === undefined || currentValue === null)
|
|
510
|
+
return true
|
|
511
|
+
if (typeof currentValue === 'string')
|
|
512
|
+
return !currentValue.trim() || areEqualNormalized(currentValue, baseValue)
|
|
513
|
+
if (Array.isArray(currentValue))
|
|
514
|
+
return currentValue.length === 0 || areEqualNormalized(currentValue, baseValue)
|
|
515
|
+
if (typeof currentValue === 'object')
|
|
516
|
+
return Object.keys(currentValue).length === 0 || areEqualNormalized(currentValue, baseValue)
|
|
517
|
+
return areEqualNormalized(currentValue, baseValue)
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
const buildThemeSettingsPayload = (themeDoc = {}, siteDoc = {}) => {
|
|
521
|
+
if (!themeDoc?.defaultSiteSettings || typeof themeDoc.defaultSiteSettings !== 'object' || Array.isArray(themeDoc.defaultSiteSettings))
|
|
522
|
+
return {}
|
|
523
|
+
const baseDefaults = createSiteSettingsDefaults()
|
|
524
|
+
const payload = {}
|
|
525
|
+
for (const [key, baseValue] of Object.entries(baseDefaults)) {
|
|
526
|
+
if (!(key in themeDoc.defaultSiteSettings))
|
|
527
|
+
continue
|
|
528
|
+
let themeValue = themeDoc.defaultSiteSettings[key]
|
|
529
|
+
if (key === 'structuredData' && typeof themeValue === 'string' && !themeValue.trim())
|
|
530
|
+
themeValue = baseValue
|
|
531
|
+
if (areEqualNormalized(themeValue, baseValue))
|
|
532
|
+
continue
|
|
533
|
+
if (shouldApplyThemeSetting(siteDoc?.[key], baseValue))
|
|
534
|
+
payload[key] = themeValue
|
|
535
|
+
}
|
|
536
|
+
return payload
|
|
537
|
+
}
|
|
538
|
+
|
|
313
539
|
const ensureTemplatePagesSnapshot = async () => {
|
|
314
540
|
if (!edgeFirebase.data?.[TEMPLATE_PAGES_PATH.value])
|
|
315
541
|
await edgeFirebase.startSnapshot(TEMPLATE_PAGES_PATH.value)
|
|
@@ -326,6 +552,10 @@ const duplicateEntriesWithPages = async (entries = [], options) => {
|
|
|
326
552
|
for (const entry of entries) {
|
|
327
553
|
if (!entry || entry.item == null)
|
|
328
554
|
continue
|
|
555
|
+
if (isExternalLinkEntry(entry)) {
|
|
556
|
+
next.push(edgeGlobal.dupObject(entry))
|
|
557
|
+
continue
|
|
558
|
+
}
|
|
329
559
|
if (typeof entry.item === 'string' || entry.item === '') {
|
|
330
560
|
const templateDoc = templatePages?.[entry.item] || null
|
|
331
561
|
const slug = ensureUniqueSlug(entry.name || '', templateDoc, usedSlugs)
|
|
@@ -363,21 +593,26 @@ const duplicateEntriesWithPages = async (entries = [], options) => {
|
|
|
363
593
|
return next
|
|
364
594
|
}
|
|
365
595
|
|
|
366
|
-
const seedNewSiteFromTheme = async (siteId, themeId) => {
|
|
596
|
+
const seedNewSiteFromTheme = async (siteId, themeId, siteDoc) => {
|
|
367
597
|
if (!siteId || !themeId)
|
|
368
598
|
return
|
|
369
599
|
const themeDoc = themeCollection.value?.[themeId]
|
|
370
600
|
if (!themeDoc)
|
|
371
601
|
return
|
|
602
|
+
const updatePayload = {}
|
|
372
603
|
const themeMenus = deriveThemeMenus(themeDoc)
|
|
373
|
-
if (
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
604
|
+
if (themeMenus) {
|
|
605
|
+
const templatePages = await ensureTemplatePagesSnapshot()
|
|
606
|
+
const usedSlugs = new Set()
|
|
607
|
+
const seededMenus = ensureMenuBuckets(themeMenus)
|
|
608
|
+
seededMenus['Site Root'] = await duplicateEntriesWithPages(seededMenus['Site Root'], { templatePages, siteId, usedSlugs })
|
|
609
|
+
seededMenus['Not In Menu'] = await duplicateEntriesWithPages(seededMenus['Not In Menu'], { templatePages, siteId, usedSlugs })
|
|
610
|
+
updatePayload.menus = seededMenus
|
|
611
|
+
}
|
|
612
|
+
const settingsPayload = buildThemeSettingsPayload(themeDoc, siteDoc || {})
|
|
613
|
+
Object.assign(updatePayload, settingsPayload)
|
|
614
|
+
if (Object.keys(updatePayload).length)
|
|
615
|
+
await edgeFirebase.changeDoc(`${edgeGlobal.edgeState.organizationDocPath}/sites`, siteId, updatePayload)
|
|
381
616
|
}
|
|
382
617
|
|
|
383
618
|
const handleNewSiteSaved = async ({ docId, data, collection }) => {
|
|
@@ -392,7 +627,7 @@ const handleNewSiteSaved = async ({ docId, data, collection }) => {
|
|
|
392
627
|
return
|
|
393
628
|
seededSiteIds.add(docId)
|
|
394
629
|
try {
|
|
395
|
-
await seedNewSiteFromTheme(docId, themeId)
|
|
630
|
+
await seedNewSiteFromTheme(docId, themeId, data)
|
|
396
631
|
}
|
|
397
632
|
catch (error) {
|
|
398
633
|
console.error('Failed to seed site from theme defaults', error)
|
|
@@ -425,6 +660,12 @@ onBeforeMount(async () => {
|
|
|
425
660
|
if (!edgeFirebase.data?.[`organizations/${edgeGlobal.edgeState.currentOrganization}/sites/${props.site}/published_posts`]) {
|
|
426
661
|
await edgeFirebase.startSnapshot(`organizations/${edgeGlobal.edgeState.currentOrganization}/sites/${props.site}/published_posts`)
|
|
427
662
|
}
|
|
663
|
+
if (props.site !== 'templates') {
|
|
664
|
+
const submissionsPath = `organizations/${edgeGlobal.edgeState.currentOrganization}/sites/${props.site}/lead-actions`
|
|
665
|
+
if (!edgeFirebase.data?.[submissionsPath]) {
|
|
666
|
+
await edgeFirebase.startSnapshot(submissionsPath, [{ field: 'action', operator: '==', value: 'Contact Form' }])
|
|
667
|
+
}
|
|
668
|
+
}
|
|
428
669
|
state.mounted = true
|
|
429
670
|
})
|
|
430
671
|
|
|
@@ -443,24 +684,54 @@ const isSiteDiff = computed(() => {
|
|
|
443
684
|
theme: publishedSite.theme,
|
|
444
685
|
allowedThemes: publishedSite.allowedThemes,
|
|
445
686
|
logo: publishedSite.logo,
|
|
687
|
+
logoLight: publishedSite.logoLight,
|
|
688
|
+
logoText: publishedSite.logoText,
|
|
689
|
+
logoType: publishedSite.logoType,
|
|
690
|
+
brandLogoDark: publishedSite.brandLogoDark,
|
|
691
|
+
brandLogoLight: publishedSite.brandLogoLight,
|
|
446
692
|
favicon: publishedSite.favicon,
|
|
447
693
|
menuPosition: publishedSite.menuPosition,
|
|
448
694
|
contactEmail: publishedSite.contactEmail,
|
|
695
|
+
contactPhone: publishedSite.contactPhone,
|
|
449
696
|
metaTitle: publishedSite.metaTitle,
|
|
450
697
|
metaDescription: publishedSite.metaDescription,
|
|
451
698
|
structuredData: publishedSite.structuredData,
|
|
699
|
+
trackingFacebookPixel: publishedSite.trackingFacebookPixel,
|
|
700
|
+
trackingGoogleAnalytics: publishedSite.trackingGoogleAnalytics,
|
|
701
|
+
trackingAdroll: publishedSite.trackingAdroll,
|
|
702
|
+
socialFacebook: publishedSite.socialFacebook,
|
|
703
|
+
socialInstagram: publishedSite.socialInstagram,
|
|
704
|
+
socialTwitter: publishedSite.socialTwitter,
|
|
705
|
+
socialLinkedIn: publishedSite.socialLinkedIn,
|
|
706
|
+
socialYouTube: publishedSite.socialYouTube,
|
|
707
|
+
socialTikTok: publishedSite.socialTikTok,
|
|
452
708
|
}, {
|
|
453
709
|
domains: siteData.value.domains,
|
|
454
710
|
menus: siteData.value.menus,
|
|
455
711
|
theme: siteData.value.theme,
|
|
456
712
|
allowedThemes: siteData.value.allowedThemes,
|
|
457
713
|
logo: siteData.value.logo,
|
|
714
|
+
logoLight: siteData.value.logoLight,
|
|
715
|
+
logoText: siteData.value.logoText,
|
|
716
|
+
logoType: siteData.value.logoType,
|
|
717
|
+
brandLogoDark: siteData.value.brandLogoDark,
|
|
718
|
+
brandLogoLight: siteData.value.brandLogoLight,
|
|
458
719
|
favicon: siteData.value.favicon,
|
|
459
720
|
menuPosition: siteData.value.menuPosition,
|
|
460
721
|
contactEmail: siteData.value.contactEmail,
|
|
722
|
+
contactPhone: siteData.value.contactPhone,
|
|
461
723
|
metaTitle: siteData.value.metaTitle,
|
|
462
724
|
metaDescription: siteData.value.metaDescription,
|
|
463
725
|
structuredData: siteData.value.structuredData,
|
|
726
|
+
trackingFacebookPixel: siteData.value.trackingFacebookPixel,
|
|
727
|
+
trackingGoogleAnalytics: siteData.value.trackingGoogleAnalytics,
|
|
728
|
+
trackingAdroll: siteData.value.trackingAdroll,
|
|
729
|
+
socialFacebook: siteData.value.socialFacebook,
|
|
730
|
+
socialInstagram: siteData.value.socialInstagram,
|
|
731
|
+
socialTwitter: siteData.value.socialTwitter,
|
|
732
|
+
socialLinkedIn: siteData.value.socialLinkedIn,
|
|
733
|
+
socialYouTube: siteData.value.socialYouTube,
|
|
734
|
+
socialTikTok: siteData.value.socialTikTok,
|
|
464
735
|
})
|
|
465
736
|
}
|
|
466
737
|
return false
|
|
@@ -481,12 +752,27 @@ const discardSiteSettings = async () => {
|
|
|
481
752
|
theme: publishedSite.theme || '',
|
|
482
753
|
allowedThemes: publishedSite.allowedThemes || [],
|
|
483
754
|
logo: publishedSite.logo || '',
|
|
755
|
+
logoLight: publishedSite.logoLight || '',
|
|
756
|
+
logoText: publishedSite.logoText || '',
|
|
757
|
+
logoType: publishedSite.logoType || 'image',
|
|
758
|
+
brandLogoDark: publishedSite.brandLogoDark || '',
|
|
759
|
+
brandLogoLight: publishedSite.brandLogoLight || '',
|
|
484
760
|
favicon: publishedSite.favicon || '',
|
|
485
761
|
menuPosition: publishedSite.menuPosition || '',
|
|
486
762
|
contactEmail: publishedSite.contactEmail || '',
|
|
763
|
+
contactPhone: publishedSite.contactPhone || '',
|
|
487
764
|
metaTitle: publishedSite.metaTitle || '',
|
|
488
765
|
metaDescription: publishedSite.metaDescription || '',
|
|
489
766
|
structuredData: publishedSite.structuredData || '',
|
|
767
|
+
trackingFacebookPixel: publishedSite.trackingFacebookPixel || '',
|
|
768
|
+
trackingGoogleAnalytics: publishedSite.trackingGoogleAnalytics || '',
|
|
769
|
+
trackingAdroll: publishedSite.trackingAdroll || '',
|
|
770
|
+
socialFacebook: publishedSite.socialFacebook || '',
|
|
771
|
+
socialInstagram: publishedSite.socialInstagram || '',
|
|
772
|
+
socialTwitter: publishedSite.socialTwitter || '',
|
|
773
|
+
socialLinkedIn: publishedSite.socialLinkedIn || '',
|
|
774
|
+
socialYouTube: publishedSite.socialYouTube || '',
|
|
775
|
+
socialTikTok: publishedSite.socialTikTok || '',
|
|
490
776
|
})
|
|
491
777
|
}
|
|
492
778
|
}
|
|
@@ -506,6 +792,19 @@ const publishSite = async () => {
|
|
|
506
792
|
}
|
|
507
793
|
}
|
|
508
794
|
|
|
795
|
+
const publishSiteAndSettings = async () => {
|
|
796
|
+
if (state.publishSiteLoading)
|
|
797
|
+
return
|
|
798
|
+
state.publishSiteLoading = true
|
|
799
|
+
try {
|
|
800
|
+
await publishSiteSettings()
|
|
801
|
+
await publishSite()
|
|
802
|
+
}
|
|
803
|
+
finally {
|
|
804
|
+
state.publishSiteLoading = false
|
|
805
|
+
}
|
|
806
|
+
}
|
|
807
|
+
|
|
509
808
|
const pages = computed(() => {
|
|
510
809
|
return edgeFirebase.data?.[`${edgeGlobal.edgeState.organizationDocPath}/sites/${props.site}/pages`] || {}
|
|
511
810
|
})
|
|
@@ -585,6 +884,8 @@ const setViewMode = (mode) => {
|
|
|
585
884
|
return
|
|
586
885
|
state.viewMode = mode
|
|
587
886
|
state.selectedPostId = ''
|
|
887
|
+
if (mode !== 'submissions')
|
|
888
|
+
state.selectedSubmissionId = ''
|
|
588
889
|
if (props.page)
|
|
589
890
|
router.replace(pageRouteBase.value)
|
|
590
891
|
}
|
|
@@ -634,13 +935,6 @@ watch(pages, (pagesCollection) => {
|
|
|
634
935
|
state.menus = nextMenu
|
|
635
936
|
}, { immediate: true, deep: true })
|
|
636
937
|
|
|
637
|
-
watch(() => state.siteSettings, (open) => {
|
|
638
|
-
if (!open)
|
|
639
|
-
state.logoPickerOpen = false
|
|
640
|
-
if (!open)
|
|
641
|
-
state.faviconPickerOpen = false
|
|
642
|
-
})
|
|
643
|
-
|
|
644
938
|
watch(() => props.page, (next) => {
|
|
645
939
|
if (next) {
|
|
646
940
|
state.selectedPostId = ''
|
|
@@ -652,6 +946,20 @@ watch(() => props.page, (next) => {
|
|
|
652
946
|
}
|
|
653
947
|
})
|
|
654
948
|
|
|
949
|
+
watch([isViewingSubmissions, sortedSubmissionIds], () => {
|
|
950
|
+
if (!isViewingSubmissions.value)
|
|
951
|
+
return
|
|
952
|
+
const ids = sortedSubmissionIds.value
|
|
953
|
+
if (!ids.length) {
|
|
954
|
+
state.selectedSubmissionId = ''
|
|
955
|
+
return
|
|
956
|
+
}
|
|
957
|
+
if (!state.selectedSubmissionId || !submissionsMap.value?.[state.selectedSubmissionId]) {
|
|
958
|
+
state.selectedSubmissionId = ids[0]
|
|
959
|
+
markSubmissionRead(ids[0])
|
|
960
|
+
}
|
|
961
|
+
}, { immediate: true })
|
|
962
|
+
|
|
655
963
|
watch(() => state.menus, async (newVal) => {
|
|
656
964
|
if (areEqualNormalized(siteData.value.menus, newVal)) {
|
|
657
965
|
return
|
|
@@ -667,6 +975,8 @@ watch(() => state.menus, async (newVal) => {
|
|
|
667
975
|
const newPage = JSON.parse(JSON.stringify(pageInit))
|
|
668
976
|
for (const [menuName, items] of Object.entries(newVal)) {
|
|
669
977
|
for (const [index, item] of items.entries()) {
|
|
978
|
+
if (isExternalLinkEntry(item))
|
|
979
|
+
continue
|
|
670
980
|
if (typeof item.item === 'string') {
|
|
671
981
|
if (item.item === '') {
|
|
672
982
|
newPage.name = item.name
|
|
@@ -682,9 +992,11 @@ watch(() => state.menus, async (newVal) => {
|
|
|
682
992
|
}
|
|
683
993
|
}
|
|
684
994
|
}
|
|
685
|
-
if (typeof item.item === 'object') {
|
|
995
|
+
if (typeof item.item === 'object' && !isExternalLinkEntry(item)) {
|
|
686
996
|
for (const [subMenuName, subItems] of Object.entries(item.item)) {
|
|
687
997
|
for (const [subIndex, subItem] of subItems.entries()) {
|
|
998
|
+
if (isExternalLinkEntry(subItem))
|
|
999
|
+
continue
|
|
688
1000
|
if (typeof subItem.item === 'string') {
|
|
689
1001
|
if (subItem.item === '') {
|
|
690
1002
|
newPage.name = subItem.name
|
|
@@ -954,18 +1266,45 @@ const pageSettingsUpdated = async (pageData) => {
|
|
|
954
1266
|
<FilePenLine class="h-4 w-4" />
|
|
955
1267
|
Posts
|
|
956
1268
|
</edge-shad-button>
|
|
1269
|
+
<edge-shad-button
|
|
1270
|
+
variant="ghost"
|
|
1271
|
+
size="sm"
|
|
1272
|
+
class="h-8 px-4 text-xs gap-2 rounded-full"
|
|
1273
|
+
:class="state.viewMode === 'submissions' ? 'bg-primary text-primary-foreground shadow-sm' : 'text-muted-foreground hover:text-foreground'"
|
|
1274
|
+
@click="setViewMode('submissions')"
|
|
1275
|
+
>
|
|
1276
|
+
<Inbox class="h-4 w-4" />
|
|
1277
|
+
Inbox
|
|
1278
|
+
<span
|
|
1279
|
+
v-if="unreadSubmissionsCount"
|
|
1280
|
+
class="ml-1 rounded-full bg-primary px-2 py-0.5 text-[10px] font-semibold text-primary-foreground"
|
|
1281
|
+
>
|
|
1282
|
+
{{ unreadSubmissionsCount }}
|
|
1283
|
+
</span>
|
|
1284
|
+
</edge-shad-button>
|
|
957
1285
|
</div>
|
|
958
1286
|
</div>
|
|
959
1287
|
<div v-if="!isTemplateSite" class="flex items-center gap-3 justify-end">
|
|
960
1288
|
<Transition name="fade" mode="out-in">
|
|
961
|
-
<div v-if="isSiteDiff" key="unpublished" class="flex gap-
|
|
962
|
-
<
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
1289
|
+
<div v-if="isSiteDiff || isAnyPagesDiff" key="unpublished" class="flex gap-2 items-center">
|
|
1290
|
+
<div class="flex gap-1 items-center bg-yellow-100 text-xs py-1 px-3 text-yellow-800 rounded">
|
|
1291
|
+
<CircleAlert class="!text-yellow-800 w-3 h-6" />
|
|
1292
|
+
<span class="font-medium text-[10px]">
|
|
1293
|
+
{{ isSiteDiff ? 'Unpublished Settings' : 'Unpublished Pages' }}
|
|
1294
|
+
</span>
|
|
1295
|
+
</div>
|
|
1296
|
+
<edge-shad-button
|
|
1297
|
+
class="h-8 px-4 text-xs gap-2 bg-primary text-primary-foreground hover:bg-primary/90 shadow-sm"
|
|
1298
|
+
:disabled="state.publishSiteLoading"
|
|
1299
|
+
@click="publishSiteAndSettings"
|
|
1300
|
+
>
|
|
1301
|
+
<Loader2 v-if="state.publishSiteLoading" class="h-3.5 w-3.5 animate-spin" />
|
|
1302
|
+
<FolderUp v-else class="h-3.5 w-3.5" />
|
|
1303
|
+
Publish Site
|
|
1304
|
+
</edge-shad-button>
|
|
966
1305
|
</div>
|
|
967
1306
|
<div v-else key="published" class="flex gap-1 items-center bg-green-100 text-xs py-1 px-3 text-green-800 rounded">
|
|
968
|
-
<FileCheck class="!text-green-800 w-3 h-
|
|
1307
|
+
<FileCheck class="!text-green-800 w-3 h-6" />
|
|
969
1308
|
<span class="font-medium text-[10px]">
|
|
970
1309
|
Settings Published
|
|
971
1310
|
</span>
|
|
@@ -1016,7 +1355,147 @@ const pageSettingsUpdated = async (pageData) => {
|
|
|
1016
1355
|
</div>
|
|
1017
1356
|
<div class="flex-1">
|
|
1018
1357
|
<Transition name="fade" mode="out-in">
|
|
1019
|
-
<div v-if="
|
|
1358
|
+
<div v-if="isViewingSubmissions" class="flex-1 overflow-y-auto p-6">
|
|
1359
|
+
<edge-dashboard
|
|
1360
|
+
:collection="submissionsCollection"
|
|
1361
|
+
query-field="action"
|
|
1362
|
+
query-value="Contact Form"
|
|
1363
|
+
query-operator="=="
|
|
1364
|
+
:filter="state.submissionFilter"
|
|
1365
|
+
:filter-fields="['data.name', 'data.fullName', 'data.firstName', 'data.lastName', 'data.email', 'data.phone', 'data.message', 'data.comments', 'data.notes']"
|
|
1366
|
+
sort-field="timestamp"
|
|
1367
|
+
sort-direction="desc"
|
|
1368
|
+
class="pt-0 flex-1"
|
|
1369
|
+
>
|
|
1370
|
+
<template #header-start>
|
|
1371
|
+
<Inbox class="mr-2 h-4 w-4" />
|
|
1372
|
+
Submissions
|
|
1373
|
+
<!-- <span class="ml-3 text-xs font-semibold uppercase tracking-wide text-muted-foreground">
|
|
1374
|
+
Contact Form
|
|
1375
|
+
</span> -->
|
|
1376
|
+
</template>
|
|
1377
|
+
<template #header-center>
|
|
1378
|
+
<div class="w-full px-4 md:px-6">
|
|
1379
|
+
<edge-shad-input
|
|
1380
|
+
v-model="state.submissionFilter"
|
|
1381
|
+
name="submissionFilter"
|
|
1382
|
+
placeholder="Search submissions..."
|
|
1383
|
+
class="w-full"
|
|
1384
|
+
/>
|
|
1385
|
+
</div>
|
|
1386
|
+
</template>
|
|
1387
|
+
<template #header-end="slotProps">
|
|
1388
|
+
<span class="text-xs font-semibold uppercase tracking-wide text-muted-foreground">
|
|
1389
|
+
{{ slotProps.recordCount }} total • {{ unreadSubmissionsCount }} unread
|
|
1390
|
+
</span>
|
|
1391
|
+
</template>
|
|
1392
|
+
<template #list="slotProps">
|
|
1393
|
+
<div class="grid gap-4 pt-4 w-full md:grid-cols-[320px_minmax(0,1fr)]">
|
|
1394
|
+
<div class="space-y-2">
|
|
1395
|
+
<div
|
|
1396
|
+
v-for="item in slotProps.filtered"
|
|
1397
|
+
:key="item.docId"
|
|
1398
|
+
role="button"
|
|
1399
|
+
tabindex="0"
|
|
1400
|
+
class="group rounded-lg border p-3 text-left transition hover:border-primary/60 hover:bg-muted/60"
|
|
1401
|
+
:class="state.selectedSubmissionId === item.docId ? 'border-primary/70 bg-muted/70 shadow-sm' : 'border-border/60 bg-card'"
|
|
1402
|
+
@click="state.selectedSubmissionId = item.docId; markSubmissionRead(item.docId)"
|
|
1403
|
+
@keyup.enter="state.selectedSubmissionId = item.docId; markSubmissionRead(item.docId)"
|
|
1404
|
+
>
|
|
1405
|
+
<div class="flex items-start justify-between gap-2">
|
|
1406
|
+
<div class="min-w-0">
|
|
1407
|
+
<div class="truncate text-sm font-semibold text-foreground">
|
|
1408
|
+
{{ getSubmissionLabel(item.data) }}
|
|
1409
|
+
</div>
|
|
1410
|
+
<div v-if="item.data?.pageName" class="truncate text-xs text-muted-foreground">
|
|
1411
|
+
{{ item.data.pageName }}
|
|
1412
|
+
</div>
|
|
1413
|
+
</div>
|
|
1414
|
+
<div class="flex items-center gap-2 text-[11px] text-muted-foreground">
|
|
1415
|
+
<span v-if="isSubmissionUnread(item)" class="rounded-full bg-primary/15 px-2 py-0.5 text-[10px] font-semibold uppercase text-primary">
|
|
1416
|
+
Unread
|
|
1417
|
+
</span>
|
|
1418
|
+
<span>{{ formatSubmissionTimestamp(item.timestamp) }}</span>
|
|
1419
|
+
</div>
|
|
1420
|
+
</div>
|
|
1421
|
+
<div v-if="getSubmissionMessage(item.data)" class="mt-2 text-xs text-muted-foreground line-clamp-2">
|
|
1422
|
+
{{ getSubmissionMessage(item.data) }}
|
|
1423
|
+
</div>
|
|
1424
|
+
</div>
|
|
1425
|
+
</div>
|
|
1426
|
+
<div>
|
|
1427
|
+
<Card v-if="selectedSubmission" class="border border-border/70 bg-card/95 shadow-sm">
|
|
1428
|
+
<CardHeader class="flex flex-col gap-2">
|
|
1429
|
+
<div class="flex flex-wrap items-start justify-between gap-2">
|
|
1430
|
+
<div>
|
|
1431
|
+
<CardTitle class="text-xl">
|
|
1432
|
+
{{ getSubmissionLabel(selectedSubmission.data) }}
|
|
1433
|
+
</CardTitle>
|
|
1434
|
+
<CardDescription class="text-xs">
|
|
1435
|
+
{{ formatSubmissionTimestamp(selectedSubmission.timestamp) }}
|
|
1436
|
+
</CardDescription>
|
|
1437
|
+
</div>
|
|
1438
|
+
<div class="flex flex-wrap items-center gap-2 text-xs font-semibold uppercase tracking-wide text-muted-foreground">
|
|
1439
|
+
<span>
|
|
1440
|
+
{{ selectedSubmission.data?.pageName || selectedSubmission.data?.pageId || 'Site submission' }}
|
|
1441
|
+
</span>
|
|
1442
|
+
<edge-shad-button
|
|
1443
|
+
v-if="isSubmissionUnread(selectedSubmission)"
|
|
1444
|
+
size="sm"
|
|
1445
|
+
variant="outline"
|
|
1446
|
+
class="h-7 gap-2 text-[11px]"
|
|
1447
|
+
@click="markSubmissionRead(selectedSubmission.docId)"
|
|
1448
|
+
>
|
|
1449
|
+
<MailOpen class="h-3.5 w-3.5" />
|
|
1450
|
+
Mark read
|
|
1451
|
+
</edge-shad-button>
|
|
1452
|
+
<edge-shad-button
|
|
1453
|
+
v-else
|
|
1454
|
+
size="sm"
|
|
1455
|
+
variant="outline"
|
|
1456
|
+
class="h-7 gap-2 text-[11px]"
|
|
1457
|
+
@click="markSubmissionUnread(selectedSubmission.docId)"
|
|
1458
|
+
>
|
|
1459
|
+
<Mail class="h-3.5 w-3.5" />
|
|
1460
|
+
Mark unread
|
|
1461
|
+
</edge-shad-button>
|
|
1462
|
+
</div>
|
|
1463
|
+
</div>
|
|
1464
|
+
</CardHeader>
|
|
1465
|
+
<CardContent class="space-y-4">
|
|
1466
|
+
<div
|
|
1467
|
+
v-if="getSubmissionMessage(selectedSubmission.data)"
|
|
1468
|
+
class="rounded-lg border border-border/60 bg-muted/40 p-3 text-sm text-foreground"
|
|
1469
|
+
>
|
|
1470
|
+
{{ getSubmissionMessage(selectedSubmission.data) }}
|
|
1471
|
+
</div>
|
|
1472
|
+
<div class="grid gap-3 md:grid-cols-2">
|
|
1473
|
+
<div
|
|
1474
|
+
v-for="entry in collectSubmissionEntries(selectedSubmission.data)"
|
|
1475
|
+
:key="entry.key"
|
|
1476
|
+
class="rounded-lg border border-border/60 bg-background p-3"
|
|
1477
|
+
>
|
|
1478
|
+
<div class="text-xs font-semibold uppercase tracking-wide text-muted-foreground">
|
|
1479
|
+
{{ formatSubmissionKey(entry.key) }}
|
|
1480
|
+
</div>
|
|
1481
|
+
<div class="mt-1 text-sm text-foreground break-words">
|
|
1482
|
+
{{ formatSubmissionValue(entry.value) }}
|
|
1483
|
+
</div>
|
|
1484
|
+
</div>
|
|
1485
|
+
</div>
|
|
1486
|
+
</CardContent>
|
|
1487
|
+
</Card>
|
|
1488
|
+
<Card v-else class="border border-dashed border-border/80 bg-muted/30">
|
|
1489
|
+
<CardContent class="py-12 text-center text-sm text-muted-foreground">
|
|
1490
|
+
Select a submission to view details.
|
|
1491
|
+
</CardContent>
|
|
1492
|
+
</Card>
|
|
1493
|
+
</div>
|
|
1494
|
+
</div>
|
|
1495
|
+
</template>
|
|
1496
|
+
</edge-dashboard>
|
|
1497
|
+
</div>
|
|
1498
|
+
<div v-else-if="isEditingPost" class="w-full h-full">
|
|
1020
1499
|
<edge-cms-posts
|
|
1021
1500
|
mode="editor"
|
|
1022
1501
|
:site="props.site"
|
|
@@ -1055,7 +1534,7 @@ const pageSettingsUpdated = async (pageData) => {
|
|
|
1055
1534
|
</ResizablePanel>
|
|
1056
1535
|
<ResizablePanel ref="mainPanel">
|
|
1057
1536
|
<Transition name="fade" mode="out-in">
|
|
1058
|
-
<div v-if="props.page && !state.updating" :key="props.page" class="max-h-[calc(100vh-
|
|
1537
|
+
<div v-if="props.page && !state.updating" :key="props.page" class="max-h-[calc(100vh-100px)] overflow-y-auto w-full">
|
|
1059
1538
|
<NuxtPage class="flex flex-col flex-1 px-0 mx-0 pt-0" />
|
|
1060
1539
|
</div>
|
|
1061
1540
|
<div v-else class="p-4 text-center flex text-slate-500 h-[calc(100vh-4rem)] justify-center items-center overflow-y-auto">
|
|
@@ -1099,176 +1578,19 @@ const pageSettingsUpdated = async (pageData) => {
|
|
|
1099
1578
|
@error="formErrors"
|
|
1100
1579
|
>
|
|
1101
1580
|
<template #main="slotProps">
|
|
1102
|
-
<div class="p-6
|
|
1103
|
-
<edge-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1581
|
+
<div class="p-6 h-[calc(100vh-140px)] overflow-y-auto">
|
|
1582
|
+
<edge-cms-site-settings-form
|
|
1583
|
+
:settings="slotProps.workingDoc"
|
|
1584
|
+
:theme-options="themeOptions"
|
|
1585
|
+
:user-options="userOptions"
|
|
1586
|
+
:has-users="Object.keys(orgUsers).length > 0"
|
|
1587
|
+
:show-users="true"
|
|
1588
|
+
:show-theme-fields="true"
|
|
1589
|
+
:is-admin="isAdmin"
|
|
1590
|
+
:enable-media-picker="true"
|
|
1591
|
+
:site-id="props.site"
|
|
1592
|
+
:domain-error="domainError"
|
|
1109
1593
|
/>
|
|
1110
|
-
<edge-shad-tags
|
|
1111
|
-
v-model="slotProps.workingDoc.domains"
|
|
1112
|
-
name="domains"
|
|
1113
|
-
label="Domains"
|
|
1114
|
-
placeholder="Add or remove domains"
|
|
1115
|
-
class="w-full"
|
|
1116
|
-
/>
|
|
1117
|
-
<edge-shad-input
|
|
1118
|
-
v-model="slotProps.workingDoc.contactEmail"
|
|
1119
|
-
name="contactEmail"
|
|
1120
|
-
label="Contact Email"
|
|
1121
|
-
placeholder="name@example.com"
|
|
1122
|
-
class="w-full"
|
|
1123
|
-
/>
|
|
1124
|
-
<edge-shad-select-tags
|
|
1125
|
-
v-if="isAdmin"
|
|
1126
|
-
:model-value="Array.isArray(slotProps.workingDoc.allowedThemes) ? slotProps.workingDoc.allowedThemes : []"
|
|
1127
|
-
name="allowedThemes"
|
|
1128
|
-
label="Allowed Themes"
|
|
1129
|
-
placeholder="Select allowed themes"
|
|
1130
|
-
class="w-full"
|
|
1131
|
-
:items="themeOptions"
|
|
1132
|
-
item-title="label"
|
|
1133
|
-
item-value="value"
|
|
1134
|
-
@update:model-value="(value) => {
|
|
1135
|
-
const normalized = Array.isArray(value) ? value : []
|
|
1136
|
-
slotProps.workingDoc.allowedThemes = normalized
|
|
1137
|
-
if (normalized.length && !normalized.includes(slotProps.workingDoc.theme)) {
|
|
1138
|
-
slotProps.workingDoc.theme = normalized[0] || ''
|
|
1139
|
-
}
|
|
1140
|
-
}"
|
|
1141
|
-
/>
|
|
1142
|
-
<edge-shad-select
|
|
1143
|
-
:model-value="slotProps.workingDoc.theme || ''"
|
|
1144
|
-
name="theme"
|
|
1145
|
-
label="Theme"
|
|
1146
|
-
placeholder="Select a theme"
|
|
1147
|
-
class="w-full"
|
|
1148
|
-
:items="themeItemsForAllowed(slotProps.workingDoc.allowedThemes, slotProps.workingDoc.theme)"
|
|
1149
|
-
item-title="label"
|
|
1150
|
-
item-value="value"
|
|
1151
|
-
@update:model-value="value => (slotProps.workingDoc.theme = value || '')"
|
|
1152
|
-
/>
|
|
1153
|
-
<div class="space-y-2">
|
|
1154
|
-
<label class="text-sm font-medium text-foreground flex items-center justify-between">
|
|
1155
|
-
Logo
|
|
1156
|
-
<edge-shad-button
|
|
1157
|
-
type="button"
|
|
1158
|
-
variant="link"
|
|
1159
|
-
class="px-0 h-auto text-sm"
|
|
1160
|
-
@click="state.logoPickerOpen = !state.logoPickerOpen"
|
|
1161
|
-
>
|
|
1162
|
-
{{ state.logoPickerOpen ? 'Hide picker' : 'Select logo' }}
|
|
1163
|
-
</edge-shad-button>
|
|
1164
|
-
</label>
|
|
1165
|
-
<div class="flex items-center gap-4">
|
|
1166
|
-
<div v-if="slotProps.workingDoc.logo" class="flex items-center gap-3">
|
|
1167
|
-
<img :src="slotProps.workingDoc.logo" alt="Logo preview" class="h-16 w-auto rounded-md border border-border bg-muted object-contain">
|
|
1168
|
-
<edge-shad-button
|
|
1169
|
-
type="button"
|
|
1170
|
-
variant="ghost"
|
|
1171
|
-
class="h-8"
|
|
1172
|
-
@click="slotProps.workingDoc.logo = ''"
|
|
1173
|
-
>
|
|
1174
|
-
Remove
|
|
1175
|
-
</edge-shad-button>
|
|
1176
|
-
</div>
|
|
1177
|
-
<span v-else class="text-sm text-muted-foreground italic">No logo selected</span>
|
|
1178
|
-
</div>
|
|
1179
|
-
<div v-if="state.logoPickerOpen" class="mt-2 border border-dashed rounded-lg p-2">
|
|
1180
|
-
<edge-cms-media-manager
|
|
1181
|
-
:site="props.site"
|
|
1182
|
-
:select-mode="true"
|
|
1183
|
-
:default-tags="['Logos']"
|
|
1184
|
-
@select="(url) => {
|
|
1185
|
-
slotProps.workingDoc.logo = url
|
|
1186
|
-
state.logoPickerOpen = false
|
|
1187
|
-
}"
|
|
1188
|
-
/>
|
|
1189
|
-
</div>
|
|
1190
|
-
</div>
|
|
1191
|
-
<div class="space-y-2">
|
|
1192
|
-
<label class="text-sm font-medium text-foreground flex items-center justify-between">
|
|
1193
|
-
Favicon
|
|
1194
|
-
<edge-shad-button
|
|
1195
|
-
type="button"
|
|
1196
|
-
variant="link"
|
|
1197
|
-
class="px-0 h-auto text-sm"
|
|
1198
|
-
@click="state.faviconPickerOpen = !state.faviconPickerOpen"
|
|
1199
|
-
>
|
|
1200
|
-
{{ state.faviconPickerOpen ? 'Hide picker' : 'Select favicon' }}
|
|
1201
|
-
</edge-shad-button>
|
|
1202
|
-
</label>
|
|
1203
|
-
<div class="flex items-center gap-4">
|
|
1204
|
-
<div v-if="slotProps.workingDoc.favicon" class="flex items-center gap-3">
|
|
1205
|
-
<img :src="slotProps.workingDoc.favicon" alt="Favicon preview" class="h-12 w-12 rounded-md border border-border bg-muted object-contain">
|
|
1206
|
-
<edge-shad-button
|
|
1207
|
-
type="button"
|
|
1208
|
-
variant="ghost"
|
|
1209
|
-
class="h-8"
|
|
1210
|
-
@click="slotProps.workingDoc.favicon = ''"
|
|
1211
|
-
>
|
|
1212
|
-
Remove
|
|
1213
|
-
</edge-shad-button>
|
|
1214
|
-
</div>
|
|
1215
|
-
<span v-else class="text-sm text-muted-foreground italic">No favicon selected</span>
|
|
1216
|
-
</div>
|
|
1217
|
-
<div v-if="state.faviconPickerOpen" class="mt-2 border border-dashed rounded-lg p-2">
|
|
1218
|
-
<edge-cms-media-manager
|
|
1219
|
-
:site="props.site"
|
|
1220
|
-
:select-mode="true"
|
|
1221
|
-
:default-tags="['Logos']"
|
|
1222
|
-
@select="(url) => {
|
|
1223
|
-
slotProps.workingDoc.favicon = url
|
|
1224
|
-
state.faviconPickerOpen = false
|
|
1225
|
-
}"
|
|
1226
|
-
/>
|
|
1227
|
-
</div>
|
|
1228
|
-
</div>
|
|
1229
|
-
<edge-shad-select
|
|
1230
|
-
:model-value="slotProps.workingDoc.menuPosition || ''"
|
|
1231
|
-
name="menuPosition"
|
|
1232
|
-
label="Menu Position"
|
|
1233
|
-
placeholder="Select menu position"
|
|
1234
|
-
class="w-full"
|
|
1235
|
-
:items="menuPositionOptions"
|
|
1236
|
-
item-title="label"
|
|
1237
|
-
item-value="value"
|
|
1238
|
-
@update:model-value="value => (slotProps.workingDoc.menuPosition = value || '')"
|
|
1239
|
-
/>
|
|
1240
|
-
<edge-shad-select-tags
|
|
1241
|
-
v-if="Object.keys(orgUsers).length > 0 && isAdmin"
|
|
1242
|
-
v-model="slotProps.workingDoc.users" :disabled="!edgeGlobal.isAdminGlobal(edgeFirebase).value"
|
|
1243
|
-
:items="Object.values(orgUsers)" name="users" label="Users"
|
|
1244
|
-
item-title="meta.name" item-value="userId" placeholder="Select users" class="w-full" :multiple="true"
|
|
1245
|
-
/>
|
|
1246
|
-
<Card>
|
|
1247
|
-
<CardHeader>
|
|
1248
|
-
<CardTitle>SEO</CardTitle>
|
|
1249
|
-
<CardDescription>Default settings if the information is not entered on the page.</CardDescription>
|
|
1250
|
-
</CardHeader>
|
|
1251
|
-
<CardContent class="pt-0">
|
|
1252
|
-
<edge-shad-input
|
|
1253
|
-
v-model="slotProps.workingDoc.metaTitle"
|
|
1254
|
-
label="Meta Title"
|
|
1255
|
-
name="metaTitle"
|
|
1256
|
-
/>
|
|
1257
|
-
<edge-shad-textarea
|
|
1258
|
-
v-model="slotProps.workingDoc.metaDescription"
|
|
1259
|
-
label="Meta Description"
|
|
1260
|
-
name="metaDescription"
|
|
1261
|
-
/>
|
|
1262
|
-
<edge-cms-code-editor
|
|
1263
|
-
v-model="slotProps.workingDoc.structuredData"
|
|
1264
|
-
title="Structured Data (JSON-LD)"
|
|
1265
|
-
language="json"
|
|
1266
|
-
name="structuredData"
|
|
1267
|
-
height="300px"
|
|
1268
|
-
class="mb-4 w-full"
|
|
1269
|
-
/>
|
|
1270
|
-
</CardContent>
|
|
1271
|
-
</Card>
|
|
1272
1594
|
</div>
|
|
1273
1595
|
<SheetFooter class="pt-2 flex justify-between">
|
|
1274
1596
|
<edge-shad-button variant="destructive" class="text-white" @click="state.siteSettings = false">
|