Skip to content

Commit

Permalink
improve time picker parsing, fix nested escape listeners, change proj…
Browse files Browse the repository at this point in the history
…ect member select
  • Loading branch information
Onatcer committed Nov 12, 2024
1 parent edcd9fc commit 636914b
Show file tree
Hide file tree
Showing 21 changed files with 224 additions and 228 deletions.
1 change: 1 addition & 0 deletions e2e/project-members.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ test('test that updating project member billable rate works for existing time en
await page.getByRole('button', { name: 'Add Member' }).click();

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

Expand Down
11 changes: 2 additions & 9 deletions e2e/time.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -191,14 +191,7 @@ test('test that updating a the start of an existing time entry in the overview w
'time_entry_range_selector'
);
await timeEntryRangeElement.click();
await page
.getByTestId('time_entry_range_start')
.getByTestId('time_picker_hour')
.fill('1');
await page
.getByTestId('time_entry_range_start')
.getByTestId('time_picker_minute')
.fill('1');
await page.getByTestId('time_picker_input').first().fill('1');
await Promise.all([
page.waitForResponse(async (response) => {
return (
Expand All @@ -213,7 +206,7 @@ test('test that updating a the start of an existing time entry in the overview w
}),
page
.getByTestId('time_entry_range_end')
.getByTestId('time_picker_minute')
.getByTestId('time_picker_input')
.press('Enter'),
]);
});
Expand Down
153 changes: 21 additions & 132 deletions resources/js/Components/Common/Member/MemberCombobox.vue
Original file line number Diff line number Diff line change
@@ -1,13 +1,12 @@
<script setup lang="ts">
import { computed, nextTick, onMounted, ref, watch } from 'vue';
import { computed, onMounted, ref, watch } from 'vue';
import { storeToRefs } from 'pinia';
import ClientDropdownItem from '@/packages/ui/src/Client/ClientDropdownItem.vue';
import { useMembersStore } from '@/utils/useMembers';
import { UserIcon, XMarkIcon } from '@heroicons/vue/24/solid';
import TextInput from '@/packages/ui/src/Input/TextInput.vue';
import { UserIcon, ChevronDownIcon } from '@heroicons/vue/24/solid';
import { useFocus } from '@vueuse/core';
import type { ProjectMember } from '@/packages/api/src';
import Dropdown from '@/packages/ui/src/Input/Dropdown.vue';
import { Badge, SelectDropdown } from '@/packages/ui/src';
import type { Member } from '@/packages/api';

Check failure on line 9 in resources/js/Components/Common/Member/MemberCombobox.vue

View workflow job for this annotation

GitHub Actions / test

Cannot find module '@/packages/api' or its corresponding type declarations.

Check failure on line 9 in resources/js/Components/Common/Member/MemberCombobox.vue

View workflow job for this annotation

GitHub Actions / build

Cannot find module '@/packages/api' or its corresponding type declarations.

Check failure on line 9 in resources/js/Components/Common/Member/MemberCombobox.vue

View workflow job for this annotation

GitHub Actions / build

Cannot find module '@/packages/api' or its corresponding type declarations.

Check failure on line 9 in resources/js/Components/Common/Member/MemberCombobox.vue

View workflow job for this annotation

GitHub Actions / phpunit

Cannot find module '@/packages/api' or its corresponding type declarations.
const membersStore = useMembersStore();
const { members } = storeToRefs(membersStore);
Expand All @@ -31,13 +30,9 @@ const searchInput = ref<HTMLInputElement | null>(null);
const searchValue = ref('');
function isMemberSelected(id: string) {
return model.value === id;
}
useFocus(searchInput, { initialValue: true });
const filteredMembers = computed(() => {
const filteredMembers = computed<Member[]>(() => {
return members.value.filter((member) => {
return (
member.name
Expand Down Expand Up @@ -65,141 +60,35 @@ function resetHighlightedItem() {
}
}
function updateSearchValue(event: Event) {
const newInput = (event.target as HTMLInputElement).value;
if (newInput === ' ') {
searchValue.value = '';
const highlightedClientId = highlightedItemId.value;
if (highlightedClientId) {
const highlightedClient = members.value.find(
(member) => member.id === highlightedClientId
);
if (highlightedClient) {
model.value = highlightedClient.id;
}
}
} else {
searchValue.value = newInput;
}
}
const emit = defineEmits(['update:modelValue', 'changed']);
function updateMember(newValue: string | null) {
if (newValue) {
model.value = newValue;
nextTick(() => {
emit('changed');
});
}
}
function moveHighlightUp() {
if (highlightedItem.value) {
const currentHightlightedIndex = filteredMembers.value.indexOf(
highlightedItem.value
);
if (currentHightlightedIndex === 0) {
highlightedItemId.value =
filteredMembers.value[filteredMembers.value.length - 1].id;
} else {
highlightedItemId.value =
filteredMembers.value[currentHightlightedIndex - 1].id;
}
}
}
function moveHighlightDown() {
if (highlightedItem.value) {
const currentHightlightedIndex = filteredMembers.value.indexOf(
highlightedItem.value
);
if (currentHightlightedIndex === filteredMembers.value.length - 1) {
highlightedItemId.value = filteredMembers.value[0].id;
} else {
highlightedItemId.value =
filteredMembers.value[currentHightlightedIndex + 1].id;
}
}
}
const highlightedItemId = ref<string | null>(null);
const highlightedItem = computed(() => {
return members.value.find(
(member) => member.id === highlightedItemId.value
);
});
const currentValue = computed(() => {
if (model.value) {
return members.value.find((member) => member.id === model.value)?.name;
}
return searchValue.value;
});
const hasMemberSelected = computed(() => {
return model.value !== '';
});
const showMembersDropdown = ref(true);
</script>

<template>
<Dropdown
align="bottom-start"
width="300"
v-model="showMembersDropdown"
:closeOnContentClick="true">
<template #trigger>
<div class="flex relative">
<div
ref="reference"
class="absolute h-full items-center px-3 w-full flex justify-between">
<UserIcon class="relative z-10 w-4 text-muted"></UserIcon>
<button
v-if="hasMemberSelected"
@click="model = ''"
class="focus:text-accent-200 focus:bg-card-background text-muted">
<XMarkIcon class="relative z-10 w-4"></XMarkIcon>
</button>
<SelectDropdown
v-model="model"
:items="filteredMembers"
:get-key-from-item="(member) => member.id"
:get-name-for-item="(member) => member.name">
<template v-slot:trigger>
<Badge
tag="button"
class="flex w-full text-base text-left space-x-3 px-3 text-text-secondary font-normal cursor py-1.5">
<UserIcon class="relative z-10 w-4 text-muted"></UserIcon>
<div v-if="currentValue" class="flex-1 truncate">
{{ currentValue }}
</div>
<TextInput
:value="currentValue"
:disabled="disabled"
@input="updateSearchValue"
data-testid="member_dropdown_search"
@keydown.enter.prevent="updateMember(highlightedItemId)"
@keydown.up.prevent="moveHighlightUp"
class="relative w-full pl-10"
@keydown.down.prevent="moveHighlightDown"
placeholder="Search for a member..."
ref="searchInput" />
</div>
</template>
<template #content>
<div
class="py-2 text-white px-3"
v-if="filteredMembers.length === 0">
All members are already added.
</div>
<div
v-for="member in filteredMembers"
:key="member.id"
role="option"
:value="member.id"
:class="{
'bg-card-background-active':
member.id === highlightedItemId,
}"
@click="updateMember(member.id)"
data-testid="client_dropdown_entries"
:data-client-id="member.id">
<ClientDropdownItem
:selected="isMemberSelected(member.id)"
:name="member.name"></ClientDropdownItem>
</div>
<div class="flex-1" v-else>Select a member...</div>
<ChevronDownIcon class="w-4 text-muted"></ChevronDownIcon>
</Badge>
</template>
</Dropdown>
</SelectDropdown>
</template>

<style scoped></style>
1 change: 1 addition & 0 deletions resources/js/Components/Common/Member/MemberEditModal.vue
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,7 @@ const roleDescription = computed(() => {
v-if="billableRateSelect === 'custom-rate'">
<InputLabel
for="memberBillableRate"
class="mb-2"
value="Billable Rate" />
<BillableRateInput
focus
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,7 @@ useFocus(projectNameInput, { initialValue: true });
<div class="col-span-3 sm:col-span-1 flex-1">
<InputLabel
for="billable_rate"
class="mb-2"
value="Billable Rate"></InputLabel>
<BillableRateInput
@keydown.enter="submit"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -219,7 +219,6 @@ const billableProxy = computed({
<InputLabel>Duration</InputLabel>
<div class="space-y-2 mt-1 flex flex-col">
<DurationHumanInput
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"
v-model:start="localStart"
v-model:end="localEnd"></DurationHumanInput>
<div class="text-sm flex space-x-1">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@ function checkForConfirmationModal() {
<div class="col-span-6 sm:col-span-4">
<InputLabel
for="organizationBillableRate"
class="mb-2"
value="Organization Billable Rate" />
<BillableRateInput
v-if="organization"
Expand Down
2 changes: 1 addition & 1 deletion resources/js/packages/ui/src/Input/BillableRateInput.vue
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@ const inputValue = ref(formatValue(model.value));
type="text"
:name="name"
placeholder="Billable Rate"
class="mt-2 block w-full"
class="block w-full"
autocomplete="teamMemberRate" />
<div
class="absolute top-0 right-0 h-full flex items-center px-4 font-medium pointer-events-none">
Expand Down
2 changes: 1 addition & 1 deletion resources/js/packages/ui/src/Input/DatePicker.vue
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ const emit = defineEmits(['changed']);
@keydown.enter="updateDate"
:class="
twMerge(
'bg-input-background border text-white border-input-border focus-visible:outline-0 focus-visible:ring-0 rounded-md',
'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',
props.class
)
"
Expand Down
27 changes: 20 additions & 7 deletions resources/js/packages/ui/src/Input/Dropdown.vue
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
<script setup lang="ts">
import { onMounted, onUnmounted, ref } from 'vue';
import { onMounted, onUnmounted, ref, watch } from 'vue';
import {
flip,
limitShift,
Expand All @@ -10,6 +10,8 @@ import {
} from '@floating-ui/vue';
import { offset } from '@floating-ui/vue';
import { autoUpdate } from '@floating-ui/vue';
import { useId } from 'radix-vue';
import { isLastLayer, layers } from '@/packages/ui/src/utils/dismissableLayer';
const props = withDefaults(
defineProps<{
Expand All @@ -24,17 +26,28 @@ const props = withDefaults(
const emit = defineEmits(['open', 'submit']);
const open = defineModel({ default: false });
const id = useId();
const closeOnEscape = (e: KeyboardEvent) => {
if (open.value && e.key === 'Escape') {
open.value = false;
}
if (open.value && e.key === 'Enter') {
emit('submit');
if (props.closeOnContentClick) open.value = false;
if (isLastLayer(id)) {
if (open.value && e.key === 'Escape') {
open.value = false;
}
if (open.value && e.key === 'Enter') {
emit('submit');
if (props.closeOnContentClick) open.value = false;
}
}
};
watch(open, (value) => {
if (value) {
layers.value.push(id);
} else {
layers.value = layers.value.filter((layer) => layer !== id);
}
});
onMounted(() => document.addEventListener('keydown', closeOnEscape));
onUnmounted(() => document.removeEventListener('keydown', closeOnEscape));
Expand Down
Loading

0 comments on commit 636914b

Please sign in to comment.