update dependencies, update eslint config, update optional ts props types

This commit is contained in:
Gregor Vostrak
2025-01-29 17:58:03 +01:00
parent 00e2518196
commit 18ab1f714b
128 changed files with 2926 additions and 3127 deletions

View File

@@ -4,11 +4,11 @@ import { computed } from 'vue';
const props = withDefaults(
defineProps<{
size: 'base' | 'large' | 'xlarge';
tag: string;
size?: 'base' | 'large' | 'xlarge';
tag?: string;
class?: string;
color: string;
border: boolean;
color?: string;
border?: boolean;
}>(),
{
size: 'base',

View File

@@ -6,9 +6,9 @@ import { twMerge } from 'tailwind-merge';
const props = withDefaults(
defineProps<{
type: HtmlButtonType;
type?: HtmlButtonType;
icon?: Component;
loading: boolean;
loading?: boolean;
}>(),
{
type: 'submit',
@@ -28,8 +28,8 @@ const props = withDefaults(
">
<LoadingSpinner v-if="loading"></LoadingSpinner>
<component
v-if="props.icon && !loading"
:is="props.icon"
v-if="props.icon && !loading"
class="text-text-secondary w-4 -ml-0.5 mr-1"></component>
<span>
<slot />

View File

@@ -6,10 +6,10 @@ import LoadingSpinner from '../LoadingSpinner.vue';
const props = withDefaults(
defineProps<{
type: HtmlButtonType;
type?: HtmlButtonType;
icon?: Component;
size: 'small' | 'base';
loading: boolean;
size?: 'small' | 'base';
loading?: boolean;
class?: string;
}>(),
{
@@ -42,8 +42,8 @@ const sizeClasses = {
">
<LoadingSpinner v-if="loading"></LoadingSpinner>
<component
v-if="props.icon && !loading"
:is="props.icon"
v-if="props.icon && !loading"
class="text-text-tertiary w-4 -ml-0.5 mr-1"></component>
<span>
<slot />

View File

@@ -12,8 +12,8 @@ defineProps<{
<h3
class="text-white font-bold text-sm lg:text-base flex items-center space-x-2 lg:space-x-2.5">
<component
v-if="icon"
:is="icon"
v-if="icon"
class="w-5 lg:w-6 text-icon-default"></component>
<span>
{{ title }}

View File

@@ -127,28 +127,28 @@ const highlightedItem = computed(() => {
</script>
<template>
<Dropdown width="120" v-model="open" :closeOnContentClick="true">
<Dropdown v-model="open" width="120" :close-on-content-click="true">
<template #trigger>
<slot name="trigger"></slot>
</template>
<template #content>
<input
ref="searchInput"
:value="searchValue"
data-testid="client_dropdown_search"
class="bg-card-background border-0 placeholder-muted text-sm text-white py-2.5 focus:ring-0 border-b border-card-background-separator focus:border-card-background-separator w-full"
placeholder="Search for a client..."
@input="updateSearchValue"
@keydown.enter="addClientIfNoneExists"
data-testid="client_dropdown_search"
@keydown.up.prevent="moveHighlightUp"
@keydown.down.prevent="moveHighlightDown"
ref="searchInput"
class="bg-card-background border-0 placeholder-muted text-sm text-white py-2.5 focus:ring-0 border-b border-card-background-separator focus:border-card-background-separator w-full"
placeholder="Search for a client..." />
@keydown.down.prevent="moveHighlightDown" />
<div ref="dropdownViewport" class="w-60 max-h-60 overflow-y-scroll">
<div
v-if="
searchValue.length > 0 && filteredClients.length === 0
"
@click="addClientIfNoneExists"
class="bg-card-background-active">
class="bg-card-background-active"
@click="addClientIfNoneExists">
<div
class="flex space-x-3 items-center px-4 py-3 text-xs text-white font-medium border-t rounded-b-lg border-card-background-separator">
<PlusCircleIcon
@@ -170,8 +170,8 @@ const highlightedItem = computed(() => {
:data-client-id="client.id">
<ClientDropdownItem
:selected="isClientSelected(client.id)"
@click="updateClient(client.id)"
:name="client.name"></ClientDropdownItem>
:name="client.name"
@click="updateClient(client.id)"></ClientDropdownItem>
</div>
</div>
</template>

View File

@@ -14,9 +14,9 @@ const emit = defineEmits(['submit']);
<InputLabel for="billable" value="Time Estimated" />
</div>
<DurationInput
@submit="emit('submit')"
v-model="model"
class="max-w-[150px]"></DurationInput>
class="max-w-[150px]"
@submit="emit('submit')"></DurationInput>
</div>
</template>

View File

@@ -5,7 +5,7 @@ import { computed } from 'vue';
const props = withDefaults(
defineProps<{
expanded?: boolean;
size: string;
size?: string;
}>(),
{
expanded: false,

View File

@@ -77,13 +77,13 @@ const inputValue = ref(formatValue(model.value));
:id="name"
ref="billableRateInput"
v-model="inputValue"
@blur="updateRate($event.target.value)"
@keydown.enter="updateRate($event.target.value)"
type="text"
:name="name"
placeholder="Billable Rate"
class="block w-full"
autocomplete="teamMemberRate" />
autocomplete="teamMemberRate"
@blur="updateRate($event.target.value)"
@keydown.enter="updateRate($event.target.value)" />
<div
class="absolute top-0 right-0 h-full flex items-center px-4 font-medium pointer-events-none">
<span>

View File

@@ -11,7 +11,7 @@ function toggleBillable() {
const props = withDefaults(
defineProps<{
size: 'small' | 'base';
size?: 'small' | 'base';
}>(),
{
size: 'base',
@@ -40,14 +40,14 @@ const iconSizeWrapperClasses =
<template>
<button
@click="toggleBillable"
:class="
twMerge(
iconColorClasses,
iconSizeWrapperClasses,
'flex-shrink-0 ring-0 focus:outline-none focus-visible:ring-2 focus-visible:ring-ring transition focus:bg-card-background-separator hover:bg-card-background-separator rounded-full flex items-center justify-center'
)
">
"
@click="toggleBillable">
<BillableIcon :class="iconSizeClasses"></BillableIcon>
</button>
</template>

View File

@@ -31,9 +31,9 @@ const proxyChecked = computed({
<template>
<input
:id="id"
v-model="proxyChecked"
type="checkbox"
:id="id"
:value="value"
class="h-4 w-4 rounded bg-card-background border-input-border text-accent-500/80 focus:outline-none focus:ring-ring/50 focus-visible:outline-none focus-visible:ring-ring/50" />
</template>

View File

@@ -48,10 +48,8 @@ const emit = defineEmits(['changed']);
<template>
<div class="flex items-center text-muted">
<input
id="start"
ref="datePicker"
@change="updateTempValue"
@blur="updateDate"
@keydown.enter="updateDate"
:class="
twMerge(
'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',
@@ -59,9 +57,11 @@ const emit = defineEmits(['changed']);
)
"
type="date"
id="start"
name="trip-start"
:value="tempDate" />
:value="tempDate"
@change="updateTempValue"
@blur="updateDate"
@keydown.enter="updateDate" />
</div>
</template>

View File

@@ -14,8 +14,8 @@ import { isLastLayer, layers } from '@/packages/ui/src/utils/dismissableLayer';
const props = withDefaults(
defineProps<{
align: Placement;
closeOnContentClick: boolean;
align?: Placement;
closeOnContentClick?: boolean;
}>(),
{
align: 'bottom-start',
@@ -94,7 +94,7 @@ const { floatingStyles } = useFloating(reference, floating, {
<template>
<div class="min-w-0 isolate">
<div @click.prevent="toggleOpen" ref="reference" class="min-w-0">
<div ref="reference" class="min-w-0" @click.prevent="toggleOpen">
<slot name="trigger" />
</div>
@@ -113,8 +113,8 @@ const { floatingStyles } = useFloating(reference, floating, {
leave-to-class="transform opacity-0 scale-95">
<div
v-if="open"
class="z-50"
ref="floating"
class="z-50"
:style="floatingStyles"
@click="onContentClick">
<div

View File

@@ -78,11 +78,11 @@ function updateTimeEntryInputValue() {
<template>
<TextInput
ref="inputField"
@blur="updateDuration"
@keydown.enter="updateDuration"
v-model="temporaryCustomTimerEntry"
:class="twMerge('text-text-secondary', props.class)"
type="text" />
type="text"
@blur="updateDuration"
@keydown.enter="updateDuration" />
</template>
<style scoped></style>

View File

@@ -54,12 +54,12 @@ function updateAndSubmit() {
<template>
<div class="relative">
<TextInput
v-model="currentTime"
class="w-full overflow-hidden pr-14"
placeholder="0"
@focus="selectInput"
@blur="updateDuration"
@keydown.enter="updateAndSubmit"
v-model="currentTime">
@keydown.enter="updateAndSubmit">
</TextInput>
<div
class="absolute top-0 right-0 h-full flex items-center px-4 font-medium">

View File

@@ -151,20 +151,20 @@ const highlightedItem = computed(() => {
</script>
<template>
<Dropdown v-model="open" align="bottom-start" :closeOnContentClick="false">
<Dropdown v-model="open" align="bottom-start" :close-on-content-click="false">
<template #trigger>
<slot name="trigger"></slot>
</template>
<template #content>
<input
ref="searchInput"
:value="searchValue"
class="bg-card-background border-0 placeholder-muted text-sm text-white py-2.5 focus:ring-0 border-b border-card-background-separator focus:border-card-background-separator w-full"
:placeholder="searchPlaceholder"
@input="updateSearchValue"
@keydown.up.prevent="moveHighlightUp"
@keydown.down.prevent="moveHighlightDown"
@keydown.enter="toggleItem(highlightedItemId)"
ref="searchInput"
class="bg-card-background border-0 placeholder-muted text-sm text-white py-2.5 focus:ring-0 border-b border-card-background-separator focus:border-card-background-separator w-full"
:placeholder="searchPlaceholder" />
@keydown.enter="toggleItem(highlightedItemId)" />
<div
ref="dropdownViewport"
class="min-w-60 max-w-80 max-h-60 overflow-y-scroll">
@@ -180,10 +180,10 @@ const highlightedItem = computed(() => {
:data-item-id="props.getKeyFromItem(item)">
<MultiselectDropdownItem
:selected="isItemSelected(props.getKeyFromItem(item))"
@click="toggleItem(props.getKeyFromItem(item))"
:name="
props.getNameForItem(item)
"></MultiselectDropdownItem>
"
@click="toggleItem(props.getKeyFromItem(item))"></MultiselectDropdownItem>
</div>
</div>
</template>

View File

@@ -155,7 +155,7 @@ watch(open, () => {
</script>
<template>
<Dropdown v-model="open" :align="align" :closeOnContentClick="false">
<Dropdown v-model="open" :align="align" :close-on-content-click="false">
<template #trigger>
<slot name="trigger"> </slot>
</template>
@@ -176,15 +176,15 @@ watch(open, () => {
:value="props.getKeyFromItem(item)"
:data-item-id="props.getKeyFromItem(item)">
<SelectDropdownItem
@mouseenter="
highlightedItemId = props.getKeyFromItem(item)
"
:highlighted="
props.getKeyFromItem(item) === highlightedItemId
"
:selected="props.getKeyFromItem(item) === model"
@click="setItem(props.getKeyFromItem(item))"
:name="props.getNameForItem(item)"></SelectDropdownItem>
:name="props.getNameForItem(item)"
@mouseenter="
highlightedItemId = props.getKeyFromItem(item)
"
@click="setItem(props.getKeyFromItem(item))"></SelectDropdownItem>
</div>
</div>
</template>

View File

@@ -22,12 +22,12 @@ const model = defineModel();
<template>
<input
ref="input"
v-model="model"
:class="
twMerge(
'border-input-border border bg-input-background text-white focus-visible:ring-2 focus-visible:ring-ring focus-visible:border-transparent rounded-md shadow-sm',
props.class
)
"
v-model="model"
:name="name" />
</template>

View File

@@ -15,8 +15,8 @@ const model = defineModel<string | null>({
const props = withDefaults(
defineProps<{
size: 'base' | 'large';
focus: boolean;
size?: 'base' | 'large';
focus?: boolean;
}>(),
{
size: 'base',
@@ -143,22 +143,24 @@ const closestValue = computed({
<template>
<div class="flex min-w-0 items-center justify-center text-white">
<SelectDropdown
:class="twMerge('mine-w-0 w-24', size === 'large' && 'w-28')"
v-model="closestValue"
v-model:open="open"
:class="twMerge('mine-w-0 w-24', size === 'large' && 'w-28')"
:get-key-from-item="(item: TimeOption) => item.timestamp"
:get-name-for-item="(item: TimeOption) => item.name"
:items="getStartOptions">
<template #trigger>
<TextInput
v-model="inputValue"
ref="timeInput"
v-model="inputValue"
:class="
twMerge(
'text-center w-24 px-3 py-2',
size === 'large' && 'w-28'
)
"
data-testid="time_picker_input"
type="text"
@blur="updateTime"
@keydown.enter="
updateTime($event);
@@ -169,9 +171,7 @@ const closestValue = computed({
@mouseup="($event.target as HTMLInputElement).select()"
@click="($event.target as HTMLInputElement).select()"
@pointerup="($event.target as HTMLInputElement).select()"
@focusin="open = true"
data-testid="time_picker_input"
type="text" />
@focusin="open = true" />
</template>
</SelectDropdown>
</div>

View File

@@ -12,8 +12,8 @@ const model = defineModel<string | null>({
const props = withDefaults(
defineProps<{
size: 'base' | 'large';
focus: boolean;
size?: 'base' | 'large';
focus?: boolean;
}>(),
{
size: 'base',
@@ -95,18 +95,18 @@ const inputValue = ref(
<template>
<TextInput
v-bind="$attrs"
v-model="inputValue"
ref="timeInput"
v-model="inputValue"
:class="
twMerge('text-center w-24 px-3 py-2', size === 'large' && 'w-28')
"
data-testid="time_picker_input"
type="text"
@blur="updateTime"
@focus="($event.target as HTMLInputElement).select()"
@mouseup="($event.target as HTMLInputElement).select()"
@click="($event.target as HTMLInputElement).select()"
@pointerup="($event.target as HTMLInputElement).select()"
data-testid="time_picker_input"
type="text" />
@pointerup="($event.target as HTMLInputElement).select()" />
</template>
<style scoped></style>

View File

@@ -61,32 +61,32 @@ watch(focused, (newValue, oldValue) => {
<div class="font-bold text-white text-sm pb-2">Start</div>
<div class="space-y-2">
<TimePickerSimple
v-model="tempStart"
data-testid="time_entry_range_start"
tabindex="0"
@keydown.exact.tab.shift.stop.prevent="emit('close')"
:focus
@changed="updateTimeEntry"
v-model="tempStart"></TimePickerSimple>
@keydown.exact.tab.shift.stop.prevent="emit('close')"
@changed="updateTimeEntry"></TimePickerSimple>
<DatePicker
v-model="tempStart"
class="text-xs text-text-tertiary max-w-24 px-1.5 py-1.5"
@changed="updateTimeEntry"
@blur.stop.prevent="emit('close')"
v-model="tempStart"></DatePicker>
@blur.stop.prevent="emit('close')"></DatePicker>
</div>
</div>
<div class="px-2">
<div class="font-bold text-white text-sm pb-2">End</div>
<div v-if="tempEnd !== null" class="space-y-2">
<TimePickerSimple
v-model="tempEnd"
data-testid="time_entry_range_end"
@changed="updateTimeEntry"
v-model="tempEnd"></TimePickerSimple>
@changed="updateTimeEntry"></TimePickerSimple>
<DatePicker
v-model="tempEnd"
class="text-xs text-text-tertiary max-w-24 px-1.5 py-1.5"
@changed="updateTimeEntry"
v-model="tempEnd"></DatePicker>
@changed="updateTimeEntry"></DatePicker>
</div>
<div class="text-muted" v-else>-- : --</div>
<div v-else class="text-muted">-- : --</div>
<div tabindex="0" @focusin="emit('close')"></div>
</div>
</div>

View File

@@ -129,8 +129,8 @@ watch(
leave-to-class="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95">
<div
v-show="show"
role="dialog"
ref="target"
role="dialog"
class="mb-6 bg-default-background border border-card-border rounded-lg shadow-xl transform transition-all sm:w-full sm:mx-auto"
:class="maxWidthClass">
<slot v-if="show" />

View File

@@ -4,12 +4,12 @@ import Badge from '@/packages/ui/src/Badge.vue';
const props = withDefaults(
defineProps<{
name: string;
size: 'base' | 'large' | 'xlarge';
tag: string;
name?: string;
size?: 'base' | 'large' | 'xlarge';
tag?: string;
class?: string;
color: string;
border: boolean;
color?: string;
border?: boolean;
}>(),
{
name: '',

View File

@@ -18,10 +18,10 @@ defineEmits<{
<template>
<BillableRateModal
@submit="$emit('submit')"
v-model:show="show"
v-model:saving="saving"
title="Update Project Billable Rate">
title="Update Project Billable Rate"
@submit="$emit('submit')">
<p class="py-1 text-center">
The billable rate of {{ projectName }} will be updated to
<strong>{{

View File

@@ -23,12 +23,12 @@ const model = defineModel<string>({ default: '' });
<div
v-for="color in colors"
:key="color"
@click="model = color"
:style="{
backgroundColor: color,
boxShadow: `var(--tw-ring-inset) 0 0 0 calc(3px + var(--tw-ring-offset-width)) ${color}30`,
}"
class="w-4 h-4 rounded-full cursor-pointer"></div>
class="w-4 h-4 rounded-full cursor-pointer"
@click="model = color"></div>
</div>
</template>
</Dropdown>

View File

@@ -86,31 +86,31 @@ const currentClientName = computed(() => {
<div class="text-center pr-5">
<InputLabel for="color" value="Color" />
<ProjectColorSelector
class="mt-2.5"
v-model="project.color"></ProjectColorSelector>
v-model="project.color"
class="mt-2.5"></ProjectColorSelector>
</div>
<div class="w-full">
<InputLabel for="projectName" value="Project name" />
<TextInput
id="projectName"
name="projectName"
ref="projectNameInput"
v-model="project.name"
name="projectName"
type="text"
placeholder="The next big thing"
@keydown.enter="submit()"
class="mt-2 block w-full"
required
autocomplete="projectName" />
autocomplete="projectName"
@keydown.enter="submit()" />
</div>
</div>
<div>
<InputLabel for="client" value="Client" />
<ClientDropdown
:createClient="createClient"
v-model="project.client_id"
:create-client="createClient"
:clients="activeClients"
class="mt-2"
v-model="project.client_id">
class="mt-2">
<template #trigger>
<Badge
tag="button"
@@ -131,17 +131,17 @@ const currentClientName = computed(() => {
<div class="lg:grid grid-cols-2 gap-12">
<div>
<ProjectEditBillableSection
:currency="currency"
v-model:isBillable="project.is_billable"
v-model:billableRate="
v-model:is-billable="project.is_billable"
v-model:billable-rate="
project.billable_rate
"></ProjectEditBillableSection>
"
:currency="currency"></ProjectEditBillableSection>
</div>
<div>
<EstimatedTimeSection
v-if="enableEstimatedTime"
@submit="submit()"
v-model="project.estimated_time"></EstimatedTimeSection>
v-model="project.estimated_time"
@submit="submit()"></EstimatedTimeSection>
</div>
</div>
</template>

View File

@@ -67,14 +67,14 @@ const emit = defineEmits(['submit']);
class="mt-2"></ProjectBillableSelect>
</div>
<div
class="sm:max-w-[120px]"
v-if="billableRateSelect === 'custom-rate'">
v-if="billableRateSelect === 'custom-rate'"
class="sm:max-w-[120px]">
<InputLabel for="billableRate" value="Billable Rate" class="mb-2" />
<BillableRateInput
@keydown.enter="emit('submit')"
:currency="currency"
v-model="billableRate"
name="billableRate" />
:currency="currency"
name="billableRate"
@keydown.enter="emit('submit')" />
</div>
</div>
<div class="flex items-center text-muted text-xs pt-2 pl-1">

View File

@@ -6,11 +6,11 @@ import { TagIcon } from '@heroicons/vue/20/solid';
const props = withDefaults(
defineProps<{
name: string;
size: 'base' | 'large';
tag: string;
size?: 'base' | 'large';
tag?: string;
class?: string;
color: string;
border: boolean;
color?: string;
border?: boolean;
}>(),
{
size: 'base',

View File

@@ -43,12 +43,12 @@ useFocus(tagNameInput, { initialValue: true });
id="tagName"
ref="tagNameInput"
v-model="tag.name"
@keydown.enter="submit"
type="text"
placeholder="Tag Name"
class="mt-1 block w-full"
required
autocomplete="tagName" />
autocomplete="tagName"
@keydown.enter="submit" />
</div>
</div>
</template>

View File

@@ -11,7 +11,7 @@ const props = withDefaults(
defineProps<{
tags: Tag[];
createTag: (name: string) => Promise<Tag | undefined>;
align: Placement;
align?: Placement;
}>(),
{
align: 'bottom-start',
@@ -166,27 +166,27 @@ const showCreateTagModal = ref(false);
<template>
<TagCreateModal
:createTag="createAndAddTag"
v-model:show="showCreateTagModal"></TagCreateModal>
v-model:show="showCreateTagModal"
:create-tag="createAndAddTag"></TagCreateModal>
<Dropdown
@submit="emit('submit')"
v-model="open"
:align="align"
:closeOnContentClick="false">
:close-on-content-click="false"
@submit="emit('submit')">
<template #trigger>
<slot name="trigger"></slot>
</template>
<template #content>
<input
ref="searchInput"
:value="searchValue"
data-testid="tag_dropdown_search"
class="bg-card-background border-0 placeholder-muted text-sm text-white py-2.5 focus:ring-0 border-b border-card-background-separator focus:border-card-background-separator w-full"
placeholder="Search for a Tag..."
@input="updateSearchValue"
@keydown.enter="addTagIfNoneExists"
data-testid="tag_dropdown_search"
@keydown.up.prevent="moveHighlightUp"
@keydown.down.prevent="moveHighlightDown"
ref="searchInput"
class="bg-card-background border-0 placeholder-muted text-sm text-white py-2.5 focus:ring-0 border-b border-card-background-separator focus:border-card-background-separator w-full"
placeholder="Search for a Tag..." />
@keydown.down.prevent="moveHighlightDown" />
<div ref="dropdownViewport" class="w-60 max-h-60 overflow-y-scroll">
<div
v-for="tag in filteredTags"
@@ -201,17 +201,17 @@ const showCreateTagModal = ref(false);
:data-tag-id="tag.id">
<MultiselectDropdownItem
:selected="isTagSelected(tag.id)"
@click="toggleTag(tag.id)"
:name="tag.name"></MultiselectDropdownItem>
:name="tag.name"
@click="toggleTag(tag.id)"></MultiselectDropdownItem>
</div>
</div>
<div class="hover:bg-card-background-active rounded-b-lg">
<button
class="text-white w-full flex space-x-3 items-center px-4 py-3 text-xs font-semibold border-t border-card-background-separator"
@click="
open = false;
showCreateTagModal = true;
"
class="text-white w-full flex space-x-3 items-center px-4 py-3 text-xs font-semibold border-t border-card-background-separator">
">
<PlusCircleIcon
class="w-5 flex-shrink-0 text-icon-default"></PlusCircleIcon>
<span>Create new Tag</span>

View File

@@ -97,7 +97,6 @@ function onSelectChange(event: Event) {
class="sm:flex py-2 items-center min-w-0 justify-between group">
<div class="flex space-x-3 items-center min-w-0">
<Checkbox
@update:checked="onSelectChange"
:checked="
timeEntry.timeEntries.every(
(aggregateTimeEntry: TimeEntry) =>
@@ -105,7 +104,8 @@ function onSelectChange(event: Event) {
aggregateTimeEntry
)
)
" />
"
@update:checked="onSelectChange" />
<div class="flex items-center min-w-0">
<GroupedItemsCountButton
:expanded="expanded"
@@ -114,36 +114,36 @@ function onSelectChange(event: Event) {
</GroupedItemsCountButton>
<TimeEntryDescriptionInput
class="min-w-0 mr-4"
@changed="updateTimeEntryDescription"
:modelValue="
:model-value="
timeEntry.description
"></TimeEntryDescriptionInput>
"
@changed="updateTimeEntryDescription"></TimeEntryDescriptionInput>
<TimeTrackerProjectTaskDropdown
:clients
:createProject
:createClient
:canCreateProject
:create-project
:create-client
:can-create-project
:projects="projects"
:tasks="tasks"
:showBadgeBorder="false"
@changed="updateProjectAndTask"
:show-badge-border="false"
:project="timeEntry.project_id"
:enableEstimatedTime
:enable-estimated-time
:currency="currency"
class="border border-border-primary"
:task="
timeEntry.task_id
"></TimeTrackerProjectTaskDropdown>
"
@changed="updateProjectAndTask"></TimeTrackerProjectTaskDropdown>
</div>
</div>
<div class="flex items-center font-medium lg:space-x-2">
<TimeEntryRowTagDropdown
:createTag
:create-tag
:tags="tags"
@changed="updateTimeEntryTags"
:modelValue="timeEntry.tags"></TimeEntryRowTagDropdown>
:model-value="timeEntry.tags"
@changed="updateTimeEntryTags"></TimeEntryRowTagDropdown>
<BillableToggleButton
:modelValue="timeEntry.billable"
:model-value="timeEntry.billable"
class="opacity-50 focus-visible:opacity-100 group-hover:opacity-100"
size="small"
@changed="
@@ -151,23 +151,23 @@ function onSelectChange(event: Event) {
"></BillableToggleButton>
<div class="flex-1">
<button
@click="expanded = !expanded"
class="hidden lg:block text-muted w-[110px] px-1 py-1.5 bg-transparent text-center hover:bg-card-background rounded-lg border border-transparent hover:border-card-border text-sm font-medium focus-visible:outline-none focus:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:bg-tertiary">
class="hidden lg:block text-muted w-[110px] px-1 py-1.5 bg-transparent text-center hover:bg-card-background rounded-lg border border-transparent hover:border-card-border text-sm font-medium focus-visible:outline-none focus:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:bg-tertiary"
@click="expanded = !expanded">
{{ formatStartEnd(timeEntry.start, timeEntry.end) }}
</button>
</div>
<button
@click="expanded = !expanded"
class="text-white min-w-[90px] px-2 py-1.5 bg-transparent text-center hover:bg-card-background rounded-lg border border-transparent hover:border-card-border text-sm font-semibold focus-visible:outline-none focus:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:bg-tertiary">
class="text-white min-w-[90px] px-2 py-1.5 bg-transparent text-center hover:bg-card-background rounded-lg border border-transparent hover:border-card-border text-sm font-semibold focus-visible:outline-none focus:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:bg-tertiary"
@click="expanded = !expanded">
{{
formatHumanReadableDuration(timeEntry.duration ?? 0)
}}
</button>
<TimeTrackerStartStop
@changed="onStartStopClick(timeEntry)"
:active="!!(timeEntry.start && !timeEntry.end)"
class="opacity-20 hidden sm:flex group-hover:opacity-100 focus-visible:opacity-100"></TimeTrackerStartStop>
class="opacity-20 hidden sm:flex group-hover:opacity-100 focus-visible:opacity-100"
@changed="onStartStopClick(timeEntry)"></TimeTrackerStartStop>
<TimeEntryMoreOptionsDropdown
@delete="
deleteTimeEntries(timeEntry?.timeEntries ?? [])
@@ -179,9 +179,11 @@ function onSelectChange(event: Event) {
v-if="expanded"
class="w-full border-t border-default-background-separator bg-black/15">
<TimeEntryRow
v-for="subEntry in timeEntry.timeEntries"
:key="subEntry.id"
:projects="projects"
:enableEstimatedTime
:canCreateProject
:enable-estimated-time
:can-create-project
:tasks="tasks"
:selected="
!!selectedTimeEntries.find(
@@ -189,23 +191,21 @@ function onSelectChange(event: Event) {
filterEntry.id === subEntry.id
)
"
@selected="emit('selected', [subEntry])"
@unselected="emit('unselected', [subEntry])"
:createClient
:create-client
:clients
:createProject
:create-project
:tags="tags"
indent
:updateTimeEntry="
:update-time-entry="
(timeEntry: TimeEntry) => updateTimeEntry(timeEntry)
"
:onStartStopClick="() => onStartStopClick(subEntry)"
:deleteTimeEntry="() => deleteTimeEntries([subEntry])"
:on-start-stop-click="() => onStartStopClick(subEntry)"
:delete-time-entry="() => deleteTimeEntries([subEntry])"
:currency="currency"
:createTag
:key="subEntry.id"
v-for="subEntry in timeEntry.timeEntries"
:time-entry="subEntry"></TimeEntryRow>
:create-tag
:time-entry="subEntry"
@selected="emit('selected', [subEntry])"
@unselected="emit('unselected', [subEntry])"></TimeEntryRow>
</div>
</div>
</template>

View File

@@ -135,11 +135,11 @@ type BillableOption = {
<TextInput
id="description"
ref="description"
placeholder="What did you work on?"
v-model="timeEntry.description"
@keydown.enter="submit"
placeholder="What did you work on?"
type="text"
class="mt-1 block w-full" />
class="mt-1 block w-full"
@keydown.enter="submit" />
</div>
</div>
<div
@@ -147,28 +147,28 @@ type BillableOption = {
<div class="flex w-full items-center space-x-2 justify-between">
<div class="flex-1 min-w-0">
<TimeTrackerProjectTaskDropdown
v-model:project="timeEntry.project_id"
v-model:task="
timeEntry.task_id
"
:clients
:createProject
:createClient
:canCreateProject="canCreateProjects()"
:create-project
:create-client
:can-create-project="canCreateProjects()"
:currency="getOrganizationCurrencyString()"
size="xlarge"
class="bg-input-background"
:projects="projects"
:tasks="tasks"
:enableEstimatedTime="enableEstimatedTime"
v-model:project="timeEntry.project_id"
v-model:task="
timeEntry.task_id
"></TimeTrackerProjectTaskDropdown>
:enable-estimated-time="enableEstimatedTime"></TimeTrackerProjectTaskDropdown>
</div>
<div class="flex items-center space-x-2">
<div class="flex-col">
<TagDropdown
:createTag
v-model="timeEntry.tags"
:create-tag
:tags="tags">
<template v-slot:trigger>
<template #trigger>
<Badge
class="bg-input-background"
tag="button"
@@ -206,7 +206,7 @@ type BillableOption = {
value: 'false',
},
]">
<template v-slot:trigger>
<template #trigger>
<Badge
class="bg-input-background"
tag="button"
@@ -246,22 +246,22 @@ type BillableOption = {
<InputLabel>Start</InputLabel>
<div class="flex flex-col items-center space-y-2 mt-1">
<TimePicker
size="large"
v-model="localStart"></TimePicker>
v-model="localStart"
size="large"></TimePicker>
<DatePicker
class="text-xs text-text-tertiary max-w-28 px-1.5 py-1.5"
v-model="localStart"></DatePicker>
v-model="localStart"
class="text-xs text-text-tertiary max-w-28 px-1.5 py-1.5"></DatePicker>
</div>
</div>
<div class="">
<InputLabel>End</InputLabel>
<div class="flex flex-col items-center space-y-2 mt-1">
<TimePicker
size="large"
v-model="localEnd"></TimePicker>
v-model="localEnd"
size="large"></TimePicker>
<DatePicker
class="text-xs text-text-tertiary max-w-28 px-1.5 py-1.5"
v-model="localEnd"></DatePicker>
v-model="localEnd"
class="text-xs text-text-tertiary max-w-28 px-1.5 py-1.5"></DatePicker>
</div>
</div>
</div>

View File

@@ -44,11 +44,11 @@ const displaysPlaceholder = computed(() => {
<input
data-testid="time_entry_description"
:value="liveDataValue"
placeholder="Add a description"
class="absolute px-0 h-full min-w-0 pl-3 pr-1 left-0 top-0 w-full text-sm text-white font-medium bg-transparent focus-visible:ring-0 rounded-lg border-0"
@blur="onChange"
@input="onInput"
@keydown.enter="onChange"
placeholder="Add a description"
class="absolute px-0 h-full min-w-0 pl-3 pr-1 left-0 top-0 w-full text-sm text-white font-medium bg-transparent focus-visible:ring-0 rounded-lg border-0" />
@keydown.enter="onChange" />
</div>
</div>
</template>

View File

@@ -152,19 +152,32 @@ function unselectAllTimeEntries(value: TimeEntriesGroupedByType[]) {
<TimeEntryRowHeading
:date="key"
:duration="sumDuration(value)"
@select-all="selectAllTimeEntries(value)"
@unselect-all="unselectAllTimeEntries(value)"
:checked="
value.every((timeEntry: TimeEntry) =>
selectedTimeEntries.includes(timeEntry)
)
"></TimeEntryRowHeading>
"
@select-all="selectAllTimeEntries(value)"
@unselect-all="unselectAllTimeEntries(value)"></TimeEntryRowHeading>
<template v-for="entry in value" :key="entry.id">
<TimeEntryAggregateRow
:createProject
:canCreateProject
:enableEstimatedTime
v-if="'timeEntries' in entry && entry.timeEntries.length > 1"
:create-project
:can-create-project
:enable-estimated-time
:selected-time-entries="selectedTimeEntries"
:create-client
:projects="projects"
:tasks="tasks"
:tags="tags"
:clients
:on-start-stop-click="startTimeEntryFromExisting"
:update-time-entries
:update-time-entry
:delete-time-entries
:create-tag
:currency="currency"
:time-entry="entry"
@selected="
(timeEntries: TimeEntry[]) => {
selectedTimeEntries = [
@@ -183,47 +196,34 @@ function unselectAllTimeEntries(value: TimeEntriesGroupedByType[]) {
)
);
}
"
:createClient
:projects="projects"
:tasks="tasks"
:tags="tags"
:clients
:onStartStopClick="startTimeEntryFromExisting"
:updateTimeEntries
:updateTimeEntry
:deleteTimeEntries
:createTag
:currency="currency"
v-if="'timeEntries' in entry && entry.timeEntries.length > 1"
:time-entry="entry"></TimeEntryAggregateRow>
"></TimeEntryAggregateRow>
<TimeEntryRow
:createClient
:enableEstimatedTime
:canCreateProject
:createProject
v-else
:create-client
:enable-estimated-time
:can-create-project
:create-project
:projects="projects"
:selected="
!!selectedTimeEntries.find(
(filterEntry: TimeEntry) => filterEntry.id === entry.id
)
"
:tasks="tasks"
:tags="tags"
:clients
:create-tag
:update-time-entry
:on-start-stop-click="() => startTimeEntryFromExisting(entry)"
:delete-time-entry="() => deleteTimeEntries([entry])"
:currency="currency"
:time-entry="entry.timeEntries[0]"
@selected="selectedTimeEntries.push(entry)"
@unselected="
selectedTimeEntries = selectedTimeEntries.filter(
(item: TimeEntry) => item.id !== entry.id
)
"
:tasks="tasks"
:tags="tags"
:clients
:createTag
:updateTimeEntry
:onStartStopClick="() => startTimeEntryFromExisting(entry)"
:deleteTimeEntry="() => deleteTimeEntries([entry])"
:currency="currency"
v-else
:time-entry="entry.timeEntries[0]"></TimeEntryRow>
"></TimeEntryRow>
</template>
</div>
</template>

View File

@@ -47,20 +47,20 @@ const showMassUpdateModal = ref(false);
<template>
<TimeEntryMassUpdateModal
v-model:show="showMassUpdateModal"
:projects
:tasks
:tags
:clients
:createTag
:createProject
:createClient
:updateTimeEntries
:enableEstimatedTime
:canCreateProject
:create-tag
:create-project
:create-client
:update-time-entries
:enable-estimated-time
:can-create-project
:currency
:time-entries="selectedTimeEntries"
@submit="emit('submit')"
v-model:show="showMassUpdateModal"></TimeEntryMassUpdateModal>
@submit="emit('submit')"></TimeEntryMassUpdateModal>
<MainContainer
:class="
twMerge(
@@ -69,35 +69,35 @@ const showMassUpdateModal = ref(false);
)
">
<Checkbox
:checked="allSelected"
id="selectAll"
:checked="allSelected"
@update:checked="
allSelected ? emit('unselectAll') : emit('selectAll')
">
</Checkbox>
<InputLabel
v-if="selectedTimeEntries.length > 0"
for="selectAll"
class="select-none text-text-secondary"
v-if="selectedTimeEntries.length > 0">
class="select-none text-text-secondary">
{{ selectedTimeEntries.length }} selected
</InputLabel>
<InputLabel
v-else
for="selectAll"
class="text-text-secondary select-none"
v-else
>Select All</InputLabel
>
<button
v-if="selectedTimeEntries.length"
class="text-text-tertiary flex space-x-1 items-center hover:text-text-secondary transition focus-visible:ring-2 outline-0 focus-visible:text-text-primary focus-visible:ring-ring rounded h-full px-2"
@click="showMassUpdateModal = true"
v-if="selectedTimeEntries.length">
@click="showMassUpdateModal = true">
<PencilSquareIcon class="w-4"></PencilSquareIcon>
<span> Edit </span>
</button>
<button
v-if="selectedTimeEntries.length"
class="text-red-400 h-full px-2 space-x-1 items-center flex hover:text-red-500 transition focus-visible:ring-2 outline-0 focus-visible:text-red-500 focus-visible:ring-ring rounded"
@click="deleteSelected"
v-if="selectedTimeEntries.length">
@click="deleteSelected">
<TrashIcon class="w-3.5"></TrashIcon>
<span> Delete </span>
</button>

View File

@@ -118,7 +118,7 @@ async function submit() {
billable.value = undefined;
saving.value = false;
removeAllTags.value = false;
} catch (e) {
} catch {
saving.value = false;
}
}
@@ -147,34 +147,34 @@ type SelectOption = { label: string; value: string };
id="description"
ref="descriptionInput"
v-model="description"
@keydown.enter="submit"
type="text"
class="mt-1 block w-full" />
class="mt-1 block w-full"
@keydown.enter="submit" />
</div>
<div class="space-y-2">
<InputLabel for="project" value="Project" />
<TimeTrackerProjectTaskDropdown
v-model:project="projectId"
v-model:task="taskId"
:clients
:createProject
:createClient
:create-project
:create-client
:currency="currency"
:canCreateProject
:can-create-project
class="mt-1"
empty-placeholder="Select project..."
allow-reset
size="xlarge"
:enableEstimatedTime
:enable-estimated-time
:projects="projects"
:tasks="tasks"
v-model:project="projectId"
v-model:task="taskId"></TimeTrackerProjectTaskDropdown>
:tasks="tasks"></TimeTrackerProjectTaskDropdown>
</div>
<div class="space-y-2">
<InputLabel for="project" value="Tag" />
<div class="flex space-x-5">
<TagDropdown
:createTag
v-model="selectedTags"
:create-tag
:tags="tags">
<template #trigger>
<Badge
@@ -190,8 +190,8 @@ type SelectOption = { label: string; value: string };
</TagDropdown>
<div class="flex items-center space-x-2">
<Checkbox
v-model:checked="removeAllTags"
id="no_tags"></Checkbox>
id="no_tags"
v-model:checked="removeAllTags"></Checkbox>
<InputLabel for="no_tags" value="Remove all tags" />
</div>
</div>
@@ -221,7 +221,7 @@ type SelectOption = { label: string; value: string };
value: 'non-billable',
},
]">
<template v-slot:trigger>
<template #trigger>
<Badge tag="button" size="xlarge">
<span v-if="billable === undefined">
Set billable status

View File

@@ -10,9 +10,9 @@ const emit = defineEmits<{
<template>
<MoreOptionsDropdown label="Actions for the time entry">
<button
@click="emit('delete')"
data-testid="time_entry_delete"
class="flex items-center space-x-3 w-full px-3 py-2.5 text-start text-sm font-medium leading-5 text-white hover:bg-card-background-active focus:outline-none focus:bg-card-background-active transition duration-150 ease-in-out">
class="flex items-center space-x-3 w-full px-3 py-2.5 text-start text-sm font-medium leading-5 text-white hover:bg-card-background-active focus:outline-none focus:bg-card-background-active transition duration-150 ease-in-out"
@click="emit('delete')">
<TrashIcon class="w-5 text-icon-active"></TrashIcon>
<span>Delete</span>
</button>

View File

@@ -26,13 +26,13 @@ const triggerElement = ref<HTMLButtonElement | null>(null);
<div class="relative">
<Dropdown
v-model="open"
@submit="open = false"
align="bottom"
:close-on-content-click="false">
:close-on-content-click="false"
@submit="open = false">
<template #trigger>
<button
data-testid="time_entry_range_selector"
ref="triggerElement"
data-testid="time_entry_range_selector"
:class="
twMerge(
'text-muted w-[110px] px-2 bg-transparent text-center hover:bg-card-background rounded-lg border border-transparent hover:border-card-border focus-visible:outline-none focus:outline-none focus-visible:ring-2 focus-visible:text-text-primary focus-visible:ring-ring focus-visible:bg-tertiary',
@@ -50,14 +50,14 @@ const triggerElement = ref<HTMLButtonElement | null>(null);
</template>
<template #content>
<TimeRangeSelector
focus
:start="start"
:end="end"
@changed="
(newStart: string, newEnd: string) =>
emit('changed', newStart, newEnd)
"
focus
@close="open = false"
:start="start"
:end="end">
@close="open = false">
</TimeRangeSelector>
</template>
</Dropdown>

View File

@@ -100,44 +100,44 @@ function onSelectChange(event: Event) {
class="sm:flex py-2 min-w-0 items-center justify-between group">
<div class="flex items-center min-w-0">
<Checkbox
@update:checked="onSelectChange"
:checked="selected" />
<div class="w-10 h-7" v-if="indent === true"></div>
:checked="selected"
@update:checked="onSelectChange" />
<div v-if="indent === true" class="w-10 h-7"></div>
<TimeEntryDescriptionInput
@changed="updateTimeEntryDescription"
class="min-w-0 mr-4"
:modelValue="
:model-value="
timeEntry.description
"></TimeEntryDescriptionInput>
"
@changed="updateTimeEntryDescription"></TimeEntryDescriptionInput>
<TimeTrackerProjectTaskDropdown
:createProject
:createClient
:canCreateProject
:create-project
:create-client
:can-create-project
:clients
:projects="projects"
:tasks="tasks"
:showBadgeBorder="false"
@changed="updateProjectAndTask"
:show-badge-border="false"
:project="timeEntry.project_id"
:currency="currency"
class="border border-border-primary"
:enableEstimatedTime
:enable-estimated-time
:task="
timeEntry.task_id
"></TimeTrackerProjectTaskDropdown>
"
@changed="updateProjectAndTask"></TimeTrackerProjectTaskDropdown>
</div>
<div
class="flex items-center font-medium space-x-1 lg:space-x-2">
<div class="text-sm px-2" v-if="showMember && members">
<div v-if="showMember && members" class="text-sm px-2">
{{ memberName }}
</div>
<TimeEntryRowTagDropdown
@changed="updateTimeEntryTags"
:createTag
:create-tag
:tags="tags"
:modelValue="timeEntry.tags"></TimeEntryRowTagDropdown>
:model-value="timeEntry.tags"
@changed="updateTimeEntryTags"></TimeEntryRowTagDropdown>
<BillableToggleButton
:modelValue="timeEntry.billable"
:model-value="timeEntry.billable"
class="opacity-50 group-hover:opacity-100 focus-visible:opacity-100"
size="small"
@changed="
@@ -148,7 +148,7 @@ function onSelectChange(event: Event) {
class="hidden lg:block"
:start="timeEntry.start"
:end="timeEntry.end"
:showDate
:show-date
@changed="
updateStartEndTime
"></TimeEntryRangeSelector>
@@ -160,9 +160,9 @@ function onSelectChange(event: Event) {
updateStartEndTime
"></TimeEntryRowDurationInput>
<TimeTrackerStartStop
@changed="onStartStopClick"
:active="!!(timeEntry.start && !timeEntry.end)"
class="opacity-20 hidden sm:flex focus-visible:opacity-100 group-hover:opacity-100"></TimeTrackerStartStop>
class="opacity-20 hidden sm:flex focus-visible:opacity-100 group-hover:opacity-100"
@changed="onStartStopClick"></TimeTrackerStartStop>
<TimeEntryMoreOptionsDropdown
@delete="
deleteTimeEntry

View File

@@ -62,13 +62,13 @@ function selectInput(event: Event) {
<template>
<input
v-model="currentTime"
data-testid="time_entry_duration_input"
class="text-white w-[90px] px-2 py-1.5 bg-transparent text-center hover:bg-card-background rounded-lg border border-transparent hover:border-card-border text-sm font-semibold focus-visible:bg-tertiary focus-visible:border-transparent focus-visible:ring-2 focus-visible:ring-ring"
@focus="selectInput"
@keydown.tab="open = false"
@blur="updateTimerAndStartLiveTimerUpdate"
@keydown.enter="updateTimerAndStartLiveTimerUpdate"
v-model="currentTime" />
@keydown.enter="updateTimerAndStartLiveTimerUpdate" />
</template>
<style scoped></style>

View File

@@ -23,11 +23,11 @@ const timeEntryTags = computed<Tag[]>(() => {
</script>
<template>
<TagDropdown
v-model="model"
:tags="tags"
align="bottom-end"
:createTag
@changed="emit('changed', model)"
v-model="model">
:create-tag
@changed="emit('changed', model)">
<template #trigger>
<button
data-testid="time_entry_tag_dropdown"

View File

@@ -97,55 +97,55 @@ function updateTimeEntryDescription() {
class="flex flex-col @2xl:flex-row w-full justify-between rounded-lg bg-card-background border-card-border border transition shadow-card">
<div class="flex flex-1 items-center pr-6">
<input
placeholder="What are you working on?"
data-testid="time_entry_description"
ref="currentTimeEntryDescriptionInput"
v-model="tempDescription"
@keydown.enter="startTimerIfNotActive"
@blur="updateTimeEntryDescription"
placeholder="What are you working on?"
data-testid="time_entry_description"
class="w-full rounded-l-lg py-4 sm:py-2.5 px-3.5 border-b border-b-card-background-separator @2xl:px-4 text-base @4xl:text-lg text-white font-medium bg-transparent border-none placeholder-muted focus:ring-0 transition"
type="text" />
type="text"
@keydown.enter="startTimerIfNotActive"
@blur="updateTimeEntryDescription" />
</div>
<div class="flex items-center justify-between pl-2 shrink min-w-0">
<div
class="flex items-center w-[130px] @2xl:w-auto shrink min-w-0">
<TimeTrackerProjectTaskDropdown
:createClient
:canCreateProject
:clients
:createProject
:currency="currency"
:projects="projects"
:tasks="tasks"
@changed="updateProject"
:enableEstimatedTime="enableEstimatedTime"
v-model:project="currentTimeEntry.project_id"
v-model:task="
currentTimeEntry.task_id
"></TimeTrackerProjectTaskDropdown>
"
:create-client
:can-create-project
:clients
:create-project
:currency="currency"
:projects="projects"
:tasks="tasks"
:enable-estimated-time="enableEstimatedTime"
@changed="updateProject"></TimeTrackerProjectTaskDropdown>
</div>
<div class="flex items-center @2xl:space-x-2 px-2 @2xl:px-4">
<TimeTrackerTagDropdown
@changed="$emit('updateTimeEntry')"
:createTag
:tags="tags"
v-model="
currentTimeEntry.tags
"></TimeTrackerTagDropdown>
"
:create-tag
:tags="tags"
@changed="$emit('updateTimeEntry')"></TimeTrackerTagDropdown>
<BillableToggleButton
@changed="$emit('updateTimeEntry')"
v-model="
currentTimeEntry.billable
"></BillableToggleButton>
"
@changed="$emit('updateTimeEntry')"></BillableToggleButton>
</div>
<div class="border-l border-card-border">
<TimeTrackerRangeSelector
@startLiveTimer="emit('startLiveTimer')"
@stopLiveTimer="emit('stopLiveTimer')"
@updateTimer="emit('updateTimeEntry')"
@startTimer="emit('startTimer')"
v-model:currentTimeEntry="currentTimeEntry"
v-model:liveTimer="liveTimer"
v-model:current-time-entry="currentTimeEntry"
v-model:live-timer="liveTimer"
@start-live-timer="emit('startLiveTimer')"
@stop-live-timer="emit('stopLiveTimer')"
@update-timer="emit('updateTimeEntry')"
@start-timer="emit('startTimer')"
@keydown.enter="
startTimerIfNotActive
"></TimeTrackerRangeSelector>
@@ -156,8 +156,8 @@ function updateTimeEntryDescription() {
class="pl-4 @2xl:pl-6 pr-3 absolute sm:relative top-[6px] sm:top-0 right-0">
<TimeTrackerStartStop
:active="isActive"
@changed="onToggleButtonPress"
size="large"></TimeTrackerStartStop>
size="large"
@changed="onToggleButtonPress"></TimeTrackerStartStop>
</div>
</div>
</template>

View File

@@ -53,8 +53,8 @@ type ClientsWithProjectsWithTasks = ClientWithProjectsWithTasks[];
const props = withDefaults(
defineProps<{
showBadgeBorder: boolean;
size: 'base' | 'large' | 'xlarge';
showBadgeBorder?: boolean;
size?: 'base' | 'large' | 'xlarge';
projects: Project[];
tasks: Task[];
clients: Client[];
@@ -63,8 +63,8 @@ const props = withDefaults(
) => Promise<Project | undefined>;
createClient: (client: CreateClientBody) => Promise<Client | undefined>;
currency: string;
emptyPlaceholder: string;
allowReset: boolean;
emptyPlaceholder?: string;
allowReset?: boolean;
enableEstimatedTime: boolean;
canCreateProject: boolean;
class?: string;
@@ -539,15 +539,15 @@ const showCreateProject = ref(false);
<template>
<div v-if="projects.length === 0 && canCreateProject">
<Badge
@click="showCreateProject = true"
size="large"
tag="button"
class="cursor-pointer hover:bg-tertiary">
class="cursor-pointer hover:bg-tertiary"
@click="showCreateProject = true">
<PlusIcon class="-ml-1 w-5"></PlusIcon>
<span>Add new project</span>
</Badge>
</div>
<Dropdown v-else v-model="open" :closeOnContentClick="false" align="bottom">
<Dropdown v-else v-model="open" :close-on-content-click="false" align="bottom">
<template #trigger>
<ProjectBadge
ref="projectDropdownTrigger"
@@ -570,18 +570,18 @@ const showCreateProject = ref(false);
v-if="currentTask"
class="w-4 lg:w-5 text-muted shrink-0"></ChevronRightIcon>
<div
class="min-w-0 shrink text-xs lg:text-sm truncate"
v-if="currentTask">
v-if="currentTask"
class="min-w-0 shrink text-xs lg:text-sm truncate">
{{ currentTask.name }}
</div>
</div>
<button
v-if="project !== null && allowReset"
class="absolute right-0 top-0 h-full flex items-center pr-3 text-text-quaternary hover:text-text-secondary"
@click.stop="
project = null;
task = null;
"
class="absolute right-0 top-0 h-full flex items-center pr-3 text-text-quaternary hover:text-text-secondary">
">
<XMarkIcon class="w-5"></XMarkIcon>
</button>
</ProjectBadge>
@@ -591,21 +591,21 @@ const showCreateProject = ref(false);
v-if="open"
:options="{ immediate: true, allowOutsideClick: true }">
<input
ref="searchInput"
:value="searchValue"
data-testid="client_dropdown_search"
class="bg-card-background border-0 placeholder-muted text-sm text-white py-2.5 focus:ring-0 border-b border-card-background-separator focus:border-card-background-separator w-full"
placeholder="Search for a project or task..."
@input="updateSearchValue"
@keydown.enter.prevent="addClientIfNoneExists"
data-testid="client_dropdown_search"
@keydown.up.prevent="moveHighlightUp"
@keydown.down.prevent="moveHighlightDown"
@keydown.right.prevent="expandProject"
@keydown.left.prevent="collapseProject"
ref="searchInput"
class="bg-card-background border-0 placeholder-muted text-sm text-white py-2.5 focus:ring-0 border-b border-card-background-separator focus:border-card-background-separator w-full"
placeholder="Search for a project or task..." />
@keydown.left.prevent="collapseProject" />
<div
ref="dropdownViewport"
@mousemove="mouseEnterHighlightActivated = true"
class="min-w-[350px] max-h-[350px] overflow-y-scroll relative">
class="min-w-[350px] max-h-[350px] overflow-y-scroll relative"
@mousemove="mouseEnterHighlightActivated = true">
<template
v-for="client in filteredResults"
:key="client.id">
@@ -623,8 +623,8 @@ const showCreateProject = ref(false);
role="option"
class="px-1 py-0.5 cursor-default"
:value="projectWithTasks.id"
@click="selectProject(projectWithTasks.id)"
:data-project-id="projectWithTasks.id">
:data-project-id="projectWithTasks.id"
@click="selectProject(projectWithTasks.id)">
<div
class="rounded-lg"
:class="{
@@ -637,34 +637,34 @@ const showCreateProject = ref(false);
:selected="
isProjectSelected(projectWithTasks)
"
:name="projectWithTasks.name"
:color="projectWithTasks.color"
@mouseenter="
setHighlightItemId(
projectWithTasks.id
)
"
:name="projectWithTasks.name"
:color="projectWithTasks.color">
">
<template #actions>
<button
tabindex="-1"
v-if="
projectWithTasks.tasks
.length > 0
"
@click.prevent.stop="
() => {
projectWithTasks.expanded =
!projectWithTasks.expanded;
searchInput?.focus();
}
"
tabindex="-1"
class="px-2 py-0.5 mr-2 relative transition items-center rounded flex space-x-0.5 text-xs"
:class="{
'bg-white/5 text-text-secondary':
projectWithTasks.expanded,
'hover:bg-white/5 hover:text-text-secondary text-text-tertiary':
!projectWithTasks.expanded,
}">
}"
@click.prevent.stop="
() => {
projectWithTasks.expanded =
!projectWithTasks.expanded;
searchInput?.focus();
}
">
<span
>{{
projectWithTasks.tasks
@@ -689,14 +689,14 @@ const showCreateProject = ref(false);
<div
v-for="task in projectWithTasks.tasks"
:key="task.id"
@click="selectTask(task.id)"
@mouseenter="setHighlightItemId(task.id)"
:data-task-id="task.id"
:class="{
'bg-card-background-active':
task.id === highlightedItemId,
}"
class="flex items-center space-x-2 w-full px-5 py-1.5 text-start text-xs font-semibold leading-5 text-white focus:outline-none focus:bg-card-background-active transition duration-150 ease-in-out">
class="flex items-center space-x-2 w-full px-5 py-1.5 text-start text-xs font-semibold leading-5 text-white focus:outline-none focus:bg-card-background-active transition duration-150 ease-in-out"
@click="selectTask(task.id)"
@mouseenter="setHighlightItemId(task.id)">
<MinusIcon
class="w-3 h-3 text-text-quaternary"></MinusIcon>
<span>{{ task.name }}</span>
@@ -709,11 +709,11 @@ const showCreateProject = ref(false);
v-if="canCreateProject"
class="hover:bg-card-background-active rounded-b-lg">
<button
class="text-white flex space-x-3 items-center px-4 py-3 text-xs font-semibold border-t border-card-background-separator"
@click="
open = false;
showCreateProject = true;
"
class="text-white flex space-x-3 items-center px-4 py-3 text-xs font-semibold border-t border-card-background-separator">
">
<PlusCircleIcon
class="w-5 flex-shrink-0 text-icon-default"></PlusCircleIcon>
<span>Create new Project</span>
@@ -723,12 +723,12 @@ const showCreateProject = ref(false);
</template>
</Dropdown>
<ProjectCreateModal
:createClient
:enableEstimatedTime="enableEstimatedTime"
v-model:show="showCreateProject"
:create-client
:enable-estimated-time="enableEstimatedTime"
:currency="currency"
:clients="clients"
:createProject
v-model:show="showCreateProject"></ProjectCreateModal>
:create-project></ProjectCreateModal>
</template>
<style scoped></style>

View File

@@ -170,31 +170,31 @@ function closeAndFocusInput() {
<div class="relative">
<Dropdown
v-model="open"
@submit="open = false"
align="bottom"
:close-on-content-click="false">
:close-on-content-click="false"
@submit="open = false">
<template #trigger>
<input
ref="inputField"
v-model="currentTime"
placeholder="00:00:00"
data-testid="time_entry_time"
class="w-[110px] lg:w-[130px] h-full text-white py-2.5 rounded-lg border-border-secondary border text-center px-4 text-base lg:text-lg font-bold bg-card-background border-none placeholder-muted focus:ring-0 transition"
type="text"
@focus="pauseLiveTimerUpdate"
@focusin="openModalOnTab"
@keydown.exact.tab="focusNextElement"
@keydown.exact.shift.tab="open = false"
ref="inputField"
data-testid="time_entry_time"
@blur="updateTimerAndStartLiveTimerUpdate"
@keydown.enter="onTimeEntryEnterPress"
v-model="currentTime"
class="w-[110px] lg:w-[130px] h-full text-white py-2.5 rounded-lg border-border-secondary border text-center px-4 text-base lg:text-lg font-bold bg-card-background border-none placeholder-muted focus:ring-0 transition"
type="text" />
@keydown.enter="onTimeEntryEnterPress" />
</template>
<template #content>
<div ref="timeRangeSelector">
<TimeRangeSelector
@changed="updateTimeRange"
@close="closeAndFocusInput"
:start="startTime"
:end="null">
:end="null"
@changed="updateTimeRange"
@close="closeAndFocusInput">
</TimeRangeSelector>
</div>
</template>

View File

@@ -9,7 +9,7 @@ const emit = defineEmits<{
changed: [];
}>();
const model = defineModel({
const model = defineModel<string[]>({
default: [],
});
const iconColorClasses = computed(() => {
@@ -27,10 +27,10 @@ defineProps<{
<template>
<TagDropdown
:createTag
@changed="emit('changed')"
v-model="model"
:tags="tags">
:create-tag
:tags="tags"
@changed="emit('changed')">
<template #trigger>
<button
data-testid="tag_dropdown"

View File

@@ -6,8 +6,8 @@ const emit = defineEmits(['changed']);
const props = withDefaults(
defineProps<{
size: 'base' | 'large' | 'small';
active: boolean;
size?: 'base' | 'large' | 'small';
active?: boolean;
}>(),
{
size: 'base',
@@ -40,7 +40,6 @@ function toggleState() {
<template>
<button
@click="toggleState"
data-testid="timer_button"
:class="
twMerge(
@@ -48,7 +47,8 @@ function toggleState() {
buttonColorClasses,
'flex items-center justify-center py-1 transition focus:outline-0 rounded-full text-white '
)
">
"
@click="toggleState">
<Transition name="fade" mode="out-in">
<svg
v-if="props.active"