mirror of
https://github.com/solidtime-io/solidtime.git
synced 2026-08-15 19:52:15 +01:00
update dependencies, update eslint config, update optional ts props types
This commit is contained in:
@@ -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',
|
||||
|
||||
@@ -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 />
|
||||
|
||||
@@ -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 />
|
||||
|
||||
@@ -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 }}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@ import { computed } from 'vue';
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
expanded?: boolean;
|
||||
size: string;
|
||||
size?: string;
|
||||
}>(),
|
||||
{
|
||||
expanded: false,
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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" />
|
||||
|
||||
@@ -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: '',
|
||||
|
||||
@@ -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>{{
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user