mirror of
https://github.com/solidtime-io/solidtime.git
synced 2026-08-14 19:22:14 +01:00
improve time picker parsing, fix nested escape listeners, change project member select
This commit is contained in:
@@ -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/src';
|
||||
|
||||
const membersStore = useMembersStore();
|
||||
const { members } = storeToRefs(membersStore);
|
||||
@@ -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
|
||||
@@ -65,70 +60,7 @@ 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) {
|
||||
@@ -136,70 +68,27 @@ const currentValue = computed(() => {
|
||||
}
|
||||
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>
|
||||
<div class="flex-1" v-else>Select a member...</div>
|
||||
<ChevronDownIcon class="w-4 text-muted"></ChevronDownIcon>
|
||||
</Badge>
|
||||
</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>
|
||||
</template>
|
||||
</Dropdown>
|
||||
</SelectDropdown>
|
||||
</template>
|
||||
|
||||
<style scoped></style>
|
||||
|
||||
@@ -151,6 +151,7 @@ const roleDescription = computed(() => {
|
||||
v-if="billableRateSelect === 'custom-rate'">
|
||||
<InputLabel
|
||||
for="memberBillableRate"
|
||||
class="mb-2"
|
||||
value="Billable Rate" />
|
||||
<BillableRateInput
|
||||
focus
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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">
|
||||
|
||||
Reference in New Issue
Block a user