@edgedev/create-edge-app 1.1.25 → 1.1.26
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +55 -20
- package/{agent.md → agents.md} +2 -0
- package/bin/cli.js +6 -6
- package/edge/components/auth/login.vue +384 -0
- package/edge/components/auth/register.vue +396 -0
- package/edge/components/auth.vue +108 -0
- package/edge/components/autoFileUpload.vue +215 -0
- package/edge/components/billing.vue +8 -0
- package/edge/components/buttonDivider.vue +14 -0
- package/edge/components/chip.vue +34 -0
- package/edge/components/clipboardButton.vue +42 -0
- package/edge/components/cms/block.vue +529 -0
- package/edge/components/cms/blockApi.vue +212 -0
- package/edge/components/cms/blockEditor.vue +725 -0
- package/edge/components/cms/blockInput.vue +66 -0
- package/edge/components/cms/blockPicker.vue +486 -0
- package/edge/components/cms/blockRender.vue +78 -0
- package/edge/components/cms/blockSheetContent.vue +28 -0
- package/edge/components/cms/codeEditor.vue +466 -0
- package/edge/components/cms/fontUpload.vue +327 -0
- package/edge/components/cms/htmlContent.vue +807 -0
- package/edge/components/cms/init_blocks/api_with_subarrays.html +17 -0
- package/edge/components/cms/init_blocks/array_with_collection.html +7 -0
- package/edge/components/cms/init_blocks/array_with_objects.html +7 -0
- package/edge/components/cms/init_blocks/carousel.html +103 -0
- package/edge/components/cms/init_blocks/contact_us.html +69 -0
- package/edge/components/cms/init_blocks/content_with_left_image.html +27 -0
- package/edge/components/cms/init_blocks/footer.html +24 -0
- package/edge/components/cms/init_blocks/header_divider.html +7 -0
- package/edge/components/cms/init_blocks/hero.html +35 -0
- package/edge/components/cms/init_blocks/hero_carousel.html +52 -0
- package/edge/components/cms/init_blocks/newsletter.html +117 -0
- package/edge/components/cms/init_blocks/post_content.html +7 -0
- package/edge/components/cms/init_blocks/post_title_header.html +21 -0
- package/edge/components/cms/init_blocks/posts_list.html +20 -0
- package/edge/components/cms/init_blocks/properties_showcase.html +100 -0
- package/edge/components/cms/init_blocks/property_carousel.html +59 -0
- package/edge/components/cms/init_blocks/property_detail.html +112 -0
- package/edge/components/cms/init_blocks/property_detail_header.html +34 -0
- package/edge/components/cms/init_blocks/property_results.html +137 -0
- package/edge/components/cms/init_blocks/property_search.html +75 -0
- package/edge/components/cms/init_blocks/simple_array.html +7 -0
- package/edge/components/cms/mediaCard.vue +116 -0
- package/edge/components/cms/mediaManager.vue +386 -0
- package/edge/components/cms/menu.vue +1103 -0
- package/edge/components/cms/optionsSelect.vue +107 -0
- package/edge/components/cms/page.vue +1785 -0
- package/edge/components/cms/posts.vue +1083 -0
- package/edge/components/cms/site.vue +1298 -0
- package/edge/components/cms/themeDefaultMenu.vue +548 -0
- package/edge/components/cms/themeEditor.vue +426 -0
- package/edge/components/dashboard.vue +776 -0
- package/edge/components/editor.vue +671 -0
- package/edge/components/fileTree.vue +72 -0
- package/edge/components/files.vue +89 -0
- package/edge/components/formSubtypes/myOrgs.vue +214 -0
- package/edge/components/formSubtypes/users.vue +336 -0
- package/edge/components/functionChips.vue +57 -0
- package/edge/components/gError.vue +98 -0
- package/edge/components/gHelper.vue +67 -0
- package/edge/components/gInput.vue +1331 -0
- package/edge/components/loggingIn.vue +41 -0
- package/edge/components/menu.vue +137 -0
- package/edge/components/menuContent.vue +132 -0
- package/edge/components/myAccount.vue +317 -0
- package/edge/components/myOrganizations.vue +75 -0
- package/edge/components/myProfile.vue +122 -0
- package/edge/components/orgSwitcher.vue +25 -0
- package/edge/components/organizationMembers.vue +522 -0
- package/edge/components/organizationSettings.vue +271 -0
- package/edge/components/shad/breadcrumbs.vue +35 -0
- package/edge/components/shad/button.vue +43 -0
- package/edge/components/shad/checkbox.vue +73 -0
- package/edge/components/shad/combobox.vue +238 -0
- package/edge/components/shad/datepicker.vue +184 -0
- package/edge/components/shad/dialog.vue +32 -0
- package/edge/components/shad/dropdownMenu.vue +54 -0
- package/edge/components/shad/dropdownMenuItem.vue +21 -0
- package/edge/components/shad/form.vue +59 -0
- package/edge/components/shad/html.vue +877 -0
- package/edge/components/shad/input.vue +139 -0
- package/edge/components/shad/number.vue +109 -0
- package/edge/components/shad/select.vue +151 -0
- package/edge/components/shad/selectTags.vue +278 -0
- package/edge/components/shad/switch.vue +67 -0
- package/edge/components/shad/tags.vue +137 -0
- package/edge/components/shad/textarea.vue +102 -0
- package/edge/components/shad/typeMoney.vue +167 -0
- package/edge/components/sideBar.vue +288 -0
- package/edge/components/sideBarContent.vue +268 -0
- package/edge/components/sidebarProvider.vue +33 -0
- package/edge/components/tooltip.vue +16 -0
- package/edge/components/userMenu.vue +148 -0
- package/edge/components/v/alert.vue +59 -0
- package/edge/components/v/alertTitle.vue +18 -0
- package/edge/components/v/card.vue +53 -0
- package/edge/components/v/cardActions.vue +18 -0
- package/edge/components/v/cardText.vue +18 -0
- package/edge/components/v/cardTitle.vue +20 -0
- package/edge/components/v/col.vue +56 -0
- package/edge/components/v/list.vue +46 -0
- package/edge/components/v/listItem.vue +26 -0
- package/edge/components/v/listItemTitle.vue +18 -0
- package/edge/components/v/row.vue +42 -0
- package/edge/components/v/toolbar.vue +24 -0
- package/edge/composables/global.ts +519 -0
- package/edge-pull.sh +2 -0
- package/edge-push.sh +1 -0
- package/edge-status.sh +14 -0
- package/package.json +1 -1
- package/edge-components-install.sh +0 -1
|
@@ -0,0 +1,725 @@
|
|
|
1
|
+
<script setup>
|
|
2
|
+
import { Maximize2, Monitor, Smartphone, Tablet } from 'lucide-vue-next'
|
|
3
|
+
import { toTypedSchema } from '@vee-validate/zod'
|
|
4
|
+
import * as z from 'zod'
|
|
5
|
+
const props = defineProps({
|
|
6
|
+
blockId: {
|
|
7
|
+
type: String,
|
|
8
|
+
required: true,
|
|
9
|
+
},
|
|
10
|
+
})
|
|
11
|
+
|
|
12
|
+
const emit = defineEmits(['head'])
|
|
13
|
+
|
|
14
|
+
const edgeFirebase = inject('edgeFirebase')
|
|
15
|
+
|
|
16
|
+
const route = useRoute()
|
|
17
|
+
|
|
18
|
+
const state = reactive({
|
|
19
|
+
filter: '',
|
|
20
|
+
newDocs: {
|
|
21
|
+
blocks: {
|
|
22
|
+
name: { value: '' },
|
|
23
|
+
content: { value: '' },
|
|
24
|
+
tags: { value: [] },
|
|
25
|
+
themes: { value: [] },
|
|
26
|
+
synced: { value: false },
|
|
27
|
+
version: 1,
|
|
28
|
+
},
|
|
29
|
+
},
|
|
30
|
+
mounted: false,
|
|
31
|
+
workingDoc: {},
|
|
32
|
+
loading: false,
|
|
33
|
+
jsonEditorOpen: false,
|
|
34
|
+
jsonEditorContent: '',
|
|
35
|
+
jsonEditorError: '',
|
|
36
|
+
editingContext: null,
|
|
37
|
+
renderSite: '',
|
|
38
|
+
initialBlocksSeeded: false,
|
|
39
|
+
seedingInitialBlocks: false,
|
|
40
|
+
previewViewport: 'full',
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
const blockSchema = toTypedSchema(z.object({
|
|
44
|
+
name: z.string({
|
|
45
|
+
required_error: 'Name is required',
|
|
46
|
+
}).min(1, { message: 'Name is required' }),
|
|
47
|
+
}))
|
|
48
|
+
|
|
49
|
+
const previewViewportOptions = [
|
|
50
|
+
{ id: 'full', label: 'Wild Width', width: '100%', icon: Maximize2 },
|
|
51
|
+
{ id: 'large', label: 'Large Screen', width: '1280px', icon: Monitor },
|
|
52
|
+
{ id: 'medium', label: 'Medium', width: '992px', icon: Tablet },
|
|
53
|
+
{ id: 'mobile', label: 'Mobile', width: '420px', icon: Smartphone },
|
|
54
|
+
]
|
|
55
|
+
|
|
56
|
+
const selectedPreviewViewport = computed(() => previewViewportOptions.find(option => option.id === state.previewViewport) || previewViewportOptions[0])
|
|
57
|
+
|
|
58
|
+
const previewViewportStyle = computed(() => {
|
|
59
|
+
const selected = selectedPreviewViewport.value
|
|
60
|
+
if (!selected || selected.id === 'full')
|
|
61
|
+
return { maxWidth: '100%' }
|
|
62
|
+
return {
|
|
63
|
+
width: '100%',
|
|
64
|
+
maxWidth: selected.width,
|
|
65
|
+
marginLeft: 'auto',
|
|
66
|
+
marginRight: 'auto',
|
|
67
|
+
}
|
|
68
|
+
})
|
|
69
|
+
|
|
70
|
+
const setPreviewViewport = (viewportId) => {
|
|
71
|
+
state.previewViewport = viewportId
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const previewViewportMode = computed(() => {
|
|
75
|
+
if (state.previewViewport === 'full')
|
|
76
|
+
return 'auto'
|
|
77
|
+
return state.previewViewport
|
|
78
|
+
})
|
|
79
|
+
|
|
80
|
+
onMounted(() => {
|
|
81
|
+
// state.mounted = true
|
|
82
|
+
})
|
|
83
|
+
|
|
84
|
+
const PLACEHOLDERS = {
|
|
85
|
+
text: 'Lorem ipsum dolor sit amet.',
|
|
86
|
+
textarea: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.',
|
|
87
|
+
richtext: '<p>Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.</p>',
|
|
88
|
+
arrayItem: [
|
|
89
|
+
'Lorem ipsum dolor sit amet.',
|
|
90
|
+
'Consectetur adipiscing elit.',
|
|
91
|
+
'Sed do eiusmod tempor incididunt.',
|
|
92
|
+
],
|
|
93
|
+
image: '/images/filler.png',
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const contentEditorRef = ref(null)
|
|
97
|
+
|
|
98
|
+
const BLOCK_CONTENT_SNIPPETS = [
|
|
99
|
+
{
|
|
100
|
+
label: 'Text Field',
|
|
101
|
+
snippet: '{{{#text {"field": "fieldName", "value": "" }}}}',
|
|
102
|
+
description: 'Simple text field placeholder',
|
|
103
|
+
},
|
|
104
|
+
{
|
|
105
|
+
label: 'Text Area',
|
|
106
|
+
snippet: '{{{#textarea {"field": "fieldName", "value": "" }}}}',
|
|
107
|
+
description: 'Textarea field placeholder',
|
|
108
|
+
},
|
|
109
|
+
{
|
|
110
|
+
label: 'Rich Text',
|
|
111
|
+
snippet: '{{{#richtext {"field": "content", "value": "" }}}}',
|
|
112
|
+
description: 'Rich text field placeholder',
|
|
113
|
+
},
|
|
114
|
+
{
|
|
115
|
+
label: 'Image',
|
|
116
|
+
snippet: '{{{#image {"field": "imageField", "value": "", "tags": ["Backgrounds"] }}}}',
|
|
117
|
+
description: 'Image field placeholder',
|
|
118
|
+
},
|
|
119
|
+
{
|
|
120
|
+
label: 'Array (Basic)',
|
|
121
|
+
snippet: `{{{#array {"field": "items", "value": [] }}}}
|
|
122
|
+
<!-- iterate with {{item}} -->
|
|
123
|
+
{{{/array}}}`,
|
|
124
|
+
description: 'Basic repeating array block',
|
|
125
|
+
},
|
|
126
|
+
{
|
|
127
|
+
label: 'Array (API)',
|
|
128
|
+
snippet: `{{{#array {"field":"List","schema":{"listing_price":"money","square_feet":"number","acres":"number"},"api":"https://api.clearwaterproperties.com/api/front/properties","apiField":"data","apiQuery":"?limit=20&filter_scope[agent][]=mt_nmar-mt.545000478","queryOptions":[{"field":"sort","optionsKey":"label","optionsValue":"value","options":[{"label":"Highest Price","value":"listing_price"},{"label":"Lowest Price","value":"-listing_price"},{"label":"Newest","value":"-list_date"}]},{"field":"filter_scope[agent][]","title":"Agent","optionsKey":"name","optionsValue":"mls.primary","options":"users"}],"limit":10,"value":[]}}}}
|
|
129
|
+
<!-- iterate with {{item}} -->
|
|
130
|
+
{{{/array}}}`,
|
|
131
|
+
description: 'Array pulling data from an API',
|
|
132
|
+
},
|
|
133
|
+
{
|
|
134
|
+
label: 'Array (Collection)',
|
|
135
|
+
snippet: `{{{#array {"field":"list","schema":[{"field":"name","value":"text"}],"collection":{"path":"users","query":[{"field":"name","operator":">","value":""}],"order":[{"field":"name","direction":"asc"}]},"queryOptions":[{"field":"office_id","title":"Office","optionsKey":"label","optionsValue":"value","options":[{"label":"Office 1","value":"7"},{"label":"Office 2","value":"39"},{"label":"Office 3","value":"32"}]},{"field":"userId","title":"Agent","optionsKey":"name","optionsValue":"userId","options":"users"}],"limit":100,"value":[]}}}}
|
|
136
|
+
<h1 class="text-4xl">
|
|
137
|
+
{{item.name}}
|
|
138
|
+
</h1>
|
|
139
|
+
{{{/array}}}`,
|
|
140
|
+
description: 'Array pulling data from a collection',
|
|
141
|
+
},
|
|
142
|
+
{
|
|
143
|
+
label: 'Subarray',
|
|
144
|
+
snippet: `{{{#subarray:child {"field": "item.children", "limit": 0 }}}}
|
|
145
|
+
{{child}}
|
|
146
|
+
{{{/subarray}}}`,
|
|
147
|
+
description: 'Nested array inside an array item',
|
|
148
|
+
},
|
|
149
|
+
{
|
|
150
|
+
label: 'If / Else',
|
|
151
|
+
snippet: `{{{#if {"cond": "condition" }}}}
|
|
152
|
+
<!-- content when condition is true -->
|
|
153
|
+
{{{#else}}}
|
|
154
|
+
<!-- content when condition is false -->
|
|
155
|
+
{{{/if}}}`,
|
|
156
|
+
description: 'Conditional block with optional else',
|
|
157
|
+
},
|
|
158
|
+
]
|
|
159
|
+
|
|
160
|
+
function insertBlockContentSnippet(snippet) {
|
|
161
|
+
if (!snippet)
|
|
162
|
+
return
|
|
163
|
+
const editor = contentEditorRef.value
|
|
164
|
+
if (!editor || typeof editor.insertSnippet !== 'function') {
|
|
165
|
+
console.warn('Block content editor is not ready for snippet insertion')
|
|
166
|
+
return
|
|
167
|
+
}
|
|
168
|
+
editor.insertSnippet(snippet)
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function normalizeConfigLiteral(str) {
|
|
172
|
+
// ensure keys are quoted: { title: "x", field: "y" } -> { "title": "x", "field": "y" }
|
|
173
|
+
return str
|
|
174
|
+
.replace(/(\{|,)\s*([A-Za-z_][\w-]*)\s*:/g, '$1"$2":')
|
|
175
|
+
// allow single quotes too
|
|
176
|
+
.replace(/'/g, '"')
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
function safeParseConfig(raw) {
|
|
180
|
+
try {
|
|
181
|
+
return JSON.parse(normalizeConfigLiteral(raw))
|
|
182
|
+
}
|
|
183
|
+
catch {
|
|
184
|
+
return null
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// --- Robust tag parsing: supports nested objects/arrays in the config ---
|
|
189
|
+
// Matches `{{{#<type> { ... }}}}` and extracts a *balanced* `{ ... }` blob.
|
|
190
|
+
const TAG_START_RE = /\{\{\{\#([A-Za-z0-9_-]+)\s*\{/g
|
|
191
|
+
|
|
192
|
+
function findMatchingBrace(str, startIdx) {
|
|
193
|
+
// startIdx points at the opening '{' of the config
|
|
194
|
+
let depth = 0
|
|
195
|
+
let inString = false
|
|
196
|
+
let quote = null
|
|
197
|
+
let escape = false
|
|
198
|
+
for (let i = startIdx; i < str.length; i++) {
|
|
199
|
+
const ch = str[i]
|
|
200
|
+
if (inString) {
|
|
201
|
+
if (escape) {
|
|
202
|
+
escape = false
|
|
203
|
+
continue
|
|
204
|
+
}
|
|
205
|
+
if (ch === '\\') {
|
|
206
|
+
escape = true
|
|
207
|
+
continue
|
|
208
|
+
}
|
|
209
|
+
if (ch === quote) {
|
|
210
|
+
inString = false
|
|
211
|
+
quote = null
|
|
212
|
+
}
|
|
213
|
+
continue
|
|
214
|
+
}
|
|
215
|
+
if (ch === '"' || ch === '\'') {
|
|
216
|
+
inString = true
|
|
217
|
+
quote = ch
|
|
218
|
+
continue
|
|
219
|
+
}
|
|
220
|
+
if (ch === '{')
|
|
221
|
+
depth++
|
|
222
|
+
else if (ch === '}') {
|
|
223
|
+
depth--
|
|
224
|
+
if (depth === 0)
|
|
225
|
+
return i
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
return -1
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
function* iterateTags(html) {
|
|
232
|
+
TAG_START_RE.lastIndex = 0
|
|
233
|
+
for (;;) {
|
|
234
|
+
const m = TAG_START_RE.exec(html)
|
|
235
|
+
if (!m)
|
|
236
|
+
break
|
|
237
|
+
|
|
238
|
+
const type = m[1]
|
|
239
|
+
const configStart = TAG_START_RE.lastIndex - 1
|
|
240
|
+
if (configStart < 0 || html[configStart] !== '{')
|
|
241
|
+
continue
|
|
242
|
+
|
|
243
|
+
const configEnd = findMatchingBrace(html, configStart)
|
|
244
|
+
if (configEnd === -1)
|
|
245
|
+
continue
|
|
246
|
+
|
|
247
|
+
const rawCfg = html.slice(configStart, configEnd + 1)
|
|
248
|
+
const tagStart = m.index
|
|
249
|
+
const closeTriple = html.indexOf('}}}', configEnd)
|
|
250
|
+
const tagEnd = closeTriple !== -1 ? closeTriple + 3 : configEnd + 1
|
|
251
|
+
|
|
252
|
+
yield { type, rawCfg, tagStart, tagEnd, configStart, configEnd }
|
|
253
|
+
|
|
254
|
+
TAG_START_RE.lastIndex = tagEnd
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
function findTagAtOffset(html, offset) {
|
|
259
|
+
for (const tag of iterateTags(html)) {
|
|
260
|
+
if (offset >= tag.tagStart && offset <= tag.tagEnd)
|
|
261
|
+
return tag
|
|
262
|
+
}
|
|
263
|
+
return null
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
const blockModel = (html) => {
|
|
267
|
+
const values = {}
|
|
268
|
+
const meta = {}
|
|
269
|
+
|
|
270
|
+
if (!html)
|
|
271
|
+
return { values, meta }
|
|
272
|
+
|
|
273
|
+
for (const { type, rawCfg } of iterateTags(html)) {
|
|
274
|
+
const cfg = safeParseConfig(rawCfg)
|
|
275
|
+
if (!cfg || !cfg.field)
|
|
276
|
+
continue
|
|
277
|
+
|
|
278
|
+
const field = String(cfg.field)
|
|
279
|
+
const title = cfg.title != null ? String(cfg.title) : ''
|
|
280
|
+
|
|
281
|
+
const { value: _omitValue, field: _omitField, ...rest } = cfg
|
|
282
|
+
meta[field] = { type, ...rest, title }
|
|
283
|
+
|
|
284
|
+
let val = cfg.value
|
|
285
|
+
|
|
286
|
+
if (type === 'image') {
|
|
287
|
+
val = !val ? PLACEHOLDERS.image : String(val)
|
|
288
|
+
}
|
|
289
|
+
else if (type === 'text') {
|
|
290
|
+
val = !val ? PLACEHOLDERS.text : String(val)
|
|
291
|
+
}
|
|
292
|
+
else if (type === 'array') {
|
|
293
|
+
if (meta[field]?.limit > 0) {
|
|
294
|
+
val = Array(meta[field].limit).fill('placeholder')
|
|
295
|
+
}
|
|
296
|
+
else {
|
|
297
|
+
if (Array.isArray(val)) {
|
|
298
|
+
console.log('Array value detected for field:', field, 'with value:', val)
|
|
299
|
+
if (val.length === 0) {
|
|
300
|
+
val = PLACEHOLDERS.arrayItem
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
else {
|
|
304
|
+
val = PLACEHOLDERS.arrayItem
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
else if (type === 'textarea') {
|
|
309
|
+
val = !val ? PLACEHOLDERS.textarea : String(val)
|
|
310
|
+
}
|
|
311
|
+
else if (type === 'richtext') {
|
|
312
|
+
val = !val ? PLACEHOLDERS.richtext : String(val)
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
values[field] = val
|
|
316
|
+
}
|
|
317
|
+
return { values, meta }
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
function resetJsonEditorState() {
|
|
321
|
+
state.jsonEditorContent = ''
|
|
322
|
+
state.jsonEditorError = ''
|
|
323
|
+
state.editingContext = null
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
function closeJsonEditor() {
|
|
327
|
+
state.jsonEditorOpen = false
|
|
328
|
+
resetJsonEditorState()
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
function handleEditorLineClick(payload, workingDoc) {
|
|
332
|
+
if (!workingDoc || !workingDoc.content)
|
|
333
|
+
return
|
|
334
|
+
|
|
335
|
+
const offset = typeof payload?.offset === 'number' ? payload.offset : null
|
|
336
|
+
if (offset == null)
|
|
337
|
+
return
|
|
338
|
+
|
|
339
|
+
const tag = findTagAtOffset(workingDoc.content, offset)
|
|
340
|
+
if (!tag)
|
|
341
|
+
return
|
|
342
|
+
|
|
343
|
+
const parsedCfg = safeParseConfig(tag.rawCfg)
|
|
344
|
+
state.jsonEditorError = ''
|
|
345
|
+
state.jsonEditorContent = parsedCfg ? JSON.stringify(parsedCfg, null, 2) : tag.rawCfg
|
|
346
|
+
state.jsonEditorOpen = true
|
|
347
|
+
state.editingContext = {
|
|
348
|
+
type: tag.type,
|
|
349
|
+
field: parsedCfg?.field != null ? String(parsedCfg.field) : null,
|
|
350
|
+
workingDoc,
|
|
351
|
+
originalTag: workingDoc.content.slice(tag.tagStart, tag.tagEnd),
|
|
352
|
+
configStartOffset: tag.configStart - tag.tagStart,
|
|
353
|
+
configEndOffset: tag.configEnd - tag.tagStart,
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
function handleJsonEditorSave() {
|
|
358
|
+
if (!state.editingContext)
|
|
359
|
+
return
|
|
360
|
+
|
|
361
|
+
let parsed
|
|
362
|
+
try {
|
|
363
|
+
parsed = JSON.parse(state.jsonEditorContent)
|
|
364
|
+
}
|
|
365
|
+
catch (error) {
|
|
366
|
+
state.jsonEditorError = `Unable to parse JSON: ${error.message}`
|
|
367
|
+
return
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
const serialized = JSON.stringify(parsed)
|
|
371
|
+
const { workingDoc, type, field, originalTag, configStartOffset, configEndOffset } = state.editingContext
|
|
372
|
+
const content = workingDoc?.content ?? ''
|
|
373
|
+
if (!content) {
|
|
374
|
+
state.jsonEditorError = 'Block content is empty.'
|
|
375
|
+
return
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
let target = null
|
|
379
|
+
for (const tag of iterateTags(content)) {
|
|
380
|
+
if (tag.type !== type)
|
|
381
|
+
continue
|
|
382
|
+
if (!field) {
|
|
383
|
+
target = tag
|
|
384
|
+
break
|
|
385
|
+
}
|
|
386
|
+
const cfg = safeParseConfig(tag.rawCfg)
|
|
387
|
+
if (cfg && String(cfg.field) === field) {
|
|
388
|
+
target = tag
|
|
389
|
+
break
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
if (!target && originalTag) {
|
|
394
|
+
const idx = content.indexOf(originalTag)
|
|
395
|
+
if (idx !== -1) {
|
|
396
|
+
const startOffset = typeof configStartOffset === 'number' ? configStartOffset : originalTag.indexOf('{')
|
|
397
|
+
const endOffset = typeof configEndOffset === 'number' ? configEndOffset : originalTag.lastIndexOf('}')
|
|
398
|
+
if (startOffset != null && endOffset != null && startOffset >= 0 && endOffset >= startOffset) {
|
|
399
|
+
target = {
|
|
400
|
+
configStart: idx + startOffset,
|
|
401
|
+
configEnd: idx + endOffset,
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
if (!target) {
|
|
408
|
+
state.jsonEditorError = 'Unable to locate the original block field in the current content.'
|
|
409
|
+
return
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
const prefix = content.slice(0, target.configStart)
|
|
413
|
+
const suffix = content.slice(target.configEnd + 1)
|
|
414
|
+
workingDoc.content = `${prefix}${serialized}${suffix}`
|
|
415
|
+
|
|
416
|
+
closeJsonEditor()
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
const theme = computed(() => {
|
|
420
|
+
const theme = edgeGlobal.edgeState.blockEditorTheme || ''
|
|
421
|
+
let themeContents = null
|
|
422
|
+
if (theme) {
|
|
423
|
+
themeContents = edgeFirebase.data?.[`${edgeGlobal.edgeState.organizationDocPath}/themes`]?.[theme]?.theme || null
|
|
424
|
+
}
|
|
425
|
+
if (themeContents) {
|
|
426
|
+
return JSON.parse(themeContents)
|
|
427
|
+
}
|
|
428
|
+
return null
|
|
429
|
+
})
|
|
430
|
+
|
|
431
|
+
const headObject = computed(() => {
|
|
432
|
+
const theme = edgeGlobal.edgeState.blockEditorTheme || ''
|
|
433
|
+
try {
|
|
434
|
+
return JSON.parse(edgeFirebase.data?.[`${edgeGlobal.edgeState.organizationDocPath}/themes`]?.[theme]?.headJSON || '{}')
|
|
435
|
+
}
|
|
436
|
+
catch (e) {
|
|
437
|
+
return {}
|
|
438
|
+
}
|
|
439
|
+
})
|
|
440
|
+
|
|
441
|
+
watch(headObject, (newHeadElements) => {
|
|
442
|
+
emit('head', newHeadElements)
|
|
443
|
+
}, { immediate: true, deep: true })
|
|
444
|
+
|
|
445
|
+
const editorDocUpdates = (workingDoc) => {
|
|
446
|
+
state.workingDoc = blockModel(workingDoc.content)
|
|
447
|
+
console.log('Editor workingDoc update:', state.workingDoc)
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
onBeforeMount(async () => {
|
|
451
|
+
console.log('Block Editor mounting - starting snapshots if needed')
|
|
452
|
+
if (!edgeFirebase.data?.[`organizations/${edgeGlobal.edgeState.currentOrganization}/themes`]) {
|
|
453
|
+
await edgeFirebase.startSnapshot(`organizations/${edgeGlobal.edgeState.currentOrganization}/themes`)
|
|
454
|
+
}
|
|
455
|
+
if (!edgeFirebase.data?.[`organizations/${edgeGlobal.edgeState.currentOrganization}/sites`]) {
|
|
456
|
+
console.log('Starting sites snapshot for block editor')
|
|
457
|
+
await edgeFirebase.startSnapshot(`organizations/${edgeGlobal.edgeState.currentOrganization}/sites`)
|
|
458
|
+
}
|
|
459
|
+
else {
|
|
460
|
+
console.log('Themes and Sites snapshots already started')
|
|
461
|
+
}
|
|
462
|
+
state.mounted = true
|
|
463
|
+
})
|
|
464
|
+
|
|
465
|
+
const themes = computed(() => {
|
|
466
|
+
return Object.values(edgeFirebase.data?.[`${edgeGlobal.edgeState.organizationDocPath}/themes`] || {})
|
|
467
|
+
})
|
|
468
|
+
|
|
469
|
+
watch (themes, async (newThemes) => {
|
|
470
|
+
state.loading = true
|
|
471
|
+
if (!edgeGlobal.edgeState.blockEditorTheme && newThemes.length > 0) {
|
|
472
|
+
edgeGlobal.edgeState.blockEditorTheme = newThemes[0].docId
|
|
473
|
+
}
|
|
474
|
+
await nextTick()
|
|
475
|
+
state.loading = false
|
|
476
|
+
}, { immediate: true, deep: true })
|
|
477
|
+
|
|
478
|
+
watch(() => state.jsonEditorOpen, (open) => {
|
|
479
|
+
if (!open)
|
|
480
|
+
resetJsonEditorState()
|
|
481
|
+
})
|
|
482
|
+
const sites = computed(() => {
|
|
483
|
+
return Object.values(edgeFirebase.data?.[`${edgeGlobal.edgeState.organizationDocPath}/sites`] || {})
|
|
484
|
+
})
|
|
485
|
+
|
|
486
|
+
watch (sites, async (newSites) => {
|
|
487
|
+
state.loading = true
|
|
488
|
+
if (!edgeGlobal.edgeState.blockEditorSite && newSites.length > 0) {
|
|
489
|
+
edgeGlobal.edgeState.blockEditorSite = newSites[0].docId
|
|
490
|
+
}
|
|
491
|
+
await nextTick()
|
|
492
|
+
state.loading = false
|
|
493
|
+
}, { immediate: true, deep: true })
|
|
494
|
+
|
|
495
|
+
const blocks = computed(() => {
|
|
496
|
+
return edgeFirebase.data?.[`${edgeGlobal.edgeState.organizationDocPath}/blocks`] || null
|
|
497
|
+
})
|
|
498
|
+
|
|
499
|
+
const getTagsFromBlocks = computed(() => {
|
|
500
|
+
const tagsSet = new Set()
|
|
501
|
+
|
|
502
|
+
Object.values(blocks.value || {}).forEach((block) => {
|
|
503
|
+
if (block.tags && Array.isArray(block.tags)) {
|
|
504
|
+
block.tags.forEach(tag => tagsSet.add(tag))
|
|
505
|
+
}
|
|
506
|
+
})
|
|
507
|
+
|
|
508
|
+
// Convert to array of objects
|
|
509
|
+
const tagsArray = Array.from(tagsSet).map(tag => ({ name: tag, title: tag }))
|
|
510
|
+
|
|
511
|
+
// Sort alphabetically
|
|
512
|
+
tagsArray.sort((a, b) => a.title.localeCompare(b.title))
|
|
513
|
+
|
|
514
|
+
// Remove "Quick Picks" if it exists
|
|
515
|
+
const filtered = tagsArray.filter(tag => tag.name !== 'Quick Picks')
|
|
516
|
+
|
|
517
|
+
// Always prepend it
|
|
518
|
+
return [{ name: 'Quick Picks', title: 'Quick Picks' }, ...filtered]
|
|
519
|
+
})
|
|
520
|
+
</script>
|
|
521
|
+
|
|
522
|
+
<template>
|
|
523
|
+
<div
|
|
524
|
+
v-if="edgeGlobal.edgeState.organizationDocPath && state.mounted"
|
|
525
|
+
>
|
|
526
|
+
<edge-editor
|
|
527
|
+
collection="blocks"
|
|
528
|
+
:doc-id="props.blockId"
|
|
529
|
+
:schema="blockSchema"
|
|
530
|
+
:new-doc-schema="state.newDocs.blocks"
|
|
531
|
+
class="w-full mx-auto flex-1 bg-transparent flex flex-col border-none shadow-none"
|
|
532
|
+
:show-footer="false"
|
|
533
|
+
:no-close-after-save="true"
|
|
534
|
+
:working-doc-overrides="state.workingDoc"
|
|
535
|
+
@working-doc="editorDocUpdates"
|
|
536
|
+
>
|
|
537
|
+
<template #header-start="slotProps">
|
|
538
|
+
<FilePenLine class="mr-2" />
|
|
539
|
+
{{ slotProps.title }}
|
|
540
|
+
</template>
|
|
541
|
+
<template #header-center>
|
|
542
|
+
<div class="w-full flex gap-1 px-4">
|
|
543
|
+
<div class="w-1/2">
|
|
544
|
+
<edge-shad-select
|
|
545
|
+
v-if="!state.loading"
|
|
546
|
+
v-model="edgeGlobal.edgeState.blockEditorTheme"
|
|
547
|
+
label="Theme Viewer Select"
|
|
548
|
+
name="theme"
|
|
549
|
+
:items="themes.map(t => ({ title: t.name, name: t.docId }))"
|
|
550
|
+
placeholder="Theme Viewer Select"
|
|
551
|
+
class="w-full"
|
|
552
|
+
/>
|
|
553
|
+
</div>
|
|
554
|
+
<div class="w-1/2">
|
|
555
|
+
<edge-shad-select
|
|
556
|
+
v-if="!state.loading"
|
|
557
|
+
v-model="edgeGlobal.edgeState.blockEditorSite"
|
|
558
|
+
label="Site"
|
|
559
|
+
name="site"
|
|
560
|
+
:items="sites.map(s => ({ title: s.name, name: s.docId }))"
|
|
561
|
+
placeholder="Select Site"
|
|
562
|
+
class="w-full"
|
|
563
|
+
/>
|
|
564
|
+
</div>
|
|
565
|
+
</div>
|
|
566
|
+
</template>
|
|
567
|
+
|
|
568
|
+
<template #main="slotProps">
|
|
569
|
+
<div class="pt-4">
|
|
570
|
+
<div class="flex w-full gap-2">
|
|
571
|
+
<div class="flex-auto">
|
|
572
|
+
<edge-shad-input
|
|
573
|
+
v-model="slotProps.workingDoc.name"
|
|
574
|
+
label="Block Name"
|
|
575
|
+
class="flex-auto"
|
|
576
|
+
name="name"
|
|
577
|
+
/>
|
|
578
|
+
</div>
|
|
579
|
+
<div class="flex-auto">
|
|
580
|
+
<edge-shad-select-tags
|
|
581
|
+
v-model="slotProps.workingDoc.tags"
|
|
582
|
+
:items="getTagsFromBlocks"
|
|
583
|
+
name="tags"
|
|
584
|
+
placeholder="Select tags"
|
|
585
|
+
label="Tags"
|
|
586
|
+
:allow-additions="true"
|
|
587
|
+
class="w-full max-w-[800px] mx-auto mb-5 text-black"
|
|
588
|
+
/>
|
|
589
|
+
</div>
|
|
590
|
+
<div class="flex-auto">
|
|
591
|
+
<edge-shad-select
|
|
592
|
+
v-model="slotProps.workingDoc.themes"
|
|
593
|
+
label="Allowed Themes"
|
|
594
|
+
name="themes"
|
|
595
|
+
:multiple="true"
|
|
596
|
+
:items="themes.map(t => ({ title: t.name, name: t.docId }))"
|
|
597
|
+
placeholder="Allowed Themes"
|
|
598
|
+
class="flex-auto"
|
|
599
|
+
/>
|
|
600
|
+
</div>
|
|
601
|
+
<div class="flex-auto pt-2">
|
|
602
|
+
<edge-shad-checkbox
|
|
603
|
+
v-model="slotProps.workingDoc.synced"
|
|
604
|
+
name="synced"
|
|
605
|
+
label="Synced Block"
|
|
606
|
+
>
|
|
607
|
+
Synced Block
|
|
608
|
+
</edge-shad-checkbox>
|
|
609
|
+
</div>
|
|
610
|
+
</div>
|
|
611
|
+
<div class="flex gap-4">
|
|
612
|
+
<div class="w-1/2">
|
|
613
|
+
<div class="flex gap-2">
|
|
614
|
+
<div class="w-2/12 mb-3 rounded-md border border-slate-200 bg-white/80 p-3 shadow-sm shadow-slate-200/60 dark:border-slate-800 dark:bg-slate-900/60">
|
|
615
|
+
<div class="text-xs font-semibold uppercase tracking-wide text-slate-600 dark:text-slate-300">
|
|
616
|
+
Dynamic Content
|
|
617
|
+
</div>
|
|
618
|
+
<div class="mt-2 flex flex-wrap gap-2">
|
|
619
|
+
<edge-tooltip
|
|
620
|
+
v-for="snippet in BLOCK_CONTENT_SNIPPETS"
|
|
621
|
+
:key="snippet.label"
|
|
622
|
+
>
|
|
623
|
+
<edge-shad-button
|
|
624
|
+
size="sm"
|
|
625
|
+
variant="outline"
|
|
626
|
+
class="text-xs w-full"
|
|
627
|
+
@click="insertBlockContentSnippet(snippet.snippet)"
|
|
628
|
+
>
|
|
629
|
+
{{ snippet.label }}
|
|
630
|
+
</edge-shad-button>
|
|
631
|
+
<template #content>
|
|
632
|
+
<pre class="max-w-[320px] whitespace-pre-wrap break-words text-left text-xs font-mono leading-tight">{{ snippet.snippet }}</pre>
|
|
633
|
+
</template>
|
|
634
|
+
</edge-tooltip>
|
|
635
|
+
</div>
|
|
636
|
+
</div>
|
|
637
|
+
<div class="w-10/12">
|
|
638
|
+
<edge-cms-code-editor
|
|
639
|
+
ref="contentEditorRef"
|
|
640
|
+
v-model="slotProps.workingDoc.content"
|
|
641
|
+
title="Block Content"
|
|
642
|
+
language="html"
|
|
643
|
+
name="content"
|
|
644
|
+
height="calc(100vh - 300px)"
|
|
645
|
+
class="mb-4 flex-1"
|
|
646
|
+
@line-click="payload => handleEditorLineClick(payload, slotProps.workingDoc)"
|
|
647
|
+
/>
|
|
648
|
+
</div>
|
|
649
|
+
</div>
|
|
650
|
+
</div>
|
|
651
|
+
<div class="w-1/2 space-y-2">
|
|
652
|
+
<div class="flex items-center justify-between">
|
|
653
|
+
<span class="text-xs font-semibold uppercase tracking-wide text-muted-foreground">Viewport</span>
|
|
654
|
+
<div class="flex items-center gap-1">
|
|
655
|
+
<edge-shad-button
|
|
656
|
+
v-for="option in previewViewportOptions"
|
|
657
|
+
:key="option.id"
|
|
658
|
+
type="button"
|
|
659
|
+
size="icon"
|
|
660
|
+
variant="ghost"
|
|
661
|
+
class="h-[28px] w-[28px] text-xs border transition-colors"
|
|
662
|
+
:class="state.previewViewport === option.id ? 'bg-primary text-primary-foreground border-primary shadow-sm' : 'bg-muted text-foreground border-border hover:bg-muted/80'"
|
|
663
|
+
@click="setPreviewViewport(option.id)"
|
|
664
|
+
>
|
|
665
|
+
<component :is="option.icon" class="w-3.5 h-3.5" />
|
|
666
|
+
</edge-shad-button>
|
|
667
|
+
</div>
|
|
668
|
+
</div>
|
|
669
|
+
<div
|
|
670
|
+
class="w-full mx-auto bg-card border border-border rounded-lg shadow-sm md:shadow-md"
|
|
671
|
+
:style="previewViewportStyle"
|
|
672
|
+
>
|
|
673
|
+
<edge-cms-block-picker
|
|
674
|
+
:site-id="edgeGlobal.edgeState.blockEditorSite"
|
|
675
|
+
:theme="theme"
|
|
676
|
+
:block-override="{ content: slotProps.workingDoc.content, values: state.workingDoc.values, meta: state.workingDoc.meta }"
|
|
677
|
+
:viewport-mode="previewViewportMode"
|
|
678
|
+
/>
|
|
679
|
+
</div>
|
|
680
|
+
</div>
|
|
681
|
+
</div>
|
|
682
|
+
</div>
|
|
683
|
+
</template>
|
|
684
|
+
</edge-editor>
|
|
685
|
+
<Sheet
|
|
686
|
+
v-model:open="state.jsonEditorOpen"
|
|
687
|
+
>
|
|
688
|
+
<SheetContent side="left" class="w-full md:w-1/2 max-w-none sm:max-w-none max-w-2xl">
|
|
689
|
+
<SheetHeader>
|
|
690
|
+
<SheetTitle class="text-left">
|
|
691
|
+
Field Editor
|
|
692
|
+
</SheetTitle>
|
|
693
|
+
<SheetDescription v-if="state.jsonEditorError" class="text-left text-sm text-gray-500">
|
|
694
|
+
<Alert variant="destructive" class="mt-2">
|
|
695
|
+
<AlertCircle class="w-4 h-4" />
|
|
696
|
+
<AlertTitle>
|
|
697
|
+
JSON Error
|
|
698
|
+
</AlertTitle>
|
|
699
|
+
<AlertDescription>
|
|
700
|
+
{{ state.jsonEditorError }}
|
|
701
|
+
</AlertDescription>
|
|
702
|
+
</Alert>
|
|
703
|
+
</SheetDescription>
|
|
704
|
+
</SheetHeader>
|
|
705
|
+
<div :class="state.jsonEditorError ? 'h-[calc(100vh-200px)]' : 'h-[calc(100vh-120px)]'" class="p-6 space-y-4 overflow-y-auto">
|
|
706
|
+
<edge-cms-code-editor
|
|
707
|
+
v-model="state.jsonEditorContent"
|
|
708
|
+
title="Fields Configuration (JSON)"
|
|
709
|
+
language="json"
|
|
710
|
+
name="content"
|
|
711
|
+
height="calc(100vh - 200px)"
|
|
712
|
+
/>
|
|
713
|
+
</div>
|
|
714
|
+
<SheetFooter class="pt-2 flex justify-between">
|
|
715
|
+
<edge-shad-button variant="destructive" class="text-white " @click="closeJsonEditor">
|
|
716
|
+
Cancel
|
|
717
|
+
</edge-shad-button>
|
|
718
|
+
<edge-shad-button class=" bg-slate-800 hover:bg-slate-400text-white w-full" @click="handleJsonEditorSave">
|
|
719
|
+
Save
|
|
720
|
+
</edge-shad-button>
|
|
721
|
+
</SheetFooter>
|
|
722
|
+
</SheetContent>
|
|
723
|
+
</Sheet>
|
|
724
|
+
</div>
|
|
725
|
+
</template>
|