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