@edgedev/create-edge-app 1.1.25 → 1.1.27
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 +1475 -0
- package/edge/components/cms/themeDefaultMenu.vue +548 -0
- package/edge/components/cms/themeEditor.vue +429 -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,271 @@
|
|
|
1
|
+
<script setup>
|
|
2
|
+
import { computed, defineProps, inject, nextTick, onBeforeMount, reactive, watch } from 'vue'
|
|
3
|
+
import CardContent from '~/components/ui/card/CardContent.vue'
|
|
4
|
+
|
|
5
|
+
import { useToast } from '@/components/ui/toast/use-toast'
|
|
6
|
+
const props = defineProps({
|
|
7
|
+
subscribeOptions: {
|
|
8
|
+
type: Array,
|
|
9
|
+
default: () => [],
|
|
10
|
+
},
|
|
11
|
+
orgFields: {
|
|
12
|
+
type: Object,
|
|
13
|
+
required: true,
|
|
14
|
+
},
|
|
15
|
+
title: {
|
|
16
|
+
type: String,
|
|
17
|
+
default: 'Organization Settings',
|
|
18
|
+
},
|
|
19
|
+
hideUniqueIdentifier: {
|
|
20
|
+
type: Boolean,
|
|
21
|
+
default: false,
|
|
22
|
+
},
|
|
23
|
+
formSchema: {
|
|
24
|
+
type: Object,
|
|
25
|
+
required: true,
|
|
26
|
+
},
|
|
27
|
+
})
|
|
28
|
+
const { toast } = useToast()
|
|
29
|
+
|
|
30
|
+
const edgeFirebase = inject('edgeFirebase')
|
|
31
|
+
// const edgeGlobal = inject('edgeGlobal')
|
|
32
|
+
|
|
33
|
+
const state = reactive({
|
|
34
|
+
data: {},
|
|
35
|
+
org: '',
|
|
36
|
+
form: false,
|
|
37
|
+
loaded: false,
|
|
38
|
+
showSnack: false,
|
|
39
|
+
successMessage: '',
|
|
40
|
+
snackColor: 'success',
|
|
41
|
+
loading: false,
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
const onSubmit = async () => {
|
|
45
|
+
state.loading = true
|
|
46
|
+
state.showSnack = false
|
|
47
|
+
const result = await edgeFirebase.changeDoc('organizations', edgeGlobal.edgeState.currentOrganization, state.data)
|
|
48
|
+
edgeGlobal.getOrganizations(edgeFirebase)
|
|
49
|
+
|
|
50
|
+
edgeGlobal.edgeState.changeTracker = {}
|
|
51
|
+
state.loaded = false
|
|
52
|
+
let message = 'Updated Successfully'
|
|
53
|
+
if (!result.success) {
|
|
54
|
+
message = 'You do not have permission'
|
|
55
|
+
}
|
|
56
|
+
toast({
|
|
57
|
+
title: '',
|
|
58
|
+
description: message,
|
|
59
|
+
duration: 1000,
|
|
60
|
+
})
|
|
61
|
+
await nextTick()
|
|
62
|
+
state.loaded = true
|
|
63
|
+
state.loading = false
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const currentOrgData = computed(() => {
|
|
67
|
+
if (edgeGlobal.objHas(edgeFirebase.data, edgeGlobal.edgeState.organizationDocPath) === false) {
|
|
68
|
+
return ''
|
|
69
|
+
}
|
|
70
|
+
return edgeFirebase.data[edgeGlobal.edgeState.organizationDocPath]
|
|
71
|
+
})
|
|
72
|
+
|
|
73
|
+
const loadStateData = () => {
|
|
74
|
+
state.data = edgeGlobal.dupObject(currentOrgData.value)
|
|
75
|
+
for (const field of props.orgFields) {
|
|
76
|
+
if (edgeGlobal.objHas(state.data, field.field) === false) {
|
|
77
|
+
if (field.type === 'section') {
|
|
78
|
+
state.data[field.field] = {}
|
|
79
|
+
for (const subField of field.fields) {
|
|
80
|
+
if (edgeGlobal.objHas(state.data[field.field], subField.field) === false) {
|
|
81
|
+
state.data[field.field][subField.field] = subField.value
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
else {
|
|
86
|
+
state.data[field.field] = field.value
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
onBeforeMount(() => {
|
|
93
|
+
loadStateData()
|
|
94
|
+
state.loaded = true
|
|
95
|
+
})
|
|
96
|
+
watch(currentOrgData, async () => {
|
|
97
|
+
edgeGlobal.edgeState.changeTracker = {}
|
|
98
|
+
loadStateData()
|
|
99
|
+
state.loaded = false
|
|
100
|
+
await nextTick()
|
|
101
|
+
state.loaded = true
|
|
102
|
+
})
|
|
103
|
+
|
|
104
|
+
const navigateToBilling = async () => {
|
|
105
|
+
state.loading = true
|
|
106
|
+
const billingLink = await edgeFirebase.runFunction('stripe-redirectToBilling', {})
|
|
107
|
+
window.location.href = billingLink.data.url
|
|
108
|
+
state.loading = false
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const gotoSubscription = async (url) => {
|
|
112
|
+
state.loading = true
|
|
113
|
+
window.location.href = url
|
|
114
|
+
state.loading = false
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const route = useRoute()
|
|
118
|
+
</script>
|
|
119
|
+
|
|
120
|
+
<template>
|
|
121
|
+
<Card class="w-full flex-1 bg-muted/50 mx-auto w-full border-none shadow-none pt-2">
|
|
122
|
+
<slot
|
|
123
|
+
name="subscription"
|
|
124
|
+
:subscribed-status="edgeGlobal.edgeState.subscribedStatus"
|
|
125
|
+
:subscribe-options="props.subscribeOptions"
|
|
126
|
+
:goto-subscription="gotoSubscription"
|
|
127
|
+
:navigate-to-billing="navigateToBilling"
|
|
128
|
+
:current-organization="edgeGlobal.edgeState.currentOrganization"
|
|
129
|
+
>
|
|
130
|
+
<Alert v-if="edgeGlobal.edgeState.subscribedStatus && props.subscribeOptions.length > 0" class="mt-0" :class="edgeGlobal.edgeState.subscribedStatus.color">
|
|
131
|
+
<component :is="edgeGlobal.edgeState.subscribedStatus.icon" />
|
|
132
|
+
<AlertTitle>
|
|
133
|
+
{{ edgeGlobal.edgeState.subscribedStatus.status }}
|
|
134
|
+
</AlertTitle>
|
|
135
|
+
<AlertDescription>
|
|
136
|
+
{{ edgeGlobal.edgeState.subscribedStatus.description }}
|
|
137
|
+
<template v-if="!edgeGlobal.edgeState.subscribedStatus.isSubscribed">
|
|
138
|
+
<div class="text-center w-full mb-4">
|
|
139
|
+
<slot name="subscribeTitle">
|
|
140
|
+
<span class="text-2xl">Subscribe now with 7 day free trial!</span>
|
|
141
|
+
</slot>
|
|
142
|
+
</div>
|
|
143
|
+
<slot name="subscribeOptions">
|
|
144
|
+
<div class="flex justify-center space-x-4">
|
|
145
|
+
<edge-shad-button
|
|
146
|
+
v-for="option in props.subscribeOptions"
|
|
147
|
+
:key="option.buttonText"
|
|
148
|
+
class="text-white w-100 bg-slate-800 hover:bg-slate-400"
|
|
149
|
+
@click="gotoSubscription(`${option.stripeSubscriptionLink}?client_reference_id=${edgeGlobal.edgeState.currentOrganization}`)"
|
|
150
|
+
>
|
|
151
|
+
{{ option.buttonText }}
|
|
152
|
+
</edge-shad-button>
|
|
153
|
+
</div>
|
|
154
|
+
</slot>
|
|
155
|
+
</template>
|
|
156
|
+
<div v-else class="flex flex-col sm:flex-row">
|
|
157
|
+
<div>
|
|
158
|
+
<edge-shad-button
|
|
159
|
+
class="text-white w-100 bg-slate-800 hover:bg-slate-400"
|
|
160
|
+
@click="navigateToBilling"
|
|
161
|
+
>
|
|
162
|
+
Manage Subscription
|
|
163
|
+
</edge-shad-button>
|
|
164
|
+
</div>
|
|
165
|
+
</div>
|
|
166
|
+
</AlertDescription>
|
|
167
|
+
</Alert>
|
|
168
|
+
</slot>
|
|
169
|
+
|
|
170
|
+
<edge-shad-form
|
|
171
|
+
:schema="props.formSchema"
|
|
172
|
+
@submit="onSubmit"
|
|
173
|
+
>
|
|
174
|
+
<slot name="header">
|
|
175
|
+
<edge-menu class="bg-secondary text-foreground rounded-none sticky top-0 py-6">
|
|
176
|
+
<template #start>
|
|
177
|
+
<slot name="header-start">
|
|
178
|
+
<component :is="edgeGlobal.iconFromMenu(route)" class="mr-2" />
|
|
179
|
+
<span class="capitalize">Organization Settings</span>
|
|
180
|
+
</slot>
|
|
181
|
+
</template>
|
|
182
|
+
<template #center>
|
|
183
|
+
<slot name="header-center">
|
|
184
|
+
<div v-if="!props.hideSearch" class="w-full px-6" />
|
|
185
|
+
</slot>
|
|
186
|
+
</template>
|
|
187
|
+
<template #end>
|
|
188
|
+
<slot name="header-end">
|
|
189
|
+
<div />
|
|
190
|
+
</slot>
|
|
191
|
+
</template>
|
|
192
|
+
</edge-menu>
|
|
193
|
+
</slot>
|
|
194
|
+
<CardContent v-if="state.loaded" class="p-3 w-full overflow-y-auto scroll-area">
|
|
195
|
+
<template v-for="field in props.orgFields" :key="field.field">
|
|
196
|
+
<Card v-if="field.type === 'section'" class="mb-2">
|
|
197
|
+
<CardHeader>
|
|
198
|
+
<CardTitle>
|
|
199
|
+
{{ field.label }}
|
|
200
|
+
</CardTitle>
|
|
201
|
+
</CardHeader>
|
|
202
|
+
<CardContent>
|
|
203
|
+
<div class="grid gap-2">
|
|
204
|
+
<template v-for="subField in field.fields" :key="subField.field">
|
|
205
|
+
<edge-g-input
|
|
206
|
+
v-model="state.data[field.field][subField.field]"
|
|
207
|
+
:name="`${field.field}.${subField.field}`"
|
|
208
|
+
:label="subField.label"
|
|
209
|
+
:field-type="subField.type"
|
|
210
|
+
:hint="subField.hint"
|
|
211
|
+
persistent-hint
|
|
212
|
+
/>
|
|
213
|
+
</template>
|
|
214
|
+
</div>
|
|
215
|
+
</CardContent>
|
|
216
|
+
</Card>
|
|
217
|
+
<edge-g-input
|
|
218
|
+
v-if="edgeGlobal.objHas(field, 'bindings')"
|
|
219
|
+
v-model="state.data[field.field]"
|
|
220
|
+
:name="field.field"
|
|
221
|
+
v-bind="field.bindings"
|
|
222
|
+
:parent-tracker-id="`org-settings-${field.field}`"
|
|
223
|
+
/>
|
|
224
|
+
<edge-g-input
|
|
225
|
+
v-else
|
|
226
|
+
v-model="state.data[field.field]"
|
|
227
|
+
:name="field.field"
|
|
228
|
+
:field-type="field.type"
|
|
229
|
+
:label="field.label"
|
|
230
|
+
parent-tracker-id="org-settings"
|
|
231
|
+
:hint="field.hint"
|
|
232
|
+
persistent-hint
|
|
233
|
+
/>
|
|
234
|
+
</template>
|
|
235
|
+
<edge-chip class="mt-3">
|
|
236
|
+
ID: {{ edgeGlobal.edgeState.currentOrganization }}
|
|
237
|
+
<edge-clipboard-button :text="edgeGlobal.edgeState.currentOrganization" />
|
|
238
|
+
</edge-chip>
|
|
239
|
+
<Alert v-if="state.showSnack" class="bg-success mt-4 py-2 flex">
|
|
240
|
+
<div>
|
|
241
|
+
{{ state.successMessage }}
|
|
242
|
+
</div>
|
|
243
|
+
<div class="grow text-right">
|
|
244
|
+
<edge-shad-button
|
|
245
|
+
|
|
246
|
+
class="mx-2 h-6 text-xs text-white bg-slate-800"
|
|
247
|
+
|
|
248
|
+
@click="state.showSnack = false"
|
|
249
|
+
>
|
|
250
|
+
Close
|
|
251
|
+
</edge-shad-button>
|
|
252
|
+
</div>
|
|
253
|
+
</Alert>
|
|
254
|
+
</CardContent>
|
|
255
|
+
<CardFooter>
|
|
256
|
+
<edge-shad-button
|
|
257
|
+
type="submit"
|
|
258
|
+
:disabled="state.loading"
|
|
259
|
+
class="text-white w-100 bg-slate-800 hover:bg-slate-400"
|
|
260
|
+
>
|
|
261
|
+
<Loader2 v-if="state.loading" class="w-4 h-4 mr-2 animate-spin" />
|
|
262
|
+
Save
|
|
263
|
+
</edge-shad-button>
|
|
264
|
+
</CardFooter>
|
|
265
|
+
</edge-shad-form>
|
|
266
|
+
</Card>
|
|
267
|
+
</template>
|
|
268
|
+
|
|
269
|
+
<style lang="scss" scoped>
|
|
270
|
+
|
|
271
|
+
</style>
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
<script setup>
|
|
2
|
+
const props = defineProps({
|
|
3
|
+
items: {
|
|
4
|
+
type: Array,
|
|
5
|
+
required: true,
|
|
6
|
+
},
|
|
7
|
+
itemClass: {
|
|
8
|
+
type: String,
|
|
9
|
+
default: 'text-secondary text-2xl sm:text-4xl font-[700]',
|
|
10
|
+
},
|
|
11
|
+
})
|
|
12
|
+
</script>
|
|
13
|
+
|
|
14
|
+
<template>
|
|
15
|
+
<Breadcrumb>
|
|
16
|
+
<BreadcrumbList>
|
|
17
|
+
<template v-for="(item, index) in props.items" :key="index">
|
|
18
|
+
<BreadcrumbItem>
|
|
19
|
+
<BreadcrumbLink v-if="item.to" class="border-b-2 border-dashed" :class="props.itemClass">
|
|
20
|
+
<NuxtLink :to="item.to">
|
|
21
|
+
{{ item.text }}
|
|
22
|
+
</NuxtLink>
|
|
23
|
+
</BreadcrumbLink>
|
|
24
|
+
<BreadcrumbPage v-else :class="props.itemClass">
|
|
25
|
+
{{ item.text }}
|
|
26
|
+
</BreadcrumbPage>
|
|
27
|
+
</BreadcrumbItem>
|
|
28
|
+
<BreadcrumbSeparator v-if="index < (props.items.length - 1)" />
|
|
29
|
+
</template>
|
|
30
|
+
</BreadcrumbList>
|
|
31
|
+
</Breadcrumb>
|
|
32
|
+
</template>
|
|
33
|
+
|
|
34
|
+
<style>
|
|
35
|
+
</style>
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
<script setup>
|
|
2
|
+
const props = defineProps({
|
|
3
|
+
type: {
|
|
4
|
+
type: String,
|
|
5
|
+
required: false,
|
|
6
|
+
default: 'button',
|
|
7
|
+
},
|
|
8
|
+
to: {
|
|
9
|
+
type: String,
|
|
10
|
+
required: false,
|
|
11
|
+
},
|
|
12
|
+
target: {
|
|
13
|
+
type: String,
|
|
14
|
+
required: false,
|
|
15
|
+
},
|
|
16
|
+
})
|
|
17
|
+
const router = useRouter()
|
|
18
|
+
function handleNavigation() {
|
|
19
|
+
if (props.target === '_blank') {
|
|
20
|
+
window.open(props.to, '_blank')
|
|
21
|
+
}
|
|
22
|
+
else {
|
|
23
|
+
router.push(props.to)
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
</script>
|
|
27
|
+
|
|
28
|
+
<script setup>
|
|
29
|
+
|
|
30
|
+
</script>
|
|
31
|
+
|
|
32
|
+
<template>
|
|
33
|
+
<Button v-if="props.to" :type="props.type" @click.stop.prevent="handleNavigation">
|
|
34
|
+
<slot />
|
|
35
|
+
</Button>
|
|
36
|
+
<Button v-else :type="props.type">
|
|
37
|
+
<slot />
|
|
38
|
+
</Button>
|
|
39
|
+
</template>
|
|
40
|
+
|
|
41
|
+
<style lang="scss" scoped>
|
|
42
|
+
|
|
43
|
+
</style>
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
<script setup>
|
|
2
|
+
import { useVModel } from '@vueuse/core'
|
|
3
|
+
const props = defineProps({
|
|
4
|
+
name: {
|
|
5
|
+
type: String,
|
|
6
|
+
required: true,
|
|
7
|
+
},
|
|
8
|
+
modelValue: {
|
|
9
|
+
type: Boolean,
|
|
10
|
+
default: false,
|
|
11
|
+
},
|
|
12
|
+
class: {
|
|
13
|
+
type: null,
|
|
14
|
+
required: false,
|
|
15
|
+
},
|
|
16
|
+
placeholder: {
|
|
17
|
+
type: String,
|
|
18
|
+
required: false,
|
|
19
|
+
},
|
|
20
|
+
label: {
|
|
21
|
+
type: String,
|
|
22
|
+
required: false,
|
|
23
|
+
},
|
|
24
|
+
description: {
|
|
25
|
+
type: String,
|
|
26
|
+
required: false,
|
|
27
|
+
},
|
|
28
|
+
})
|
|
29
|
+
|
|
30
|
+
const emits = defineEmits(['update:modelValue'])
|
|
31
|
+
|
|
32
|
+
const modelValue = useVModel(props, 'modelValue', emits, {
|
|
33
|
+
passive: true,
|
|
34
|
+
defaultValue: props.modelValue,
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
const formHandleChange = ref(() => {})
|
|
38
|
+
|
|
39
|
+
watch(modelValue, (val) => {
|
|
40
|
+
formHandleChange.value(val)
|
|
41
|
+
})
|
|
42
|
+
</script>
|
|
43
|
+
|
|
44
|
+
<template>
|
|
45
|
+
<div>
|
|
46
|
+
<FormField v-slot="{ handleChange }" type="checkbox" :name="props.name">
|
|
47
|
+
<FormItem class="flex flex-row items-start gap-x-3 space-y-0 rounded-md border p-3 mt-3" @click.capture.once="formHandleChange(modelValue)">
|
|
48
|
+
<FormControl>
|
|
49
|
+
<Checkbox
|
|
50
|
+
:id="props.name"
|
|
51
|
+
:ref="() => { formHandleChange = handleChange }"
|
|
52
|
+
v-model="modelValue"
|
|
53
|
+
:class="props.class"
|
|
54
|
+
class="bg-slate-200"
|
|
55
|
+
/>
|
|
56
|
+
</FormControl>
|
|
57
|
+
<div class="space-y-1 leading-none w-full">
|
|
58
|
+
<div class="relative w-full items-center text-left">
|
|
59
|
+
<FormLabel><slot /></FormLabel>
|
|
60
|
+
<span class="absolute end-0 inset-y-0 flex items-center justify-center pl-2 pr-0">
|
|
61
|
+
<slot name="icon" /></span>
|
|
62
|
+
</div>
|
|
63
|
+
<FormDescription />
|
|
64
|
+
<FormMessage />
|
|
65
|
+
</div>
|
|
66
|
+
</FormItem>
|
|
67
|
+
</FormField>
|
|
68
|
+
</div>
|
|
69
|
+
</template>
|
|
70
|
+
|
|
71
|
+
<style lang="scss" scoped>
|
|
72
|
+
|
|
73
|
+
</style>
|
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
<script setup>
|
|
2
|
+
import { useVModel } from '@vueuse/core'
|
|
3
|
+
import { useField } from 'vee-validate'
|
|
4
|
+
import { cn } from '@/lib/utils'
|
|
5
|
+
const props = defineProps({
|
|
6
|
+
name: {
|
|
7
|
+
type: String,
|
|
8
|
+
required: true,
|
|
9
|
+
},
|
|
10
|
+
modelValue: {
|
|
11
|
+
type: [String, Array],
|
|
12
|
+
required: false,
|
|
13
|
+
},
|
|
14
|
+
class: {
|
|
15
|
+
type: null,
|
|
16
|
+
required: false,
|
|
17
|
+
default: 'w-100',
|
|
18
|
+
},
|
|
19
|
+
placeholder: {
|
|
20
|
+
type: String,
|
|
21
|
+
default: 'Make a selection',
|
|
22
|
+
required: false,
|
|
23
|
+
},
|
|
24
|
+
label: {
|
|
25
|
+
type: String,
|
|
26
|
+
required: false,
|
|
27
|
+
},
|
|
28
|
+
description: {
|
|
29
|
+
type: String,
|
|
30
|
+
required: false,
|
|
31
|
+
},
|
|
32
|
+
disabled: {
|
|
33
|
+
type: Boolean,
|
|
34
|
+
required: false,
|
|
35
|
+
default: false,
|
|
36
|
+
},
|
|
37
|
+
items: {
|
|
38
|
+
type: Array,
|
|
39
|
+
required: false,
|
|
40
|
+
default: () => [],
|
|
41
|
+
},
|
|
42
|
+
itemTitle: {
|
|
43
|
+
type: String,
|
|
44
|
+
required: false,
|
|
45
|
+
default: 'title',
|
|
46
|
+
},
|
|
47
|
+
itemValue: {
|
|
48
|
+
type: String,
|
|
49
|
+
required: false,
|
|
50
|
+
default: 'name',
|
|
51
|
+
},
|
|
52
|
+
multiple: {
|
|
53
|
+
type: Boolean,
|
|
54
|
+
required: false,
|
|
55
|
+
default: false,
|
|
56
|
+
},
|
|
57
|
+
})
|
|
58
|
+
|
|
59
|
+
const emits = defineEmits(['update:modelValue'])
|
|
60
|
+
|
|
61
|
+
const state = reactive({
|
|
62
|
+
width: 255,
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
const open = ref(false)
|
|
66
|
+
|
|
67
|
+
const computedItems = computed(() => {
|
|
68
|
+
return props.items.map((item) => {
|
|
69
|
+
if (typeof item === 'string') {
|
|
70
|
+
return { [props.itemTitle]: item, [props.itemValue]: item }
|
|
71
|
+
}
|
|
72
|
+
const getNestedValue = (obj, path) => path.split('.').reduce((acc, key) => acc && acc[key], obj)
|
|
73
|
+
return {
|
|
74
|
+
[props.itemTitle]: getNestedValue(item, props.itemTitle),
|
|
75
|
+
[props.itemValue]: getNestedValue(item, props.itemValue),
|
|
76
|
+
}
|
|
77
|
+
})
|
|
78
|
+
})
|
|
79
|
+
|
|
80
|
+
const modelValue = useVModel(props, 'modelValue', emits, {
|
|
81
|
+
passive: false,
|
|
82
|
+
prop: 'modelValue',
|
|
83
|
+
})
|
|
84
|
+
|
|
85
|
+
// Ensure model is an array when multiple
|
|
86
|
+
if (props.multiple && !Array.isArray(modelValue.value)) {
|
|
87
|
+
modelValue.value = modelValue.value ? [modelValue.value] : []
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const triggerButton = ref(null)
|
|
91
|
+
|
|
92
|
+
watch(open, (isOpen) => {
|
|
93
|
+
if (isOpen) {
|
|
94
|
+
nextTick(() => {
|
|
95
|
+
if (triggerButton.value) {
|
|
96
|
+
state.width = triggerButton.value.$el.offsetWidth
|
|
97
|
+
}
|
|
98
|
+
})
|
|
99
|
+
}
|
|
100
|
+
})
|
|
101
|
+
|
|
102
|
+
const updateCheck = ref(true)
|
|
103
|
+
watch(() => modelValue.value, () => {
|
|
104
|
+
updateCheck.value = false
|
|
105
|
+
nextTick(() => {
|
|
106
|
+
updateCheck.value = true
|
|
107
|
+
})
|
|
108
|
+
})
|
|
109
|
+
|
|
110
|
+
const { setValue } = useField(props.name)
|
|
111
|
+
|
|
112
|
+
const handleItemSelect = (item) => {
|
|
113
|
+
const selectedValue = item[props.itemValue] ?? ''
|
|
114
|
+
|
|
115
|
+
if (props.multiple) {
|
|
116
|
+
// Start from an array
|
|
117
|
+
const current = Array.isArray(modelValue.value) ? [...modelValue.value] : []
|
|
118
|
+
const idx = current.indexOf(selectedValue)
|
|
119
|
+
if (idx === -1) {
|
|
120
|
+
current.push(selectedValue)
|
|
121
|
+
}
|
|
122
|
+
else {
|
|
123
|
+
current.splice(idx, 1)
|
|
124
|
+
}
|
|
125
|
+
modelValue.value = current
|
|
126
|
+
emits('update:modelValue', current)
|
|
127
|
+
setValue(current)
|
|
128
|
+
// keep popover open for multi-select
|
|
129
|
+
}
|
|
130
|
+
else {
|
|
131
|
+
modelValue.value = selectedValue
|
|
132
|
+
emits('update:modelValue', selectedValue)
|
|
133
|
+
setValue(selectedValue)
|
|
134
|
+
open.value = false
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
watch(() => modelValue.value, (newValue) => {
|
|
139
|
+
emits('update:modelValue', newValue)
|
|
140
|
+
})
|
|
141
|
+
|
|
142
|
+
const isSelected = (item) => {
|
|
143
|
+
const val = item[props.itemValue] ?? ''
|
|
144
|
+
if (props.multiple) {
|
|
145
|
+
return Array.isArray(modelValue.value) && modelValue.value.includes(val)
|
|
146
|
+
}
|
|
147
|
+
return modelValue.value === val
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
const triggerTitle = computed(() => {
|
|
151
|
+
const placeholder = props.placeholder
|
|
152
|
+
const items = computedItems.value
|
|
153
|
+
|
|
154
|
+
if (props.multiple) {
|
|
155
|
+
const values = Array.isArray(modelValue.value) ? modelValue.value : []
|
|
156
|
+
if (values.length === 0)
|
|
157
|
+
return placeholder
|
|
158
|
+
const titles = values
|
|
159
|
+
.map(v => items.find(i => (i[props.itemValue] ?? '') === v))
|
|
160
|
+
.filter(Boolean)
|
|
161
|
+
.map(i => i[props.itemTitle])
|
|
162
|
+
if (titles.length === 0)
|
|
163
|
+
return placeholder
|
|
164
|
+
if (titles.length <= 3)
|
|
165
|
+
return titles.join(', ')
|
|
166
|
+
return `${titles.length} selected`
|
|
167
|
+
}
|
|
168
|
+
else {
|
|
169
|
+
if (modelValue.value) {
|
|
170
|
+
const item = items.find(i => (i[props.itemValue] ?? '') === modelValue.value)
|
|
171
|
+
if (item && edgeGlobal.objHas(item, props.itemTitle))
|
|
172
|
+
return item[props.itemTitle]
|
|
173
|
+
}
|
|
174
|
+
return placeholder
|
|
175
|
+
}
|
|
176
|
+
})
|
|
177
|
+
</script>
|
|
178
|
+
|
|
179
|
+
<template>
|
|
180
|
+
<FormField :name="props.name">
|
|
181
|
+
<FormItem class="flex flex-col space-y-1">
|
|
182
|
+
<FormLabel>{{ props.label }}</FormLabel>
|
|
183
|
+
<Popover v-model:open="open">
|
|
184
|
+
<PopoverTrigger as-child>
|
|
185
|
+
<FormControl>
|
|
186
|
+
<Button
|
|
187
|
+
ref="triggerButton"
|
|
188
|
+
variant="outline"
|
|
189
|
+
role="combobox"
|
|
190
|
+
:aria-expanded="open"
|
|
191
|
+
class="w-[200px] justify-between text-foreground"
|
|
192
|
+
:class="props.class"
|
|
193
|
+
:disabled="props.disabled"
|
|
194
|
+
>
|
|
195
|
+
{{ triggerTitle }}
|
|
196
|
+
<ChevronsUpDown class="ml-2 h-4 w-4 shrink-0 opacity-50" />
|
|
197
|
+
</Button>
|
|
198
|
+
</FormControl>
|
|
199
|
+
</PopoverTrigger>
|
|
200
|
+
<PopoverContent class="p-0" :style="`width: ${state.width}px !important;`">
|
|
201
|
+
<Command>
|
|
202
|
+
<CommandInput class="h-9" placeholder="Search..." />
|
|
203
|
+
<CommandEmpty>Not found.</CommandEmpty>
|
|
204
|
+
<CommandList>
|
|
205
|
+
<CommandGroup>
|
|
206
|
+
<CommandItem
|
|
207
|
+
v-for="item in computedItems"
|
|
208
|
+
:key="item[props.itemTitle]"
|
|
209
|
+
:value="item[props.itemTitle]"
|
|
210
|
+
@select="() => handleItemSelect(item)"
|
|
211
|
+
>
|
|
212
|
+
<Check
|
|
213
|
+
:class="cn(
|
|
214
|
+
'h-4 w-4',
|
|
215
|
+
isSelected(item) ? 'opacity-100' : 'opacity-0',
|
|
216
|
+
)"
|
|
217
|
+
/>
|
|
218
|
+
{{ item[props.itemTitle] }}
|
|
219
|
+
</CommandItem>
|
|
220
|
+
</CommandGroup>
|
|
221
|
+
</CommandList>
|
|
222
|
+
</Command>
|
|
223
|
+
</PopoverContent>
|
|
224
|
+
</Popover>
|
|
225
|
+
<FormDescription>
|
|
226
|
+
{{ props.description }}
|
|
227
|
+
</FormDescription>
|
|
228
|
+
<FormMessage />
|
|
229
|
+
</FormItem>
|
|
230
|
+
</FormField>
|
|
231
|
+
</template>
|
|
232
|
+
|
|
233
|
+
<style lang="scss" scoped>
|
|
234
|
+
.PopoverContent {
|
|
235
|
+
width: 100vw;
|
|
236
|
+
max-height: var(--radix-popover-content-available-height);
|
|
237
|
+
}
|
|
238
|
+
</style>
|