Skip to content

Commit bca1e8b

Browse files
committed
migrate select/multiselect components to Radix Vue primitives
1 parent 44bcce9 commit bca1e8b

24 files changed

+608
-923
lines changed

resources/js/Components/Common/Client/ClientMultiselectDropdown.vue

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,14 +12,20 @@ function getKeyFromItem(item: Client) {
1212
function getNameForItem(item: Client) {
1313
return item.name;
1414
}
15+
16+
const emit = defineEmits<{
17+
submit: [];
18+
}>();
1519
</script>
1620

1721
<template>
1822
<MultiselectDropdown
1923
search-placeholder="Search for a Client..."
2024
:items="clients"
2125
:get-key-from-item="getKeyFromItem"
22-
:get-name-for-item="getNameForItem">
26+
:get-name-for-item="getNameForItem"
27+
no-item-label="No Client"
28+
@submit="emit('submit')">
2329
<template #trigger>
2430
<slot name="trigger"></slot>
2531
</template>
Lines changed: 19 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,11 @@
11
<script setup lang="ts">
2-
import SelectDropdown from '@/packages/ui/src/Input/SelectDropdown.vue';
3-
import Badge from '@/packages/ui/src/Badge.vue';
4-
import { ChevronDownIcon } from '@heroicons/vue/20/solid';
2+
import {
3+
Select,
4+
SelectContent,
5+
SelectItem,
6+
SelectTrigger,
7+
SelectValue,
8+
} from '@/Components/ui/select';
59
import type { BillableKey } from '@/types/projects';
610
711
const model = defineModel<BillableKey>({
@@ -21,38 +25,26 @@ const options: Option[] = [
2125
},
2226
];
2327
24-
function getKeyFromItem(item: Option) {
25-
return item.key;
26-
}
27-
28-
function getNameFromItem(item: Option) {
29-
return item.name;
30-
}
31-
3228
function getNameForKey(key: BillableKey | undefined) {
33-
const item = options.find((item) => getKeyFromItem(item) === key);
29+
const item = options.find((item) => item.key === key);
3430
if (item) {
35-
return getNameFromItem(item);
31+
return item.name;
3632
}
3733
return '';
3834
}
3935
</script>
4036

4137
<template>
42-
<SelectDropdown
43-
v-model="model"
44-
:get-key-from-item="getKeyFromItem"
45-
:get-name-for-item="getNameFromItem"
46-
:items="options">
47-
<template #trigger>
48-
<Badge size="xlarge" class="bg-input-background cursor-pointer">
49-
<span>
50-
{{ getNameForKey(model) }}
51-
</span>
52-
<ChevronDownIcon class="text-text-secondary w-5"></ChevronDownIcon>
53-
</Badge>
54-
</template>
55-
</SelectDropdown>
38+
<Select v-model="model">
39+
<SelectTrigger>
40+
<SelectValue>{{ getNameForKey(model) }}</SelectValue>
41+
</SelectTrigger>
42+
<SelectContent>
43+
<SelectItem v-for="option in options" :key="option.key" :value="option.key">
44+
{{ option.name }}
45+
</SelectItem>
46+
</SelectContent>
47+
</Select>
5648
</template>
5749

5850
<style scoped></style>
Lines changed: 77 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,20 @@
11
<script setup lang="ts">
2-
import { computed, ref } from 'vue';
2+
import { computed, nextTick, ref, watch } from 'vue';
33
import { useMembersQuery } from '@/utils/useMembersQuery';
4-
import { UserIcon, ChevronDownIcon } from '@heroicons/vue/24/solid';
5-
import { useFocus } from '@vueuse/core';
4+
import { UserIcon } from '@heroicons/vue/24/solid';
5+
import { ChevronDown } from 'lucide-vue-next';
66
import type { ProjectMember } from '@/packages/api/src';
7-
import { Badge, SelectDropdown } from '@/packages/ui/src';
87
import type { Member } from '@/packages/api/src';
8+
import {
9+
ComboboxAnchor,
10+
ComboboxContent,
11+
ComboboxInput,
12+
ComboboxItem,
13+
ComboboxRoot,
14+
ComboboxViewport,
15+
} from 'radix-vue';
16+
import Dropdown from '@/packages/ui/src/Input/Dropdown.vue';
17+
import { Button } from '@/Components/ui/button';
918
1019
const { members } = useMembersQuery();
1120
@@ -24,16 +33,24 @@ const props = withDefaults(
2433
}
2534
);
2635
27-
const searchInput = ref<HTMLInputElement | null>(null);
28-
36+
const open = ref(false);
2937
const searchValue = ref('');
38+
const searchInput = ref<HTMLElement | null>(null);
3039
31-
useFocus(searchInput, { initialValue: true });
40+
watch(open, (isOpen) => {
41+
if (isOpen) {
42+
searchValue.value = '';
43+
nextTick(() => {
44+
// @ts-expect-error We need to access the actual HTML Element to focus
45+
searchInput.value?.$el?.focus();
46+
});
47+
}
48+
});
3249
3350
const filteredMembers = computed<Member[]>(() => {
3451
return members.value.filter((member) => {
3552
return (
36-
member.name.toLowerCase().includes(searchValue.value?.toLowerCase()?.trim() || '') &&
53+
member.name.toLowerCase().includes(searchValue.value.toLowerCase().trim() || '') &&
3754
!props.hiddenMembers.some((hiddenMember) => hiddenMember.member_id === member.id) &&
3855
member.is_placeholder === false
3956
);
@@ -44,29 +61,65 @@ const currentValue = computed(() => {
4461
if (model.value) {
4562
return members.value.find((member) => member.id === model.value)?.name;
4663
}
47-
return searchValue.value;
64+
return '';
4865
});
66+
67+
function selectMember(member: Member) {
68+
model.value = member.id;
69+
open.value = false;
70+
}
4971
</script>
5072

5173
<template>
52-
<SelectDropdown
53-
v-model="model"
54-
:items="filteredMembers"
55-
:get-key-from-item="(member) => member.id"
56-
:get-name-for-item="(member) => member.name">
74+
<Dropdown v-model="open" align="start" :close-on-content-click="false">
5775
<template #trigger>
58-
<Badge
59-
tag="button"
60-
class="flex w-full text-base text-left space-x-3 px-3 text-text-secondary bg-input-background font-normal cursor py-1.5">
61-
<UserIcon class="relative z-10 w-4 text-text-secondary"></UserIcon>
62-
<div v-if="currentValue" class="flex-1 truncate">
63-
{{ currentValue }}
76+
<Button
77+
:disabled="disabled"
78+
type="button"
79+
variant="input"
80+
size="input"
81+
class="w-full justify-between text-start font-normal">
82+
<div class="flex items-center gap-3 truncate">
83+
<UserIcon class="w-4 text-text-secondary shrink-0" />
84+
<span v-if="currentValue" class="truncate text-text-primary">{{
85+
currentValue
86+
}}</span>
87+
<span v-else class="text-muted-foreground">Select a member...</span>
6488
</div>
65-
<div v-else class="flex-1">Select a member...</div>
66-
<ChevronDownIcon class="w-4 text-text-secondary"></ChevronDownIcon>
67-
</Badge>
89+
<ChevronDown class="w-4 h-4 text-icon-default shrink-0" />
90+
</Button>
91+
</template>
92+
<template #content>
93+
<ComboboxRoot
94+
v-model:search-term="searchValue"
95+
:open="open"
96+
class="relative"
97+
:filter-function="(val: string[]) => val">
98+
<ComboboxAnchor>
99+
<ComboboxInput
100+
ref="searchInput"
101+
class="bg-card-background border-0 placeholder-text-tertiary text-sm text-text-primary py-2.5 focus:ring-0 border-b border-card-background-separator focus:border-card-background-separator w-full"
102+
placeholder="Search for a member..." />
103+
</ComboboxAnchor>
104+
<ComboboxContent
105+
:dismiss-able="false"
106+
position="inline"
107+
class="w-60 max-h-60 overflow-y-auto">
108+
<ComboboxViewport>
109+
<ComboboxItem
110+
v-for="member in filteredMembers"
111+
:key="member.id"
112+
:value="member.id"
113+
class="flex items-center gap-3 px-3 py-2.5 text-sm text-text-primary data-[highlighted]:bg-card-background-active cursor-default"
114+
@select.prevent="selectMember(member)">
115+
<UserIcon class="w-4 text-text-secondary shrink-0" />
116+
<span class="truncate">{{ member.name }}</span>
117+
</ComboboxItem>
118+
</ComboboxViewport>
119+
</ComboboxContent>
120+
</ComboboxRoot>
68121
</template>
69-
</SelectDropdown>
122+
</Dropdown>
70123
</template>
71124

72125
<style scoped></style>

resources/js/Components/Common/Member/MemberMergeModal.vue

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,10 @@ import PrimaryButton from '@/packages/ui/src/Buttons/PrimaryButton.vue';
77
import MemberCombobox from '@/Components/Common/Member/MemberCombobox.vue';
88
import { UserIcon, ArrowRightIcon } from '@heroicons/vue/24/solid';
99
import { Badge } from '@/packages/ui/src';
10-
import { useMutation } from '@tanstack/vue-query';
10+
import { useMutation, useQueryClient } from '@tanstack/vue-query';
1111
import { getCurrentOrganizationId } from '@/utils/useUser';
1212
import { useNotificationsStore } from '@/utils/notification';
13+
const queryClient = useQueryClient();
1314
const { handleApiRequestNotifications, addNotification } = useNotificationsStore();
1415
1516
const show = defineModel('show', { default: false });
@@ -50,6 +51,7 @@ async function submit() {
5051
'Members successfully merged!',
5152
'There was an error merging the members.',
5253
() => {
54+
queryClient.invalidateQueries({ queryKey: ['members'] });
5355
show.value = false;
5456
}
5557
);

resources/js/Components/Common/Member/MemberMultiselectDropdown.vue

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,14 +12,19 @@ function getKeyFromItem(item: Member) {
1212
function getNameForItem(item: Member) {
1313
return item.name;
1414
}
15+
16+
const emit = defineEmits<{
17+
submit: [];
18+
}>();
1519
</script>
1620

1721
<template>
1822
<MultiselectDropdown
1923
search-placeholder="Search for a Member..."
2024
:items="members"
2125
:get-key-from-item="getKeyFromItem"
22-
:get-name-for-item="getNameForItem">
26+
:get-name-for-item="getNameForItem"
27+
@submit="emit('submit')">
2328
<template #trigger>
2429
<slot name="trigger"></slot>
2530
</template>
Lines changed: 19 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,11 @@
11
<script setup lang="ts">
2-
import SelectDropdown from '@/packages/ui/src/Input/SelectDropdown.vue';
3-
import Badge from '@/packages/ui/src/Badge.vue';
4-
import { ChevronDownIcon } from '@heroicons/vue/20/solid';
2+
import {
3+
Select,
4+
SelectContent,
5+
SelectItem,
6+
SelectTrigger,
7+
SelectValue,
8+
} from '@/Components/ui/select';
59
import type { Role } from '@/types/jetstream';
610
import { usePage } from '@inertiajs/vue3';
711
@@ -13,38 +17,26 @@ const page = usePage<{
1317
availableRoles: Role[];
1418
}>();
1519
16-
function getKeyFromItem(item: Role) {
17-
return item.key;
18-
}
19-
20-
function getNameFromItem(item: Role) {
21-
return item.name;
22-
}
23-
2420
function getNameForKey(key: string | undefined) {
25-
const item = page.props.availableRoles.find((item) => getKeyFromItem(item) === key);
21+
const item = page.props.availableRoles.find((item) => item.key === key);
2622
if (item) {
27-
return getNameFromItem(item);
23+
return item.name;
2824
}
2925
return '';
3026
}
3127
</script>
3228

3329
<template>
34-
<SelectDropdown
35-
v-model="model"
36-
:get-key-from-item="getKeyFromItem"
37-
:get-name-for-item="getNameFromItem"
38-
:items="page.props.availableRoles">
39-
<template #trigger>
40-
<Badge size="xlarge" class="bg-input-background cursor-pointer">
41-
<span>
42-
{{ getNameForKey(model) }}
43-
</span>
44-
<ChevronDownIcon class="text-text-secondary w-5"></ChevronDownIcon>
45-
</Badge>
46-
</template>
47-
</SelectDropdown>
30+
<Select v-model="model">
31+
<SelectTrigger>
32+
<SelectValue>{{ getNameForKey(model) }}</SelectValue>
33+
</SelectTrigger>
34+
<SelectContent>
35+
<SelectItem v-for="role in page.props.availableRoles" :key="role.key" :value="role.key">
36+
{{ role.name }}
37+
</SelectItem>
38+
</SelectContent>
39+
</Select>
4840
</template>
4941

5042
<style scoped></style>

resources/js/Components/Common/Project/ProjectMultiselectDropdown.vue

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,14 +12,20 @@ function getKeyFromItem(item: Project) {
1212
function getNameForItem(item: Project) {
1313
return item.name;
1414
}
15+
16+
const emit = defineEmits<{
17+
submit: [];
18+
}>();
1519
</script>
1620

1721
<template>
1822
<MultiselectDropdown
1923
search-placeholder="Search for a Project..."
2024
:items="projects"
2125
:get-key-from-item="getKeyFromItem"
22-
:get-name-for-item="getNameForItem">
26+
:get-name-for-item="getNameForItem"
27+
no-item-label="No Project"
28+
@submit="emit('submit')">
2329
<template #trigger>
2430
<slot name="trigger"></slot>
2531
</template>

0 commit comments

Comments
 (0)