@edgedev/create-edge-app 1.1.28 → 1.1.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/edge/components/auth/register.vue +51 -0
- package/edge/components/cms/block.vue +29 -16
- 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 +131 -19
- package/edge/components/cms/optionsSelect.vue +20 -3
- package/edge/components/cms/page.vue +9 -0
- package/edge/components/cms/site.vue +31 -2
- package/edge/components/cms/siteSettingsForm.vue +7 -0
- 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 +496 -261
- package/edge/components/shad/number.vue +2 -2
- package/edge/composables/global.ts +4 -1
- package/edge/composables/structuredDataTemplates.js +6 -6
- package/package.json +1 -1
|
@@ -1,12 +1,19 @@
|
|
|
1
1
|
<script setup>
|
|
2
2
|
// TODO: pass possible roles in prop
|
|
3
3
|
import { toTypedSchema } from '@vee-validate/zod'
|
|
4
|
+
import { ArrowLeft, Loader2, Trash2, User } from 'lucide-vue-next'
|
|
4
5
|
import * as z from 'zod'
|
|
5
6
|
const props = defineProps({
|
|
6
7
|
usersCollectionPath: {
|
|
7
8
|
type: String,
|
|
8
9
|
default: () => `organizations/${edgeGlobal.edgeState.currentOrganization}`,
|
|
9
10
|
},
|
|
11
|
+
defaultImageTags: {
|
|
12
|
+
type: Array,
|
|
13
|
+
default: () => [
|
|
14
|
+
'Headshots',
|
|
15
|
+
],
|
|
16
|
+
},
|
|
10
17
|
metaFields: {
|
|
11
18
|
type: Array,
|
|
12
19
|
default: () => [
|
|
@@ -49,6 +56,11 @@ const props = defineProps({
|
|
|
49
56
|
name: z
|
|
50
57
|
.string({ required_error: 'Name is required' })
|
|
51
58
|
.min(1, { message: 'Name is required' }),
|
|
59
|
+
email: z
|
|
60
|
+
.string({ required_error: 'Email is required' })
|
|
61
|
+
.email({ message: 'Invalid email address' })
|
|
62
|
+
.min(6, { message: 'Email must be at least 6 characters long' })
|
|
63
|
+
.max(50, { message: 'Email must be less than 50 characters long' }),
|
|
52
64
|
}),
|
|
53
65
|
role: z
|
|
54
66
|
.string({ required_error: 'Role is required' })
|
|
@@ -56,6 +68,10 @@ const props = defineProps({
|
|
|
56
68
|
}),
|
|
57
69
|
),
|
|
58
70
|
},
|
|
71
|
+
metaFieldsSchema: {
|
|
72
|
+
type: Object,
|
|
73
|
+
default: null,
|
|
74
|
+
},
|
|
59
75
|
})
|
|
60
76
|
// TODO: If a removed user no longer has roles to any organiztions, need to a create new organization for them with
|
|
61
77
|
// default name of "Personal". This will allow them to continue to use the app.
|
|
@@ -120,12 +136,22 @@ const showInviteOrgSelect = computed(() => inviteOrgOptions.value.length > 1)
|
|
|
120
136
|
const showEditOrgSelect = computed(() => editOrgOptions.value.length > 1)
|
|
121
137
|
const showRemoveOrgSelect = computed(() => removeOrgOptions.value.length > 1)
|
|
122
138
|
|
|
139
|
+
const adminCount = computed(() => {
|
|
140
|
+
return users.value.filter((item) => {
|
|
141
|
+
return item.roles.find((role) => {
|
|
142
|
+
return role.collectionPath === edgeGlobal.edgeState.organizationDocPath.replaceAll('/', '-') && role.role === 'admin'
|
|
143
|
+
})
|
|
144
|
+
}).length
|
|
145
|
+
})
|
|
146
|
+
|
|
123
147
|
const selfRemoveBlocked = computed(() => {
|
|
124
148
|
return state.workingItem.userId === edgeFirebase.user.uid
|
|
125
149
|
&& adminCount.value === 1
|
|
126
150
|
&& state.removeOrgIds.includes(edgeGlobal.edgeState.currentOrganization)
|
|
127
151
|
})
|
|
128
152
|
|
|
153
|
+
const emailDisabledHint = 'This field is tied to the user\'s username and can only be changed by them.'
|
|
154
|
+
|
|
129
155
|
const WIDTHS = {
|
|
130
156
|
1: 'md:col-span-1',
|
|
131
157
|
2: 'md:col-span-2',
|
|
@@ -143,6 +169,42 @@ const WIDTHS = {
|
|
|
143
169
|
|
|
144
170
|
const numColsToTailwind = cols => WIDTHS[cols] || 'md:col-span-12'
|
|
145
171
|
|
|
172
|
+
const disabledNoteText = 'Contact admin to change.'
|
|
173
|
+
|
|
174
|
+
const getDisabledNote = (field) => {
|
|
175
|
+
if (!field?.disabled)
|
|
176
|
+
return ''
|
|
177
|
+
return field?.disabledNote || disabledNoteText
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
const mergeDisabledNote = (text, field) => {
|
|
181
|
+
const note = getDisabledNote(field)
|
|
182
|
+
if (!note)
|
|
183
|
+
return text || ''
|
|
184
|
+
if (text)
|
|
185
|
+
return `${text} ${note}`
|
|
186
|
+
return note
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
const userKey = (user) => {
|
|
190
|
+
return user?.docId || user?.userId || user?.id || user?.uid || ''
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
const PROFILE_IMAGE_SIZE = 96
|
|
194
|
+
const PROFILE_IMAGE_VARIANT = `width=${PROFILE_IMAGE_SIZE},height=${PROFILE_IMAGE_SIZE},fit=cover,quality=85`
|
|
195
|
+
|
|
196
|
+
const profileImageUrl = (url) => {
|
|
197
|
+
if (!url || typeof url !== 'string')
|
|
198
|
+
return ''
|
|
199
|
+
if (url.includes('/cdn-cgi/image/'))
|
|
200
|
+
return url
|
|
201
|
+
if (url.includes('width=') && url.includes('height='))
|
|
202
|
+
return url
|
|
203
|
+
if (url.endsWith('/thumbnail'))
|
|
204
|
+
return url.replace(/\/thumbnail$/, `/${PROFILE_IMAGE_VARIANT}`)
|
|
205
|
+
return url
|
|
206
|
+
}
|
|
207
|
+
|
|
146
208
|
// Helpers to read/write nested keys like "profile.firstName" on plain objects
|
|
147
209
|
function getByPath(obj, path, fallback = undefined) {
|
|
148
210
|
if (!obj || !path)
|
|
@@ -176,36 +238,33 @@ function setByPath(obj, path, value) {
|
|
|
176
238
|
}
|
|
177
239
|
|
|
178
240
|
const sortedFilteredUsers = computed(() => {
|
|
179
|
-
const filter = state.filter.toLowerCase()
|
|
241
|
+
const filter = String(state.filter || '').toLowerCase()
|
|
180
242
|
|
|
181
243
|
const getLastName = (fullName) => {
|
|
182
244
|
if (!fullName)
|
|
183
245
|
return ''
|
|
184
|
-
const parts = fullName.trim().split(/\s+/)
|
|
246
|
+
const parts = String(fullName).trim().split(/\s+/)
|
|
185
247
|
return parts[parts.length - 1] || ''
|
|
186
248
|
}
|
|
187
249
|
|
|
188
250
|
return users.value
|
|
189
|
-
.filter(user =>
|
|
251
|
+
.filter((user) => {
|
|
252
|
+
const name = String(user?.meta?.name || '')
|
|
253
|
+
return name.toLowerCase().includes(filter)
|
|
254
|
+
})
|
|
190
255
|
.sort((a, b) => {
|
|
191
|
-
const lastA = getLastName(a
|
|
192
|
-
const lastB = getLastName(b
|
|
256
|
+
const lastA = getLastName(a?.meta?.name).toLowerCase()
|
|
257
|
+
const lastB = getLastName(b?.meta?.name).toLowerCase()
|
|
193
258
|
return lastA.localeCompare(lastB)
|
|
194
259
|
})
|
|
195
260
|
})
|
|
196
261
|
|
|
197
|
-
const adminCount = computed(() => {
|
|
198
|
-
return users.value.filter((item) => {
|
|
199
|
-
return item.roles.find((role) => {
|
|
200
|
-
return role.collectionPath === edgeGlobal.edgeState.organizationDocPath.replaceAll('/', '-') && role.role === 'admin'
|
|
201
|
-
})
|
|
202
|
-
}).length
|
|
203
|
-
})
|
|
204
|
-
|
|
205
262
|
const addItem = () => {
|
|
206
263
|
state.saveButton = 'Invite User'
|
|
207
264
|
const newItem = edgeGlobal.dupObject(state.newItem)
|
|
208
265
|
newItem.meta.email = ''
|
|
266
|
+
newItem.meta.name = ''
|
|
267
|
+
newItem.meta.profilephoto = ''
|
|
209
268
|
state.workingItem = newItem
|
|
210
269
|
state.workingItem.id = edgeGlobal.generateShortId()
|
|
211
270
|
state.currentTitle = 'Invite User'
|
|
@@ -367,10 +426,48 @@ const onSubmit = async () => {
|
|
|
367
426
|
state.dialog = false
|
|
368
427
|
}
|
|
369
428
|
|
|
429
|
+
const roleSchema = z
|
|
430
|
+
.string({ required_error: 'Role is required' })
|
|
431
|
+
.min(1, { message: 'Role is required' })
|
|
432
|
+
|
|
433
|
+
const baseMetaSchema = z.object({
|
|
434
|
+
name: z
|
|
435
|
+
.string({ required_error: 'Name is required' })
|
|
436
|
+
.min(1, { message: 'Name is required' }),
|
|
437
|
+
email: z
|
|
438
|
+
.string({ required_error: 'Email is required' })
|
|
439
|
+
.email({ message: 'Invalid email address' })
|
|
440
|
+
.min(6, { message: 'Email must be at least 6 characters long' })
|
|
441
|
+
.max(50, { message: 'Email must be less than 50 characters long' }),
|
|
442
|
+
})
|
|
443
|
+
|
|
444
|
+
const buildMetaSchema = () => {
|
|
445
|
+
const extra = props.metaFieldsSchema
|
|
446
|
+
if (!extra)
|
|
447
|
+
return baseMetaSchema
|
|
448
|
+
if (extra?.shape && typeof extra.shape === 'object')
|
|
449
|
+
return baseMetaSchema.merge(extra)
|
|
450
|
+
if (typeof extra === 'object')
|
|
451
|
+
return baseMetaSchema.extend(extra)
|
|
452
|
+
return baseMetaSchema
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
const computedNewUserSchema = computed(() => {
|
|
456
|
+
if (!props.metaFieldsSchema)
|
|
457
|
+
return props.newUserSchema
|
|
458
|
+
return toTypedSchema(z.object({ meta: buildMetaSchema(), role: roleSchema }))
|
|
459
|
+
})
|
|
460
|
+
|
|
461
|
+
const computedUpdateUserSchema = computed(() => {
|
|
462
|
+
if (!props.metaFieldsSchema)
|
|
463
|
+
return props.updateUserSchema
|
|
464
|
+
return toTypedSchema(z.object({ meta: buildMetaSchema(), role: roleSchema }))
|
|
465
|
+
})
|
|
466
|
+
|
|
370
467
|
const computedUserSchema = computed(() =>
|
|
371
468
|
state.saveButton === 'Invite User'
|
|
372
|
-
?
|
|
373
|
-
:
|
|
469
|
+
? computedNewUserSchema.value
|
|
470
|
+
: computedUpdateUserSchema.value,
|
|
374
471
|
)
|
|
375
472
|
|
|
376
473
|
const currentOrganization = computed(() => {
|
|
@@ -406,267 +503,405 @@ onBeforeMount(async () => {
|
|
|
406
503
|
</script>
|
|
407
504
|
|
|
408
505
|
<template>
|
|
409
|
-
<
|
|
410
|
-
<
|
|
411
|
-
<
|
|
412
|
-
<
|
|
413
|
-
<
|
|
414
|
-
<
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
<template #center>
|
|
419
|
-
<slot name="header-center">
|
|
420
|
-
<div class="w-full px-6" />
|
|
421
|
-
</slot>
|
|
422
|
-
</template>
|
|
423
|
-
<template #end>
|
|
424
|
-
<slot name="header-end" :add-item="addItem">
|
|
425
|
-
<edge-shad-button class="bg-primary mx-2 h-6 text-xs" @click="addItem()">
|
|
426
|
-
Invite Member
|
|
427
|
-
</edge-shad-button>
|
|
428
|
-
</slot>
|
|
429
|
-
</template>
|
|
430
|
-
</edge-menu>
|
|
431
|
-
</slot>
|
|
432
|
-
<CardContent class="p-3 w-full overflow-y-auto scroll-area">
|
|
433
|
-
<Input
|
|
434
|
-
v-model="state.filter"
|
|
435
|
-
class="mb-2"
|
|
436
|
-
placeholder="Filter members..."
|
|
437
|
-
/>
|
|
438
|
-
<div v-if="sortedFilteredUsers.length > 0">
|
|
439
|
-
<div v-for="user in sortedFilteredUsers" :key="user.id" class="flex w-full py-2 justify-between items-center cursor-pointer" @click="editItem(user)">
|
|
440
|
-
<slot name="user" :user="user">
|
|
441
|
-
<Avatar class="handle pointer p-0 h-6 w-6 mr-2">
|
|
442
|
-
<User width="18" height="18" />
|
|
443
|
-
</Avatar>
|
|
444
|
-
<div class="flex gap-2 mr-2 items-center">
|
|
445
|
-
<div class="text-md text-bold mr-2">
|
|
446
|
-
{{ user.meta.name }}
|
|
506
|
+
<div v-if="state.loaded" class="w-full flex-1 min-h-0 h-[calc(100vh-58px)] overflow-hidden">
|
|
507
|
+
<ResizablePanelGroup direction="horizontal" class="w-full h-full flex-1">
|
|
508
|
+
<ResizablePanel class="bg-sidebar text-sidebar-foreground min-w-[400px]" :default-size="22" :min-size="30">
|
|
509
|
+
<div class="flex flex-col h-full">
|
|
510
|
+
<div class="px-3 py-3 border-b border-sidebar-border bg-sidebar/90">
|
|
511
|
+
<div class="flex items-center justify-between gap-2">
|
|
512
|
+
<div class="flex items-center gap-2 text-sm font-semibold">
|
|
513
|
+
<component :is="edgeGlobal.iconFromMenu(route)" class="h-4 w-4" />
|
|
514
|
+
<span>Members</span>
|
|
447
515
|
</div>
|
|
448
|
-
<edge-
|
|
449
|
-
|
|
450
|
-
</edge-
|
|
451
|
-
<!-- <edge-chip v-if="!user.userId" class="bg-primary">
|
|
452
|
-
Invited, Not Registered
|
|
453
|
-
</edge-chip> -->
|
|
454
|
-
</div>
|
|
455
|
-
<div class="grow flex gap-2 justify-end">
|
|
456
|
-
<template v-if="!user.userId">
|
|
457
|
-
<edge-chip class="bg-slate-600 w-[200px]">
|
|
458
|
-
{{ user.docId }}
|
|
459
|
-
<edge-clipboard-button class="relative ml-1 top-[2px] mt-0" :text="user.docId" />
|
|
460
|
-
</edge-chip>
|
|
461
|
-
</template>
|
|
462
|
-
<edge-chip>
|
|
463
|
-
{{ edgeGlobal.getRoleName(user.roles, edgeGlobal.edgeState.currentOrganization) }}
|
|
464
|
-
</edge-chip>
|
|
465
|
-
</div>
|
|
466
|
-
<edge-shad-button
|
|
467
|
-
:disabled="users.length === 1"
|
|
468
|
-
class="bg-red-400 mx-2 h-6 w-[80px] text-xs"
|
|
469
|
-
variant="outline"
|
|
470
|
-
@click.stop="deleteConfirm(user)"
|
|
471
|
-
>
|
|
472
|
-
<span v-if="user.userId === edgeFirebase.user.uid">Leave</span>
|
|
473
|
-
<span v-else>Remove</span>
|
|
474
|
-
</edge-shad-button>
|
|
475
|
-
</slot>
|
|
476
|
-
</div>
|
|
477
|
-
</div>
|
|
478
|
-
<edge-shad-dialog
|
|
479
|
-
v-model="state.deleteDialog"
|
|
480
|
-
>
|
|
481
|
-
<DialogContent>
|
|
482
|
-
<DialogHeader>
|
|
483
|
-
<DialogTitle>
|
|
484
|
-
<span v-if="state.workingItem.userId === edgeFirebase.user.uid">
|
|
485
|
-
Remove Yourself?
|
|
486
|
-
</span>
|
|
487
|
-
<span v-else>
|
|
488
|
-
Remove "{{ state.workingItem.meta.name }}"
|
|
489
|
-
</span>
|
|
490
|
-
</DialogTitle>
|
|
491
|
-
<DialogDescription />
|
|
492
|
-
</DialogHeader>
|
|
493
|
-
|
|
494
|
-
<h3 v-if="selfRemoveBlocked">
|
|
495
|
-
You cannot remove yourself from this organization because you are the only admin. You can delete the organization or add another admin.
|
|
496
|
-
</h3>
|
|
497
|
-
<h3 v-else-if="state.workingItem.userId === edgeFirebase.user.uid">
|
|
498
|
-
<span v-if="showRemoveOrgSelect">Select the organizations you want to leave.</span>
|
|
499
|
-
<span v-else>Are you sure you want to remove yourself from the organization "{{ currentOrganization.name }}"? You will no longer have access to any of the organization's data.</span>
|
|
500
|
-
</h3>
|
|
501
|
-
<h3 v-else>
|
|
502
|
-
<span v-if="showRemoveOrgSelect">Select the organizations you want to remove "{{ state.workingItem.meta.name }}" from.</span>
|
|
503
|
-
<span v-else>Are you sure you want to remove "{{ state.workingItem.meta.name }}" from the organization "{{ currentOrganization.name }}"?</span>
|
|
504
|
-
</h3>
|
|
505
|
-
<div v-if="showRemoveOrgSelect" class="mt-4 w-full flex flex-wrap gap-2">
|
|
506
|
-
<div v-for="org in removeOrgOptions" :key="org.docId" class="flex-1 min-w-[220px]">
|
|
507
|
-
<edge-shad-checkbox
|
|
508
|
-
:name="`remove-org-${org.docId}`"
|
|
509
|
-
:model-value="state.removeOrgIds.includes(org.docId)"
|
|
510
|
-
@update:model-value="val => updateRemoveOrgSelection(org.docId, val)"
|
|
511
|
-
>
|
|
512
|
-
{{ org.name }}
|
|
513
|
-
</edge-shad-checkbox>
|
|
516
|
+
<edge-shad-button size="sm" class="h-7 text-xs bg-primary" @click="addItem()">
|
|
517
|
+
Invite
|
|
518
|
+
</edge-shad-button>
|
|
514
519
|
</div>
|
|
520
|
+
<Input
|
|
521
|
+
v-model="state.filter"
|
|
522
|
+
class="mt-3 h-8 w-full"
|
|
523
|
+
placeholder="Filter members..."
|
|
524
|
+
/>
|
|
515
525
|
</div>
|
|
516
|
-
<
|
|
517
|
-
<
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
<edge-shad-checkbox
|
|
565
|
-
:name="`edit-add-org-${org.docId}`"
|
|
566
|
-
:model-value="state.editOrgIds.includes(org.docId)"
|
|
567
|
-
@update:model-value="val => updateEditOrgSelection(org.docId, val)"
|
|
526
|
+
<div class="flex-1 overflow-y-auto">
|
|
527
|
+
<SidebarMenu class="px-2 py-2 space-y-0">
|
|
528
|
+
<SidebarMenuItem
|
|
529
|
+
v-for="user in sortedFilteredUsers"
|
|
530
|
+
:key="userKey(user)"
|
|
531
|
+
>
|
|
532
|
+
<SidebarMenuButton
|
|
533
|
+
class="w-full !h-auto items-start px-3 py-2"
|
|
534
|
+
:class="state.dialog && userKey(state.workingItem) && userKey(state.workingItem) === userKey(user) ? 'bg-sidebar-accent text-sidebar-accent-foreground' : ''"
|
|
535
|
+
@click="editItem(user)"
|
|
536
|
+
>
|
|
537
|
+
<div class="flex w-full items-start gap-3" :class="!user.userId ? 'opacity-70' : ''">
|
|
538
|
+
<Avatar class="h-12 w-12 rounded-md border bg-muted/40 flex items-center justify-center overflow-hidden">
|
|
539
|
+
<img
|
|
540
|
+
v-if="user?.meta?.profilephoto"
|
|
541
|
+
:src="profileImageUrl(user.meta.profilephoto)"
|
|
542
|
+
alt=""
|
|
543
|
+
class="h-full w-full object-cover"
|
|
544
|
+
>
|
|
545
|
+
<User v-else width="24" height="24" />
|
|
546
|
+
</Avatar>
|
|
547
|
+
<div class="min-w-0 flex-1">
|
|
548
|
+
<div class="flex items-center gap-2">
|
|
549
|
+
<span class="text-sm font-medium leading-snug whitespace-normal uppercase">
|
|
550
|
+
{{ user?.meta?.name || user?.meta?.email || 'Unnamed Member' }}
|
|
551
|
+
</span>
|
|
552
|
+
<!-- <span v-if="!user.userId" class="text-[10px] uppercase tracking-wide text-muted-foreground">
|
|
553
|
+
-
|
|
554
|
+
</span> -->
|
|
555
|
+
<edge-chip v-if="user.userId === edgeFirebase.user.uid">
|
|
556
|
+
You
|
|
557
|
+
</edge-chip>
|
|
558
|
+
</div>
|
|
559
|
+
<div class="mt-1 flex flex-wrap items-center gap-2 text-[11px] text-muted-foreground leading-snug">
|
|
560
|
+
<span class="rounded-full bg-secondary px-2 py-0.5 text-[10px] text-secondary-foreground">
|
|
561
|
+
{{ edgeGlobal.getRoleName(user.roles, edgeGlobal.edgeState.currentOrganization) }}
|
|
562
|
+
</span>
|
|
563
|
+
<span v-if="!user.userId && user.docId" class="inline-flex items-center gap-1 whitespace-normal">
|
|
564
|
+
{{ user.docId }}
|
|
565
|
+
<edge-clipboard-button class="relative top-[1px]" :text="user.docId" />
|
|
566
|
+
</span>
|
|
567
|
+
</div>
|
|
568
|
+
</div>
|
|
569
|
+
<edge-shad-button
|
|
570
|
+
size="icon"
|
|
571
|
+
variant="ghost"
|
|
572
|
+
class="h-7 w-7 text-destructive/80 hover:text-destructive"
|
|
573
|
+
@click.stop="deleteConfirm(user)"
|
|
568
574
|
>
|
|
569
|
-
|
|
570
|
-
</edge-shad-
|
|
575
|
+
<Trash2 class="h-4 w-4" />
|
|
576
|
+
</edge-shad-button>
|
|
571
577
|
</div>
|
|
572
|
-
</
|
|
578
|
+
</SidebarMenuButton>
|
|
579
|
+
<Separator class="my-1" />
|
|
580
|
+
</SidebarMenuItem>
|
|
581
|
+
<div v-if="sortedFilteredUsers.length === 0" class="px-4 py-6 text-xs text-muted-foreground">
|
|
582
|
+
No members found.
|
|
573
583
|
</div>
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
584
|
+
</SidebarMenu>
|
|
585
|
+
</div>
|
|
586
|
+
</div>
|
|
587
|
+
</ResizablePanel>
|
|
588
|
+
<ResizablePanel class="bg-background">
|
|
589
|
+
<div class="h-full flex flex-col">
|
|
590
|
+
<div v-if="state.dialog" class="h-full flex flex-col">
|
|
591
|
+
<edge-shad-form
|
|
592
|
+
:key="userKey(state.workingItem) || state.workingItem?.id || 'member-form'"
|
|
593
|
+
:initial-values="state.workingItem"
|
|
594
|
+
:schema="computedUserSchema"
|
|
595
|
+
class="flex flex-col h-full"
|
|
596
|
+
@submit="onSubmit"
|
|
597
|
+
>
|
|
598
|
+
<div class="flex items-center justify-between border-b bg-secondary px-4 py-3">
|
|
599
|
+
<div class="text-sm font-semibold">
|
|
600
|
+
{{ state.currentTitle || 'Member' }}
|
|
577
601
|
</div>
|
|
578
|
-
<div class="
|
|
579
|
-
<
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
602
|
+
<div class="flex items-center gap-2">
|
|
603
|
+
<edge-shad-button variant="text" class="text-xs text-red-700" @click="closeDialog">
|
|
604
|
+
Close
|
|
605
|
+
</edge-shad-button>
|
|
606
|
+
<edge-shad-button
|
|
607
|
+
type="submit"
|
|
608
|
+
class="text-xs bg-primary"
|
|
609
|
+
:disabled="state.loading"
|
|
610
|
+
>
|
|
611
|
+
<Loader2 v-if="state.loading" class="w-4 h-4 mr-2 animate-spin" />
|
|
612
|
+
{{ state.saveButton }}
|
|
613
|
+
</edge-shad-button>
|
|
588
614
|
</div>
|
|
589
615
|
</div>
|
|
590
|
-
<
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
616
|
+
<div class="flex-1 overflow-y-auto p-6 space-y-4">
|
|
617
|
+
<slot name="edit-fields" :working-item="state.workingItem">
|
|
618
|
+
<div class="rounded-xl border bg-card p-4 space-y-4 shadow-sm">
|
|
619
|
+
<div class="text-xs font-semibold uppercase tracking-wide text-muted-foreground">
|
|
620
|
+
Member Details
|
|
621
|
+
</div>
|
|
622
|
+
<div class="flex flex-col gap-4 md:flex-row md:items-stretch">
|
|
623
|
+
<div class="w-full md:w-[260px] self-stretch">
|
|
624
|
+
<edge-image-picker
|
|
625
|
+
v-model="state.workingItem.meta.profilephoto"
|
|
626
|
+
label="Profile Photo"
|
|
627
|
+
dialog-title="Select Profile Photo"
|
|
628
|
+
site="clearwater-hub-images"
|
|
629
|
+
:default-tags="props.defaultImageTags"
|
|
630
|
+
height-class="h-full min-h-[180px]"
|
|
631
|
+
:include-cms-all="false"
|
|
632
|
+
/>
|
|
633
|
+
</div>
|
|
634
|
+
<div class="flex-1 space-y-4">
|
|
635
|
+
<edge-g-input
|
|
636
|
+
v-model="state.workingItem.role"
|
|
637
|
+
name="role"
|
|
638
|
+
:disable-tracking="true"
|
|
639
|
+
:items="roleNamesOnly"
|
|
640
|
+
field-type="select"
|
|
641
|
+
label="Role"
|
|
642
|
+
:parent-tracker-id="`inviteUser-${state.workingItem.id}`"
|
|
643
|
+
:disabled="state.workingItem.userId === edgeFirebase.user.uid"
|
|
644
|
+
/>
|
|
645
|
+
<edge-g-input
|
|
646
|
+
v-model="state.workingItem.meta.name"
|
|
647
|
+
name="meta.name"
|
|
648
|
+
:disable-tracking="true"
|
|
649
|
+
field-type="text"
|
|
650
|
+
label="Name"
|
|
651
|
+
:parent-tracker-id="`inviteUser-${state.workingItem.id}`"
|
|
652
|
+
/>
|
|
653
|
+
<edge-g-input
|
|
654
|
+
v-model="state.workingItem.meta.email"
|
|
655
|
+
name="meta.email"
|
|
656
|
+
:disable-tracking="true"
|
|
657
|
+
field-type="text"
|
|
658
|
+
label="Email"
|
|
659
|
+
:disabled="state.saveButton !== 'Invite User'"
|
|
660
|
+
:hint="state.saveButton !== 'Invite User' ? emailDisabledHint : ''"
|
|
661
|
+
:persistent-hint="state.saveButton !== 'Invite User'"
|
|
662
|
+
:parent-tracker-id="`inviteUser-${state.workingItem.id}`"
|
|
663
|
+
/>
|
|
664
|
+
<edge-g-input
|
|
665
|
+
v-model="state.workingItem.meta.phone"
|
|
666
|
+
name="meta.phone"
|
|
667
|
+
:disable-tracking="true"
|
|
668
|
+
field-type="text"
|
|
669
|
+
label="Phone"
|
|
670
|
+
:mask-options="{ mask: '(###) ###-####' }"
|
|
671
|
+
:parent-tracker-id="`inviteUser-${state.workingItem.id}`"
|
|
672
|
+
/>
|
|
673
|
+
</div>
|
|
674
|
+
</div>
|
|
675
|
+
</div>
|
|
676
|
+
<div v-if="state.saveButton !== 'Invite User' && showEditOrgSelect" class="mt-4 w-full">
|
|
677
|
+
<div class="text-sm font-medium text-foreground">
|
|
678
|
+
Organizations
|
|
679
|
+
</div>
|
|
680
|
+
<div class="mt-2 w-full flex flex-wrap gap-2">
|
|
681
|
+
<div v-for="org in editOrgOptions" :key="org.docId" class="flex-1 min-w-[220px]">
|
|
682
|
+
<edge-shad-checkbox
|
|
683
|
+
:name="`edit-add-org-${org.docId}`"
|
|
684
|
+
:model-value="state.editOrgIds.includes(org.docId)"
|
|
685
|
+
@update:model-value="val => updateEditOrgSelection(org.docId, val)"
|
|
686
|
+
>
|
|
687
|
+
{{ org.name }}
|
|
688
|
+
</edge-shad-checkbox>
|
|
689
|
+
</div>
|
|
690
|
+
</div>
|
|
691
|
+
</div>
|
|
692
|
+
<div v-if="state.saveButton === 'Invite User' && showInviteOrgSelect" class="mt-4 w-full">
|
|
693
|
+
<div class="text-sm font-medium text-foreground">
|
|
694
|
+
Add to organizations
|
|
695
|
+
</div>
|
|
696
|
+
<div class="mt-2 w-full flex flex-wrap gap-2">
|
|
697
|
+
<div v-for="org in inviteOrgOptions" :key="org.docId" class="flex-1 min-w-[220px]">
|
|
698
|
+
<edge-shad-checkbox
|
|
699
|
+
:name="`invite-org-${org.docId}`"
|
|
700
|
+
:model-value="state.inviteOrgIds.includes(org.docId)"
|
|
701
|
+
@update:model-value="val => updateInviteOrgSelection(org.docId, val)"
|
|
702
|
+
>
|
|
703
|
+
{{ org.name }}
|
|
704
|
+
</edge-shad-checkbox>
|
|
705
|
+
</div>
|
|
706
|
+
</div>
|
|
707
|
+
</div>
|
|
708
|
+
<Separator class="my-6" />
|
|
709
|
+
<div class="grid grid-cols-12 gap-2">
|
|
710
|
+
<div v-for="field in props.metaFields" :key="field.field" class="mb-3 col-span-12" :class="numColsToTailwind(field.cols)">
|
|
711
|
+
<!-- Use explicit model binding so dotted paths (e.g., "address.street") work -->
|
|
712
|
+
<edge-image-picker
|
|
713
|
+
v-if="field?.type === 'imagePicker'"
|
|
714
|
+
:model-value="getByPath(state.workingItem.meta, field.field, '')"
|
|
715
|
+
:label="field?.label || 'Photo'"
|
|
716
|
+
:dialog-title="field?.dialogTitle || 'Select Image'"
|
|
717
|
+
:site="field?.site || 'clearwater-hub-images'"
|
|
718
|
+
:default-tags="field?.tags || []"
|
|
719
|
+
:height-class="field?.heightClass || 'h-[160px]'"
|
|
720
|
+
:disabled="field?.disabled || false"
|
|
721
|
+
:include-cms-all="false"
|
|
722
|
+
@update:model-value="val => setByPath(state.workingItem.meta, field.field, val)"
|
|
723
|
+
/>
|
|
724
|
+
<p v-if="field?.type === 'imagePicker' && field?.disabled" class="mt-1 text-xs text-muted-foreground">
|
|
725
|
+
{{ getDisabledNote(field) }}
|
|
726
|
+
</p>
|
|
727
|
+
<div v-else-if="field?.type === 'richText'" class="member-richtext">
|
|
728
|
+
<edge-shad-html
|
|
729
|
+
:model-value="getByPath(state.workingItem.meta, field.field, '')"
|
|
730
|
+
:name="`meta.${field.field}`"
|
|
731
|
+
:label="field?.label"
|
|
732
|
+
:disabled="field?.disabled || false"
|
|
733
|
+
:description="mergeDisabledNote(field?.description, field)"
|
|
734
|
+
:enabled-toggles="field?.enabledToggles || ['bold', 'italic', 'strike', 'bulletlist', 'orderedlist', 'underline']"
|
|
735
|
+
@update:model-value="val => setByPath(state.workingItem.meta, field.field, val)"
|
|
736
|
+
/>
|
|
737
|
+
</div>
|
|
738
|
+
<edge-shad-select-tags
|
|
739
|
+
v-else-if="field?.type === 'selectTags'"
|
|
740
|
+
:model-value="getByPath(state.workingItem.meta, field.field, [])"
|
|
741
|
+
:name="`meta.${field.field}`"
|
|
742
|
+
:label="field?.label"
|
|
743
|
+
:description="mergeDisabledNote(field?.description, field)"
|
|
744
|
+
:items="field?.items || []"
|
|
745
|
+
:item-title="field?.itemTitle || 'title'"
|
|
746
|
+
:item-value="field?.itemValue || 'name'"
|
|
747
|
+
:allow-additions="field?.allowAdditions || false"
|
|
748
|
+
:placeholder="field?.placeholder"
|
|
749
|
+
:disabled="field?.disabled || false"
|
|
750
|
+
@update:model-value="val => setByPath(state.workingItem.meta, field.field, val)"
|
|
751
|
+
/>
|
|
752
|
+
<div v-else-if="field?.type === 'boolean'" class="space-y-1 -mt-3">
|
|
753
|
+
<div class="text-sm font-medium leading-none opacity-0 select-none h-4">
|
|
754
|
+
{{ field?.label || '' }}
|
|
755
|
+
</div>
|
|
756
|
+
<edge-g-input
|
|
757
|
+
:model-value="getByPath(state.workingItem.meta, field.field, false)"
|
|
758
|
+
:name="`meta.${field.field}`"
|
|
759
|
+
:field-type="field?.type"
|
|
760
|
+
:label="field?.label"
|
|
761
|
+
parent-tracker-id="user-settings"
|
|
762
|
+
:disable-tracking="true"
|
|
763
|
+
:disabled="field?.disabled || false"
|
|
764
|
+
@update:model-value="val => setByPath(state.workingItem.meta, field.field, val)"
|
|
765
|
+
/>
|
|
766
|
+
<p v-if="mergeDisabledNote(field?.hint, field)" class="text-xs text-muted-foreground">
|
|
767
|
+
{{ mergeDisabledNote(field?.hint, field) }}
|
|
768
|
+
</p>
|
|
769
|
+
</div>
|
|
770
|
+
<edge-g-input
|
|
771
|
+
v-else-if="field?.type === 'textarea'"
|
|
772
|
+
:model-value="getByPath(state.workingItem.meta, field.field, '')"
|
|
773
|
+
:name="`meta.${field.field}`"
|
|
774
|
+
:field-type="field?.type"
|
|
775
|
+
:label="field?.label"
|
|
776
|
+
parent-tracker-id="user-settings"
|
|
777
|
+
:hint="mergeDisabledNote(field?.hint, field)"
|
|
778
|
+
:persistent-hint="Boolean(mergeDisabledNote(field?.hint, field))"
|
|
779
|
+
:disable-tracking="true"
|
|
780
|
+
:bindings="{ class: 'h-60' }"
|
|
781
|
+
:mask-options="field?.maskOptions"
|
|
782
|
+
:disabled="field?.disabled || false"
|
|
783
|
+
@update:model-value="val => setByPath(state.workingItem.meta, field.field, val)"
|
|
784
|
+
/>
|
|
785
|
+
<edge-shad-tags
|
|
786
|
+
v-else-if="field?.type === 'tags' || field?.type === 'commaTags'"
|
|
787
|
+
:model-value="getByPath(state.workingItem.meta, field.field, '')"
|
|
788
|
+
:name="`meta.${field.field}`"
|
|
789
|
+
:field-type="field?.type"
|
|
790
|
+
:label="field?.label"
|
|
791
|
+
parent-tracker-id="user-settings"
|
|
792
|
+
:description="mergeDisabledNote(field?.description || field?.hint, field)"
|
|
793
|
+
:disable-tracking="true"
|
|
794
|
+
:disabled="field?.disabled || false"
|
|
795
|
+
@update:model-value="val => setByPath(state.workingItem.meta, field.field, val)"
|
|
796
|
+
/>
|
|
797
|
+
<edge-g-input
|
|
798
|
+
v-else
|
|
799
|
+
:model-value="getByPath(state.workingItem.meta, field.field, '')"
|
|
800
|
+
:name="`meta.${field.field}`"
|
|
801
|
+
:field-type="field?.type"
|
|
802
|
+
:label="field?.label"
|
|
803
|
+
parent-tracker-id="user-settings"
|
|
804
|
+
:hint="mergeDisabledNote(field?.hint, field)"
|
|
805
|
+
:persistent-hint="Boolean(mergeDisabledNote(field?.hint, field))"
|
|
806
|
+
:disable-tracking="true"
|
|
807
|
+
:mask-options="field?.maskOptions"
|
|
808
|
+
:disabled="field?.disabled || false"
|
|
809
|
+
@update:model-value="val => setByPath(state.workingItem.meta, field.field, val)"
|
|
810
|
+
/>
|
|
811
|
+
</div>
|
|
812
|
+
</div>
|
|
813
|
+
|
|
626
814
|
<edge-g-input
|
|
627
|
-
v-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
:field-type="field?.type"
|
|
631
|
-
:label="field?.label"
|
|
632
|
-
parent-tracker-id="user-settings"
|
|
633
|
-
:hint="field?.hint"
|
|
815
|
+
v-if="state.saveButton === 'Invite User'"
|
|
816
|
+
v-model="state.workingItem.isTemplate"
|
|
817
|
+
name="isTemplate"
|
|
634
818
|
:disable-tracking="true"
|
|
635
|
-
|
|
819
|
+
field-type="boolean"
|
|
820
|
+
label="Template User"
|
|
821
|
+
:parent-tracker-id="`inviteUser-${state.workingItem.id}`"
|
|
636
822
|
/>
|
|
637
|
-
</
|
|
823
|
+
</slot>
|
|
638
824
|
</div>
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
</
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
825
|
+
</edge-shad-form>
|
|
826
|
+
</div>
|
|
827
|
+
<div v-else class="p-4 text-center flex text-slate-500 h-[calc(100vh-4rem)] justify-center items-center overflow-y-auto">
|
|
828
|
+
<div class="text-4xl">
|
|
829
|
+
<ArrowLeft class="inline-block w-12 h-12 mr-2" /> Select a member to view details.
|
|
830
|
+
</div>
|
|
831
|
+
</div>
|
|
832
|
+
</div>
|
|
833
|
+
</ResizablePanel>
|
|
834
|
+
</ResizablePanelGroup>
|
|
835
|
+
|
|
836
|
+
<edge-shad-dialog
|
|
837
|
+
v-model="state.deleteDialog"
|
|
838
|
+
>
|
|
839
|
+
<DialogContent>
|
|
840
|
+
<DialogHeader>
|
|
841
|
+
<DialogTitle>
|
|
842
|
+
<span v-if="state.workingItem.userId === edgeFirebase.user.uid">
|
|
843
|
+
Remove Yourself?
|
|
844
|
+
</span>
|
|
845
|
+
<span v-else>
|
|
846
|
+
Remove "{{ state.workingItem.meta.name }}"
|
|
847
|
+
</span>
|
|
848
|
+
</DialogTitle>
|
|
849
|
+
<DialogDescription />
|
|
850
|
+
</DialogHeader>
|
|
851
|
+
|
|
852
|
+
<h3 v-if="selfRemoveBlocked">
|
|
853
|
+
You cannot remove yourself from this organization because you are the only admin. You can delete the organization or add another admin.
|
|
854
|
+
</h3>
|
|
855
|
+
<h3 v-else-if="state.workingItem.userId === edgeFirebase.user.uid">
|
|
856
|
+
<span v-if="showRemoveOrgSelect">Select the organizations you want to leave.</span>
|
|
857
|
+
<span v-else>Are you sure you want to remove yourself from the organization "{{ currentOrganization.name }}"? You will no longer have access to any of the organization's data.</span>
|
|
858
|
+
</h3>
|
|
859
|
+
<h3 v-else>
|
|
860
|
+
<span v-if="showRemoveOrgSelect">Select the organizations you want to remove "{{ state.workingItem.meta.name }}" from.</span>
|
|
861
|
+
<span v-else>Are you sure you want to remove "{{ state.workingItem.meta.name }}" from the organization "{{ currentOrganization.name }}"?</span>
|
|
862
|
+
</h3>
|
|
863
|
+
<div v-if="showRemoveOrgSelect" class="mt-4 w-full flex flex-wrap gap-2">
|
|
864
|
+
<div v-for="org in removeOrgOptions" :key="org.docId" class="flex-1 min-w-[220px]">
|
|
865
|
+
<edge-shad-checkbox
|
|
866
|
+
:name="`remove-org-${org.docId}`"
|
|
867
|
+
:model-value="state.removeOrgIds.includes(org.docId)"
|
|
868
|
+
@update:model-value="val => updateRemoveOrgSelection(org.docId, val)"
|
|
869
|
+
>
|
|
870
|
+
{{ org.name }}
|
|
871
|
+
</edge-shad-checkbox>
|
|
872
|
+
</div>
|
|
873
|
+
</div>
|
|
874
|
+
<DialogFooter class="pt-6 flex justify-between">
|
|
875
|
+
<edge-shad-button class="text-white bg-slate-800 hover:bg-slate-400" @click="state.deleteDialog = false">
|
|
876
|
+
Cancel
|
|
877
|
+
</edge-shad-button>
|
|
878
|
+
<edge-shad-button
|
|
879
|
+
:disabled="selfRemoveBlocked"
|
|
880
|
+
class="w-full"
|
|
881
|
+
variant="destructive"
|
|
882
|
+
@click="deleteAction()"
|
|
883
|
+
>
|
|
884
|
+
<span v-if="state.workingItem.userId === edgeFirebase.user.uid">
|
|
885
|
+
Leave
|
|
886
|
+
</span>
|
|
887
|
+
<span v-else>
|
|
888
|
+
Remove
|
|
889
|
+
</span>
|
|
890
|
+
</edge-shad-button>
|
|
891
|
+
</DialogFooter>
|
|
892
|
+
</DialogContent>
|
|
893
|
+
</edge-shad-dialog>
|
|
894
|
+
</div>
|
|
668
895
|
</template>
|
|
669
896
|
|
|
670
897
|
<style lang="scss" scoped>
|
|
898
|
+
:deep(.member-richtext .tiptap) {
|
|
899
|
+
min-height: 220px;
|
|
900
|
+
padding: 0.75rem 1rem;
|
|
901
|
+
}
|
|
671
902
|
|
|
903
|
+
:deep(.member-richtext .tiptap p) {
|
|
904
|
+
margin-top: 0;
|
|
905
|
+
margin-bottom: 1rem;
|
|
906
|
+
}
|
|
672
907
|
</style>
|