@edgedev/create-edge-app 1.2.32 → 1.2.34

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (33) hide show
  1. package/deploy.sh +77 -0
  2. package/edge/components/cms/block.vue +228 -18
  3. package/edge/components/cms/blockApi.vue +3 -3
  4. package/edge/components/cms/blockEditor.vue +374 -85
  5. package/edge/components/cms/blockPicker.vue +29 -3
  6. package/edge/components/cms/blockRender.vue +3 -3
  7. package/edge/components/cms/blocksManager.vue +755 -82
  8. package/edge/components/cms/codeEditor.vue +15 -6
  9. package/edge/components/cms/fontUpload.vue +318 -2
  10. package/edge/components/cms/htmlContent.vue +230 -89
  11. package/edge/components/cms/menu.vue +5 -4
  12. package/edge/components/cms/page.vue +750 -21
  13. package/edge/components/cms/site.vue +624 -84
  14. package/edge/components/cms/sitesManager.vue +5 -4
  15. package/edge/components/cms/themeEditor.vue +196 -162
  16. package/edge/components/editor.vue +5 -1
  17. package/edge/composables/global.ts +37 -5
  18. package/edge/composables/useCmsNewDocs.js +100 -0
  19. package/edge/composables/useEdgeCmsDialogPositionFix.js +19 -0
  20. package/edge/routes/cms/dashboard/blocks/[block].vue +5 -0
  21. package/edge/routes/cms/dashboard/blocks/index.vue +12 -1
  22. package/edge/routes/cms/dashboard/media/index.vue +5 -0
  23. package/edge/routes/cms/dashboard/sites/[site]/[[page]].vue +4 -0
  24. package/edge/routes/cms/dashboard/sites/[site].vue +4 -0
  25. package/edge/routes/cms/dashboard/sites/index.vue +4 -0
  26. package/edge/routes/cms/dashboard/templates/[page].vue +4 -0
  27. package/edge/routes/cms/dashboard/templates/index.vue +4 -0
  28. package/edge/routes/cms/dashboard/themes/[theme].vue +5 -0
  29. package/edge/routes/cms/dashboard/themes/index.vue +330 -1
  30. package/firebase.json +4 -0
  31. package/nuxt.config.ts +1 -1
  32. package/package.json +2 -2
  33. package/pages/app.vue +12 -12
@@ -1,9 +1,14 @@
1
1
  <script setup>
2
+ import { useEdgeCmsDialogPositionFix } from '~/edge/composables/useEdgeCmsDialogPositionFix'
3
+
2
4
  const route = useRoute()
3
5
  const state = reactive({
4
6
  mounted: false,
5
7
  head: null,
6
8
  })
9
+
10
+ useEdgeCmsDialogPositionFix()
11
+
7
12
  definePageMeta({
8
13
  middleware: 'auth',
9
14
  })
@@ -1,21 +1,32 @@
1
1
  <script setup>
2
+ import { useEdgeCmsDialogPositionFix } from '~/edge/composables/useEdgeCmsDialogPositionFix'
3
+
2
4
  const state = reactive({
3
5
  mounted: false,
6
+ head: null,
4
7
  })
5
8
 
9
+ useEdgeCmsDialogPositionFix()
10
+
6
11
  definePageMeta({
7
12
  middleware: 'auth',
8
13
  })
9
14
 
15
+ useHead(() => (state.head || {}))
16
+
10
17
  onMounted(() => {
11
18
  state.mounted = true
12
19
  })
20
+
21
+ const setHead = (newHead) => {
22
+ state.head = newHead
23
+ }
13
24
  </script>
14
25
 
15
26
  <template>
16
27
  <div
17
28
  v-if="edgeGlobal.edgeState.organizationDocPath && state.mounted"
18
29
  >
19
- <edge-cms-blocks-manager />
30
+ <edge-cms-blocks-manager @head="setHead" />
20
31
  </div>
21
32
  </template>
@@ -1,7 +1,12 @@
1
1
  <script setup>
2
+ import { useEdgeCmsDialogPositionFix } from '~/edge/composables/useEdgeCmsDialogPositionFix'
3
+
2
4
  const state = reactive({
3
5
  mounted: false,
4
6
  })
7
+
8
+ useEdgeCmsDialogPositionFix()
9
+
5
10
  definePageMeta({
6
11
  middleware: 'auth',
7
12
  })
@@ -1,4 +1,6 @@
1
1
  <script setup>
2
+ import { useEdgeCmsDialogPositionFix } from '~/edge/composables/useEdgeCmsDialogPositionFix'
3
+
2
4
  const route = useRoute()
3
5
 
4
6
  // const edgeGlobal = inject('edgeGlobal')
@@ -8,6 +10,8 @@ const state = reactive({
8
10
  head: null,
9
11
  })
10
12
 
13
+ useEdgeCmsDialogPositionFix()
14
+
11
15
  const page = computed(() => {
12
16
  if (route.params?.page) {
13
17
  return route.params.page
@@ -1,4 +1,6 @@
1
1
  <script setup>
2
+ import { useEdgeCmsDialogPositionFix } from '~/edge/composables/useEdgeCmsDialogPositionFix'
3
+
2
4
  const route = useRoute()
3
5
 
4
6
  // const edgeGlobal = inject('edgeGlobal')
@@ -7,6 +9,8 @@ const state = reactive({
7
9
  mounted: false,
8
10
  })
9
11
 
12
+ useEdgeCmsDialogPositionFix()
13
+
10
14
  const page = computed(() => {
11
15
  if (route.params?.page) {
12
16
  return route.params.page
@@ -1,4 +1,8 @@
1
1
  <script setup>
2
+ import { useEdgeCmsDialogPositionFix } from '~/edge/composables/useEdgeCmsDialogPositionFix'
3
+
4
+ useEdgeCmsDialogPositionFix()
5
+
2
6
  definePageMeta({
3
7
  middleware: 'auth',
4
8
  })
@@ -1,4 +1,6 @@
1
1
  <script setup>
2
+ import { useEdgeCmsDialogPositionFix } from '~/edge/composables/useEdgeCmsDialogPositionFix'
3
+
2
4
  const route = useRoute()
3
5
 
4
6
  const state = reactive({
@@ -6,6 +8,8 @@ const state = reactive({
6
8
  head: null,
7
9
  })
8
10
 
11
+ useEdgeCmsDialogPositionFix()
12
+
9
13
  const page = computed(() => {
10
14
  if (route.params?.page) {
11
15
  return route.params.page
@@ -1,4 +1,6 @@
1
1
  <script setup>
2
+ import { useEdgeCmsDialogPositionFix } from '~/edge/composables/useEdgeCmsDialogPositionFix'
3
+
2
4
  const route = useRoute()
3
5
 
4
6
  // const edgeGlobal = inject('edgeGlobal')
@@ -7,6 +9,8 @@ const state = reactive({
7
9
  mounted: false,
8
10
  })
9
11
 
12
+ useEdgeCmsDialogPositionFix()
13
+
10
14
  const page = computed(() => {
11
15
  if (route.params?.page) {
12
16
  return route.params.page
@@ -1,8 +1,13 @@
1
1
  <script setup>
2
+ import { useEdgeCmsDialogPositionFix } from '~/edge/composables/useEdgeCmsDialogPositionFix'
3
+
2
4
  const route = useRoute()
3
5
  const state = reactive({
4
6
  mounted: false,
5
7
  })
8
+
9
+ useEdgeCmsDialogPositionFix()
10
+
6
11
  definePageMeta({
7
12
  middleware: 'auth',
8
13
  head: null,
@@ -1,18 +1,281 @@
1
1
  <script setup>
2
+ import { useEdgeCmsDialogPositionFix } from '~/edge/composables/useEdgeCmsDialogPositionFix'
3
+
4
+ const edgeFirebase = inject('edgeFirebase')
5
+ const { themes: themeNewDocSchema } = useCmsNewDocs()
2
6
  const state = reactive({
3
7
  filter: '',
8
+ importingJson: false,
9
+ importDocIdDialogOpen: false,
10
+ importDocIdValue: '',
11
+ importConflictDialogOpen: false,
12
+ importConflictDocId: '',
13
+ importErrorDialogOpen: false,
14
+ importErrorMessage: '',
4
15
  })
16
+ const themeImportInputRef = ref(null)
17
+ const themeImportDocIdResolver = ref(null)
18
+ const themeImportConflictResolver = ref(null)
19
+ const INVALID_THEME_IMPORT_MESSAGE = 'Invalid file. Please import a valid theme file.'
20
+
21
+ useEdgeCmsDialogPositionFix()
22
+
23
+ useEdgeCmsDialogPositionFix()
5
24
 
6
25
  definePageMeta({
7
26
  middleware: 'auth',
8
27
  })
28
+
29
+ const themesCollectionPath = computed(() => `${edgeGlobal.edgeState.organizationDocPath}/themes`)
30
+ const themesCollection = computed(() => edgeFirebase.data?.[themesCollectionPath.value] || {})
31
+
32
+ const readTextFile = file => new Promise((resolve, reject) => {
33
+ if (typeof FileReader === 'undefined') {
34
+ reject(new Error('File import is only available in the browser.'))
35
+ return
36
+ }
37
+ const reader = new FileReader()
38
+ reader.onload = () => resolve(String(reader.result || ''))
39
+ reader.onerror = () => reject(new Error('Could not read the selected file.'))
40
+ reader.readAsText(file)
41
+ })
42
+
43
+ const normalizeImportedDoc = (payload, fallbackDocId = '') => {
44
+ if (!payload || typeof payload !== 'object' || Array.isArray(payload))
45
+ throw new Error(INVALID_THEME_IMPORT_MESSAGE)
46
+
47
+ if (payload.document && typeof payload.document === 'object' && !Array.isArray(payload.document)) {
48
+ const normalized = { ...payload.document }
49
+ if (!normalized.docId && payload.docId)
50
+ normalized.docId = payload.docId
51
+ if (!normalized.docId && fallbackDocId)
52
+ normalized.docId = fallbackDocId
53
+ return normalized
54
+ }
55
+
56
+ const normalized = { ...payload }
57
+ if (!normalized.docId && fallbackDocId)
58
+ normalized.docId = fallbackDocId
59
+ return normalized
60
+ }
61
+
62
+ const isPlainObject = value => !!value && typeof value === 'object' && !Array.isArray(value)
63
+
64
+ const cloneSchemaValue = (value) => {
65
+ if (isPlainObject(value) || Array.isArray(value))
66
+ return edgeGlobal.dupObject(value)
67
+ return value
68
+ }
69
+
70
+ const getDocDefaultsFromSchema = (schema = {}) => {
71
+ const defaults = {}
72
+ for (const [key, schemaEntry] of Object.entries(schema || {})) {
73
+ const hasValueProp = isPlainObject(schemaEntry) && Object.prototype.hasOwnProperty.call(schemaEntry, 'value')
74
+ const baseValue = hasValueProp ? schemaEntry.value : schemaEntry
75
+ defaults[key] = cloneSchemaValue(baseValue)
76
+ }
77
+ return defaults
78
+ }
79
+
80
+ const getThemeDocDefaults = () => getDocDefaultsFromSchema(themeNewDocSchema.value || {})
81
+
82
+ const validateImportedThemeDoc = (doc) => {
83
+ if (!isPlainObject(doc))
84
+ throw new Error(INVALID_THEME_IMPORT_MESSAGE)
85
+
86
+ const requiredKeys = Object.keys(themeNewDocSchema.value || {})
87
+ const missing = requiredKeys.filter(key => !Object.prototype.hasOwnProperty.call(doc, key))
88
+ if (missing.length)
89
+ throw new Error(INVALID_THEME_IMPORT_MESSAGE)
90
+
91
+ return doc
92
+ }
93
+
94
+ const makeUniqueDocId = (baseDocId, docsMap = {}) => {
95
+ const cleanBase = String(baseDocId || '').trim() || 'theme'
96
+ let nextDocId = `${cleanBase}-copy`
97
+ let suffix = 2
98
+ while (docsMap[nextDocId]) {
99
+ nextDocId = `${cleanBase}-copy-${suffix}`
100
+ suffix += 1
101
+ }
102
+ return nextDocId
103
+ }
104
+
105
+ const requestThemeImportDocId = (initialValue = '') => {
106
+ state.importDocIdValue = String(initialValue || '')
107
+ state.importDocIdDialogOpen = true
108
+ return new Promise((resolve) => {
109
+ themeImportDocIdResolver.value = resolve
110
+ })
111
+ }
112
+
113
+ const resolveThemeImportDocId = (value = '') => {
114
+ const resolver = themeImportDocIdResolver.value
115
+ themeImportDocIdResolver.value = null
116
+ state.importDocIdDialogOpen = false
117
+ if (resolver)
118
+ resolver(String(value || '').trim())
119
+ }
120
+
121
+ const requestThemeImportConflict = (docId) => {
122
+ state.importConflictDocId = String(docId || '')
123
+ state.importConflictDialogOpen = true
124
+ return new Promise((resolve) => {
125
+ themeImportConflictResolver.value = resolve
126
+ })
127
+ }
128
+
129
+ const resolveThemeImportConflict = (action = 'cancel') => {
130
+ const resolver = themeImportConflictResolver.value
131
+ themeImportConflictResolver.value = null
132
+ state.importConflictDialogOpen = false
133
+ if (resolver)
134
+ resolver(action)
135
+ }
136
+
137
+ watch(() => state.importDocIdDialogOpen, (open) => {
138
+ if (!open && themeImportDocIdResolver.value) {
139
+ const resolver = themeImportDocIdResolver.value
140
+ themeImportDocIdResolver.value = null
141
+ resolver('')
142
+ }
143
+ })
144
+
145
+ watch(() => state.importConflictDialogOpen, (open) => {
146
+ if (!open && themeImportConflictResolver.value) {
147
+ const resolver = themeImportConflictResolver.value
148
+ themeImportConflictResolver.value = null
149
+ resolver('cancel')
150
+ }
151
+ })
152
+
153
+ const getImportDocId = async (incomingDoc, fallbackDocId = '') => {
154
+ let nextDocId = String(incomingDoc?.docId || '').trim()
155
+ if (!nextDocId)
156
+ nextDocId = await requestThemeImportDocId(fallbackDocId)
157
+ if (!nextDocId)
158
+ throw new Error('Import canceled. A docId is required.')
159
+ if (nextDocId.includes('/'))
160
+ throw new Error('docId cannot include "/".')
161
+ return nextDocId
162
+ }
163
+
164
+ const openImportErrorDialog = (message) => {
165
+ state.importErrorMessage = String(message || 'Failed to import theme JSON.')
166
+ state.importErrorDialogOpen = true
167
+ }
168
+
169
+ const triggerThemeImport = () => {
170
+ themeImportInputRef.value?.click()
171
+ }
172
+
173
+ const importSingleThemeFile = async (file, existingThemes = {}) => {
174
+ const fileText = await readTextFile(file)
175
+ const parsed = JSON.parse(fileText)
176
+ const importedDoc = validateImportedThemeDoc(normalizeImportedDoc(parsed, ''))
177
+ const incomingDocId = await getImportDocId(importedDoc, '')
178
+ let targetDocId = incomingDocId
179
+ let importDecision = 'create'
180
+
181
+ if (existingThemes[targetDocId]) {
182
+ const decision = await requestThemeImportConflict(targetDocId)
183
+ if (decision === 'cancel')
184
+ return
185
+ if (decision === 'new') {
186
+ targetDocId = makeUniqueDocId(targetDocId, existingThemes)
187
+ if (typeof importedDoc.name === 'string' && importedDoc.name.trim() && !/\(Copy\)$/i.test(importedDoc.name.trim()))
188
+ importedDoc.name = `${importedDoc.name} (Copy)`
189
+ importDecision = 'new'
190
+ }
191
+ else {
192
+ importDecision = 'overwrite'
193
+ }
194
+ }
195
+
196
+ const payload = { ...getThemeDocDefaults(), ...importedDoc, docId: targetDocId }
197
+ await edgeFirebase.storeDoc(themesCollectionPath.value, payload, targetDocId)
198
+ existingThemes[targetDocId] = payload
199
+
200
+ if (importDecision === 'overwrite')
201
+ edgeFirebase?.toast?.success?.(`Overwrote theme "${targetDocId}".`)
202
+ else if (importDecision === 'new')
203
+ edgeFirebase?.toast?.success?.(`Imported theme as new "${targetDocId}".`)
204
+ else
205
+ edgeFirebase?.toast?.success?.(`Imported theme "${targetDocId}".`)
206
+ }
207
+
208
+ const handleThemeImport = async (event) => {
209
+ const input = event?.target
210
+ const files = Array.from(input?.files || [])
211
+ if (!files.length)
212
+ return
213
+
214
+ state.importingJson = true
215
+ const existingThemes = { ...(themesCollection.value || {}) }
216
+ try {
217
+ for (const file of files) {
218
+ try {
219
+ await importSingleThemeFile(file, existingThemes)
220
+ }
221
+ catch (error) {
222
+ console.error('Failed to import theme JSON', error)
223
+ const message = error?.message || 'Failed to import theme JSON.'
224
+ if (/^Import canceled\./i.test(message))
225
+ continue
226
+ if (error instanceof SyntaxError || message === INVALID_THEME_IMPORT_MESSAGE)
227
+ openImportErrorDialog(INVALID_THEME_IMPORT_MESSAGE)
228
+ else
229
+ openImportErrorDialog(message)
230
+ }
231
+ }
232
+ }
233
+ finally {
234
+ state.importingJson = false
235
+ if (input)
236
+ input.value = ''
237
+ }
238
+ }
9
239
  </script>
10
240
 
11
241
  <template>
12
242
  <div
13
243
  v-if="edgeGlobal.edgeState.organizationDocPath"
14
244
  >
15
- <edge-dashboard :filter="state.filter" collection="themes" class="pt-0 flex-1">
245
+ <edge-dashboard
246
+ :filter="state.filter"
247
+ collection="themes"
248
+ class="pt-0 flex-1"
249
+ header-class="bg-secondary py-2 border"
250
+ >
251
+ <template #header-end>
252
+ <div class="flex items-center gap-2">
253
+ <input
254
+ ref="themeImportInputRef"
255
+ type="file"
256
+ multiple
257
+ accept=".json,application/json"
258
+ class="hidden"
259
+ @change="handleThemeImport"
260
+ >
261
+ <edge-shad-button
262
+ type="button"
263
+ size="icon"
264
+ variant="outline"
265
+ class="h-9 w-9"
266
+ :disabled="state.importingJson"
267
+ title="Import Themes"
268
+ aria-label="Import Themes"
269
+ @click="triggerThemeImport"
270
+ >
271
+ <Loader2 v-if="state.importingJson" class="h-4 w-4 animate-spin" />
272
+ <Upload v-else class="h-4 w-4" />
273
+ </edge-shad-button>
274
+ <edge-shad-button class="uppercase bg-primary" to="/app/dashboard/themes/new">
275
+ Add Theme
276
+ </edge-shad-button>
277
+ </div>
278
+ </template>
16
279
  <template #list="slotProps">
17
280
  <template v-for="item in slotProps.filtered" :key="item.docId">
18
281
  <edge-shad-button variant="text" class="cursor-pointer w-full flex justify-between items-center py-2 gap-3" :to="`/app/dashboard/themes/${item.docId}`">
@@ -40,5 +303,71 @@ definePageMeta({
40
303
  </template>
41
304
  </template>
42
305
  </edge-dashboard>
306
+ <edge-shad-dialog v-model="state.importDocIdDialogOpen">
307
+ <DialogContent class="pt-8">
308
+ <DialogHeader>
309
+ <DialogTitle class="text-left">
310
+ Enter Theme Doc ID
311
+ </DialogTitle>
312
+ <DialogDescription>
313
+ This JSON file does not include a <code>docId</code>. Enter the doc ID you want to import into.
314
+ </DialogDescription>
315
+ </DialogHeader>
316
+ <edge-shad-input
317
+ v-model="state.importDocIdValue"
318
+ name="theme-import-doc-id"
319
+ label="Doc ID"
320
+ placeholder="example-theme-id"
321
+ />
322
+ <DialogFooter class="pt-2 flex justify-between">
323
+ <edge-shad-button variant="outline" @click="resolveThemeImportDocId('')">
324
+ Cancel
325
+ </edge-shad-button>
326
+ <edge-shad-button @click="resolveThemeImportDocId(state.importDocIdValue)">
327
+ Continue
328
+ </edge-shad-button>
329
+ </DialogFooter>
330
+ </DialogContent>
331
+ </edge-shad-dialog>
332
+ <edge-shad-dialog v-model="state.importConflictDialogOpen">
333
+ <DialogContent class="pt-8">
334
+ <DialogHeader>
335
+ <DialogTitle class="text-left">
336
+ Theme Already Exists
337
+ </DialogTitle>
338
+ <DialogDescription>
339
+ <code>{{ state.importConflictDocId }}</code> already exists. Choose to overwrite it or import as a new theme.
340
+ </DialogDescription>
341
+ </DialogHeader>
342
+ <DialogFooter class="pt-2 flex justify-between">
343
+ <edge-shad-button variant="outline" @click="resolveThemeImportConflict('cancel')">
344
+ Cancel
345
+ </edge-shad-button>
346
+ <edge-shad-button variant="outline" @click="resolveThemeImportConflict('new')">
347
+ Add As New
348
+ </edge-shad-button>
349
+ <edge-shad-button @click="resolveThemeImportConflict('overwrite')">
350
+ Overwrite
351
+ </edge-shad-button>
352
+ </DialogFooter>
353
+ </DialogContent>
354
+ </edge-shad-dialog>
355
+ <edge-shad-dialog v-model="state.importErrorDialogOpen">
356
+ <DialogContent class="pt-8">
357
+ <DialogHeader>
358
+ <DialogTitle class="text-left">
359
+ Import Failed
360
+ </DialogTitle>
361
+ <DialogDescription class="text-left">
362
+ {{ state.importErrorMessage }}
363
+ </DialogDescription>
364
+ </DialogHeader>
365
+ <DialogFooter class="pt-2">
366
+ <edge-shad-button @click="state.importErrorDialogOpen = false">
367
+ Close
368
+ </edge-shad-button>
369
+ </DialogFooter>
370
+ </DialogContent>
371
+ </edge-shad-dialog>
43
372
  </div>
44
373
  </template>
package/firebase.json CHANGED
@@ -23,6 +23,10 @@
23
23
  "**/node_modules/**"
24
24
  ],
25
25
  "rewrites": [
26
+ {
27
+ "source": "/api/history/**",
28
+ "function": "cms-trackHistory"
29
+ },
26
30
  {
27
31
  "source": "/api/stripe",
28
32
  "function": {
package/nuxt.config.ts CHANGED
@@ -24,7 +24,7 @@ export default defineNuxtConfig({
24
24
  meta: [
25
25
  {
26
26
  'http-equiv': 'Content-Security-Policy',
27
- 'content': 'font-src \'self\' https://files.edgemarketingdesign.com https://use.typekit.net https://p.typekit.net data:;',
27
+ 'content': 'font-src \'self\' https://files.edgemarketingdesign.com https://use.typekit.net https://p.typekit.net https://fonts.gstatic.com data:;',
28
28
  },
29
29
  ],
30
30
  },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@edgedev/create-edge-app",
3
- "version": "1.2.32",
3
+ "version": "1.2.34",
4
4
  "description": "Create Edge Starter App",
5
5
  "bin": {
6
6
  "create-edge-app": "./bin/cli.js"
@@ -22,7 +22,7 @@
22
22
  "@capacitor/push-notifications": "5.1.0",
23
23
  "@chenfengyuan/vue-number-input": "2",
24
24
  "@edgedev/firebase": "latest",
25
- "@edgedev/template-engine": "^0.1.12",
25
+ "@edgedev/template-engine": "latest",
26
26
  "@guolao/vue-monaco-editor": "^1.5.5",
27
27
  "@tiptap/extension-image": "^2.11.5",
28
28
  "@tiptap/extension-text-style": "^2.11.5",
package/pages/app.vue CHANGED
@@ -260,18 +260,18 @@ edgeGlobal.edgeState.userRoles = [
260
260
  },
261
261
  ]
262
262
 
263
- edgeGlobal.edgeState.menuItems = [
264
- {
265
- title: 'Dashboard',
266
- to: '/app/dashboard/things',
267
- icon: 'LayoutDashboard',
268
- },
269
- {
270
- title: 'Sub Things',
271
- to: '/app/dashboard/subthings',
272
- icon: 'Package',
273
- },
274
- ]
263
+ // edgeGlobal.edgeState.menuItems = [
264
+ // {
265
+ // title: 'Dashboard',
266
+ // to: '/app/dashboard/things',
267
+ // icon: 'LayoutDashboard',
268
+ // },
269
+ // {
270
+ // title: 'Sub Things',
271
+ // to: '/app/dashboard/subthings',
272
+ // icon: 'Package',
273
+ // },
274
+ // ]
275
275
  </script>
276
276
 
277
277
  <template>