@edgedev/create-edge-app 1.2.32 → 1.2.34

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (33) hide show
  1. package/deploy.sh +77 -0
  2. package/edge/components/cms/block.vue +228 -18
  3. package/edge/components/cms/blockApi.vue +3 -3
  4. package/edge/components/cms/blockEditor.vue +374 -85
  5. package/edge/components/cms/blockPicker.vue +29 -3
  6. package/edge/components/cms/blockRender.vue +3 -3
  7. package/edge/components/cms/blocksManager.vue +755 -82
  8. package/edge/components/cms/codeEditor.vue +15 -6
  9. package/edge/components/cms/fontUpload.vue +318 -2
  10. package/edge/components/cms/htmlContent.vue +230 -89
  11. package/edge/components/cms/menu.vue +5 -4
  12. package/edge/components/cms/page.vue +750 -21
  13. package/edge/components/cms/site.vue +624 -84
  14. package/edge/components/cms/sitesManager.vue +5 -4
  15. package/edge/components/cms/themeEditor.vue +196 -162
  16. package/edge/components/editor.vue +5 -1
  17. package/edge/composables/global.ts +37 -5
  18. package/edge/composables/useCmsNewDocs.js +100 -0
  19. package/edge/composables/useEdgeCmsDialogPositionFix.js +19 -0
  20. package/edge/routes/cms/dashboard/blocks/[block].vue +5 -0
  21. package/edge/routes/cms/dashboard/blocks/index.vue +12 -1
  22. package/edge/routes/cms/dashboard/media/index.vue +5 -0
  23. package/edge/routes/cms/dashboard/sites/[site]/[[page]].vue +4 -0
  24. package/edge/routes/cms/dashboard/sites/[site].vue +4 -0
  25. package/edge/routes/cms/dashboard/sites/index.vue +4 -0
  26. package/edge/routes/cms/dashboard/templates/[page].vue +4 -0
  27. package/edge/routes/cms/dashboard/templates/index.vue +4 -0
  28. package/edge/routes/cms/dashboard/themes/[theme].vue +5 -0
  29. package/edge/routes/cms/dashboard/themes/index.vue +330 -1
  30. package/firebase.json +4 -0
  31. package/nuxt.config.ts +1 -1
  32. package/package.json +2 -2
  33. package/pages/app.vue +12 -12
package/deploy.sh CHANGED
@@ -81,12 +81,89 @@ warn_edge_subtree_sync() {
81
81
  fi
82
82
  }
83
83
 
84
+ resolve_functions_region() {
85
+ if [ -n "${FIREBASE_STORE_REGION:-}" ]; then
86
+ echo "$FIREBASE_STORE_REGION"
87
+ return
88
+ fi
89
+
90
+ if [ -f "functions/.env.prod" ]; then
91
+ local env_region
92
+ env_region="$(rg '^FIREBASE_STORE_REGION=' functions/.env.prod -N | tail -n 1 | sed 's/^FIREBASE_STORE_REGION=//; s/^"//; s/"$//')"
93
+ if [ -n "$env_region" ]; then
94
+ echo "$env_region"
95
+ return
96
+ fi
97
+ fi
98
+
99
+ echo "us-west1"
100
+ }
101
+
102
+ ensure_callable_public_access() {
103
+ if ! command -v gcloud >/dev/null 2>&1; then
104
+ echo "Deploy aborted: gcloud CLI not found. It is required to enforce callable invoker access." >&2
105
+ exit 1
106
+ fi
107
+
108
+ local region
109
+ region="$(resolve_functions_region)"
110
+
111
+ local callable_functions
112
+ callable_functions="$(
113
+ node - <<'NODE'
114
+ const fs = require('fs')
115
+ const path = require('path')
116
+
117
+ const root = process.cwd()
118
+ const indexPath = path.join(root, 'functions', 'index.js')
119
+ const indexSource = fs.readFileSync(indexPath, 'utf8')
120
+
121
+ const moduleRequirePattern = /^\s*exports\.(\w+)\s*=\s*require\(\s*['"]\.\/([^'"]+)['"]\s*\)/gm
122
+ const callablePattern = /^\s*exports\.(\w+)\s*=\s*onCall\s*\(/gm
123
+
124
+ const functions = new Set()
125
+ let moduleMatch
126
+
127
+ while ((moduleMatch = moduleRequirePattern.exec(indexSource)) !== null) {
128
+ const prefix = moduleMatch[1]
129
+ const modulePath = path.join(root, 'functions', `${moduleMatch[2]}.js`)
130
+ if (!fs.existsSync(modulePath))
131
+ continue
132
+
133
+ const moduleSource = fs.readFileSync(modulePath, 'utf8')
134
+ let callableMatch
135
+
136
+ while ((callableMatch = callablePattern.exec(moduleSource)) !== null)
137
+ functions.add(`${prefix}-${callableMatch[1]}`)
138
+ }
139
+
140
+ console.log(Array.from(functions).join('\n'))
141
+ NODE
142
+ )"
143
+
144
+ if [ -z "$callable_functions" ]; then
145
+ echo "No onCall functions detected. Skipping public invoker enforcement."
146
+ return
147
+ fi
148
+
149
+ echo "Ensuring public invoker access for callable functions in region '${region}'..."
150
+ while IFS= read -r function_name; do
151
+ [ -z "$function_name" ] && continue
152
+ echo " - ${function_name}"
153
+ gcloud functions add-invoker-policy-binding "$function_name" \
154
+ --gen2 \
155
+ --region "$region" \
156
+ --member="allUsers" >/dev/null
157
+ done <<< "$callable_functions"
158
+ }
159
+
84
160
  check_repo "." "root"
85
161
  warn_edge_subtree_sync
86
162
 
87
163
  pnpm run generate
88
164
  export NODE_ENV=production
89
165
  firebase deploy --only functions
166
+ ensure_callable_public_access
90
167
  firebase deploy --only hosting
91
168
  firebase deploy --only firestore
92
169
  firebase deploy --only storage
@@ -26,25 +26,118 @@ const props = defineProps({
26
26
  type: String,
27
27
  default: 'auto',
28
28
  },
29
+ allowDelete: {
30
+ type: Boolean,
31
+ default: true,
32
+ },
33
+ containFixed: {
34
+ type: Boolean,
35
+ default: false,
36
+ },
29
37
  })
30
38
  const emit = defineEmits(['update:modelValue', 'delete'])
31
39
  const edgeFirebase = inject('edgeFirebase')
40
+
41
+ function normalizeConfigLiteral(str) {
42
+ return str
43
+ .replace(/(\{|,)\s*([A-Za-z_][\w-]*)\s*:/g, '$1"$2":')
44
+ .replace(/'/g, '"')
45
+ }
46
+
47
+ function safeParseTagConfig(raw) {
48
+ try {
49
+ return JSON.parse(normalizeConfigLiteral(raw))
50
+ }
51
+ catch {
52
+ return null
53
+ }
54
+ }
55
+
56
+ function findMatchingBrace(str, startIdx) {
57
+ let depth = 0
58
+ let inString = false
59
+ let quote = null
60
+ let escape = false
61
+
62
+ for (let i = startIdx; i < str.length; i++) {
63
+ const ch = str[i]
64
+ if (inString) {
65
+ if (escape) {
66
+ escape = false
67
+ continue
68
+ }
69
+ if (ch === '\\') {
70
+ escape = true
71
+ continue
72
+ }
73
+ if (ch === quote) {
74
+ inString = false
75
+ quote = null
76
+ }
77
+ continue
78
+ }
79
+
80
+ if (ch === '"' || ch === '\'') {
81
+ inString = true
82
+ quote = ch
83
+ continue
84
+ }
85
+ if (ch === '{')
86
+ depth++
87
+ else if (ch === '}') {
88
+ depth--
89
+ if (depth === 0)
90
+ return i
91
+ }
92
+ }
93
+
94
+ return -1
95
+ }
96
+
32
97
  function extractFieldsInOrder(template) {
33
98
  if (!template || typeof template !== 'string')
34
99
  return []
100
+
35
101
  const fields = []
36
102
  const seen = new Set()
37
- const TAG_RE = /\{\{\{#[^\s]+\s+(\{[\s\S]*?\})\}\}\}/g
38
- let m = TAG_RE.exec(template)
39
- while (m) {
40
- const cfg = m[1]
41
- const fm = cfg.match(/"field"\s*:\s*"([^"]+)"/)
42
- if (fm && !seen.has(fm[1])) {
43
- fields.push(fm[1])
44
- seen.add(fm[1])
103
+
104
+ const TAG_START_RE = /\{\{\{\#([A-Za-z0-9_-]+)\s*\{/g
105
+ TAG_START_RE.lastIndex = 0
106
+
107
+ for (;;) {
108
+ const m = TAG_START_RE.exec(template)
109
+ if (!m)
110
+ break
111
+
112
+ const configStart = TAG_START_RE.lastIndex - 1
113
+ if (configStart < 0 || template[configStart] !== '{')
114
+ continue
115
+
116
+ const configEnd = findMatchingBrace(template, configStart)
117
+ if (configEnd === -1)
118
+ continue
119
+
120
+ const rawCfg = template.slice(configStart, configEnd + 1)
121
+ const parsedCfg = safeParseTagConfig(rawCfg)
122
+
123
+ let field = typeof parsedCfg?.field === 'string'
124
+ ? parsedCfg.field.trim()
125
+ : ''
126
+
127
+ if (!field) {
128
+ const fm = rawCfg.match(/["']?field["']?\s*:\s*["']([^"']+)["']/)
129
+ field = fm?.[1]?.trim() || ''
130
+ }
131
+
132
+ if (field && !seen.has(field)) {
133
+ fields.push(field)
134
+ seen.add(field)
45
135
  }
46
- m = TAG_RE.exec(template)
136
+
137
+ const closeTriple = template.indexOf('}}}', configEnd)
138
+ TAG_START_RE.lastIndex = closeTriple !== -1 ? closeTriple + 3 : configEnd + 1
47
139
  }
140
+
48
141
  return fields
49
142
  }
50
143
 
@@ -71,6 +164,70 @@ const state = reactive({
71
164
  validationErrors: [],
72
165
  })
73
166
 
167
+ const INTERACTIVE_CLICK_SELECTOR = [
168
+ '[data-cms-interactive]',
169
+ '.cms-block-interactive',
170
+ '.cms-nav-toggle',
171
+ '.cms-nav-overlay',
172
+ '.cms-nav-panel',
173
+ '.cms-nav-close',
174
+ '.cms-nav-link',
175
+ ].join(', ')
176
+
177
+ const hasFixedPositionInContent = computed(() => {
178
+ const content = String(modelValue.value?.content || '')
179
+ return /\bfixed\b/.test(content)
180
+ })
181
+
182
+ const normalizePreviewType = (value) => {
183
+ return value === 'dark' ? 'dark' : 'light'
184
+ }
185
+
186
+ const resolvedPreviewType = computed(() => normalizePreviewType(modelValue.value?.previewType))
187
+ const sourceBlockDocId = computed(() => {
188
+ const direct = String(modelValue.value?.blockId || '').trim()
189
+ if (direct)
190
+ return direct
191
+ return String(props.blockId || '').trim()
192
+ })
193
+
194
+ const inheritedPreviewType = computed(() => {
195
+ const explicit = modelValue.value?.previewType
196
+ if (explicit === 'light' || explicit === 'dark')
197
+ return explicit
198
+ const docId = sourceBlockDocId.value
199
+ if (!docId)
200
+ return null
201
+ const blockDoc = edgeFirebase.data?.[`${edgeGlobal.edgeState.organizationDocPath}/blocks`]?.[docId]
202
+ const inherited = blockDoc?.previewType
203
+ return (inherited === 'light' || inherited === 'dark') ? inherited : null
204
+ })
205
+
206
+ const effectivePreviewType = computed(() => {
207
+ return normalizePreviewType(inheritedPreviewType.value ?? resolvedPreviewType.value)
208
+ })
209
+
210
+ const shouldContainFixedPreview = computed(() => {
211
+ return (props.editMode || props.containFixed) && hasFixedPositionInContent.value
212
+ })
213
+
214
+ const blockWrapperClass = computed(() => ({
215
+ 'overflow-visible': shouldContainFixedPreview.value,
216
+ 'min-h-[88px]': props.editMode && shouldContainFixedPreview.value,
217
+ 'z-30': shouldContainFixedPreview.value,
218
+ 'bg-white text-black': props.editMode && effectivePreviewType.value === 'light',
219
+ 'bg-neutral-950 text-neutral-50': props.editMode && effectivePreviewType.value === 'dark',
220
+ 'cms-nav-edit-static': props.editMode,
221
+ }))
222
+
223
+ const blockWrapperStyle = computed(() => {
224
+ if (!shouldContainFixedPreview.value || !props.editMode)
225
+ return null
226
+ return {
227
+ transform: 'translateZ(0)',
228
+ }
229
+ })
230
+
74
231
  const isLightName = (value) => {
75
232
  if (!value)
76
233
  return false
@@ -147,9 +304,12 @@ const resetArrayItems = (field, metaSource = null) => {
147
304
  }
148
305
  }
149
306
 
150
- const openEditor = async () => {
307
+ const openEditor = async (event) => {
151
308
  if (!props.editMode)
152
309
  return
310
+ const target = event?.target
311
+ if (target?.closest?.(INTERACTIVE_CLICK_SELECTOR))
312
+ return
153
313
  const blockData = edgeFirebase.data[`${edgeGlobal.edgeState.organizationDocPath}/blocks`]?.[modelValue.value.blockId]
154
314
  const templateMeta = blockData?.meta || modelValue.value?.meta || {}
155
315
  const storedMeta = modelValue.value?.meta || {}
@@ -192,6 +352,11 @@ const openEditor = async () => {
192
352
  state.afterLoad = true
193
353
  }
194
354
 
355
+ const isLimitOne = (field) => {
356
+ const limit = Number(state.meta?.[field]?.limit)
357
+ return Number.isFinite(limit) && limit === 1
358
+ }
359
+
195
360
  const normalizeValidationNumber = (value) => {
196
361
  if (value === null || value === undefined || value === '')
197
362
  return null
@@ -264,6 +429,31 @@ const orderedMeta = computed(() => {
264
429
  return out
265
430
  })
266
431
 
432
+ const hasEditableArrayControls = (entry) => {
433
+ if (!entry?.meta)
434
+ return false
435
+
436
+ // Manual arrays are editable through the schema/list UI.
437
+ if (!entry.meta?.api && !entry.meta?.collection)
438
+ return true
439
+
440
+ const collectionPath = entry.meta?.collection?.path
441
+ const supportsQueryControls = collectionPath !== 'post'
442
+ const queryOptions = Array.isArray(entry.meta?.queryOptions) ? entry.meta.queryOptions : []
443
+ const hasQueryOptions = supportsQueryControls && queryOptions.length > 0
444
+ const hasLimitControl = supportsQueryControls && !isLimitOne(entry.field)
445
+
446
+ return hasQueryOptions || hasLimitControl
447
+ }
448
+
449
+ const editableMetaEntries = computed(() => {
450
+ return orderedMeta.value.filter((entry) => {
451
+ if (entry?.meta?.type === 'array')
452
+ return hasEditableArrayControls(entry)
453
+ return true
454
+ })
455
+ })
456
+
267
457
  const genTitleFromField = (field) => {
268
458
  if (field?.title)
269
459
  return field.title
@@ -324,7 +514,7 @@ const save = () => {
324
514
  }
325
515
 
326
516
  const aiFieldOptions = computed(() => {
327
- return orderedMeta.value
517
+ return editableMetaEntries.value
328
518
  .map(entry => ({
329
519
  id: entry.field,
330
520
  label: genTitleFromField(entry),
@@ -479,9 +669,10 @@ const getTagsFromPosts = computed(() => {
479
669
  <template>
480
670
  <div>
481
671
  <div
482
- :class="{ 'cursor-pointer': props.editMode }"
483
- class="relative group "
484
- @click="openEditor"
672
+ :class="[{ 'cursor-pointer': props.editMode }, blockWrapperClass]"
673
+ :style="blockWrapperStyle"
674
+ class="relative group"
675
+ @click="openEditor($event)"
485
676
  >
486
677
  <!-- Content -->
487
678
  <edge-cms-block-api :site-id="props.siteId" :theme="props.theme" :content="modelValue?.content" :values="modelValue?.values" :meta="modelValue?.meta" :viewport-mode="props.viewportMode" @pending="state.loading = $event" />
@@ -499,7 +690,7 @@ const getTagsFromPosts = computed(() => {
499
690
  <!-- Hover controls -->
500
691
  <div v-if="props.editMode" class="absolute inset-0 opacity-0 group-hover:opacity-100 transition-opacity duration-200 z-20">
501
692
  <!-- Delete button top right -->
502
- <div class="absolute top-2 right-2">
693
+ <div v-if="props.allowDelete" class="absolute top-2 right-2">
503
694
  <edge-shad-button
504
695
  variant="destructive"
505
696
  size="icon"
@@ -562,7 +753,7 @@ const getTagsFromPosts = computed(() => {
562
753
  </SheetHeader>
563
754
 
564
755
  <edge-shad-form ref="blockFormRef">
565
- <div v-if="orderedMeta.length === 0">
756
+ <div v-if="editableMetaEntries.length === 0">
566
757
  <Alert variant="info" class="mt-4 mb-4">
567
758
  <AlertTitle>No editable fields found</AlertTitle>
568
759
  <AlertDescription class="text-sm">
@@ -571,7 +762,7 @@ const getTagsFromPosts = computed(() => {
571
762
  </Alert>
572
763
  </div>
573
764
  <div :class="modelValue.synced ? 'h-[calc(100vh-160px)]' : 'h-[calc(100vh-130px)]'" class="p-6 space-y-4 overflow-y-auto">
574
- <template v-for="entry in orderedMeta" :key="entry.field">
765
+ <template v-for="entry in editableMetaEntries" :key="entry.field">
575
766
  <div v-if="entry.meta.type === 'array'">
576
767
  <div v-if="!entry.meta?.api && !entry.meta?.collection">
577
768
  <div v-if="entry.meta?.schema">
@@ -698,7 +889,12 @@ const getTagsFromPosts = computed(() => {
698
889
  />
699
890
  </div>
700
891
  </template>
701
- <edge-shad-number v-if="entry.meta?.collection?.path !== 'post'" v-model="state.meta[entry.field].limit" name="limit" label="Limit" />
892
+ <edge-shad-number
893
+ v-if="entry.meta?.collection?.path !== 'post' && !isLimitOne(entry.field)"
894
+ v-model="state.meta[entry.field].limit"
895
+ name="limit"
896
+ label="Limit"
897
+ />
702
898
  </div>
703
899
  </div>
704
900
  <div v-else-if="entry.meta?.type === 'image'" class="w-full">
@@ -848,3 +1044,17 @@ const getTagsFromPosts = computed(() => {
848
1044
  </Sheet>
849
1045
  </div>
850
1046
  </template>
1047
+
1048
+ <style scoped>
1049
+ .cms-nav-edit-static :deep([data-cms-nav-root] .cms-nav-toggle),
1050
+ .cms-nav-edit-static :deep([data-cms-nav-root] .cms-nav-close),
1051
+ .cms-nav-edit-static :deep([data-cms-nav-root] .cms-nav-link) {
1052
+ pointer-events: none !important;
1053
+ }
1054
+
1055
+ .cms-nav-edit-static :deep([data-cms-nav-root] .cms-nav-overlay),
1056
+ .cms-nav-edit-static :deep([data-cms-nav-root] .cms-nav-panel) {
1057
+ display: none !important;
1058
+ pointer-events: none !important;
1059
+ }
1060
+ </style>
@@ -11,15 +11,15 @@ import { computedAsync } from '@vueuse/core'
11
11
  const props = defineProps({
12
12
  content: {
13
13
  type: String,
14
- required: true,
14
+ default: '',
15
15
  },
16
16
  values: {
17
17
  type: Object,
18
- required: true,
18
+ default: () => ({}),
19
19
  },
20
20
  meta: {
21
21
  type: Object,
22
- required: true,
22
+ default: () => ({}),
23
23
  },
24
24
  theme: {
25
25
  type: Object,