@edgedev/create-edge-app 1.2.33 → 1.2.35
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 +1 -0
- package/agents.md +95 -2
- package/deploy.sh +136 -0
- package/edge/components/cms/block.vue +977 -305
- package/edge/components/cms/blockApi.vue +3 -3
- package/edge/components/cms/blockEditor.vue +688 -86
- package/edge/components/cms/blockPicker.vue +31 -5
- package/edge/components/cms/blockRender.vue +3 -3
- package/edge/components/cms/blocksManager.vue +790 -82
- package/edge/components/cms/codeEditor.vue +15 -6
- package/edge/components/cms/fontUpload.vue +318 -2
- package/edge/components/cms/htmlContent.vue +825 -93
- package/edge/components/cms/init_blocks/contact_us.html +55 -47
- package/edge/components/cms/init_blocks/newsletter.html +56 -96
- package/edge/components/cms/menu.vue +96 -34
- package/edge/components/cms/page.vue +902 -58
- package/edge/components/cms/posts.vue +13 -4
- package/edge/components/cms/site.vue +638 -87
- package/edge/components/cms/siteSettingsForm.vue +19 -9
- package/edge/components/cms/sitesManager.vue +5 -4
- package/edge/components/cms/themeDefaultMenu.vue +20 -2
- package/edge/components/cms/themeEditor.vue +196 -162
- package/edge/components/editor.vue +5 -1
- package/edge/composables/global.ts +37 -5
- package/edge/composables/siteSettingsTemplate.js +2 -0
- package/edge/composables/useCmsNewDocs.js +100 -0
- package/edge/composables/useEdgeCmsDialogPositionFix.js +19 -0
- package/edge/routes/cms/dashboard/blocks/[block].vue +5 -0
- package/edge/routes/cms/dashboard/blocks/index.vue +12 -1
- package/edge/routes/cms/dashboard/media/index.vue +5 -0
- package/edge/routes/cms/dashboard/sites/[site]/[[page]].vue +4 -0
- package/edge/routes/cms/dashboard/sites/[site].vue +4 -0
- package/edge/routes/cms/dashboard/sites/index.vue +4 -0
- package/edge/routes/cms/dashboard/templates/[page].vue +4 -0
- package/edge/routes/cms/dashboard/templates/index.vue +4 -0
- package/edge/routes/cms/dashboard/themes/[theme].vue +5 -0
- package/edge/routes/cms/dashboard/themes/index.vue +330 -1
- package/edge-pull.sh +16 -2
- package/edge-push.sh +9 -1
- package/edge-remote.sh +20 -0
- package/edge-status.sh +9 -5
- package/edge-update-all.sh +127 -0
- package/firebase.json +4 -0
- package/nuxt.config.ts +1 -1
- package/package.json +2 -2
|
@@ -1,10 +1,22 @@
|
|
|
1
1
|
<script setup>
|
|
2
|
+
const emit = defineEmits(['head'])
|
|
2
3
|
const edgeFirebase = inject('edgeFirebase')
|
|
4
|
+
const { blocks: blockNewDocSchema } = useCmsNewDocs()
|
|
3
5
|
const state = reactive({
|
|
4
6
|
filter: '',
|
|
5
7
|
mounted: false,
|
|
6
8
|
picksFilter: [],
|
|
7
9
|
themesFilter: [],
|
|
10
|
+
selectedBlockDocIds: [],
|
|
11
|
+
bulkDeleteDialogOpen: false,
|
|
12
|
+
bulkDeleting: false,
|
|
13
|
+
importingJson: false,
|
|
14
|
+
importDocIdDialogOpen: false,
|
|
15
|
+
importDocIdValue: '',
|
|
16
|
+
importConflictDialogOpen: false,
|
|
17
|
+
importConflictDocId: '',
|
|
18
|
+
importErrorDialogOpen: false,
|
|
19
|
+
importErrorMessage: '',
|
|
8
20
|
})
|
|
9
21
|
|
|
10
22
|
const rawInitBlockFiles = import.meta.glob('./init_blocks/*.html', {
|
|
@@ -28,6 +40,11 @@ const INITIAL_BLOCKS = Object.entries(rawInitBlockFiles).map(([path, content]) =
|
|
|
28
40
|
})
|
|
29
41
|
|
|
30
42
|
const router = useRouter()
|
|
43
|
+
const blockImportInputRef = ref(null)
|
|
44
|
+
const blockImportDocIdResolver = ref(null)
|
|
45
|
+
const blockImportConflictResolver = ref(null)
|
|
46
|
+
const DEFAULT_BLOCK_IMPORT_ERROR_MESSAGE = 'Failed to import block JSON.'
|
|
47
|
+
const OPTIONAL_BLOCK_IMPORT_KEYS = new Set(['previewType'])
|
|
31
48
|
|
|
32
49
|
const seedInitialBlocks = async () => {
|
|
33
50
|
console.log('Seeding initial blocks...')
|
|
@@ -50,6 +67,7 @@ const seedInitialBlocks = async () => {
|
|
|
50
67
|
docId: block.docId,
|
|
51
68
|
name: block.name,
|
|
52
69
|
content: block.content,
|
|
70
|
+
previewType: 'light',
|
|
53
71
|
tags: [],
|
|
54
72
|
themes: [],
|
|
55
73
|
synced: false,
|
|
@@ -67,17 +85,27 @@ const seedInitialBlocks = async () => {
|
|
|
67
85
|
}
|
|
68
86
|
|
|
69
87
|
const getThemeFromId = (themeId) => {
|
|
70
|
-
const theme = edgeFirebase.data[`organizations/${edgeGlobal.edgeState.currentOrganization}/themes`]?.[themeId]
|
|
71
|
-
console.log('getThemeFromId', themeId, theme.name)
|
|
88
|
+
const theme = edgeFirebase.data?.[`organizations/${edgeGlobal.edgeState.currentOrganization}/themes`]?.[themeId]
|
|
72
89
|
return theme?.name || 'Unknown'
|
|
73
90
|
}
|
|
74
91
|
|
|
92
|
+
const normalizePreviewType = (value) => {
|
|
93
|
+
return value === 'dark' ? 'dark' : 'light'
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const previewSurfaceClass = (value) => {
|
|
97
|
+
return normalizePreviewType(value) === 'dark'
|
|
98
|
+
? 'preview-surface-dark'
|
|
99
|
+
: 'preview-surface-light'
|
|
100
|
+
}
|
|
101
|
+
|
|
75
102
|
const loadingRender = (content) => {
|
|
76
103
|
const safeContent = typeof content === 'string' ? content : ''
|
|
77
104
|
return safeContent.replaceAll('{{loading}}', '').replaceAll('{{loaded}}', 'hidden')
|
|
78
105
|
}
|
|
79
106
|
|
|
80
107
|
const FILTER_STORAGE_KEY = 'edge.blocks.filters'
|
|
108
|
+
const NO_TAGS_FILTER_VALUE = '__no_tags__'
|
|
81
109
|
|
|
82
110
|
const restoreFilters = () => {
|
|
83
111
|
if (typeof localStorage === 'undefined')
|
|
@@ -128,7 +156,8 @@ const tagOptions = computed(() => {
|
|
|
128
156
|
if (Array.isArray(block.tags))
|
|
129
157
|
block.tags.forEach(tag => tagsSet.add(tag))
|
|
130
158
|
})
|
|
131
|
-
|
|
159
|
+
const tagItems = Array.from(tagsSet).sort((a, b) => a.localeCompare(b)).map(tag => ({ name: tag, title: tag }))
|
|
160
|
+
return [{ name: NO_TAGS_FILTER_VALUE, title: 'No Tags' }, ...tagItems]
|
|
132
161
|
})
|
|
133
162
|
|
|
134
163
|
const themeOptions = computed(() => {
|
|
@@ -138,14 +167,505 @@ const themeOptions = computed(() => {
|
|
|
138
167
|
.sort((a, b) => a.title.localeCompare(b.title))
|
|
139
168
|
})
|
|
140
169
|
|
|
170
|
+
const themesCollection = computed(() => {
|
|
171
|
+
return edgeFirebase.data?.[`organizations/${edgeGlobal.edgeState.currentOrganization}/themes`] || {}
|
|
172
|
+
})
|
|
173
|
+
|
|
174
|
+
const parseHeadJson = (raw) => {
|
|
175
|
+
if (!raw)
|
|
176
|
+
return {}
|
|
177
|
+
if (typeof raw === 'object' && !Array.isArray(raw))
|
|
178
|
+
return raw
|
|
179
|
+
if (typeof raw !== 'string')
|
|
180
|
+
return {}
|
|
181
|
+
try {
|
|
182
|
+
return JSON.parse(raw)
|
|
183
|
+
}
|
|
184
|
+
catch {
|
|
185
|
+
return {}
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
const dedupeHeadEntries = (entries) => {
|
|
190
|
+
const seen = new Set()
|
|
191
|
+
return entries.filter((entry) => {
|
|
192
|
+
const key = JSON.stringify(entry || {})
|
|
193
|
+
if (seen.has(key))
|
|
194
|
+
return false
|
|
195
|
+
seen.add(key)
|
|
196
|
+
return true
|
|
197
|
+
})
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
const mergedThemeHeadObject = computed(() => {
|
|
201
|
+
const themes = Object.values(themesCollection.value || {})
|
|
202
|
+
const link = []
|
|
203
|
+
const script = []
|
|
204
|
+
const style = []
|
|
205
|
+
const meta = []
|
|
206
|
+
|
|
207
|
+
themes.forEach((themeDoc) => {
|
|
208
|
+
const parsedHead = parseHeadJson(themeDoc?.headJSON)
|
|
209
|
+
if (Array.isArray(parsedHead?.link))
|
|
210
|
+
link.push(...parsedHead.link)
|
|
211
|
+
if (Array.isArray(parsedHead?.script))
|
|
212
|
+
script.push(...parsedHead.script)
|
|
213
|
+
if (Array.isArray(parsedHead?.style))
|
|
214
|
+
style.push(...parsedHead.style)
|
|
215
|
+
if (Array.isArray(parsedHead?.meta))
|
|
216
|
+
meta.push(...parsedHead.meta)
|
|
217
|
+
})
|
|
218
|
+
|
|
219
|
+
return {
|
|
220
|
+
link: dedupeHeadEntries(link),
|
|
221
|
+
script: dedupeHeadEntries(script),
|
|
222
|
+
style: dedupeHeadEntries(style),
|
|
223
|
+
meta: dedupeHeadEntries(meta),
|
|
224
|
+
}
|
|
225
|
+
})
|
|
226
|
+
|
|
227
|
+
watch(mergedThemeHeadObject, (newHeadElements) => {
|
|
228
|
+
emit('head', newHeadElements || {})
|
|
229
|
+
}, { immediate: true, deep: true })
|
|
230
|
+
|
|
231
|
+
const parsedThemesById = computed(() => {
|
|
232
|
+
const parsed = {}
|
|
233
|
+
for (const [themeId, themeDoc] of Object.entries(themesCollection.value || {})) {
|
|
234
|
+
const rawTheme = themeDoc?.theme
|
|
235
|
+
if (!rawTheme)
|
|
236
|
+
continue
|
|
237
|
+
const extraCSS = typeof themeDoc?.extraCSS === 'string' ? themeDoc.extraCSS : ''
|
|
238
|
+
if (typeof rawTheme === 'string') {
|
|
239
|
+
try {
|
|
240
|
+
const parsedTheme = JSON.parse(rawTheme)
|
|
241
|
+
if (!parsedTheme || typeof parsedTheme !== 'object' || Array.isArray(parsedTheme))
|
|
242
|
+
continue
|
|
243
|
+
parsed[themeId] = { ...parsedTheme, extraCSS }
|
|
244
|
+
}
|
|
245
|
+
catch {
|
|
246
|
+
continue
|
|
247
|
+
}
|
|
248
|
+
continue
|
|
249
|
+
}
|
|
250
|
+
if (typeof rawTheme === 'object' && !Array.isArray(rawTheme))
|
|
251
|
+
parsed[themeId] = { ...rawTheme, extraCSS }
|
|
252
|
+
}
|
|
253
|
+
return parsed
|
|
254
|
+
})
|
|
255
|
+
|
|
256
|
+
const firstThemeId = computed(() => themeOptions.value?.[0]?.name || '')
|
|
257
|
+
|
|
258
|
+
const getPreviewThemeForBlock = (block) => {
|
|
259
|
+
const allowedThemeIds = Array.isArray(block?.themes)
|
|
260
|
+
? block.themes.map(themeId => String(themeId || '').trim()).filter(Boolean)
|
|
261
|
+
: []
|
|
262
|
+
|
|
263
|
+
let preferredThemeId = allowedThemeIds.find(themeId => !!parsedThemesById.value?.[themeId]) || ''
|
|
264
|
+
if (!preferredThemeId)
|
|
265
|
+
preferredThemeId = firstThemeId.value
|
|
266
|
+
if (!preferredThemeId)
|
|
267
|
+
return null
|
|
268
|
+
|
|
269
|
+
return parsedThemesById.value?.[preferredThemeId] || null
|
|
270
|
+
}
|
|
271
|
+
|
|
141
272
|
const listFilters = computed(() => {
|
|
142
273
|
const filters = []
|
|
143
|
-
if (state.picksFilter.length)
|
|
144
|
-
filters.push({ filterFields: ['tags'], value: state.picksFilter })
|
|
145
274
|
if (state.themesFilter.length)
|
|
146
275
|
filters.push({ filterFields: ['themes'], value: state.themesFilter })
|
|
147
276
|
return filters
|
|
148
277
|
})
|
|
278
|
+
|
|
279
|
+
const applyTagSelectionFilter = (items = []) => {
|
|
280
|
+
const selectedFilters = Array.isArray(state.picksFilter) ? state.picksFilter : []
|
|
281
|
+
if (!selectedFilters.length)
|
|
282
|
+
return items
|
|
283
|
+
|
|
284
|
+
const includeNoTags = selectedFilters.includes(NO_TAGS_FILTER_VALUE)
|
|
285
|
+
const selectedTags = selectedFilters.filter(value => value !== NO_TAGS_FILTER_VALUE)
|
|
286
|
+
|
|
287
|
+
return items.filter((item) => {
|
|
288
|
+
const tags = Array.isArray(item?.tags) ? item.tags : []
|
|
289
|
+
const hasNoTags = tags.length === 0
|
|
290
|
+
const hasSelectedTag = selectedTags.length > 0
|
|
291
|
+
? tags.some(tag => selectedTags.includes(tag))
|
|
292
|
+
: false
|
|
293
|
+
|
|
294
|
+
if (includeNoTags && selectedTags.length > 0)
|
|
295
|
+
return hasNoTags || hasSelectedTag
|
|
296
|
+
if (includeNoTags)
|
|
297
|
+
return hasNoTags
|
|
298
|
+
return hasSelectedTag
|
|
299
|
+
})
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
const blockCollectionPath = computed(() => `${edgeGlobal.edgeState.organizationDocPath}/blocks`)
|
|
303
|
+
const blocksCollection = computed(() => edgeFirebase.data?.[blockCollectionPath.value] || {})
|
|
304
|
+
const selectedBlockSet = computed(() => new Set(state.selectedBlockDocIds))
|
|
305
|
+
const selectedBlockCount = computed(() => state.selectedBlockDocIds.length)
|
|
306
|
+
|
|
307
|
+
const normalizeDocId = value => String(value || '').trim()
|
|
308
|
+
|
|
309
|
+
const isBlockSelected = docId => selectedBlockSet.value.has(normalizeDocId(docId))
|
|
310
|
+
|
|
311
|
+
const setBlockSelection = (docId, checked) => {
|
|
312
|
+
const normalizedDocId = normalizeDocId(docId)
|
|
313
|
+
if (!normalizedDocId)
|
|
314
|
+
return
|
|
315
|
+
const shouldSelect = checked === true || checked === 'indeterminate'
|
|
316
|
+
if (shouldSelect) {
|
|
317
|
+
if (!selectedBlockSet.value.has(normalizedDocId))
|
|
318
|
+
state.selectedBlockDocIds = [...state.selectedBlockDocIds, normalizedDocId]
|
|
319
|
+
return
|
|
320
|
+
}
|
|
321
|
+
state.selectedBlockDocIds = state.selectedBlockDocIds.filter(id => id !== normalizedDocId)
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
const getVisibleSelectionState = (visibleItems = []) => {
|
|
325
|
+
if (!visibleItems.length)
|
|
326
|
+
return false
|
|
327
|
+
let selectedVisibleCount = 0
|
|
328
|
+
for (const item of visibleItems) {
|
|
329
|
+
if (isBlockSelected(item?.docId))
|
|
330
|
+
selectedVisibleCount += 1
|
|
331
|
+
}
|
|
332
|
+
if (selectedVisibleCount === 0)
|
|
333
|
+
return false
|
|
334
|
+
if (selectedVisibleCount === visibleItems.length)
|
|
335
|
+
return true
|
|
336
|
+
return 'indeterminate'
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
const toggleVisibleBlockSelection = (visibleItems = [], checked) => {
|
|
340
|
+
if (!visibleItems.length)
|
|
341
|
+
return
|
|
342
|
+
const shouldSelect = checked === true || checked === 'indeterminate'
|
|
343
|
+
const visibleDocIds = visibleItems.map(item => normalizeDocId(item?.docId)).filter(Boolean)
|
|
344
|
+
if (shouldSelect) {
|
|
345
|
+
state.selectedBlockDocIds = [...new Set([...state.selectedBlockDocIds, ...visibleDocIds])]
|
|
346
|
+
return
|
|
347
|
+
}
|
|
348
|
+
const visibleDocIdSet = new Set(visibleDocIds)
|
|
349
|
+
state.selectedBlockDocIds = state.selectedBlockDocIds.filter(id => !visibleDocIdSet.has(id))
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
const clearSelectedBlocks = () => {
|
|
353
|
+
state.selectedBlockDocIds = []
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
const openBulkDeleteDialog = () => {
|
|
357
|
+
if (!selectedBlockCount.value)
|
|
358
|
+
return
|
|
359
|
+
state.bulkDeleteDialogOpen = true
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
const bulkDeleteAction = async () => {
|
|
363
|
+
if (state.bulkDeleting || !selectedBlockCount.value) {
|
|
364
|
+
state.bulkDeleteDialogOpen = false
|
|
365
|
+
return
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
state.bulkDeleting = true
|
|
369
|
+
const selectedDocIds = [...state.selectedBlockDocIds]
|
|
370
|
+
const blocks = blocksCollection.value || {}
|
|
371
|
+
const failedDocIds = []
|
|
372
|
+
let deletedCount = 0
|
|
373
|
+
|
|
374
|
+
try {
|
|
375
|
+
for (const docId of selectedDocIds) {
|
|
376
|
+
if (!blocks[docId])
|
|
377
|
+
continue
|
|
378
|
+
try {
|
|
379
|
+
await edgeFirebase.removeDoc(blockCollectionPath.value, docId)
|
|
380
|
+
deletedCount += 1
|
|
381
|
+
}
|
|
382
|
+
catch (error) {
|
|
383
|
+
failedDocIds.push(docId)
|
|
384
|
+
console.error(`Failed to delete block "${docId}"`, error)
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
state.selectedBlockDocIds = failedDocIds
|
|
389
|
+
state.bulkDeleteDialogOpen = false
|
|
390
|
+
|
|
391
|
+
if (deletedCount)
|
|
392
|
+
edgeFirebase?.toast?.success?.(`Deleted ${deletedCount} block${deletedCount === 1 ? '' : 's'}.`)
|
|
393
|
+
if (failedDocIds.length)
|
|
394
|
+
edgeFirebase?.toast?.error?.(`Failed to delete ${failedDocIds.length} block${failedDocIds.length === 1 ? '' : 's'}.`)
|
|
395
|
+
}
|
|
396
|
+
finally {
|
|
397
|
+
state.bulkDeleting = false
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
watch(blocksCollection, (collection) => {
|
|
402
|
+
const validDocIds = new Set(Object.keys(collection || {}))
|
|
403
|
+
state.selectedBlockDocIds = state.selectedBlockDocIds.filter(docId => validDocIds.has(docId))
|
|
404
|
+
}, { deep: true })
|
|
405
|
+
|
|
406
|
+
const readTextFile = file => new Promise((resolve, reject) => {
|
|
407
|
+
if (typeof FileReader === 'undefined') {
|
|
408
|
+
reject(new Error('File import is only available in the browser.'))
|
|
409
|
+
return
|
|
410
|
+
}
|
|
411
|
+
const reader = new FileReader()
|
|
412
|
+
reader.onload = () => resolve(String(reader.result || ''))
|
|
413
|
+
reader.onerror = () => reject(new Error('Could not read the selected file.'))
|
|
414
|
+
reader.readAsText(file)
|
|
415
|
+
})
|
|
416
|
+
|
|
417
|
+
const normalizeImportedDoc = (payload, fallbackDocId = '') => {
|
|
418
|
+
if (!payload || typeof payload !== 'object' || Array.isArray(payload))
|
|
419
|
+
throw new Error('Invalid JSON payload. Expected an object.')
|
|
420
|
+
|
|
421
|
+
if (payload.document && typeof payload.document === 'object' && !Array.isArray(payload.document)) {
|
|
422
|
+
const normalized = { ...payload.document }
|
|
423
|
+
if (!normalized.docId && payload.docId)
|
|
424
|
+
normalized.docId = payload.docId
|
|
425
|
+
if (!normalized.docId && fallbackDocId)
|
|
426
|
+
normalized.docId = fallbackDocId
|
|
427
|
+
return normalized
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
const normalized = { ...payload }
|
|
431
|
+
if (!normalized.docId && fallbackDocId)
|
|
432
|
+
normalized.docId = fallbackDocId
|
|
433
|
+
return normalized
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
const isPlainObject = value => !!value && typeof value === 'object' && !Array.isArray(value)
|
|
437
|
+
|
|
438
|
+
const cloneSchemaValue = (value) => {
|
|
439
|
+
if (isPlainObject(value) || Array.isArray(value))
|
|
440
|
+
return edgeGlobal.dupObject(value)
|
|
441
|
+
return value
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
const getDocDefaultsFromSchema = (schema = {}) => {
|
|
445
|
+
const defaults = {}
|
|
446
|
+
for (const [key, schemaEntry] of Object.entries(schema || {})) {
|
|
447
|
+
const hasValueProp = isPlainObject(schemaEntry) && Object.prototype.hasOwnProperty.call(schemaEntry, 'value')
|
|
448
|
+
const baseValue = hasValueProp ? schemaEntry.value : schemaEntry
|
|
449
|
+
defaults[key] = cloneSchemaValue(baseValue)
|
|
450
|
+
}
|
|
451
|
+
return defaults
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
const getBlockDocDefaults = () => getDocDefaultsFromSchema(blockNewDocSchema.value || {})
|
|
455
|
+
|
|
456
|
+
const validateImportedBlockDoc = (doc) => {
|
|
457
|
+
if (!isPlainObject(doc))
|
|
458
|
+
throw new Error('Invalid block document. Expected an object.')
|
|
459
|
+
|
|
460
|
+
const requiredKeys = Object.keys(blockNewDocSchema.value || {})
|
|
461
|
+
.filter(key => !OPTIONAL_BLOCK_IMPORT_KEYS.has(key))
|
|
462
|
+
const missing = requiredKeys.filter(key => !Object.prototype.hasOwnProperty.call(doc, key))
|
|
463
|
+
if (missing.length)
|
|
464
|
+
throw new Error(`Missing required block key(s): ${missing.join(', ')}`)
|
|
465
|
+
|
|
466
|
+
return doc
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
const validateImportedBlockThemes = (doc) => {
|
|
470
|
+
if (Object.prototype.hasOwnProperty.call(doc || {}, 'themes') && !Array.isArray(doc?.themes)) {
|
|
471
|
+
throw new Error('Invalid "themes" value. Expected an array of theme docIds.')
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
const importedThemes = Array.isArray(doc?.themes) ? doc.themes : []
|
|
475
|
+
if (!importedThemes.length)
|
|
476
|
+
return doc
|
|
477
|
+
|
|
478
|
+
const orgThemes = edgeFirebase.data?.[`organizations/${edgeGlobal.edgeState.currentOrganization}/themes`] || {}
|
|
479
|
+
const missingThemeIds = []
|
|
480
|
+
let hasEmptyThemeId = false
|
|
481
|
+
const normalizedThemes = []
|
|
482
|
+
for (const themeId of importedThemes) {
|
|
483
|
+
const normalizedThemeId = String(themeId || '').trim()
|
|
484
|
+
if (!normalizedThemeId) {
|
|
485
|
+
hasEmptyThemeId = true
|
|
486
|
+
continue
|
|
487
|
+
}
|
|
488
|
+
if (!orgThemes[normalizedThemeId]) {
|
|
489
|
+
missingThemeIds.push(normalizedThemeId)
|
|
490
|
+
continue
|
|
491
|
+
}
|
|
492
|
+
normalizedThemes.push(normalizedThemeId)
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
if (hasEmptyThemeId)
|
|
496
|
+
throw new Error('Themes include an empty theme id.')
|
|
497
|
+
if (missingThemeIds.length)
|
|
498
|
+
throw new Error(`Theme id(s) not found in this organization: ${[...new Set(missingThemeIds)].join(', ')}`)
|
|
499
|
+
|
|
500
|
+
doc.themes = [...new Set(normalizedThemes)]
|
|
501
|
+
return doc
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
const makeUniqueDocId = (baseDocId, docsMap = {}) => {
|
|
505
|
+
const cleanBase = String(baseDocId || '').trim() || 'block'
|
|
506
|
+
let nextDocId = `${cleanBase}-copy`
|
|
507
|
+
let suffix = 2
|
|
508
|
+
while (docsMap[nextDocId]) {
|
|
509
|
+
nextDocId = `${cleanBase}-copy-${suffix}`
|
|
510
|
+
suffix += 1
|
|
511
|
+
}
|
|
512
|
+
return nextDocId
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
const requestBlockImportDocId = (initialValue = '') => {
|
|
516
|
+
state.importDocIdValue = String(initialValue || '')
|
|
517
|
+
state.importDocIdDialogOpen = true
|
|
518
|
+
return new Promise((resolve) => {
|
|
519
|
+
blockImportDocIdResolver.value = resolve
|
|
520
|
+
})
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
const resolveBlockImportDocId = (value = '') => {
|
|
524
|
+
const resolver = blockImportDocIdResolver.value
|
|
525
|
+
blockImportDocIdResolver.value = null
|
|
526
|
+
state.importDocIdDialogOpen = false
|
|
527
|
+
if (resolver)
|
|
528
|
+
resolver(String(value || '').trim())
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
const requestBlockImportConflict = (docId) => {
|
|
532
|
+
state.importConflictDocId = String(docId || '')
|
|
533
|
+
state.importConflictDialogOpen = true
|
|
534
|
+
return new Promise((resolve) => {
|
|
535
|
+
blockImportConflictResolver.value = resolve
|
|
536
|
+
})
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
const resolveBlockImportConflict = (action = 'cancel') => {
|
|
540
|
+
const resolver = blockImportConflictResolver.value
|
|
541
|
+
blockImportConflictResolver.value = null
|
|
542
|
+
state.importConflictDialogOpen = false
|
|
543
|
+
if (resolver)
|
|
544
|
+
resolver(action)
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
watch(() => state.importDocIdDialogOpen, (open) => {
|
|
548
|
+
if (!open && blockImportDocIdResolver.value) {
|
|
549
|
+
const resolver = blockImportDocIdResolver.value
|
|
550
|
+
blockImportDocIdResolver.value = null
|
|
551
|
+
resolver('')
|
|
552
|
+
}
|
|
553
|
+
})
|
|
554
|
+
|
|
555
|
+
watch(() => state.importConflictDialogOpen, (open) => {
|
|
556
|
+
if (!open && blockImportConflictResolver.value) {
|
|
557
|
+
const resolver = blockImportConflictResolver.value
|
|
558
|
+
blockImportConflictResolver.value = null
|
|
559
|
+
resolver('cancel')
|
|
560
|
+
}
|
|
561
|
+
})
|
|
562
|
+
|
|
563
|
+
const getImportDocId = async (incomingDoc, fallbackDocId = '') => {
|
|
564
|
+
let nextDocId = String(incomingDoc?.docId || '').trim()
|
|
565
|
+
if (!nextDocId)
|
|
566
|
+
nextDocId = await requestBlockImportDocId(fallbackDocId)
|
|
567
|
+
if (!nextDocId)
|
|
568
|
+
throw new Error('Import canceled. A docId is required.')
|
|
569
|
+
if (nextDocId.includes('/'))
|
|
570
|
+
throw new Error('docId cannot include "/".')
|
|
571
|
+
return nextDocId
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
const openImportErrorDialog = (message) => {
|
|
575
|
+
state.importErrorMessage = String(message || DEFAULT_BLOCK_IMPORT_ERROR_MESSAGE)
|
|
576
|
+
state.importErrorDialogOpen = true
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
const getBlockImportFailureReason = (error, message) => {
|
|
580
|
+
if (error instanceof SyntaxError)
|
|
581
|
+
return `Invalid JSON syntax: ${String(error?.message || 'Unable to parse JSON.')}`
|
|
582
|
+
if (/^Import canceled\./i.test(message))
|
|
583
|
+
return message
|
|
584
|
+
return message
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
const logBlockImportFailure = (file, error, message) => {
|
|
588
|
+
const fileName = String(file?.name || 'unknown-file')
|
|
589
|
+
const reason = getBlockImportFailureReason(error, message)
|
|
590
|
+
console.error(`[BlocksManager] Block import failed for "${fileName}". Reason: ${reason}`, {
|
|
591
|
+
fileName,
|
|
592
|
+
reason,
|
|
593
|
+
errorMessage: message,
|
|
594
|
+
error,
|
|
595
|
+
})
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
const triggerBlockImport = () => {
|
|
599
|
+
blockImportInputRef.value?.click()
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
const importSingleBlockFile = async (file, existingBlocks = {}) => {
|
|
603
|
+
const fileText = await readTextFile(file)
|
|
604
|
+
const parsed = JSON.parse(fileText)
|
|
605
|
+
const importedDoc = validateImportedBlockThemes(validateImportedBlockDoc(normalizeImportedDoc(parsed, '')))
|
|
606
|
+
const incomingDocId = await getImportDocId(importedDoc, '')
|
|
607
|
+
let targetDocId = incomingDocId
|
|
608
|
+
let importDecision = 'create'
|
|
609
|
+
|
|
610
|
+
if (existingBlocks[targetDocId]) {
|
|
611
|
+
const decision = await requestBlockImportConflict(targetDocId)
|
|
612
|
+
if (decision === 'cancel')
|
|
613
|
+
return
|
|
614
|
+
if (decision === 'new') {
|
|
615
|
+
targetDocId = makeUniqueDocId(targetDocId, existingBlocks)
|
|
616
|
+
if (typeof importedDoc.name === 'string' && importedDoc.name.trim() && !/\(Copy\)$/i.test(importedDoc.name.trim()))
|
|
617
|
+
importedDoc.name = `${importedDoc.name} (Copy)`
|
|
618
|
+
importDecision = 'new'
|
|
619
|
+
}
|
|
620
|
+
else {
|
|
621
|
+
importDecision = 'overwrite'
|
|
622
|
+
}
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
const payload = { ...getBlockDocDefaults(), ...importedDoc, docId: targetDocId }
|
|
626
|
+
await edgeFirebase.storeDoc(blockCollectionPath.value, payload, targetDocId)
|
|
627
|
+
existingBlocks[targetDocId] = payload
|
|
628
|
+
|
|
629
|
+
if (importDecision === 'overwrite')
|
|
630
|
+
edgeFirebase?.toast?.success?.(`Overwrote block "${targetDocId}".`)
|
|
631
|
+
else if (importDecision === 'new')
|
|
632
|
+
edgeFirebase?.toast?.success?.(`Imported block as new "${targetDocId}".`)
|
|
633
|
+
else
|
|
634
|
+
edgeFirebase?.toast?.success?.(`Imported block "${targetDocId}".`)
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
const handleBlockImport = async (event) => {
|
|
638
|
+
const input = event?.target
|
|
639
|
+
const files = Array.from(input?.files || [])
|
|
640
|
+
if (!files.length)
|
|
641
|
+
return
|
|
642
|
+
|
|
643
|
+
state.importingJson = true
|
|
644
|
+
const existingBlocks = { ...(blocksCollection.value || {}) }
|
|
645
|
+
try {
|
|
646
|
+
const themesCollectionPath = `organizations/${edgeGlobal.edgeState.currentOrganization}/themes`
|
|
647
|
+
if (!edgeFirebase.data?.[themesCollectionPath])
|
|
648
|
+
await edgeFirebase.startSnapshot(themesCollectionPath)
|
|
649
|
+
|
|
650
|
+
for (const file of files) {
|
|
651
|
+
try {
|
|
652
|
+
await importSingleBlockFile(file, existingBlocks)
|
|
653
|
+
}
|
|
654
|
+
catch (error) {
|
|
655
|
+
const message = error?.message || DEFAULT_BLOCK_IMPORT_ERROR_MESSAGE
|
|
656
|
+
logBlockImportFailure(file, error, message)
|
|
657
|
+
if (/^Import canceled\./i.test(message))
|
|
658
|
+
continue
|
|
659
|
+
openImportErrorDialog(getBlockImportFailureReason(error, message))
|
|
660
|
+
}
|
|
661
|
+
}
|
|
662
|
+
}
|
|
663
|
+
finally {
|
|
664
|
+
state.importingJson = false
|
|
665
|
+
if (input)
|
|
666
|
+
input.value = ''
|
|
667
|
+
}
|
|
668
|
+
}
|
|
149
669
|
</script>
|
|
150
670
|
|
|
151
671
|
<template>
|
|
@@ -172,7 +692,7 @@ const listFilters = computed(() => {
|
|
|
172
692
|
</template>
|
|
173
693
|
<template #header-center>
|
|
174
694
|
<edge-shad-form class="w-full">
|
|
175
|
-
<div class="w-full px-4 md:px-6
|
|
695
|
+
<div class="w-full px-4 md:px-6 flex flex-col gap-2 md:flex-row md:items-center">
|
|
176
696
|
<div class="grow">
|
|
177
697
|
<edge-shad-input
|
|
178
698
|
v-model="state.filter"
|
|
@@ -200,80 +720,252 @@ const listFilters = computed(() => {
|
|
|
200
720
|
</div>
|
|
201
721
|
</edge-shad-form>
|
|
202
722
|
</template>
|
|
723
|
+
<template #header-end>
|
|
724
|
+
<div class="flex items-center gap-2">
|
|
725
|
+
<input
|
|
726
|
+
ref="blockImportInputRef"
|
|
727
|
+
type="file"
|
|
728
|
+
multiple
|
|
729
|
+
accept=".json,application/json"
|
|
730
|
+
class="hidden"
|
|
731
|
+
@change="handleBlockImport"
|
|
732
|
+
>
|
|
733
|
+
<edge-shad-button
|
|
734
|
+
type="button"
|
|
735
|
+
size="icon"
|
|
736
|
+
variant="outline"
|
|
737
|
+
class="h-9 w-9"
|
|
738
|
+
:disabled="state.importingJson"
|
|
739
|
+
title="Import Blocks"
|
|
740
|
+
aria-label="Import Blocks"
|
|
741
|
+
@click="triggerBlockImport"
|
|
742
|
+
>
|
|
743
|
+
<Loader2 v-if="state.importingJson" class="h-4 w-4 animate-spin" />
|
|
744
|
+
<Upload v-else class="h-4 w-4" />
|
|
745
|
+
</edge-shad-button>
|
|
746
|
+
<edge-shad-button class="uppercase bg-primary" to="/app/dashboard/blocks/new">
|
|
747
|
+
Add Block
|
|
748
|
+
</edge-shad-button>
|
|
749
|
+
</div>
|
|
750
|
+
</template>
|
|
203
751
|
<template #list="slotProps">
|
|
204
|
-
<div
|
|
205
|
-
class="
|
|
206
|
-
|
|
207
|
-
|
|
752
|
+
<div class="w-full pt-4 space-y-3">
|
|
753
|
+
<div class="flex flex-wrap items-center justify-between gap-2 rounded-md border border-border/50 bg-background/60 px-3 py-2">
|
|
754
|
+
<div class="flex items-center gap-2">
|
|
755
|
+
<Checkbox
|
|
756
|
+
:model-value="getVisibleSelectionState(applyTagSelectionFilter(slotProps.filtered))"
|
|
757
|
+
aria-label="Select visible blocks"
|
|
758
|
+
class="border-border bg-background/90 shadow-sm data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground"
|
|
759
|
+
@click.stop
|
|
760
|
+
@update:model-value="toggleVisibleBlockSelection(applyTagSelectionFilter(slotProps.filtered), $event)"
|
|
761
|
+
/>
|
|
762
|
+
<span class="text-xs text-muted-foreground">
|
|
763
|
+
Select visible ({{ applyTagSelectionFilter(slotProps.filtered).length }})
|
|
764
|
+
</span>
|
|
765
|
+
</div>
|
|
766
|
+
<div class="flex items-center gap-2">
|
|
767
|
+
<span class="text-xs text-muted-foreground">{{ selectedBlockCount }} selected</span>
|
|
768
|
+
<edge-shad-button
|
|
769
|
+
variant="outline"
|
|
770
|
+
class="h-8 text-xs"
|
|
771
|
+
:disabled="selectedBlockCount === 0"
|
|
772
|
+
@click="clearSelectedBlocks"
|
|
773
|
+
>
|
|
774
|
+
Clear
|
|
775
|
+
</edge-shad-button>
|
|
776
|
+
<edge-shad-button
|
|
777
|
+
variant="destructive"
|
|
778
|
+
class="h-8 text-xs text-white"
|
|
779
|
+
:disabled="selectedBlockCount === 0"
|
|
780
|
+
@click="openBulkDeleteDialog"
|
|
781
|
+
>
|
|
782
|
+
Delete selected
|
|
783
|
+
</edge-shad-button>
|
|
784
|
+
</div>
|
|
785
|
+
</div>
|
|
208
786
|
<div
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
role="button"
|
|
212
|
-
tabindex="0"
|
|
213
|
-
class="w-full h-full"
|
|
214
|
-
@click="router.push(`/app/dashboard/blocks/${item.docId}`)"
|
|
215
|
-
@keyup.enter="router.push(`/app/dashboard/blocks/${item.docId}`)"
|
|
787
|
+
class="grid gap-4 w-full"
|
|
788
|
+
style="grid-template-columns: repeat(auto-fill, minmax(260px, 1fr));"
|
|
216
789
|
>
|
|
217
|
-
<
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
:
|
|
237
|
-
|
|
238
|
-
:
|
|
790
|
+
<div
|
|
791
|
+
v-for="item in applyTagSelectionFilter(slotProps.filtered)"
|
|
792
|
+
:key="item.docId"
|
|
793
|
+
role="button"
|
|
794
|
+
tabindex="0"
|
|
795
|
+
class="w-full h-full"
|
|
796
|
+
@click="router.push(`/app/dashboard/blocks/${item.docId}`)"
|
|
797
|
+
@keyup.enter="router.push(`/app/dashboard/blocks/${item.docId}`)"
|
|
798
|
+
>
|
|
799
|
+
<Card
|
|
800
|
+
class="h-full cursor-pointer border border-border/60 bg-card/40 hover:border-primary/50 hover:shadow-[0_22px_55px_-24px_rgba(0,0,0,0.4)] transition"
|
|
801
|
+
:class="isBlockSelected(item.docId) ? 'border-primary/70 ring-2 ring-primary/50' : 'border-white/5'"
|
|
802
|
+
>
|
|
803
|
+
<CardContent class="flex flex-col gap-1 p-4 sm:p-5">
|
|
804
|
+
<div class="flex items-start gap-3">
|
|
805
|
+
<div class="pt-1" @click.stop>
|
|
806
|
+
<Checkbox
|
|
807
|
+
:model-value="isBlockSelected(item.docId)"
|
|
808
|
+
:aria-label="`Select block ${item.name || item.docId}`"
|
|
809
|
+
class="border-border bg-background/90 shadow-sm data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground"
|
|
810
|
+
@click.stop
|
|
811
|
+
@update:model-value="setBlockSelection(item.docId, $event)"
|
|
239
812
|
/>
|
|
240
813
|
</div>
|
|
814
|
+
<p class="text-lg font-semibold leading-snug line-clamp-2 text-foreground flex-1">
|
|
815
|
+
{{ item.name }}
|
|
816
|
+
</p>
|
|
817
|
+
<edge-shad-button
|
|
818
|
+
size="icon"
|
|
819
|
+
variant="ghost"
|
|
820
|
+
class="h-8 w-8 text-foreground/75 hover:text-foreground hover:bg-muted/80"
|
|
821
|
+
@click.stop="slotProps.deleteItem(item.docId)"
|
|
822
|
+
>
|
|
823
|
+
<Trash class="h-4 w-4" />
|
|
824
|
+
</edge-shad-button>
|
|
241
825
|
</div>
|
|
242
|
-
<div class="preview
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
</
|
|
255
|
-
<
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
class="text-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
826
|
+
<div v-if="item.content" class="block-preview" :class="previewSurfaceClass(item.previewType)">
|
|
827
|
+
<div class="scale-wrapper">
|
|
828
|
+
<div class="scale-inner scale p-4">
|
|
829
|
+
<edge-cms-block-render
|
|
830
|
+
:content="loadingRender(item.content)"
|
|
831
|
+
:values="item.values"
|
|
832
|
+
:meta="item.meta"
|
|
833
|
+
:theme="getPreviewThemeForBlock(item)"
|
|
834
|
+
/>
|
|
835
|
+
</div>
|
|
836
|
+
</div>
|
|
837
|
+
<div class="preview-overlay" />
|
|
838
|
+
</div>
|
|
839
|
+
<div v-else class="block-preview-empty" :class="previewSurfaceClass(item.previewType)">
|
|
840
|
+
Preview unavailable for this block.
|
|
841
|
+
</div>
|
|
842
|
+
<div class="flex flex-wrap items-center gap-1 text-[11px] text-slate-300 uppercase tracking-wide overflow-hidden">
|
|
843
|
+
<edge-chip
|
|
844
|
+
v-for="tag in item.tags?.slice(0, 3) ?? []"
|
|
845
|
+
:key="tag"
|
|
846
|
+
class="bg-primary/40 text-white px-2 py-0.5 text-[10px]"
|
|
847
|
+
>
|
|
848
|
+
{{ tag }}
|
|
849
|
+
</edge-chip>
|
|
850
|
+
<span v-if="item.tags?.length > 3" class="text-white/60">+{{ item.tags.length - 3 }}</span>
|
|
851
|
+
<edge-chip
|
|
852
|
+
v-for="theme in item.themes?.slice(0, 2) ?? []"
|
|
853
|
+
:key="theme"
|
|
854
|
+
class="bg-slate-800 text-white px-2 py-0.5 text-[10px]"
|
|
855
|
+
>
|
|
856
|
+
{{ getThemeFromId(theme) }}
|
|
857
|
+
</edge-chip>
|
|
858
|
+
<span v-if="item.themes?.length > 2" class="text-white/60">+{{ item.themes.length - 2 }}</span>
|
|
859
|
+
<span
|
|
860
|
+
v-if="!(item.tags?.length) && !(item.themes?.length)"
|
|
861
|
+
class="text-slate-500 lowercase"
|
|
862
|
+
>
|
|
863
|
+
none
|
|
864
|
+
</span>
|
|
865
|
+
</div>
|
|
866
|
+
</CardContent>
|
|
867
|
+
</Card>
|
|
868
|
+
</div>
|
|
273
869
|
</div>
|
|
274
870
|
</div>
|
|
275
871
|
</template>
|
|
276
872
|
</edge-dashboard>
|
|
873
|
+
<edge-shad-dialog v-model="state.importDocIdDialogOpen">
|
|
874
|
+
<DialogContent class="pt-8">
|
|
875
|
+
<DialogHeader>
|
|
876
|
+
<DialogTitle class="text-left">
|
|
877
|
+
Enter Block Doc ID
|
|
878
|
+
</DialogTitle>
|
|
879
|
+
<DialogDescription>
|
|
880
|
+
This JSON file does not include a <code>docId</code>. Enter the doc ID you want to import into.
|
|
881
|
+
</DialogDescription>
|
|
882
|
+
</DialogHeader>
|
|
883
|
+
<edge-shad-input
|
|
884
|
+
v-model="state.importDocIdValue"
|
|
885
|
+
name="block-import-doc-id"
|
|
886
|
+
label="Doc ID"
|
|
887
|
+
placeholder="example-block-id"
|
|
888
|
+
/>
|
|
889
|
+
<DialogFooter class="pt-2 flex justify-between">
|
|
890
|
+
<edge-shad-button variant="outline" @click="resolveBlockImportDocId('')">
|
|
891
|
+
Cancel
|
|
892
|
+
</edge-shad-button>
|
|
893
|
+
<edge-shad-button @click="resolveBlockImportDocId(state.importDocIdValue)">
|
|
894
|
+
Continue
|
|
895
|
+
</edge-shad-button>
|
|
896
|
+
</DialogFooter>
|
|
897
|
+
</DialogContent>
|
|
898
|
+
</edge-shad-dialog>
|
|
899
|
+
<edge-shad-dialog v-model="state.importConflictDialogOpen">
|
|
900
|
+
<DialogContent class="pt-8">
|
|
901
|
+
<DialogHeader>
|
|
902
|
+
<DialogTitle class="text-left">
|
|
903
|
+
Block Already Exists
|
|
904
|
+
</DialogTitle>
|
|
905
|
+
<DialogDescription>
|
|
906
|
+
<code>{{ state.importConflictDocId }}</code> already exists. Choose to overwrite it or import as a new block.
|
|
907
|
+
</DialogDescription>
|
|
908
|
+
</DialogHeader>
|
|
909
|
+
<DialogFooter class="pt-2 flex justify-between">
|
|
910
|
+
<edge-shad-button variant="outline" @click="resolveBlockImportConflict('cancel')">
|
|
911
|
+
Cancel
|
|
912
|
+
</edge-shad-button>
|
|
913
|
+
<edge-shad-button variant="outline" @click="resolveBlockImportConflict('new')">
|
|
914
|
+
Add As New
|
|
915
|
+
</edge-shad-button>
|
|
916
|
+
<edge-shad-button @click="resolveBlockImportConflict('overwrite')">
|
|
917
|
+
Overwrite
|
|
918
|
+
</edge-shad-button>
|
|
919
|
+
</DialogFooter>
|
|
920
|
+
</DialogContent>
|
|
921
|
+
</edge-shad-dialog>
|
|
922
|
+
<edge-shad-dialog v-model="state.importErrorDialogOpen">
|
|
923
|
+
<DialogContent class="pt-8">
|
|
924
|
+
<DialogHeader>
|
|
925
|
+
<DialogTitle class="text-left">
|
|
926
|
+
Import Failed
|
|
927
|
+
</DialogTitle>
|
|
928
|
+
<DialogDescription class="text-left">
|
|
929
|
+
{{ state.importErrorMessage }}
|
|
930
|
+
</DialogDescription>
|
|
931
|
+
</DialogHeader>
|
|
932
|
+
<DialogFooter class="pt-2">
|
|
933
|
+
<edge-shad-button @click="state.importErrorDialogOpen = false">
|
|
934
|
+
Close
|
|
935
|
+
</edge-shad-button>
|
|
936
|
+
</DialogFooter>
|
|
937
|
+
</DialogContent>
|
|
938
|
+
</edge-shad-dialog>
|
|
939
|
+
<edge-shad-dialog v-model="state.bulkDeleteDialogOpen">
|
|
940
|
+
<DialogContent class="pt-8">
|
|
941
|
+
<DialogHeader>
|
|
942
|
+
<DialogTitle class="text-left">
|
|
943
|
+
Delete Selected Blocks?
|
|
944
|
+
</DialogTitle>
|
|
945
|
+
<DialogDescription class="text-left">
|
|
946
|
+
This action cannot be undone. {{ selectedBlockCount }} block{{ selectedBlockCount === 1 ? '' : 's' }} will be permanently deleted.
|
|
947
|
+
</DialogDescription>
|
|
948
|
+
</DialogHeader>
|
|
949
|
+
<DialogFooter class="pt-2 flex justify-between">
|
|
950
|
+
<edge-shad-button
|
|
951
|
+
class="text-white bg-slate-800 hover:bg-slate-400"
|
|
952
|
+
:disabled="state.bulkDeleting"
|
|
953
|
+
@click="state.bulkDeleteDialogOpen = false"
|
|
954
|
+
>
|
|
955
|
+
Cancel
|
|
956
|
+
</edge-shad-button>
|
|
957
|
+
<edge-shad-button
|
|
958
|
+
variant="destructive"
|
|
959
|
+
class="text-white w-full"
|
|
960
|
+
:disabled="state.bulkDeleting || selectedBlockCount === 0"
|
|
961
|
+
@click="bulkDeleteAction"
|
|
962
|
+
>
|
|
963
|
+
<Loader2 v-if="state.bulkDeleting" class="h-4 w-4 animate-spin" />
|
|
964
|
+
<span v-else>Delete Selected</span>
|
|
965
|
+
</edge-shad-button>
|
|
966
|
+
</DialogFooter>
|
|
967
|
+
</DialogContent>
|
|
968
|
+
</edge-shad-dialog>
|
|
277
969
|
</div>
|
|
278
970
|
</template>
|
|
279
971
|
|
|
@@ -282,15 +974,22 @@ const listFilters = computed(() => {
|
|
|
282
974
|
position: relative;
|
|
283
975
|
height: 220px;
|
|
284
976
|
border-radius: 14px;
|
|
285
|
-
border: 1px solid rgba(
|
|
286
|
-
background:
|
|
287
|
-
radial-gradient(140% 120% at 15% 15%, rgba(96, 165, 250, 0.08), transparent),
|
|
288
|
-
radial-gradient(120% 120% at 85% 0%, rgba(168, 85, 247, 0.07), transparent),
|
|
289
|
-
linear-gradient(145deg, rgba(10, 14, 26, 0.95), rgba(17, 24, 39, 0.7));
|
|
977
|
+
border: 1px solid rgba(148, 163, 184, 0.28);
|
|
978
|
+
background: transparent;
|
|
290
979
|
overflow: hidden;
|
|
291
|
-
box-shadow:
|
|
292
|
-
|
|
293
|
-
|
|
980
|
+
box-shadow: none;
|
|
981
|
+
}
|
|
982
|
+
|
|
983
|
+
.block-preview.preview-surface-light {
|
|
984
|
+
background: #ffffff;
|
|
985
|
+
color: #0f172a;
|
|
986
|
+
border-color: rgba(15, 23, 42, 0.15);
|
|
987
|
+
}
|
|
988
|
+
|
|
989
|
+
.block-preview.preview-surface-dark {
|
|
990
|
+
background: #020617;
|
|
991
|
+
color: #f8fafc;
|
|
992
|
+
border-color: rgba(248, 250, 252, 0.18);
|
|
294
993
|
}
|
|
295
994
|
|
|
296
995
|
.block-preview-empty {
|
|
@@ -305,11 +1004,20 @@ const listFilters = computed(() => {
|
|
|
305
1004
|
letter-spacing: 0.01em;
|
|
306
1005
|
}
|
|
307
1006
|
|
|
1007
|
+
.block-preview-empty.preview-surface-light {
|
|
1008
|
+
border-color: rgba(15, 23, 42, 0.18);
|
|
1009
|
+
background: linear-gradient(135deg, rgba(255, 255, 255, 0.95), rgba(241, 245, 249, 0.95));
|
|
1010
|
+
color: rgba(15, 23, 42, 0.72);
|
|
1011
|
+
}
|
|
1012
|
+
|
|
1013
|
+
.block-preview-empty.preview-surface-dark {
|
|
1014
|
+
border-color: rgba(255, 255, 255, 0.12);
|
|
1015
|
+
background: linear-gradient(135deg, rgba(10, 14, 26, 0.65), rgba(17, 24, 39, 0.5));
|
|
1016
|
+
color: rgba(255, 255, 255, 0.6);
|
|
1017
|
+
}
|
|
1018
|
+
|
|
308
1019
|
.preview-overlay {
|
|
309
|
-
|
|
310
|
-
position: absolute;
|
|
311
|
-
inset: 0;
|
|
312
|
-
background: linear-gradient(180deg, rgba(15, 23, 42, 0) 20%, rgba(15, 23, 42, 0.35) 100%);
|
|
1020
|
+
display: none;
|
|
313
1021
|
}
|
|
314
1022
|
|
|
315
1023
|
.scale-wrapper {
|