@edgedev/create-edge-app 1.1.28 → 1.2.29
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/agents.md +2 -0
- package/bin/cli.js +13 -5
- package/deploy-services.sh +237 -0
- package/deploy.sh +88 -1
- package/edge/components/auth/register.vue +51 -0
- package/edge/components/cms/block.vue +29 -16
- package/edge/components/cms/blockEditor.vue +748 -7
- package/edge/components/cms/codeEditor.vue +24 -2
- package/edge/components/cms/htmlContent.vue +10 -2
- package/edge/components/cms/mediaManager.vue +19 -3
- package/edge/components/cms/menu.vue +231 -34
- package/edge/components/cms/optionsSelect.vue +20 -3
- package/edge/components/cms/page.vue +9 -0
- package/edge/components/cms/site.vue +114 -5
- package/edge/components/cms/siteSettingsForm.vue +7 -0
- package/edge/components/cms/themeEditor.vue +9 -3
- package/edge/components/dashboard.vue +22 -3
- package/edge/components/imagePicker.vue +126 -0
- package/edge/components/myAccount.vue +1 -0
- package/edge/components/myProfile.vue +345 -61
- package/edge/components/organizationMembers.vue +569 -261
- package/edge/components/shad/combobox.vue +2 -2
- package/edge/components/shad/number.vue +2 -2
- package/edge/composables/global.ts +5 -2
- package/edge/composables/structuredDataTemplates.js +6 -6
- package/firebase_init.sh +63 -2
- package/package.json +1 -1
- package/services/.deploy.shared.env.example +12 -0
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
<script setup>
|
|
2
|
-
import { computed, defineProps, inject, nextTick, onBeforeMount, reactive, watch } from 'vue'
|
|
2
|
+
import { computed, defineProps, inject, nextTick, onBeforeMount, reactive, ref, watch } from 'vue'
|
|
3
|
+
import { toTypedSchema } from '@vee-validate/zod'
|
|
3
4
|
import { useToast } from '@/components/ui/toast/use-toast'
|
|
4
5
|
const props = defineProps({
|
|
5
6
|
metaFields: {
|
|
@@ -15,7 +16,9 @@ const props = defineProps({
|
|
|
15
16
|
const { toast } = useToast()
|
|
16
17
|
|
|
17
18
|
const edgeFirebase = inject('edgeFirebase')
|
|
18
|
-
|
|
19
|
+
const edgeGlobal = inject('edgeGlobal')
|
|
20
|
+
|
|
21
|
+
const formRef = ref(null)
|
|
19
22
|
|
|
20
23
|
const state = reactive({
|
|
21
24
|
meta: {},
|
|
@@ -24,10 +27,142 @@ const state = reactive({
|
|
|
24
27
|
loaded: true,
|
|
25
28
|
loading: false,
|
|
26
29
|
})
|
|
27
|
-
|
|
30
|
+
|
|
31
|
+
const cloneMeta = (meta) => {
|
|
32
|
+
if (edgeGlobal?.dupObject)
|
|
33
|
+
return edgeGlobal.dupObject(meta || {})
|
|
34
|
+
return JSON.parse(JSON.stringify(meta || {}))
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const disabledNoteText = 'Contact admin to change.'
|
|
38
|
+
|
|
39
|
+
const getDisabledNote = (field) => {
|
|
40
|
+
if (!field?.disabled)
|
|
41
|
+
return ''
|
|
42
|
+
return field?.disabledNote || disabledNoteText
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const mergeDisabledNote = (text, field) => {
|
|
46
|
+
const note = getDisabledNote(field)
|
|
47
|
+
if (!note)
|
|
48
|
+
return text || ''
|
|
49
|
+
if (text)
|
|
50
|
+
return `${text} ${note}`
|
|
51
|
+
return note
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const WIDTHS = {
|
|
55
|
+
1: 'md:col-span-1',
|
|
56
|
+
2: 'md:col-span-2',
|
|
57
|
+
3: 'md:col-span-3',
|
|
58
|
+
4: 'md:col-span-4',
|
|
59
|
+
5: 'md:col-span-5',
|
|
60
|
+
6: 'md:col-span-6',
|
|
61
|
+
7: 'md:col-span-7',
|
|
62
|
+
8: 'md:col-span-8',
|
|
63
|
+
9: 'md:col-span-9',
|
|
64
|
+
10: 'md:col-span-10',
|
|
65
|
+
11: 'md:col-span-11',
|
|
66
|
+
12: 'md:col-span-12',
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const numColsToTailwind = cols => WIDTHS[cols] || 'md:col-span-12'
|
|
70
|
+
|
|
71
|
+
const resolvedSchema = computed(() => {
|
|
72
|
+
if (!props.formSchema)
|
|
73
|
+
return undefined
|
|
74
|
+
if (props.formSchema?.safeParse && props.formSchema?._def)
|
|
75
|
+
return toTypedSchema(props.formSchema)
|
|
76
|
+
return props.formSchema
|
|
77
|
+
})
|
|
78
|
+
|
|
79
|
+
const defaultFields = [
|
|
80
|
+
{ field: 'profilephoto', value: '' },
|
|
81
|
+
{ field: 'name', value: '' },
|
|
82
|
+
{ field: 'phone', value: '' },
|
|
83
|
+
]
|
|
84
|
+
|
|
85
|
+
const filteredMetaFields = computed(() => {
|
|
86
|
+
const skipFields = new Set(['profilephoto', 'name', 'phone', 'email', 'role'])
|
|
87
|
+
return (props.metaFields || []).filter(field => !skipFields.has(field.field))
|
|
88
|
+
})
|
|
89
|
+
|
|
90
|
+
const initializeDefaults = (meta) => {
|
|
91
|
+
defaultFields.forEach((field) => {
|
|
92
|
+
if (!(field.field in meta))
|
|
93
|
+
meta[field.field] = field.value
|
|
94
|
+
})
|
|
95
|
+
filteredMetaFields.value.forEach((field) => {
|
|
96
|
+
const current = getByPath(meta, field.field, undefined)
|
|
97
|
+
if (current === undefined)
|
|
98
|
+
setByPath(meta, field.field, field.value)
|
|
99
|
+
})
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// Helpers to read/write nested keys like "profile.firstName" on plain objects
|
|
103
|
+
function getByPath(obj, path, fallback = undefined) {
|
|
104
|
+
if (!obj || !path)
|
|
105
|
+
return fallback
|
|
106
|
+
const parts = String(path).split('.')
|
|
107
|
+
let cur = obj
|
|
108
|
+
for (const p of parts) {
|
|
109
|
+
if (cur == null || typeof cur !== 'object' || !(p in cur))
|
|
110
|
+
return fallback
|
|
111
|
+
cur = cur[p]
|
|
112
|
+
}
|
|
113
|
+
return cur
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function setByPath(obj, path, value) {
|
|
117
|
+
if (!obj || !path)
|
|
118
|
+
return
|
|
119
|
+
const parts = String(path).split('.')
|
|
120
|
+
let cur = obj
|
|
121
|
+
for (let i = 0; i < parts.length; i++) {
|
|
122
|
+
const key = parts[i]
|
|
123
|
+
if (i === parts.length - 1) {
|
|
124
|
+
cur[key] = value
|
|
125
|
+
}
|
|
126
|
+
else {
|
|
127
|
+
if (cur[key] == null || typeof cur[key] !== 'object')
|
|
128
|
+
cur[key] = {}
|
|
129
|
+
cur = cur[key]
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const mergeDeep = (target, source) => {
|
|
135
|
+
if (!source || typeof source !== 'object')
|
|
136
|
+
return target
|
|
137
|
+
Object.keys(source).forEach((key) => {
|
|
138
|
+
const value = source[key]
|
|
139
|
+
if (Array.isArray(value)) {
|
|
140
|
+
target[key] = [...value]
|
|
141
|
+
return
|
|
142
|
+
}
|
|
143
|
+
if (value && typeof value === 'object') {
|
|
144
|
+
if (!target[key] || typeof target[key] !== 'object' || Array.isArray(target[key]))
|
|
145
|
+
target[key] = {}
|
|
146
|
+
mergeDeep(target[key], value)
|
|
147
|
+
return
|
|
148
|
+
}
|
|
149
|
+
target[key] = value
|
|
150
|
+
})
|
|
151
|
+
return target
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
const syncFormValues = (meta) => {
|
|
155
|
+
if (formRef.value?.setValues)
|
|
156
|
+
formRef.value.setValues(meta || {})
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
const onSubmit = async (values = {}) => {
|
|
28
160
|
state.loading = true
|
|
29
|
-
|
|
30
|
-
|
|
161
|
+
const mergedMeta = mergeDeep(cloneMeta(state.meta), values || {})
|
|
162
|
+
state.meta = mergedMeta
|
|
163
|
+
await edgeFirebase.setUserMeta(mergedMeta)
|
|
164
|
+
if (edgeGlobal?.edgeState)
|
|
165
|
+
edgeGlobal.edgeState.changeTracker = {}
|
|
31
166
|
state.loaded = false
|
|
32
167
|
toast({
|
|
33
168
|
title: 'Updated Successfully',
|
|
@@ -44,79 +179,228 @@ const currentMeta = computed(() => {
|
|
|
44
179
|
})
|
|
45
180
|
|
|
46
181
|
onBeforeMount(() => {
|
|
47
|
-
state.meta = currentMeta.value
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
state.meta[field.field] = field.value
|
|
51
|
-
}
|
|
52
|
-
})
|
|
182
|
+
state.meta = cloneMeta(currentMeta.value)
|
|
183
|
+
initializeDefaults(state.meta)
|
|
184
|
+
syncFormValues(state.meta)
|
|
53
185
|
})
|
|
54
186
|
|
|
55
187
|
watch(currentMeta, async () => {
|
|
56
|
-
state.meta = currentMeta.value
|
|
57
|
-
|
|
188
|
+
state.meta = cloneMeta(currentMeta.value)
|
|
189
|
+
initializeDefaults(state.meta)
|
|
190
|
+
syncFormValues(state.meta)
|
|
191
|
+
if (edgeGlobal?.edgeState)
|
|
192
|
+
edgeGlobal.edgeState.changeTracker = {}
|
|
58
193
|
state.loaded = false
|
|
59
194
|
await nextTick()
|
|
60
195
|
state.loaded = true
|
|
61
196
|
})
|
|
62
197
|
const route = useRoute()
|
|
198
|
+
const menuIcon = computed(() => {
|
|
199
|
+
if (edgeGlobal?.iconFromMenu)
|
|
200
|
+
return edgeGlobal.iconFromMenu(route)
|
|
201
|
+
return null
|
|
202
|
+
})
|
|
63
203
|
</script>
|
|
64
204
|
|
|
65
205
|
<template>
|
|
66
206
|
<Card class="w-full flex-1 bg-muted/50 mx-auto w-full border-none shadow-none pt-2">
|
|
67
|
-
<
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
<
|
|
82
|
-
<
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
207
|
+
<edge-shad-form
|
|
208
|
+
ref="formRef"
|
|
209
|
+
:schema="resolvedSchema"
|
|
210
|
+
:initial-values="state.meta"
|
|
211
|
+
@submit="onSubmit"
|
|
212
|
+
>
|
|
213
|
+
<slot name="header">
|
|
214
|
+
<edge-menu class="bg-secondary text-foreground rounded-none sticky top-0 py-3">
|
|
215
|
+
<template #start>
|
|
216
|
+
<slot name="header-start">
|
|
217
|
+
<component :is="menuIcon" class="mr-2" />
|
|
218
|
+
<span class="capitalize">My Profile</span>
|
|
219
|
+
</slot>
|
|
220
|
+
</template>
|
|
221
|
+
<template #center>
|
|
222
|
+
<slot name="header-center">
|
|
223
|
+
<div class="w-full px-6" />
|
|
224
|
+
</slot>
|
|
225
|
+
</template>
|
|
226
|
+
<template #end>
|
|
227
|
+
<slot name="header-end">
|
|
228
|
+
<edge-shad-button
|
|
229
|
+
type="submit"
|
|
230
|
+
:disabled="state.loading"
|
|
231
|
+
class="text-white bg-slate-800 hover:bg-slate-400"
|
|
232
|
+
>
|
|
233
|
+
<Loader2 v-if="state.loading" class="w-4 h-4 mr-2 animate-spin" />
|
|
234
|
+
Update Profile
|
|
235
|
+
</edge-shad-button>
|
|
236
|
+
</slot>
|
|
237
|
+
</template>
|
|
238
|
+
</edge-menu>
|
|
239
|
+
</slot>
|
|
240
|
+
<CardContent v-if="state.loaded" class="p-3 w-full overflow-y-auto scroll-area">
|
|
241
|
+
<CardContent class="space-y-6">
|
|
242
|
+
<div class="rounded-xl border bg-card p-4 space-y-4 shadow-sm">
|
|
243
|
+
<div class="text-xs font-semibold uppercase tracking-wide text-muted-foreground">
|
|
244
|
+
Profile Details
|
|
245
|
+
</div>
|
|
246
|
+
<div class="flex flex-col gap-4 md:flex-row md:items-stretch">
|
|
247
|
+
<div class="w-full md:w-[260px] self-stretch">
|
|
248
|
+
<edge-image-picker
|
|
249
|
+
v-model="state.meta.profilephoto"
|
|
250
|
+
label="Profile Photo"
|
|
251
|
+
dialog-title="Select Profile Photo"
|
|
252
|
+
:site="`${clearwater - hub - images}-${edgeFirebase.user.uid}`"
|
|
253
|
+
:default-tags="['Headshots']"
|
|
254
|
+
height-class="h-full min-h-[180px]"
|
|
255
|
+
:include-cms-all="false"
|
|
256
|
+
/>
|
|
257
|
+
</div>
|
|
258
|
+
<div class="flex-1 space-y-4">
|
|
259
|
+
<edge-shad-input
|
|
260
|
+
v-model="state.meta.name"
|
|
261
|
+
name="name"
|
|
262
|
+
label="Name"
|
|
263
|
+
/>
|
|
264
|
+
<edge-shad-input
|
|
265
|
+
v-model="state.meta.email"
|
|
266
|
+
name="email"
|
|
267
|
+
label="Email"
|
|
268
|
+
disabled
|
|
269
|
+
description="This field is tied to your username and can only be updated on the Account page."
|
|
270
|
+
/>
|
|
271
|
+
<edge-shad-input
|
|
272
|
+
v-model="state.meta.phone"
|
|
273
|
+
name="phone"
|
|
274
|
+
label="Phone"
|
|
275
|
+
:mask-options="{ mask: '(###) ###-####' }"
|
|
276
|
+
/>
|
|
277
|
+
</div>
|
|
278
|
+
</div>
|
|
279
|
+
</div>
|
|
280
|
+
|
|
281
|
+
<div class="rounded-xl border bg-card p-4 space-y-4 shadow-sm">
|
|
282
|
+
<div class="text-xs font-semibold uppercase tracking-wide text-muted-foreground">
|
|
283
|
+
Additional Details
|
|
284
|
+
</div>
|
|
285
|
+
<div class="grid grid-cols-12 gap-4">
|
|
286
|
+
<div
|
|
287
|
+
v-for="field in filteredMetaFields"
|
|
288
|
+
:key="field.field"
|
|
289
|
+
class="col-span-12 mb-3"
|
|
290
|
+
:class="numColsToTailwind(field.cols)"
|
|
291
|
+
>
|
|
292
|
+
<edge-image-picker
|
|
293
|
+
v-if="field?.type === 'imagePicker'"
|
|
294
|
+
:model-value="getByPath(state.meta, field.field, '')"
|
|
295
|
+
:label="field?.label || 'Photo'"
|
|
296
|
+
:dialog-title="field?.dialogTitle || 'Select Image'"
|
|
297
|
+
:site="field?.site || 'clearwater-hub-images'"
|
|
298
|
+
:include-cms-all="false"
|
|
299
|
+
:default-tags="field?.tags || []"
|
|
300
|
+
:height-class="field?.heightClass || 'h-[160px]'"
|
|
301
|
+
:disabled="field?.disabled || false"
|
|
302
|
+
@update:model-value="val => setByPath(state.meta, field.field, val)"
|
|
303
|
+
/>
|
|
304
|
+
<p v-if="field?.type === 'imagePicker' && field?.disabled" class="mt-1 text-xs text-muted-foreground">
|
|
305
|
+
{{ getDisabledNote(field) }}
|
|
306
|
+
</p>
|
|
307
|
+
<div v-else-if="field?.type === 'richText'" class="profile-richtext">
|
|
308
|
+
<edge-shad-html
|
|
309
|
+
:model-value="getByPath(state.meta, field.field, '')"
|
|
310
|
+
:name="field.field"
|
|
311
|
+
:label="field?.label"
|
|
312
|
+
:disabled="field?.disabled || false"
|
|
313
|
+
:description="mergeDisabledNote(field?.description, field)"
|
|
314
|
+
:enabled-toggles="field?.enabledToggles || ['bold', 'italic', 'strike', 'bulletlist', 'orderedlist', 'underline']"
|
|
315
|
+
@update:model-value="val => setByPath(state.meta, field.field, val)"
|
|
316
|
+
/>
|
|
317
|
+
</div>
|
|
318
|
+
<edge-shad-select-tags
|
|
319
|
+
v-else-if="field?.type === 'selectTags'"
|
|
320
|
+
:model-value="getByPath(state.meta, field.field, [])"
|
|
321
|
+
:name="field.field"
|
|
322
|
+
:label="field?.label"
|
|
323
|
+
:description="mergeDisabledNote(field?.description, field)"
|
|
324
|
+
:items="field?.items || []"
|
|
325
|
+
:item-title="field?.itemTitle || 'title'"
|
|
326
|
+
:item-value="field?.itemValue || 'name'"
|
|
327
|
+
:allow-additions="field?.allowAdditions || false"
|
|
328
|
+
:placeholder="field?.placeholder"
|
|
329
|
+
:disabled="field?.disabled || false"
|
|
330
|
+
@update:model-value="val => setByPath(state.meta, field.field, val)"
|
|
331
|
+
/>
|
|
332
|
+
<div v-else-if="field?.type === 'boolean'" class="space-y-1 -mt-3">
|
|
333
|
+
<div class="text-sm font-medium leading-none opacity-0 select-none h-4">
|
|
334
|
+
{{ field?.label || '' }}
|
|
335
|
+
</div>
|
|
336
|
+
<edge-g-input
|
|
337
|
+
:model-value="getByPath(state.meta, field.field, false)"
|
|
338
|
+
:name="field.field"
|
|
339
|
+
:field-type="field?.type"
|
|
340
|
+
:label="field?.label"
|
|
341
|
+
parent-tracker-id="profile-settings"
|
|
342
|
+
:disable-tracking="true"
|
|
343
|
+
:disabled="field?.disabled || false"
|
|
344
|
+
@update:model-value="val => setByPath(state.meta, field.field, val)"
|
|
345
|
+
/>
|
|
346
|
+
<p v-if="mergeDisabledNote(field?.hint, field)" class="text-xs text-muted-foreground">
|
|
347
|
+
{{ mergeDisabledNote(field?.hint, field) }}
|
|
348
|
+
</p>
|
|
349
|
+
</div>
|
|
350
|
+
<edge-g-input
|
|
351
|
+
v-else-if="field?.type === 'textarea'"
|
|
352
|
+
:model-value="getByPath(state.meta, field.field, '')"
|
|
353
|
+
:name="field.field"
|
|
354
|
+
:field-type="field?.type"
|
|
355
|
+
:label="field?.label"
|
|
356
|
+
parent-tracker-id="profile-settings"
|
|
357
|
+
:hint="mergeDisabledNote(field?.hint, field)"
|
|
358
|
+
:persistent-hint="Boolean(mergeDisabledNote(field?.hint, field))"
|
|
359
|
+
:disable-tracking="true"
|
|
360
|
+
:bindings="{ class: 'h-60' }"
|
|
361
|
+
:mask-options="field?.maskOptions"
|
|
362
|
+
:disabled="field?.disabled || false"
|
|
363
|
+
@update:model-value="val => setByPath(state.meta, field.field, val)"
|
|
364
|
+
/>
|
|
365
|
+
<edge-shad-tags
|
|
366
|
+
v-else-if="field?.type === 'tags' || field?.type === 'commaTags'"
|
|
367
|
+
:model-value="getByPath(state.meta, field.field, '')"
|
|
368
|
+
:name="field.field"
|
|
369
|
+
:label="field?.label"
|
|
370
|
+
parent-tracker-id="profile-settings"
|
|
371
|
+
:description="mergeDisabledNote(field?.description || field?.hint, field)"
|
|
372
|
+
:disable-tracking="true"
|
|
373
|
+
:disabled="field?.disabled || false"
|
|
374
|
+
@update:model-value="val => setByPath(state.meta, field.field, val)"
|
|
375
|
+
/>
|
|
376
|
+
<edge-shad-input
|
|
377
|
+
v-else
|
|
378
|
+
:model-value="getByPath(state.meta, field.field, '')"
|
|
379
|
+
:name="field.field"
|
|
380
|
+
:type="field?.type === 'number' ? 'number' : 'text'"
|
|
381
|
+
:label="field?.label"
|
|
382
|
+
:mask-options="field?.maskOptions"
|
|
383
|
+
:description="mergeDisabledNote(field?.description, field)"
|
|
384
|
+
:disabled="field?.disabled || false"
|
|
385
|
+
@update:model-value="val => setByPath(state.meta, field.field, val)"
|
|
386
|
+
/>
|
|
387
|
+
</div>
|
|
388
|
+
</div>
|
|
103
389
|
</div>
|
|
104
390
|
</CardContent>
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
type="submit"
|
|
108
|
-
:disabled="state.loading"
|
|
109
|
-
class="text-white bg-slate-800 hover:bg-slate-400"
|
|
110
|
-
>
|
|
111
|
-
<Loader2 v-if="state.loading" class="w-4 h-4 mr-2 animate-spin" />
|
|
112
|
-
Save
|
|
113
|
-
</edge-shad-button>
|
|
114
|
-
</CardFooter>
|
|
115
|
-
</edge-shad-form>
|
|
116
|
-
</CardContent>
|
|
391
|
+
</CardContent>
|
|
392
|
+
</edge-shad-form>
|
|
117
393
|
</Card>
|
|
118
394
|
</template>
|
|
119
395
|
|
|
120
396
|
<style lang="scss" scoped>
|
|
397
|
+
:deep(.profile-richtext .tiptap) {
|
|
398
|
+
min-height: 220px;
|
|
399
|
+
padding: 0.75rem 1rem;
|
|
400
|
+
}
|
|
121
401
|
|
|
402
|
+
:deep(.profile-richtext .tiptap p) {
|
|
403
|
+
margin-top: 0;
|
|
404
|
+
margin-bottom: 1rem;
|
|
405
|
+
}
|
|
122
406
|
</style>
|