Skip to content

Commit 4b0cb2e

Browse files
committed
improve time picker parsing, fix nested escape listeners, change project member select
1 parent d5699da commit 4b0cb2e

21 files changed

+224
-228
lines changed

e2e/project-members.spec.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ test('test that updating project member billable rate works for existing time en
2424
await page.getByRole('button', { name: 'Add Member' }).click();
2525

2626
await expect(page.getByText('Add Project Member').first()).toBeVisible();
27+
await page.getByRole('button', { name: 'Select a member' }).click();
2728
await page.keyboard.press('Enter');
2829
await page.getByRole('button', { name: 'Add Project Member' }).click();
2930

e2e/time.spec.ts

Lines changed: 2 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -191,14 +191,7 @@ test('test that updating a the start of an existing time entry in the overview w
191191
'time_entry_range_selector'
192192
);
193193
await timeEntryRangeElement.click();
194-
await page
195-
.getByTestId('time_entry_range_start')
196-
.getByTestId('time_picker_hour')
197-
.fill('1');
198-
await page
199-
.getByTestId('time_entry_range_start')
200-
.getByTestId('time_picker_minute')
201-
.fill('1');
194+
await page.getByTestId('time_picker_input').first().fill('1');
202195
await Promise.all([
203196
page.waitForResponse(async (response) => {
204197
return (
@@ -213,7 +206,7 @@ test('test that updating a the start of an existing time entry in the overview w
213206
}),
214207
page
215208
.getByTestId('time_entry_range_end')
216-
.getByTestId('time_picker_minute')
209+
.getByTestId('time_picker_input')
217210
.press('Enter'),
218211
]);
219212
});
Lines changed: 21 additions & 132 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,12 @@
11
<script setup lang="ts">
2-
import { computed, nextTick, onMounted, ref, watch } from 'vue';
2+
import { computed, onMounted, ref, watch } from 'vue';
33
import { storeToRefs } from 'pinia';
4-
import ClientDropdownItem from '@/packages/ui/src/Client/ClientDropdownItem.vue';
54
import { useMembersStore } from '@/utils/useMembers';
6-
import { UserIcon, XMarkIcon } from '@heroicons/vue/24/solid';
7-
import TextInput from '@/packages/ui/src/Input/TextInput.vue';
5+
import { UserIcon, ChevronDownIcon } from '@heroicons/vue/24/solid';
86
import { useFocus } from '@vueuse/core';
97
import type { ProjectMember } from '@/packages/api/src';
10-
import Dropdown from '@/packages/ui/src/Input/Dropdown.vue';
8+
import { Badge, SelectDropdown } from '@/packages/ui/src';
9+
import type { Member } from '@/packages/api/src';
1110
1211
const membersStore = useMembersStore();
1312
const { members } = storeToRefs(membersStore);
@@ -31,13 +30,9 @@ const searchInput = ref<HTMLInputElement | null>(null);
3130
3231
const searchValue = ref('');
3332
34-
function isMemberSelected(id: string) {
35-
return model.value === id;
36-
}
37-
3833
useFocus(searchInput, { initialValue: true });
3934
40-
const filteredMembers = computed(() => {
35+
const filteredMembers = computed<Member[]>(() => {
4136
return members.value.filter((member) => {
4237
return (
4338
member.name
@@ -65,141 +60,35 @@ function resetHighlightedItem() {
6560
}
6661
}
6762
68-
function updateSearchValue(event: Event) {
69-
const newInput = (event.target as HTMLInputElement).value;
70-
if (newInput === ' ') {
71-
searchValue.value = '';
72-
const highlightedClientId = highlightedItemId.value;
73-
if (highlightedClientId) {
74-
const highlightedClient = members.value.find(
75-
(member) => member.id === highlightedClientId
76-
);
77-
if (highlightedClient) {
78-
model.value = highlightedClient.id;
79-
}
80-
}
81-
} else {
82-
searchValue.value = newInput;
83-
}
84-
}
85-
86-
const emit = defineEmits(['update:modelValue', 'changed']);
87-
88-
function updateMember(newValue: string | null) {
89-
if (newValue) {
90-
model.value = newValue;
91-
nextTick(() => {
92-
emit('changed');
93-
});
94-
}
95-
}
96-
97-
function moveHighlightUp() {
98-
if (highlightedItem.value) {
99-
const currentHightlightedIndex = filteredMembers.value.indexOf(
100-
highlightedItem.value
101-
);
102-
if (currentHightlightedIndex === 0) {
103-
highlightedItemId.value =
104-
filteredMembers.value[filteredMembers.value.length - 1].id;
105-
} else {
106-
highlightedItemId.value =
107-
filteredMembers.value[currentHightlightedIndex - 1].id;
108-
}
109-
}
110-
}
111-
112-
function moveHighlightDown() {
113-
if (highlightedItem.value) {
114-
const currentHightlightedIndex = filteredMembers.value.indexOf(
115-
highlightedItem.value
116-
);
117-
if (currentHightlightedIndex === filteredMembers.value.length - 1) {
118-
highlightedItemId.value = filteredMembers.value[0].id;
119-
} else {
120-
highlightedItemId.value =
121-
filteredMembers.value[currentHightlightedIndex + 1].id;
122-
}
123-
}
124-
}
125-
12663
const highlightedItemId = ref<string | null>(null);
127-
const highlightedItem = computed(() => {
128-
return members.value.find(
129-
(member) => member.id === highlightedItemId.value
130-
);
131-
});
13264
13365
const currentValue = computed(() => {
13466
if (model.value) {
13567
return members.value.find((member) => member.id === model.value)?.name;
13668
}
13769
return searchValue.value;
13870
});
139-
140-
const hasMemberSelected = computed(() => {
141-
return model.value !== '';
142-
});
143-
144-
const showMembersDropdown = ref(true);
14571
</script>
14672

14773
<template>
148-
<Dropdown
149-
align="bottom-start"
150-
width="300"
151-
v-model="showMembersDropdown"
152-
:closeOnContentClick="true">
153-
<template #trigger>
154-
<div class="flex relative">
155-
<div
156-
ref="reference"
157-
class="absolute h-full items-center px-3 w-full flex justify-between">
158-
<UserIcon class="relative z-10 w-4 text-muted"></UserIcon>
159-
<button
160-
v-if="hasMemberSelected"
161-
@click="model = ''"
162-
class="focus:text-accent-200 focus:bg-card-background text-muted">
163-
<XMarkIcon class="relative z-10 w-4"></XMarkIcon>
164-
</button>
74+
<SelectDropdown
75+
v-model="model"
76+
:items="filteredMembers"
77+
:get-key-from-item="(member) => member.id"
78+
:get-name-for-item="(member) => member.name">
79+
<template v-slot:trigger>
80+
<Badge
81+
tag="button"
82+
class="flex w-full text-base text-left space-x-3 px-3 text-text-secondary font-normal cursor py-1.5">
83+
<UserIcon class="relative z-10 w-4 text-muted"></UserIcon>
84+
<div v-if="currentValue" class="flex-1 truncate">
85+
{{ currentValue }}
16586
</div>
166-
<TextInput
167-
:value="currentValue"
168-
:disabled="disabled"
169-
@input="updateSearchValue"
170-
data-testid="member_dropdown_search"
171-
@keydown.enter.prevent="updateMember(highlightedItemId)"
172-
@keydown.up.prevent="moveHighlightUp"
173-
class="relative w-full pl-10"
174-
@keydown.down.prevent="moveHighlightDown"
175-
placeholder="Search for a member..."
176-
ref="searchInput" />
177-
</div>
178-
</template>
179-
<template #content>
180-
<div
181-
class="py-2 text-white px-3"
182-
v-if="filteredMembers.length === 0">
183-
All members are already added.
184-
</div>
185-
<div
186-
v-for="member in filteredMembers"
187-
:key="member.id"
188-
role="option"
189-
:value="member.id"
190-
:class="{
191-
'bg-card-background-active':
192-
member.id === highlightedItemId,
193-
}"
194-
@click="updateMember(member.id)"
195-
data-testid="client_dropdown_entries"
196-
:data-client-id="member.id">
197-
<ClientDropdownItem
198-
:selected="isMemberSelected(member.id)"
199-
:name="member.name"></ClientDropdownItem>
200-
</div>
87+
<div class="flex-1" v-else>Select a member...</div>
88+
<ChevronDownIcon class="w-4 text-muted"></ChevronDownIcon>
89+
</Badge>
20190
</template>
202-
</Dropdown>
91+
</SelectDropdown>
20392
</template>
20493

20594
<style scoped></style>

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -151,6 +151,7 @@ const roleDescription = computed(() => {
151151
v-if="billableRateSelect === 'custom-rate'">
152152
<InputLabel
153153
for="memberBillableRate"
154+
class="mb-2"
154155
value="Billable Rate" />
155156
<BillableRateInput
156157
focus

resources/js/Components/Common/ProjectMember/ProjectMemberEditModal.vue

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,7 @@ useFocus(projectNameInput, { initialValue: true });
8989
<div class="col-span-3 sm:col-span-1 flex-1">
9090
<InputLabel
9191
for="billable_rate"
92+
class="mb-2"
9293
value="Billable Rate"></InputLabel>
9394
<BillableRateInput
9495
@keydown.enter="submit"

resources/js/Components/Common/TimeEntry/TimeEntryCreateModal.vue

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -219,7 +219,6 @@ const billableProxy = computed({
219219
<InputLabel>Duration</InputLabel>
220220
<div class="space-y-2 mt-1 flex flex-col">
221221
<DurationHumanInput
222-
class="h-full text-white py-2 flex-1 rounded-r-lg text-left px-3 text-base lg:text-lg font-bold border-input-border border rounded-lg bg-card-background placeholder-muted focus:ring-0 transition"
223222
v-model:start="localStart"
224223
v-model:end="localEnd"></DurationHumanInput>
225224
<div class="text-sm flex space-x-1">

resources/js/Pages/Teams/Partials/OrganizationBillableRate.vue

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,7 @@ function checkForConfirmationModal() {
7171
<div class="col-span-6 sm:col-span-4">
7272
<InputLabel
7373
for="organizationBillableRate"
74+
class="mb-2"
7475
value="Organization Billable Rate" />
7576
<BillableRateInput
7677
v-if="organization"

resources/js/packages/ui/src/Input/BillableRateInput.vue

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,7 @@ const inputValue = ref(formatValue(model.value));
8282
type="text"
8383
:name="name"
8484
placeholder="Billable Rate"
85-
class="mt-2 block w-full"
85+
class="block w-full"
8686
autocomplete="teamMemberRate" />
8787
<div
8888
class="absolute top-0 right-0 h-full flex items-center px-4 font-medium pointer-events-none">

resources/js/packages/ui/src/Input/DatePicker.vue

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ const emit = defineEmits(['changed']);
5454
@keydown.enter="updateDate"
5555
:class="
5656
twMerge(
57-
'bg-input-background border text-white border-input-border focus-visible:outline-0 focus-visible:ring-0 rounded-md',
57+
'bg-input-background border text-white border-input-border focus-visible:outline-0 focus-visible:border-input-border-active focus-visible:ring-0 rounded-md',
5858
props.class
5959
)
6060
"

resources/js/packages/ui/src/Input/Dropdown.vue

Lines changed: 20 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
<script setup lang="ts">
2-
import { onMounted, onUnmounted, ref } from 'vue';
2+
import { onMounted, onUnmounted, ref, watch } from 'vue';
33
import {
44
flip,
55
limitShift,
@@ -10,6 +10,8 @@ import {
1010
} from '@floating-ui/vue';
1111
import { offset } from '@floating-ui/vue';
1212
import { autoUpdate } from '@floating-ui/vue';
13+
import { useId } from 'radix-vue';
14+
import { isLastLayer, layers } from '@/packages/ui/src/utils/dismissableLayer';
1315
1416
const props = withDefaults(
1517
defineProps<{
@@ -24,17 +26,28 @@ const props = withDefaults(
2426
2527
const emit = defineEmits(['open', 'submit']);
2628
const open = defineModel({ default: false });
29+
const id = useId();
2730
2831
const closeOnEscape = (e: KeyboardEvent) => {
29-
if (open.value && e.key === 'Escape') {
30-
open.value = false;
31-
}
32-
if (open.value && e.key === 'Enter') {
33-
emit('submit');
34-
if (props.closeOnContentClick) open.value = false;
32+
if (isLastLayer(id)) {
33+
if (open.value && e.key === 'Escape') {
34+
open.value = false;
35+
}
36+
if (open.value && e.key === 'Enter') {
37+
emit('submit');
38+
if (props.closeOnContentClick) open.value = false;
39+
}
3540
}
3641
};
3742
43+
watch(open, (value) => {
44+
if (value) {
45+
layers.value.push(id);
46+
} else {
47+
layers.value = layers.value.filter((layer) => layer !== id);
48+
}
49+
});
50+
3851
onMounted(() => document.addEventListener('keydown', closeOnEscape));
3952
onUnmounted(() => document.removeEventListener('keydown', closeOnEscape));
4053

0 commit comments

Comments
 (0)