@edgedev/create-edge-app 1.1.25 → 1.1.27
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +55 -20
- package/{agent.md → agents.md} +2 -0
- package/bin/cli.js +6 -6
- package/edge/components/auth/login.vue +384 -0
- package/edge/components/auth/register.vue +396 -0
- package/edge/components/auth.vue +108 -0
- package/edge/components/autoFileUpload.vue +215 -0
- package/edge/components/billing.vue +8 -0
- package/edge/components/buttonDivider.vue +14 -0
- package/edge/components/chip.vue +34 -0
- package/edge/components/clipboardButton.vue +42 -0
- package/edge/components/cms/block.vue +529 -0
- package/edge/components/cms/blockApi.vue +212 -0
- package/edge/components/cms/blockEditor.vue +725 -0
- package/edge/components/cms/blockInput.vue +66 -0
- package/edge/components/cms/blockPicker.vue +486 -0
- package/edge/components/cms/blockRender.vue +78 -0
- package/edge/components/cms/blockSheetContent.vue +28 -0
- package/edge/components/cms/codeEditor.vue +466 -0
- package/edge/components/cms/fontUpload.vue +327 -0
- package/edge/components/cms/htmlContent.vue +807 -0
- package/edge/components/cms/init_blocks/api_with_subarrays.html +17 -0
- package/edge/components/cms/init_blocks/array_with_collection.html +7 -0
- package/edge/components/cms/init_blocks/array_with_objects.html +7 -0
- package/edge/components/cms/init_blocks/carousel.html +103 -0
- package/edge/components/cms/init_blocks/contact_us.html +69 -0
- package/edge/components/cms/init_blocks/content_with_left_image.html +27 -0
- package/edge/components/cms/init_blocks/footer.html +24 -0
- package/edge/components/cms/init_blocks/header_divider.html +7 -0
- package/edge/components/cms/init_blocks/hero.html +35 -0
- package/edge/components/cms/init_blocks/hero_carousel.html +52 -0
- package/edge/components/cms/init_blocks/newsletter.html +117 -0
- package/edge/components/cms/init_blocks/post_content.html +7 -0
- package/edge/components/cms/init_blocks/post_title_header.html +21 -0
- package/edge/components/cms/init_blocks/posts_list.html +20 -0
- package/edge/components/cms/init_blocks/properties_showcase.html +100 -0
- package/edge/components/cms/init_blocks/property_carousel.html +59 -0
- package/edge/components/cms/init_blocks/property_detail.html +112 -0
- package/edge/components/cms/init_blocks/property_detail_header.html +34 -0
- package/edge/components/cms/init_blocks/property_results.html +137 -0
- package/edge/components/cms/init_blocks/property_search.html +75 -0
- package/edge/components/cms/init_blocks/simple_array.html +7 -0
- package/edge/components/cms/mediaCard.vue +116 -0
- package/edge/components/cms/mediaManager.vue +386 -0
- package/edge/components/cms/menu.vue +1103 -0
- package/edge/components/cms/optionsSelect.vue +107 -0
- package/edge/components/cms/page.vue +1785 -0
- package/edge/components/cms/posts.vue +1083 -0
- package/edge/components/cms/site.vue +1475 -0
- package/edge/components/cms/themeDefaultMenu.vue +548 -0
- package/edge/components/cms/themeEditor.vue +429 -0
- package/edge/components/dashboard.vue +776 -0
- package/edge/components/editor.vue +671 -0
- package/edge/components/fileTree.vue +72 -0
- package/edge/components/files.vue +89 -0
- package/edge/components/formSubtypes/myOrgs.vue +214 -0
- package/edge/components/formSubtypes/users.vue +336 -0
- package/edge/components/functionChips.vue +57 -0
- package/edge/components/gError.vue +98 -0
- package/edge/components/gHelper.vue +67 -0
- package/edge/components/gInput.vue +1331 -0
- package/edge/components/loggingIn.vue +41 -0
- package/edge/components/menu.vue +137 -0
- package/edge/components/menuContent.vue +132 -0
- package/edge/components/myAccount.vue +317 -0
- package/edge/components/myOrganizations.vue +75 -0
- package/edge/components/myProfile.vue +122 -0
- package/edge/components/orgSwitcher.vue +25 -0
- package/edge/components/organizationMembers.vue +522 -0
- package/edge/components/organizationSettings.vue +271 -0
- package/edge/components/shad/breadcrumbs.vue +35 -0
- package/edge/components/shad/button.vue +43 -0
- package/edge/components/shad/checkbox.vue +73 -0
- package/edge/components/shad/combobox.vue +238 -0
- package/edge/components/shad/datepicker.vue +184 -0
- package/edge/components/shad/dialog.vue +32 -0
- package/edge/components/shad/dropdownMenu.vue +54 -0
- package/edge/components/shad/dropdownMenuItem.vue +21 -0
- package/edge/components/shad/form.vue +59 -0
- package/edge/components/shad/html.vue +877 -0
- package/edge/components/shad/input.vue +139 -0
- package/edge/components/shad/number.vue +109 -0
- package/edge/components/shad/select.vue +151 -0
- package/edge/components/shad/selectTags.vue +278 -0
- package/edge/components/shad/switch.vue +67 -0
- package/edge/components/shad/tags.vue +137 -0
- package/edge/components/shad/textarea.vue +102 -0
- package/edge/components/shad/typeMoney.vue +167 -0
- package/edge/components/sideBar.vue +288 -0
- package/edge/components/sideBarContent.vue +268 -0
- package/edge/components/sidebarProvider.vue +33 -0
- package/edge/components/tooltip.vue +16 -0
- package/edge/components/userMenu.vue +148 -0
- package/edge/components/v/alert.vue +59 -0
- package/edge/components/v/alertTitle.vue +18 -0
- package/edge/components/v/card.vue +53 -0
- package/edge/components/v/cardActions.vue +18 -0
- package/edge/components/v/cardText.vue +18 -0
- package/edge/components/v/cardTitle.vue +20 -0
- package/edge/components/v/col.vue +56 -0
- package/edge/components/v/list.vue +46 -0
- package/edge/components/v/listItem.vue +26 -0
- package/edge/components/v/listItemTitle.vue +18 -0
- package/edge/components/v/row.vue +42 -0
- package/edge/components/v/toolbar.vue +24 -0
- package/edge/composables/global.ts +519 -0
- package/edge-pull.sh +2 -0
- package/edge-push.sh +1 -0
- package/edge-status.sh +14 -0
- package/package.json +1 -1
- package/edge-components-install.sh +0 -1
|
@@ -0,0 +1,1103 @@
|
|
|
1
|
+
<script setup lang="js">
|
|
2
|
+
import { useVModel } from '@vueuse/core'
|
|
3
|
+
import { File, FileCheck, FileCog, FileDown, FileMinus2, FilePen, FilePlus2, FileUp, FileWarning, FileX, Folder, FolderMinus, FolderOpen, FolderPen, FolderPlus } from 'lucide-vue-next'
|
|
4
|
+
import { toTypedSchema } from '@vee-validate/zod'
|
|
5
|
+
import * as z from 'zod'
|
|
6
|
+
|
|
7
|
+
const props = defineProps({
|
|
8
|
+
prevModelValue: {
|
|
9
|
+
type: Object,
|
|
10
|
+
required: false,
|
|
11
|
+
default: () => ({}),
|
|
12
|
+
},
|
|
13
|
+
modelValue: {
|
|
14
|
+
type: Object,
|
|
15
|
+
required: true,
|
|
16
|
+
},
|
|
17
|
+
prevMenu: {
|
|
18
|
+
type: String,
|
|
19
|
+
default: '',
|
|
20
|
+
},
|
|
21
|
+
dataDraggable: {
|
|
22
|
+
type: Boolean,
|
|
23
|
+
default: true,
|
|
24
|
+
},
|
|
25
|
+
prevIndex: {
|
|
26
|
+
type: Number,
|
|
27
|
+
default: -1,
|
|
28
|
+
},
|
|
29
|
+
site: {
|
|
30
|
+
type: String,
|
|
31
|
+
required: true,
|
|
32
|
+
},
|
|
33
|
+
page: {
|
|
34
|
+
type: String,
|
|
35
|
+
required: false,
|
|
36
|
+
default: '',
|
|
37
|
+
},
|
|
38
|
+
isTemplateSite: {
|
|
39
|
+
type: Boolean,
|
|
40
|
+
default: false,
|
|
41
|
+
},
|
|
42
|
+
themeOptions: {
|
|
43
|
+
type: Array,
|
|
44
|
+
default: () => [],
|
|
45
|
+
},
|
|
46
|
+
})
|
|
47
|
+
const emit = defineEmits(['update:modelValue', 'pageSettingsUpdate'])
|
|
48
|
+
const ROOT_MENUS = ['Site Root', 'Not In Menu']
|
|
49
|
+
const router = useRouter()
|
|
50
|
+
const modelValue = useVModel(props, 'modelValue', emit)
|
|
51
|
+
const route = useRoute()
|
|
52
|
+
const edgeFirebase = inject('edgeFirebase')
|
|
53
|
+
|
|
54
|
+
const normalizeForCompare = (value) => {
|
|
55
|
+
if (Array.isArray(value))
|
|
56
|
+
return value.map(normalizeForCompare)
|
|
57
|
+
if (value && typeof value === 'object') {
|
|
58
|
+
return Object.keys(value).sort().reduce((acc, key) => {
|
|
59
|
+
acc[key] = normalizeForCompare(value[key])
|
|
60
|
+
return acc
|
|
61
|
+
}, {})
|
|
62
|
+
}
|
|
63
|
+
return value
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const stableSerialize = value => JSON.stringify(normalizeForCompare(value))
|
|
67
|
+
const areEqualNormalized = (a, b) => stableSerialize(a) === stableSerialize(b)
|
|
68
|
+
|
|
69
|
+
const orderedMenus = computed(() => {
|
|
70
|
+
const menuEntries = Object.entries(modelValue.value || {}).map(([name, menu], originalIndex) => ({
|
|
71
|
+
name,
|
|
72
|
+
menu,
|
|
73
|
+
originalIndex,
|
|
74
|
+
}))
|
|
75
|
+
const priority = (name) => {
|
|
76
|
+
if (name === 'Site Root')
|
|
77
|
+
return 0
|
|
78
|
+
if (name === 'Not In Menu')
|
|
79
|
+
return 2
|
|
80
|
+
return 1
|
|
81
|
+
}
|
|
82
|
+
return menuEntries.sort((a, b) => priority(a.name) - priority(b.name) || a.originalIndex - b.originalIndex)
|
|
83
|
+
})
|
|
84
|
+
|
|
85
|
+
const pageRouteBase = computed(() => {
|
|
86
|
+
return props.site === 'templates'
|
|
87
|
+
? '/app/dashboard/templates'
|
|
88
|
+
: `/app/dashboard/sites/${props.site}`
|
|
89
|
+
})
|
|
90
|
+
|
|
91
|
+
const isPublishedPageDiff = (pageId) => {
|
|
92
|
+
const publishedPage = edgeFirebase.data?.[`${edgeGlobal.edgeState.organizationDocPath}/sites/${props.site}/published`]?.[pageId]
|
|
93
|
+
const draftPage = edgeFirebase.data?.[`${edgeGlobal.edgeState.organizationDocPath}/sites/${props.site}/pages`]?.[pageId]
|
|
94
|
+
if (!publishedPage && draftPage) {
|
|
95
|
+
return true
|
|
96
|
+
}
|
|
97
|
+
if (publishedPage && !draftPage) {
|
|
98
|
+
return true
|
|
99
|
+
}
|
|
100
|
+
if (publishedPage && draftPage) {
|
|
101
|
+
return !areEqualNormalized(
|
|
102
|
+
{
|
|
103
|
+
content: publishedPage.content,
|
|
104
|
+
postContent: publishedPage.postContent,
|
|
105
|
+
structure: publishedPage.structure,
|
|
106
|
+
postStructure: publishedPage.postStructure,
|
|
107
|
+
metaTitle: publishedPage.metaTitle,
|
|
108
|
+
metaDescription: publishedPage.metaDescription,
|
|
109
|
+
structuredData: publishedPage.structuredData,
|
|
110
|
+
},
|
|
111
|
+
{
|
|
112
|
+
content: draftPage.content,
|
|
113
|
+
postContent: draftPage.postContent,
|
|
114
|
+
structure: draftPage.structure,
|
|
115
|
+
postStructure: draftPage.postStructure,
|
|
116
|
+
metaTitle: draftPage.metaTitle,
|
|
117
|
+
metaDescription: draftPage.metaDescription,
|
|
118
|
+
structuredData: draftPage.structuredData,
|
|
119
|
+
},
|
|
120
|
+
)
|
|
121
|
+
}
|
|
122
|
+
return false
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const isPublished = (pageId) => {
|
|
126
|
+
const publishedPage = edgeFirebase.data?.[`${edgeGlobal.edgeState.organizationDocPath}/sites/${props.site}/published`]?.[pageId]
|
|
127
|
+
return !!publishedPage
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const schemas = {
|
|
131
|
+
pages: toTypedSchema(z.object({
|
|
132
|
+
name: z.string({
|
|
133
|
+
required_error: 'Name is required',
|
|
134
|
+
}).min(1, { message: 'Name is required' }),
|
|
135
|
+
tags: z.array(z.string()).optional(),
|
|
136
|
+
allowedThemes: z.array(z.string()).optional(),
|
|
137
|
+
})),
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
const state = reactive({
|
|
141
|
+
addPageDialog: false,
|
|
142
|
+
newPageName: '',
|
|
143
|
+
indexPath: '',
|
|
144
|
+
addMenu: false,
|
|
145
|
+
deletePage: {},
|
|
146
|
+
renameItem: {},
|
|
147
|
+
renameFolderOrPageDialog: false,
|
|
148
|
+
deletePageDialog: false,
|
|
149
|
+
pageSettings: false,
|
|
150
|
+
pageData: {},
|
|
151
|
+
newDocs: {
|
|
152
|
+
pages: {
|
|
153
|
+
name: { value: '' },
|
|
154
|
+
content: { value: [] },
|
|
155
|
+
blockIds: { value: [] },
|
|
156
|
+
tags: { value: [] },
|
|
157
|
+
allowedThemes: { value: [] },
|
|
158
|
+
},
|
|
159
|
+
},
|
|
160
|
+
hasErrors: false,
|
|
161
|
+
templateFilter: 'quick-picks',
|
|
162
|
+
selectedTemplateId: 'blank',
|
|
163
|
+
showTemplatePicker: false,
|
|
164
|
+
})
|
|
165
|
+
|
|
166
|
+
const templateTagItems = computed(() => {
|
|
167
|
+
if (!props.isTemplateSite)
|
|
168
|
+
return []
|
|
169
|
+
const pages
|
|
170
|
+
= edgeFirebase.data?.[`${edgeGlobal.edgeState.organizationDocPath}/sites/${props.site}/pages`] || {}
|
|
171
|
+
const tags = new Set()
|
|
172
|
+
for (const doc of Object.values(pages)) {
|
|
173
|
+
if (Array.isArray(doc?.tags)) {
|
|
174
|
+
for (const tag of doc.tags) {
|
|
175
|
+
const normalized = typeof tag === 'string' ? tag.trim() : ''
|
|
176
|
+
if (normalized && normalized.toLowerCase() !== 'quick picks')
|
|
177
|
+
tags.add(normalized)
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
const tagList = Array.from(tags).sort((a, b) => a.localeCompare(b)).map(tag => ({ name: tag, title: tag }))
|
|
182
|
+
return [{ name: 'Quick Picks', title: 'Quick Picks' }, ...tagList]
|
|
183
|
+
})
|
|
184
|
+
|
|
185
|
+
const resetAddPageDialogState = () => {
|
|
186
|
+
state.newPageName = ''
|
|
187
|
+
state.templateFilter = 'quick-picks'
|
|
188
|
+
state.selectedTemplateId = BLANK_TEMPLATE_ID
|
|
189
|
+
state.showTemplatePicker = false
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
watch(() => state.addPageDialog, (open) => {
|
|
193
|
+
if (!open)
|
|
194
|
+
resetAddPageDialogState()
|
|
195
|
+
})
|
|
196
|
+
|
|
197
|
+
onMounted(async () => {
|
|
198
|
+
if (!edgeGlobal.edgeState.organizationDocPath)
|
|
199
|
+
return
|
|
200
|
+
const path = TEMPLATE_COLLECTION_PATH.value
|
|
201
|
+
if (!edgeFirebase.data?.[path])
|
|
202
|
+
await edgeFirebase.startSnapshot(path)
|
|
203
|
+
})
|
|
204
|
+
|
|
205
|
+
const TEMPLATE_COLLECTION_PATH = computed(() => `${edgeGlobal.edgeState.organizationDocPath}/sites/templates/pages`)
|
|
206
|
+
|
|
207
|
+
const templatePagesCollection = computed(() => {
|
|
208
|
+
return edgeFirebase.data?.[TEMPLATE_COLLECTION_PATH.value] || {}
|
|
209
|
+
})
|
|
210
|
+
|
|
211
|
+
const templatePagesList = computed(() => {
|
|
212
|
+
return Object.entries(templatePagesCollection.value).map(([docId, doc]) => ({
|
|
213
|
+
docId,
|
|
214
|
+
...(doc || {}),
|
|
215
|
+
name: doc?.name || 'Untitled Template',
|
|
216
|
+
tags: Array.isArray(doc?.tags) ? doc.tags : [],
|
|
217
|
+
description: doc?.metaDescription || doc?.description || '',
|
|
218
|
+
content: Array.isArray(doc?.content) ? doc.content : [],
|
|
219
|
+
}))
|
|
220
|
+
})
|
|
221
|
+
|
|
222
|
+
const templateFilterOptions = computed(() => {
|
|
223
|
+
const tagSet = new Set()
|
|
224
|
+
for (const template of templatePagesList.value) {
|
|
225
|
+
for (const tag of template.tags || []) {
|
|
226
|
+
if (!tag)
|
|
227
|
+
continue
|
|
228
|
+
if (tag.toLowerCase() === 'quick picks')
|
|
229
|
+
continue
|
|
230
|
+
tagSet.add(tag)
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
const tagOptions = Array.from(tagSet)
|
|
234
|
+
.sort((a, b) => a.localeCompare(b))
|
|
235
|
+
.map(tag => ({ label: tag, value: tag }))
|
|
236
|
+
return [
|
|
237
|
+
{ label: 'Quick Picks', value: 'quick-picks' },
|
|
238
|
+
...tagOptions,
|
|
239
|
+
]
|
|
240
|
+
})
|
|
241
|
+
|
|
242
|
+
const filterMatchesTemplate = (template, filterValue) => {
|
|
243
|
+
if (filterValue === 'all')
|
|
244
|
+
return true
|
|
245
|
+
if (filterValue === 'quick-picks')
|
|
246
|
+
return template.tags?.some(tag => tag?.toLowerCase() === 'quick picks'.toLowerCase())
|
|
247
|
+
return template.tags?.some(tag => tag === filterValue)
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
const filteredTemplates = computed(() => {
|
|
251
|
+
const templates = templatePagesList.value
|
|
252
|
+
const filterValue = state.templateFilter
|
|
253
|
+
const filtered = templates.filter(template => filterMatchesTemplate(template, filterValue))
|
|
254
|
+
if (filtered.length === 0 && filterValue === 'quick-picks')
|
|
255
|
+
return templates
|
|
256
|
+
if (filtered.length === 0 && filterValue !== 'all')
|
|
257
|
+
return templates
|
|
258
|
+
return filtered
|
|
259
|
+
})
|
|
260
|
+
|
|
261
|
+
watch(filteredTemplates, (templates) => {
|
|
262
|
+
if (state.selectedTemplateId === BLANK_TEMPLATE_ID)
|
|
263
|
+
return
|
|
264
|
+
if (!templates.some(template => template.docId === state.selectedTemplateId))
|
|
265
|
+
state.selectedTemplateId = BLANK_TEMPLATE_ID
|
|
266
|
+
})
|
|
267
|
+
|
|
268
|
+
const BLANK_TEMPLATE_ID = 'blank'
|
|
269
|
+
|
|
270
|
+
const blankTemplateTile = {
|
|
271
|
+
docId: BLANK_TEMPLATE_ID,
|
|
272
|
+
name: 'Blank Page',
|
|
273
|
+
tags: ['Start from scratch'],
|
|
274
|
+
description: 'Create a new page without any blocks.',
|
|
275
|
+
content: [],
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
const templateGridItems = computed(() => {
|
|
279
|
+
return [blankTemplateTile, ...filteredTemplates.value]
|
|
280
|
+
})
|
|
281
|
+
|
|
282
|
+
const hasValidNewPageName = computed(() => !!(state.newPageName && state.newPageName.trim().length))
|
|
283
|
+
|
|
284
|
+
const blocksCollection = computed(() => {
|
|
285
|
+
return edgeFirebase.data?.[`${edgeGlobal.edgeState.organizationDocPath}/blocks`] || {}
|
|
286
|
+
})
|
|
287
|
+
|
|
288
|
+
const resolveBlockForPreview = (block) => {
|
|
289
|
+
if (!block)
|
|
290
|
+
return null
|
|
291
|
+
if (block.content)
|
|
292
|
+
return {
|
|
293
|
+
content: block.content,
|
|
294
|
+
values: block.values || {},
|
|
295
|
+
meta: block.meta || {},
|
|
296
|
+
}
|
|
297
|
+
if (block.blockId && blocksCollection.value?.[block.blockId]) {
|
|
298
|
+
const libraryBlock = blocksCollection.value[block.blockId]
|
|
299
|
+
return {
|
|
300
|
+
content: libraryBlock.content,
|
|
301
|
+
values: block.values || libraryBlock.values || {},
|
|
302
|
+
meta: block.meta || libraryBlock.meta || {},
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
return null
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
const templateHasBlocks = template => Array.isArray(template?.content) && template.content.length > 0
|
|
309
|
+
|
|
310
|
+
const templatePreviewBlocks = (template) => {
|
|
311
|
+
if (!templateHasBlocks(template))
|
|
312
|
+
return []
|
|
313
|
+
return template.content
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
const renameFolderOrPageShow = (item) => {
|
|
317
|
+
// Work on a copy so edits in the dialog do not mutate the live menu entry.
|
|
318
|
+
state.renameItem = edgeGlobal.dupObject(item || {})
|
|
319
|
+
state.renameItem.previousName = item?.name
|
|
320
|
+
state.renameFolderOrPageDialog = true
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
const addPageShow = (menuName, isMenu = false) => {
|
|
324
|
+
state.addMenu = isMenu
|
|
325
|
+
state.menuName = menuName
|
|
326
|
+
resetAddPageDialogState()
|
|
327
|
+
state.addPageDialog = true
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
const deletePageShow = (page) => {
|
|
331
|
+
state.deletePage = page
|
|
332
|
+
state.deletePageDialog = true
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
const collectRootLevelSlugs = (excludeName = '') => {
|
|
336
|
+
const slugs = new Set()
|
|
337
|
+
if (!props.prevMenu) {
|
|
338
|
+
for (const root of ROOT_MENUS) {
|
|
339
|
+
const arr = modelValue.value?.[root] || []
|
|
340
|
+
for (const entry of arr) {
|
|
341
|
+
// Top-level page at "/<slug>"
|
|
342
|
+
if (typeof entry.item === 'string') {
|
|
343
|
+
if (entry.name && entry.name !== excludeName)
|
|
344
|
+
slugs.add(entry.name)
|
|
345
|
+
}
|
|
346
|
+
// Top-level folder at "/<folder>/*"
|
|
347
|
+
else if (entry && typeof entry.item === 'object') {
|
|
348
|
+
const key = Object.keys(entry.item)[0]
|
|
349
|
+
if (key && key !== excludeName)
|
|
350
|
+
slugs.add(key)
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
else {
|
|
356
|
+
if (state.renameItem.item === '') {
|
|
357
|
+
for (const root of ROOT_MENUS) {
|
|
358
|
+
const arr = props.prevModelValue?.[root] || []
|
|
359
|
+
for (const entry of arr) {
|
|
360
|
+
// Top-level page at "/<slug>"
|
|
361
|
+
if (typeof entry.item === 'string') {
|
|
362
|
+
if (entry.name && entry.name !== excludeName)
|
|
363
|
+
slugs.add(entry.name)
|
|
364
|
+
}
|
|
365
|
+
// Top-level folder at "/<folder>/*"
|
|
366
|
+
else if (entry && typeof entry.item === 'object') {
|
|
367
|
+
const key = Object.keys(entry.item)[0]
|
|
368
|
+
if (key && key !== excludeName)
|
|
369
|
+
slugs.add(key)
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
else {
|
|
375
|
+
const key = Object.keys(modelValue.value)[0]
|
|
376
|
+
const arr = modelValue.value?.[key] || []
|
|
377
|
+
for (const entry of arr) {
|
|
378
|
+
// Top-level page at "/<slug>"
|
|
379
|
+
if (typeof entry.item === 'string') {
|
|
380
|
+
if (entry.name && entry.name !== excludeName)
|
|
381
|
+
slugs.add(entry.name)
|
|
382
|
+
}
|
|
383
|
+
// Top-level folder at "/<folder>/*"
|
|
384
|
+
else if (entry && typeof entry.item === 'object') {
|
|
385
|
+
const key = Object.keys(entry.item)[0]
|
|
386
|
+
if (key && key !== excludeName)
|
|
387
|
+
slugs.add(key)
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
return slugs
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
const slugGenerator = (name, excludeName = '') => {
|
|
396
|
+
// Build a set of existing slugs that map to URLs off of "/" from *both* root menus.
|
|
397
|
+
const existing = collectRootLevelSlugs(excludeName)
|
|
398
|
+
console.log('Existing slugs:', existing)
|
|
399
|
+
|
|
400
|
+
const base = name ? name.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/(^-|-$)+/g, '') : ''
|
|
401
|
+
let unique = base
|
|
402
|
+
let suffix = 1
|
|
403
|
+
while (existing.has(unique)) {
|
|
404
|
+
unique = `${base}-${suffix}`
|
|
405
|
+
suffix += 1
|
|
406
|
+
}
|
|
407
|
+
return unique
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
const selectTemplate = (templateId) => {
|
|
411
|
+
state.selectedTemplateId = templateId
|
|
412
|
+
state.showTemplatePicker = false
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
const isTemplateSelected = templateId => state.selectedTemplateId === templateId
|
|
416
|
+
|
|
417
|
+
const getTemplateDoc = (templateId) => {
|
|
418
|
+
if (templateId === BLANK_TEMPLATE_ID)
|
|
419
|
+
return null
|
|
420
|
+
return templatePagesCollection.value?.[templateId] || null
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
const extractBlockIds = (blocks = []) => {
|
|
424
|
+
if (!Array.isArray(blocks))
|
|
425
|
+
return []
|
|
426
|
+
return blocks
|
|
427
|
+
.map(block => block?.blockId || block?.id)
|
|
428
|
+
.filter(Boolean)
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
const deriveBlockIds = (pageDoc = {}) => {
|
|
432
|
+
const ids = [
|
|
433
|
+
...extractBlockIds(pageDoc.content),
|
|
434
|
+
...extractBlockIds(pageDoc.postContent),
|
|
435
|
+
]
|
|
436
|
+
return Array.from(new Set(ids))
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
const getSyncedBlockFromSite = (blockId) => {
|
|
440
|
+
if (!blockId)
|
|
441
|
+
return null
|
|
442
|
+
const pages = edgeFirebase.data?.[`${edgeGlobal.edgeState.organizationDocPath}/sites/${props.site}/pages`] || {}
|
|
443
|
+
for (const page of Object.values(pages)) {
|
|
444
|
+
const contentBlocks = Array.isArray(page?.content) ? page.content : []
|
|
445
|
+
const postBlocks = Array.isArray(page?.postContent) ? page.postContent : []
|
|
446
|
+
for (const candidate of [...contentBlocks, ...postBlocks]) {
|
|
447
|
+
if (candidate?.blockId === blockId && candidate?.synced)
|
|
448
|
+
return edgeGlobal.dupObject(candidate)
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
return null
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
const hydrateSyncedBlocksFromSite = (blocks = []) => {
|
|
455
|
+
if (!Array.isArray(blocks) || !blocks.length)
|
|
456
|
+
return blocks
|
|
457
|
+
|
|
458
|
+
return blocks.map((block) => {
|
|
459
|
+
if (!block?.synced || !block.blockId)
|
|
460
|
+
return block
|
|
461
|
+
const existing = getSyncedBlockFromSite(block.blockId)
|
|
462
|
+
if (!existing)
|
|
463
|
+
return block
|
|
464
|
+
const hydrated = edgeGlobal.dupObject(existing)
|
|
465
|
+
hydrated.id = block.id || hydrated.id || edgeGlobal.generateShortId()
|
|
466
|
+
hydrated.blockId = block.blockId
|
|
467
|
+
hydrated.name = block.name || hydrated.name
|
|
468
|
+
return hydrated
|
|
469
|
+
})
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
const buildPagePayloadFromTemplate = (templateDoc, slug) => {
|
|
473
|
+
const timestamp = Date.now()
|
|
474
|
+
const basePayload = {
|
|
475
|
+
name: slug,
|
|
476
|
+
content: [],
|
|
477
|
+
postContent: [],
|
|
478
|
+
blockIds: [],
|
|
479
|
+
metaTitle: '',
|
|
480
|
+
metaDescription: '',
|
|
481
|
+
structuredData: '',
|
|
482
|
+
doc_created_at: timestamp,
|
|
483
|
+
last_updated: timestamp,
|
|
484
|
+
}
|
|
485
|
+
if (!templateDoc)
|
|
486
|
+
return basePayload
|
|
487
|
+
const copy = JSON.parse(JSON.stringify(templateDoc || {}))
|
|
488
|
+
delete copy.docId
|
|
489
|
+
copy.name = slug
|
|
490
|
+
copy.doc_created_at = timestamp
|
|
491
|
+
copy.last_updated = timestamp
|
|
492
|
+
copy.content = Array.isArray(copy.content) ? hydrateSyncedBlocksFromSite(copy.content) : []
|
|
493
|
+
copy.postContent = Array.isArray(copy.postContent) ? hydrateSyncedBlocksFromSite(copy.postContent) : []
|
|
494
|
+
copy.blockIds = deriveBlockIds(copy)
|
|
495
|
+
return { ...basePayload, ...copy }
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
const renameFolderOrPageAction = async () => {
|
|
499
|
+
const newSlug = slugGenerator(state.renameItem.name, state.renameItem.previousName || '')
|
|
500
|
+
|
|
501
|
+
if (state.renameItem.name === state.renameItem.previousName) {
|
|
502
|
+
state.renameFolderOrPageDialog = false
|
|
503
|
+
state.renameItem = {}
|
|
504
|
+
return
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
// If the item is an empty string, we are renaming a top-level folder (handled here)
|
|
508
|
+
if (state.renameItem.item === '') {
|
|
509
|
+
const original = edgeGlobal.dupObject(modelValue.value)
|
|
510
|
+
const originalItem = edgeGlobal.dupObject(modelValue.value[state.renameItem.previousName])
|
|
511
|
+
// Renaming a folder: if the new name is empty, abort and reset dialog state
|
|
512
|
+
if (!state.renameItem.name) {
|
|
513
|
+
state.renameFolderOrPageDialog = false
|
|
514
|
+
state.renameItem = {}
|
|
515
|
+
return
|
|
516
|
+
}
|
|
517
|
+
// Move the array from the old key to the new key, then delete the old key
|
|
518
|
+
modelValue.value[newSlug] = originalItem
|
|
519
|
+
console.log('updated modelValue:', modelValue.value)
|
|
520
|
+
delete modelValue.value[state.renameItem.previousName]
|
|
521
|
+
state.renameFolderOrPageDialog = false
|
|
522
|
+
state.renameItem = {}
|
|
523
|
+
return
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
// Renaming a page: the page is uniquely identified by its docId in `state.renameItem.item`.
|
|
527
|
+
// Traverse all menus and submenus; update the `name` where the `item` matches that docId (strings only).
|
|
528
|
+
const targetDocId = state.renameItem.item
|
|
529
|
+
// const newName = state.renameItem.name || ''
|
|
530
|
+
|
|
531
|
+
let renamed = false
|
|
532
|
+
for (const [menuName, items] of Object.entries(modelValue.value)) {
|
|
533
|
+
for (const item of items) {
|
|
534
|
+
if (typeof item.item === 'string' && item.item === targetDocId) {
|
|
535
|
+
const results = await edgeFirebase.changeDoc(`${edgeGlobal.edgeState.organizationDocPath}/sites/${props.site}/pages`, targetDocId, { name: newSlug })
|
|
536
|
+
if (results.success) {
|
|
537
|
+
item.name = newSlug
|
|
538
|
+
renamed = true
|
|
539
|
+
}
|
|
540
|
+
break
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
if (renamed)
|
|
544
|
+
break
|
|
545
|
+
}
|
|
546
|
+
console.log(modelValue.value)
|
|
547
|
+
// Close dialog and reset state regardless
|
|
548
|
+
state.renameFolderOrPageDialog = false
|
|
549
|
+
state.renameItem = {}
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
const addPageAction = async () => {
|
|
553
|
+
state.newPageName = state.newPageName?.trim() || ''
|
|
554
|
+
if (!state.newPageName)
|
|
555
|
+
return
|
|
556
|
+
const slug = slugGenerator(state.newPageName)
|
|
557
|
+
if (!state.menuName) {
|
|
558
|
+
modelValue.value[state.newPageName] = []
|
|
559
|
+
state.newPageName = ''
|
|
560
|
+
state.addPageDialog = false
|
|
561
|
+
return
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
if (!Array.isArray(modelValue.value[state.menuName]))
|
|
565
|
+
modelValue.value[state.menuName] = []
|
|
566
|
+
|
|
567
|
+
if (state.addMenu) {
|
|
568
|
+
modelValue.value[state.menuName].push({ item: { [slug]: [] } })
|
|
569
|
+
}
|
|
570
|
+
else {
|
|
571
|
+
const templateDoc = getTemplateDoc(state.selectedTemplateId)
|
|
572
|
+
const payload = buildPagePayloadFromTemplate(templateDoc, slug)
|
|
573
|
+
const result = await edgeFirebase.storeDoc(`${edgeGlobal.edgeState.organizationDocPath}/sites/${props.site}/pages`, payload)
|
|
574
|
+
const docId = result?.meta?.docId
|
|
575
|
+
if (docId) {
|
|
576
|
+
const targetMenu = modelValue.value[state.menuName]
|
|
577
|
+
const alreadyExists = Array.isArray(targetMenu) && targetMenu.some(entry => entry?.item === docId)
|
|
578
|
+
if (!alreadyExists)
|
|
579
|
+
targetMenu.push({ name: slug, item: docId })
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
state.addPageDialog = false
|
|
584
|
+
}
|
|
585
|
+
const deletePageAction = async () => {
|
|
586
|
+
if (state.deletePage.item === '') {
|
|
587
|
+
// deleting a folder
|
|
588
|
+
delete modelValue.value[state.deletePage.name]
|
|
589
|
+
state.deletePageDialog = false
|
|
590
|
+
state.deletePage = {}
|
|
591
|
+
return
|
|
592
|
+
}
|
|
593
|
+
if (props.page === state.deletePage.item) {
|
|
594
|
+
router.replace(pageRouteBase.value)
|
|
595
|
+
}
|
|
596
|
+
for (const [menuName, items] of Object.entries(modelValue.value)) {
|
|
597
|
+
for (const item of items) {
|
|
598
|
+
if (typeof item.item === 'string' && item.item === state.deletePage.item) {
|
|
599
|
+
item.name = 'Deleting...'
|
|
600
|
+
}
|
|
601
|
+
if (typeof item.item === 'object') {
|
|
602
|
+
for (const [subMenuName, subItems] of Object.entries(item.item)) {
|
|
603
|
+
for (const subItem of subItems) {
|
|
604
|
+
if (typeof subItem.item === 'string' && subItem.item === state.deletePage.item) {
|
|
605
|
+
subItem.name = 'Deleting...'
|
|
606
|
+
}
|
|
607
|
+
}
|
|
608
|
+
}
|
|
609
|
+
}
|
|
610
|
+
}
|
|
611
|
+
}
|
|
612
|
+
state.deletePageDialog = false
|
|
613
|
+
state.deletePage = {}
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
const pages = toTypedSchema(z.object({
|
|
617
|
+
name: z.string({
|
|
618
|
+
required_error: 'Name is required',
|
|
619
|
+
}).min(1, { message: 'Name is required' }),
|
|
620
|
+
}))
|
|
621
|
+
|
|
622
|
+
const disabledFolderDelete = (menuName, menu) => {
|
|
623
|
+
if (menuName === 'Site Root') {
|
|
624
|
+
return true
|
|
625
|
+
}
|
|
626
|
+
if (menu.length > 0) {
|
|
627
|
+
return true
|
|
628
|
+
}
|
|
629
|
+
return false
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
const canRename = (menuName) => {
|
|
633
|
+
if (props.prevMenu) {
|
|
634
|
+
return true
|
|
635
|
+
}
|
|
636
|
+
if (menuName === 'Site Root') {
|
|
637
|
+
return false
|
|
638
|
+
}
|
|
639
|
+
return true
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
const publishPage = async (pageId) => {
|
|
643
|
+
const pageData = edgeFirebase.data?.[`${edgeGlobal.edgeState.organizationDocPath}/sites/${props.site}/pages`] || {}
|
|
644
|
+
if (pageData[pageId]) {
|
|
645
|
+
await edgeFirebase.storeDoc(`${edgeGlobal.edgeState.organizationDocPath}/sites/${props.site}/published`, pageData[pageId])
|
|
646
|
+
}
|
|
647
|
+
}
|
|
648
|
+
const unPublishPage = async (pageId) => {
|
|
649
|
+
await edgeFirebase.removeDoc(`${edgeGlobal.edgeState.organizationDocPath}/sites/${props.site}/published`, pageId)
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
const discardPageChanges = async (pageId) => {
|
|
653
|
+
const publishedPage = edgeFirebase.data?.[`${edgeGlobal.edgeState.organizationDocPath}/sites/${props.site}/published`]?.[pageId]
|
|
654
|
+
if (publishedPage) {
|
|
655
|
+
await edgeFirebase.storeDoc(`${edgeGlobal.edgeState.organizationDocPath}/sites/${props.site}/pages`, publishedPage, pageId)
|
|
656
|
+
}
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
const showPageSettings = (page) => {
|
|
660
|
+
console.log('showPageSettings', page)
|
|
661
|
+
state.pageData = page
|
|
662
|
+
state.pageSettings = true
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
const formErrors = (error) => {
|
|
666
|
+
console.log('Form errors:', error)
|
|
667
|
+
console.log(Object.values(error))
|
|
668
|
+
if (Object.values(error).length > 0) {
|
|
669
|
+
console.log('Form errors found')
|
|
670
|
+
state.hasError = true
|
|
671
|
+
console.log(state.hasError)
|
|
672
|
+
}
|
|
673
|
+
state.hasError = false
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
const onSubmit = () => {
|
|
677
|
+
if (!state.hasError) {
|
|
678
|
+
emit('pageSettingsUpdate', state.pageData)
|
|
679
|
+
state.pageSettings = false
|
|
680
|
+
}
|
|
681
|
+
}
|
|
682
|
+
const titleFromSlug = (slug) => {
|
|
683
|
+
if (!slug)
|
|
684
|
+
return ''
|
|
685
|
+
return slug.replace(/-/g, ' ').replace(/\b\w/g, l => l.toUpperCase())
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
const theme = computed(() => {
|
|
689
|
+
const theme = edgeFirebase.data?.[`${edgeGlobal.edgeState.organizationDocPath}/sites`]?.[props.site]?.theme || ''
|
|
690
|
+
console.log(`${edgeGlobal.edgeState.organizationDocPath}/sites/${props.site}`)
|
|
691
|
+
let themeContents = null
|
|
692
|
+
if (theme) {
|
|
693
|
+
themeContents = edgeFirebase.data?.[`${edgeGlobal.edgeState.organizationDocPath}/themes`]?.[theme]?.theme || null
|
|
694
|
+
}
|
|
695
|
+
if (themeContents) {
|
|
696
|
+
return JSON.parse(themeContents)
|
|
697
|
+
}
|
|
698
|
+
return null
|
|
699
|
+
})
|
|
700
|
+
</script>
|
|
701
|
+
|
|
702
|
+
<template>
|
|
703
|
+
<SidebarMenuItem v-for="({ menu, name: menuName }) in orderedMenus" :key="menuName">
|
|
704
|
+
<SidebarMenuButton class="!px-0 hover:!bg-transparent">
|
|
705
|
+
<FolderOpen
|
|
706
|
+
class="mr-2"
|
|
707
|
+
/>
|
|
708
|
+
<span v-if="!props.isTemplateSite">{{ menuName === 'Site Root' ? 'Site Menu' : menuName }}</span>
|
|
709
|
+
<SidebarGroupAction class="absolute right-2 top-0 hover:!bg-transparent">
|
|
710
|
+
<DropdownMenu>
|
|
711
|
+
<DropdownMenuTrigger as-child>
|
|
712
|
+
<SidebarMenuAction>
|
|
713
|
+
<PlusIcon />
|
|
714
|
+
</SidebarMenuAction>
|
|
715
|
+
</DropdownMenuTrigger>
|
|
716
|
+
<DropdownMenuContent side="right" align="start">
|
|
717
|
+
<DropdownMenuLabel v-if="props.prevMenu" class="flex items-center gap-2">
|
|
718
|
+
<Folder class="w-5 h-5" /> {{ ROOT_MENUS.includes(props.prevMenu) ? '' : props.prevMenu }}/{{ menuName }}/
|
|
719
|
+
</DropdownMenuLabel>
|
|
720
|
+
<DropdownMenuLabel v-else class="flex items-center gap-2">
|
|
721
|
+
<Folder class="w-5 h-5" /> {{ ROOT_MENUS.includes(menuName) ? '' : menuName }}/
|
|
722
|
+
</DropdownMenuLabel>
|
|
723
|
+
<DropdownMenuSeparator />
|
|
724
|
+
<DropdownMenuItem @click="addPageShow(menuName, false)">
|
|
725
|
+
<FilePlus2 />
|
|
726
|
+
<span>New Page</span>
|
|
727
|
+
</DropdownMenuItem>
|
|
728
|
+
<DropdownMenuItem v-if="!props.prevMenu && !props.isTemplateSite" @click="addPageShow(menuName, true)">
|
|
729
|
+
<FolderPlus />
|
|
730
|
+
<span>New Folder</span>
|
|
731
|
+
</DropdownMenuItem>
|
|
732
|
+
<DropdownMenuItem v-if="canRename(menuName)" @click="renameFolderOrPageShow({ name: menuName, item: '' })">
|
|
733
|
+
<FolderPen />
|
|
734
|
+
<span>Rename Folder</span>
|
|
735
|
+
</DropdownMenuItem>
|
|
736
|
+
<DropdownMenuItem class="flex-col gap-0 items-start text-destructive" :disabled="disabledFolderDelete(menuName, menu) || ROOT_MENUS.includes(menuName)" @click="deletePageShow({ name: menuName, item: '' })">
|
|
737
|
+
<span class="my-0 py-0 flex"> <FolderMinus class="mr-2 h-4 w-4" />Delete Folder</span>
|
|
738
|
+
<span v-if="ROOT_MENUS.includes(menuName)" class="my-0 text-gray-400 py-0 text-xs">(Cannot delete {{ menuName }})</span>
|
|
739
|
+
<span v-else-if="disabledFolderDelete(menuName, menu)" class="my-0 text-gray-400 py-0 text-xs">(Folder must be empty to delete)</span>
|
|
740
|
+
</DropdownMenuItem>
|
|
741
|
+
</DropdownMenuContent>
|
|
742
|
+
</DropdownMenu>
|
|
743
|
+
</SidebarGroupAction>
|
|
744
|
+
</SidebarMenuButton>
|
|
745
|
+
|
|
746
|
+
<SidebarMenuSub class="mx-0 px-2">
|
|
747
|
+
<draggable
|
|
748
|
+
:list="modelValue[menuName]"
|
|
749
|
+
handle=".handle"
|
|
750
|
+
item-key="subindex"
|
|
751
|
+
class="list-group"
|
|
752
|
+
:group="{ name: 'menus', pull: true, put: true }"
|
|
753
|
+
>
|
|
754
|
+
<template #item="{ element, index }">
|
|
755
|
+
<div class="handle list-group-item">
|
|
756
|
+
<edge-cms-menu
|
|
757
|
+
v-if="typeof element.item === 'object'"
|
|
758
|
+
v-model="modelValue[menuName][index].item"
|
|
759
|
+
:prev-menu="menuName"
|
|
760
|
+
:prev-model-value="modelValue"
|
|
761
|
+
:site="props.site"
|
|
762
|
+
:page="props.page"
|
|
763
|
+
:prev-index="index"
|
|
764
|
+
:is-template-site="props.isTemplateSite"
|
|
765
|
+
/>
|
|
766
|
+
<SidebarMenuSubItem v-else class="relative">
|
|
767
|
+
<SidebarMenuSubButton :class="{ 'text-gray-400': element.item === '' }" as-child :is-active="element.item === props.page">
|
|
768
|
+
<NuxtLink :disabled="element.item === ''" :class="{ '!text-red-500': element.name === 'Deleting...' }" class="text-xs" :to="`${pageRouteBase}/${element.item}`">
|
|
769
|
+
<Loader2 v-if="element.item === '' || element.name === 'Deleting...'" :class="{ '!text-red-500': element.name === 'Deleting...' }" class="w-4 h-4 animate-spin" />
|
|
770
|
+
<FileWarning v-else-if="isPublishedPageDiff(element.item) && !props.isTemplateSite" class="!text-yellow-600" />
|
|
771
|
+
<FileCheck v-else class="text-xs !text-green-700 font-normal" />
|
|
772
|
+
<span>{{ element.name }}</span>
|
|
773
|
+
</NuxtLink>
|
|
774
|
+
</SidebarMenuSubButton>
|
|
775
|
+
<div class="absolute right-0 -top-0.5">
|
|
776
|
+
<DropdownMenu>
|
|
777
|
+
<DropdownMenuTrigger as-child>
|
|
778
|
+
<SidebarMenuAction>
|
|
779
|
+
<MoreHorizontal />
|
|
780
|
+
</SidebarMenuAction>
|
|
781
|
+
</DropdownMenuTrigger>
|
|
782
|
+
<DropdownMenuContent side="right" align="start">
|
|
783
|
+
<DropdownMenuLabel v-if="props.prevMenu" class="flex items-center gap-2">
|
|
784
|
+
<File class="w-5 h-5" /> {{ ROOT_MENUS.includes(props.prevMenu) ? '' : props.prevMenu }}/{{ menuName }}/{{ element.name }}
|
|
785
|
+
</DropdownMenuLabel>
|
|
786
|
+
<DropdownMenuLabel v-else class="flex items-center gap-2">
|
|
787
|
+
<File class="w-5 h-5" /> {{ ROOT_MENUS.includes(menuName) ? '' : menuName }}/{{ element.name }}
|
|
788
|
+
</DropdownMenuLabel>
|
|
789
|
+
<DropdownMenuSeparator />
|
|
790
|
+
<DropdownMenuItem :disabled="edgeGlobal.edgeState.cmsPageWithUnsavedChanges === element.item" @click="showPageSettings(element)">
|
|
791
|
+
<FileCog />
|
|
792
|
+
<div class="flex flex-col">
|
|
793
|
+
<span>Settings</span>
|
|
794
|
+
<span v-if="edgeGlobal.edgeState.cmsPageWithUnsavedChanges === element.item" class="text-xs text-red-500">(Unsaved Changes)</span>
|
|
795
|
+
</div>
|
|
796
|
+
</DropdownMenuItem>
|
|
797
|
+
<DropdownMenuItem v-if="!props.isTemplateSite && isPublishedPageDiff(element.item)" @click="publishPage(element.item)">
|
|
798
|
+
<FileUp />
|
|
799
|
+
Publish
|
|
800
|
+
</DropdownMenuItem>
|
|
801
|
+
<DropdownMenuItem @click="renameFolderOrPageShow(element)">
|
|
802
|
+
<FilePen />
|
|
803
|
+
<span>Rename</span>
|
|
804
|
+
</DropdownMenuItem>
|
|
805
|
+
<DropdownMenuSeparator />
|
|
806
|
+
<DropdownMenuItem v-if="!props.isTemplateSite && isPublishedPageDiff(element.item) && isPublished(element.item)" @click="discardPageChanges(element.item)">
|
|
807
|
+
<FileX />
|
|
808
|
+
Discard Changes
|
|
809
|
+
</DropdownMenuItem>
|
|
810
|
+
<DropdownMenuItem v-if="!props.isTemplateSite && isPublished(element.item)" @click="unPublishPage(element.item)">
|
|
811
|
+
<FileDown />
|
|
812
|
+
Unpublish
|
|
813
|
+
</DropdownMenuItem>
|
|
814
|
+
<DropdownMenuItem class="text-destructive" @click="deletePageShow(element)">
|
|
815
|
+
<FileMinus2 />
|
|
816
|
+
<span>Delete</span>
|
|
817
|
+
</DropdownMenuItem>
|
|
818
|
+
</DropdownMenuContent>
|
|
819
|
+
</DropdownMenu>
|
|
820
|
+
</div>
|
|
821
|
+
</SidebarMenuSubItem>
|
|
822
|
+
</div>
|
|
823
|
+
</template>
|
|
824
|
+
</draggable>
|
|
825
|
+
</SidebarMenuSub>
|
|
826
|
+
</SidebarMenuItem>
|
|
827
|
+
<edge-shad-dialog
|
|
828
|
+
v-model="state.deletePageDialog"
|
|
829
|
+
>
|
|
830
|
+
<DialogContent class="pt-10">
|
|
831
|
+
<DialogHeader>
|
|
832
|
+
<DialogTitle class="text-left">
|
|
833
|
+
<span v-if="state.deletePage.item === ''">Delete Folder "{{ state.deletePage.name }}"</span>
|
|
834
|
+
<span v-else>Delete Page "{{ state.deletePage.name }}"</span>
|
|
835
|
+
</DialogTitle>
|
|
836
|
+
<DialogDescription />
|
|
837
|
+
</DialogHeader>
|
|
838
|
+
<div class="text-left px-1">
|
|
839
|
+
Are you sure you want to delete "{{ state.deletePage.name }}"? This action cannot be undone.
|
|
840
|
+
</div>
|
|
841
|
+
<DialogFooter class="pt-2 flex justify-between">
|
|
842
|
+
<edge-shad-button
|
|
843
|
+
class="text-white bg-slate-800 hover:bg-slate-400" @click="state.deletePageDialog = false"
|
|
844
|
+
>
|
|
845
|
+
Cancel
|
|
846
|
+
</edge-shad-button>
|
|
847
|
+
<edge-shad-button
|
|
848
|
+
variant="destructive" class="text-white w-full" @click="deletePageAction()"
|
|
849
|
+
>
|
|
850
|
+
Delete Page
|
|
851
|
+
</edge-shad-button>
|
|
852
|
+
</DialogFooter>
|
|
853
|
+
</DialogContent>
|
|
854
|
+
</edge-shad-dialog>
|
|
855
|
+
<edge-shad-dialog v-model="state.addPageDialog">
|
|
856
|
+
<DialogContent v-if="state.addMenu" class="pt-10">
|
|
857
|
+
<edge-shad-form :schema="pages" @submit="addPageAction">
|
|
858
|
+
<DialogHeader>
|
|
859
|
+
<DialogTitle class="text-left">
|
|
860
|
+
<span v-if="!state.menuName">Add Menu</span>
|
|
861
|
+
<span v-else>Add folder to "{{ state.menuName }}"</span>
|
|
862
|
+
</DialogTitle>
|
|
863
|
+
<DialogDescription />
|
|
864
|
+
</DialogHeader>
|
|
865
|
+
<edge-shad-input v-model="state.newPageName" name="name" placeholder="Folder Name" />
|
|
866
|
+
<DialogFooter class="pt-2 flex justify-between">
|
|
867
|
+
<edge-shad-button type="button" variant="destructive" @click="state.addPageDialog = false">
|
|
868
|
+
Cancel
|
|
869
|
+
</edge-shad-button>
|
|
870
|
+
<edge-shad-button type="submit" class="text-white bg-slate-800 hover:bg-slate-400 w-full">
|
|
871
|
+
Add Folder
|
|
872
|
+
</edge-shad-button>
|
|
873
|
+
</DialogFooter>
|
|
874
|
+
</edge-shad-form>
|
|
875
|
+
</DialogContent>
|
|
876
|
+
<DialogContent v-else class="pt-6 w-full max-w-6xl h-[90vh] flex flex-col">
|
|
877
|
+
<edge-shad-form :schema="pages" class="flex flex-col h-full" @submit="addPageAction">
|
|
878
|
+
<DialogHeader class="pb-2">
|
|
879
|
+
<DialogTitle class="text-left">
|
|
880
|
+
Add page to "{{ state.menuName }}"
|
|
881
|
+
</DialogTitle>
|
|
882
|
+
<DialogDescription>
|
|
883
|
+
Choose a template or start with a blank page. You can always customize it later.
|
|
884
|
+
</DialogDescription>
|
|
885
|
+
</DialogHeader>
|
|
886
|
+
<div>
|
|
887
|
+
<div class="w-full space-y-4">
|
|
888
|
+
<edge-shad-input v-model="state.newPageName" name="name" label="Page Name" placeholder="Enter page name" />
|
|
889
|
+
<edge-shad-select
|
|
890
|
+
v-model="state.templateFilter"
|
|
891
|
+
label="Template Tags"
|
|
892
|
+
:items="templateFilterOptions"
|
|
893
|
+
item-title="label"
|
|
894
|
+
item-value="value"
|
|
895
|
+
placeholder="Select tag"
|
|
896
|
+
/>
|
|
897
|
+
<p class="text-xs text-muted-foreground">
|
|
898
|
+
Filter templates by tag or choose Quick Picks for the most commonly used layouts.
|
|
899
|
+
</p>
|
|
900
|
+
</div>
|
|
901
|
+
<edge-button-divider class="my-4">
|
|
902
|
+
<span class="text-xs text-muted-foreground !nowrap text-center">Select Template</span>
|
|
903
|
+
</edge-button-divider>
|
|
904
|
+
<div class="overflow-y-auto !h-[calc(100vh-510px)] pr-1">
|
|
905
|
+
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-3 auto-rows-fr pb-2">
|
|
906
|
+
<button
|
|
907
|
+
v-for="template in templateGridItems"
|
|
908
|
+
:key="template.docId"
|
|
909
|
+
type="button"
|
|
910
|
+
class="rounded-lg border bg-card text-left p-3 flex flex-col gap-3 transition focus:outline-none focus-visible:ring-2"
|
|
911
|
+
:class="isTemplateSelected(template.docId) ? 'border-primary ring-2 ring-primary/50 shadow-lg' : 'border-border hover:border-primary/40'"
|
|
912
|
+
:aria-pressed="isTemplateSelected(template.docId)"
|
|
913
|
+
@click="selectTemplate(template.docId)"
|
|
914
|
+
>
|
|
915
|
+
<div class="flex items-center justify-between gap-2">
|
|
916
|
+
<span class="font-semibold truncate">{{ template.name }}</span>
|
|
917
|
+
<File class="w-4 h-4 text-muted-foreground" />
|
|
918
|
+
</div>
|
|
919
|
+
<div class="template-scale-wrapper border border-dashed border-border/60 rounded-md bg-background/80">
|
|
920
|
+
<div class="template-scale-inner">
|
|
921
|
+
<div class="template-scale-content space-y-4">
|
|
922
|
+
<template v-if="template.docId === BLANK_TEMPLATE_ID">
|
|
923
|
+
<div class="flex h-32 items-center justify-center text-[100px] mt-[100px] text-muted-foreground">
|
|
924
|
+
Blank page
|
|
925
|
+
</div>
|
|
926
|
+
</template>
|
|
927
|
+
<template v-else-if="templateHasBlocks(template)">
|
|
928
|
+
<div
|
|
929
|
+
v-for="(block, idx) in templatePreviewBlocks(template)"
|
|
930
|
+
:key="`${template.docId}-block-${idx}`"
|
|
931
|
+
>
|
|
932
|
+
<edge-cms-block-api
|
|
933
|
+
v-if="resolveBlockForPreview(block)"
|
|
934
|
+
:content="resolveBlockForPreview(block).content"
|
|
935
|
+
:values="resolveBlockForPreview(block).values"
|
|
936
|
+
:meta="resolveBlockForPreview(block).meta"
|
|
937
|
+
:theme="theme"
|
|
938
|
+
:isolated="true"
|
|
939
|
+
/>
|
|
940
|
+
</div>
|
|
941
|
+
</template>
|
|
942
|
+
<template v-else>
|
|
943
|
+
<div class="flex h-32 items-center justify-center text-[100px] mt-[100px] text-muted-foreground">
|
|
944
|
+
No blocks yet
|
|
945
|
+
</div>
|
|
946
|
+
</template>
|
|
947
|
+
</div>
|
|
948
|
+
</div>
|
|
949
|
+
</div>
|
|
950
|
+
</button>
|
|
951
|
+
</div>
|
|
952
|
+
</div>
|
|
953
|
+
</div>
|
|
954
|
+
<DialogFooter class="pt-4">
|
|
955
|
+
<edge-shad-button type="button" variant="destructive" @click="state.addPageDialog = false">
|
|
956
|
+
Cancel
|
|
957
|
+
</edge-shad-button>
|
|
958
|
+
<edge-shad-button type="submit" class="bg-slate-800 hover:bg-slate-400 text-white" :disabled="!hasValidNewPageName">
|
|
959
|
+
Create Page
|
|
960
|
+
</edge-shad-button>
|
|
961
|
+
</DialogFooter>
|
|
962
|
+
</edge-shad-form>
|
|
963
|
+
</DialogContent>
|
|
964
|
+
</edge-shad-dialog>
|
|
965
|
+
<edge-shad-dialog
|
|
966
|
+
v-model="state.renameFolderOrPageDialog"
|
|
967
|
+
>
|
|
968
|
+
<DialogContent class="pt-10">
|
|
969
|
+
<edge-shad-form :schema="pages" @submit="renameFolderOrPageAction">
|
|
970
|
+
<DialogHeader>
|
|
971
|
+
<DialogTitle class="text-left">
|
|
972
|
+
<span v-if="state.renameItem.item === ''">Rename Folder "{{ state.renameItem.name }}"</span>
|
|
973
|
+
<span v-else>Rename Page "{{ state.renameItem.name }}"</span>
|
|
974
|
+
</DialogTitle>
|
|
975
|
+
<DialogDescription />
|
|
976
|
+
</DialogHeader>
|
|
977
|
+
<edge-shad-input v-model="state.renameItem.name" name="name" placeholder="New Name" />
|
|
978
|
+
<DialogFooter class="pt-2 flex justify-between">
|
|
979
|
+
<edge-shad-button variant="destructive" @click="state.renameFolderOrPageDialog = false">
|
|
980
|
+
Cancel
|
|
981
|
+
</edge-shad-button>
|
|
982
|
+
<edge-shad-button type="submit" class="text-white bg-slate-800 hover:bg-slate-400 w-full">
|
|
983
|
+
Rename
|
|
984
|
+
</edge-shad-button>
|
|
985
|
+
</DialogFooter>
|
|
986
|
+
</edge-shad-form>
|
|
987
|
+
</DialogContent>
|
|
988
|
+
</edge-shad-dialog>
|
|
989
|
+
<Sheet v-model:open="state.pageSettings">
|
|
990
|
+
<SheetContent side="left" class="w-full md:w-1/2 max-w-none sm:max-w-none max-w-2xl">
|
|
991
|
+
<SheetHeader>
|
|
992
|
+
<SheetTitle>{{ state.pageData.name || 'Site' }}</SheetTitle>
|
|
993
|
+
<SheetDescription />
|
|
994
|
+
</SheetHeader>
|
|
995
|
+
<edge-editor
|
|
996
|
+
:collection="`sites/${props.site}/pages`"
|
|
997
|
+
:doc-id="state.pageData.item"
|
|
998
|
+
:schema="schemas.pages"
|
|
999
|
+
:new-doc-schema="state.newDocs.pages"
|
|
1000
|
+
class="w-full mx-auto flex-1 bg-transparent flex flex-col border-none px-0shadow-none"
|
|
1001
|
+
:show-footer="false"
|
|
1002
|
+
:show-header="false"
|
|
1003
|
+
:save-function-override="onSubmit"
|
|
1004
|
+
card-content-class="px-0"
|
|
1005
|
+
@error="formErrors"
|
|
1006
|
+
>
|
|
1007
|
+
<template #main="slotProps">
|
|
1008
|
+
<div class="p-6 space-y-4 h-[calc(100vh-142px)] overflow-y-auto">
|
|
1009
|
+
<edge-shad-checkbox
|
|
1010
|
+
v-model="slotProps.workingDoc.post"
|
|
1011
|
+
label="Is a Post Template"
|
|
1012
|
+
name="post"
|
|
1013
|
+
>
|
|
1014
|
+
Creates both an Index Page and a Detail Page for this section.
|
|
1015
|
+
The Index Page lists all items (e.g., /{{ slotProps.workingDoc.name }}), while the Detail Page displays a single item (e.g., /{{ slotProps.workingDoc.name }}/:slug).
|
|
1016
|
+
</edge-shad-checkbox>
|
|
1017
|
+
<edge-shad-select-tags
|
|
1018
|
+
v-if="props.isTemplateSite"
|
|
1019
|
+
v-model="slotProps.workingDoc.tags"
|
|
1020
|
+
name="tags"
|
|
1021
|
+
label="Tags"
|
|
1022
|
+
placeholder="Add tags"
|
|
1023
|
+
:items="templateTagItems"
|
|
1024
|
+
:allow-additions="true"
|
|
1025
|
+
/>
|
|
1026
|
+
<edge-shad-select-tags
|
|
1027
|
+
v-if="props.themeOptions.length"
|
|
1028
|
+
:model-value="Array.isArray(slotProps.workingDoc.allowedThemes) ? slotProps.workingDoc.allowedThemes : []"
|
|
1029
|
+
name="allowedThemes"
|
|
1030
|
+
label="Allowed Themes"
|
|
1031
|
+
placeholder="Select allowed themes"
|
|
1032
|
+
:items="props.themeOptions"
|
|
1033
|
+
item-title="label"
|
|
1034
|
+
item-value="value"
|
|
1035
|
+
@update:model-value="(value) => {
|
|
1036
|
+
slotProps.workingDoc.allowedThemes = Array.isArray(value) ? value : []
|
|
1037
|
+
}"
|
|
1038
|
+
/>
|
|
1039
|
+
<Card>
|
|
1040
|
+
<CardHeader>
|
|
1041
|
+
<CardTitle>SEO</CardTitle>
|
|
1042
|
+
<CardDescription>Meta tags for the page.</CardDescription>
|
|
1043
|
+
</CardHeader>
|
|
1044
|
+
<CardContent class="pt-0">
|
|
1045
|
+
<edge-shad-input
|
|
1046
|
+
v-model="slotProps.workingDoc.metaTitle"
|
|
1047
|
+
label="Meta Title"
|
|
1048
|
+
name="metaTitle"
|
|
1049
|
+
/>
|
|
1050
|
+
<edge-shad-textarea
|
|
1051
|
+
v-model="slotProps.workingDoc.metaDescription"
|
|
1052
|
+
label="Meta Description"
|
|
1053
|
+
name="metaDescription"
|
|
1054
|
+
/>
|
|
1055
|
+
<edge-cms-code-editor
|
|
1056
|
+
v-model="slotProps.workingDoc.structuredData"
|
|
1057
|
+
title="Structured Data (JSON-LD)"
|
|
1058
|
+
language="json"
|
|
1059
|
+
name="structuredData"
|
|
1060
|
+
height="300px"
|
|
1061
|
+
class="mb-4 w-full"
|
|
1062
|
+
/>
|
|
1063
|
+
</CardContent>
|
|
1064
|
+
</Card>
|
|
1065
|
+
</div>
|
|
1066
|
+
<SheetFooter class="pt-2 flex justify-between">
|
|
1067
|
+
<edge-shad-button variant="destructive" class="text-white" @click="state.pageSettings = false">
|
|
1068
|
+
Cancel
|
|
1069
|
+
</edge-shad-button>
|
|
1070
|
+
<edge-shad-button :disabled="slotProps.submitting" type="submit" class=" bg-slate-800 hover:bg-slate-400 w-full">
|
|
1071
|
+
<Loader2 v-if="slotProps.submitting" class=" h-4 w-4 animate-spin" />
|
|
1072
|
+
Update
|
|
1073
|
+
</edge-shad-button>
|
|
1074
|
+
</SheetFooter>
|
|
1075
|
+
</template>
|
|
1076
|
+
</edge-editor>
|
|
1077
|
+
</SheetContent>
|
|
1078
|
+
</Sheet>
|
|
1079
|
+
</template>
|
|
1080
|
+
|
|
1081
|
+
<style lang="scss">
|
|
1082
|
+
.template-scale-wrapper {
|
|
1083
|
+
width: 100%;
|
|
1084
|
+
overflow: hidden;
|
|
1085
|
+
position: relative;
|
|
1086
|
+
border-radius: 0.5rem;
|
|
1087
|
+
height: 400px;
|
|
1088
|
+
}
|
|
1089
|
+
|
|
1090
|
+
.template-scale-inner {
|
|
1091
|
+
transform-origin: top left;
|
|
1092
|
+
display: inline-block;
|
|
1093
|
+
width: 100%;
|
|
1094
|
+
height: 400px;
|
|
1095
|
+
overflow: hidden;
|
|
1096
|
+
}
|
|
1097
|
+
|
|
1098
|
+
.template-scale-content {
|
|
1099
|
+
transform: scale(0.2);
|
|
1100
|
+
transform-origin: top left;
|
|
1101
|
+
width: 500%;
|
|
1102
|
+
}
|
|
1103
|
+
</style>
|