move time entry mass updates to ui package and remove its dependencies

This commit is contained in:
Gregor Vostrak
2024-10-25 16:46:55 +02:00
parent fd77e1e901
commit eb4debe481
16 changed files with 172 additions and 119 deletions

View File

@@ -39,6 +39,7 @@ const props = defineProps<{
deleteTimeEntries: (timeEntries: TimeEntry[]) => void;
currency: string;
selectedTimeEntries: TimeEntry[];
enableEstimatedTime: boolean;
}>();
const emit = defineEmits<{
selected: [TimeEntry[]];
@@ -125,6 +126,7 @@ function onSelectChange(event: Event) {
:showBadgeBorder="false"
@changed="updateProjectAndTask"
:project="timeEntry.project_id"
:enableEstimatedTime
:currency="currency"
:task="
timeEntry.task_id
@@ -174,6 +176,7 @@ function onSelectChange(event: Event) {
class="w-full border-t border-default-background-separator bg-black/15">
<TimeEntryRow
:projects="projects"
:enableEstimatedTime
:tasks="tasks"
:selected="
!!selectedTimeEntries.find(

View File

@@ -37,6 +37,7 @@ const props = defineProps<{
createProject: (project: CreateProjectBody) => Promise<Project | undefined>;
createClient: (client: CreateClientBody) => Promise<Client | undefined>;
currency: string;
enableEstimatedTime: boolean;
}>();
const groupedTimeEntries = computed(() => {
@@ -160,6 +161,7 @@ function unselectAllTimeEntries(value: TimeEntriesGroupedByType[]) {
<template v-for="entry in value" :key="entry.id">
<TimeEntryAggregateRow
:createProject
:enableEstimatedTime
:selected-time-entries="selectedTimeEntries"
@selected="
(timeEntries) => {
@@ -194,6 +196,7 @@ function unselectAllTimeEntries(value: TimeEntriesGroupedByType[]) {
:time-entry="entry"></TimeEntryAggregateRow>
<TimeEntryRow
:createClient
:enableEstimatedTime
:createProject
:projects="projects"
:selected="

View File

@@ -0,0 +1,105 @@
<script setup lang="ts">
import MainContainer from '@/packages/ui/src/MainContainer.vue';
import { PencilSquareIcon, TrashIcon } from '@heroicons/vue/20/solid';
import TimeEntryMassUpdateModal from '@/packages/ui/src/TimeEntry/TimeEntryMassUpdateModal.vue';
import type {
Client,
CreateClientBody,
CreateProjectBody,
Project,
Tag,
Task,
TimeEntry,
UpdateMultipleTimeEntriesChangeset,
} from '@/packages/api/src';
import { ref } from 'vue';
import { twMerge } from 'tailwind-merge';
import { Checkbox, InputLabel } from '@/packages/ui/src';
const props = defineProps<{
selectedTimeEntries: TimeEntry[];
deleteSelected: () => void;
class?: string;
allSelected: boolean;
projects: Project[];
tasks: Task[];
tags: Tag[];
clients: Client[];
createTag: (name: string) => Promise<Tag | undefined>;
createProject: (project: CreateProjectBody) => Promise<Project | undefined>;
createClient: (client: CreateClientBody) => Promise<Client | undefined>;
updateTimeEntries: (
changeset: UpdateMultipleTimeEntriesChangeset
) => Promise<void>;
currency: string;
enableEstimatedTime: boolean;
}>();
const emit = defineEmits<{
submit: [];
selectAll: [];
unselectAll: [];
}>();
const showMassUpdateModal = ref(false);
</script>
<template>
<TimeEntryMassUpdateModal
:projects
:tasks
:tags
:clients
:createTag
:createProject
:createClient
:updateTimeEntries
:enableEstimatedTime
:currency
:time-entries="selectedTimeEntries"
@submit="emit('submit')"
v-model:show="showMassUpdateModal"></TimeEntryMassUpdateModal>
<MainContainer
:class="
twMerge(
props.class,
'text-sm py-1.5 font-medium border-b border-t border-border-secondary flex items-center space-x-3'
)
">
<Checkbox
:checked="allSelected"
id="selectAll"
@update:checked="
allSelected ? emit('unselectAll') : emit('selectAll')
">
</Checkbox>
<InputLabel
for="selectAll"
class="select-none text-text-secondary"
v-if="selectedTimeEntries.length > 0">
{{ selectedTimeEntries.length }} selected
</InputLabel>
<InputLabel
for="selectAll"
class="text-text-secondary select-none"
v-else
>Select All</InputLabel
>
<button
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-white/80 rounded h-full px-2"
@click="showMassUpdateModal = true"
v-if="selectedTimeEntries.length">
<PencilSquareIcon class="w-4"></PencilSquareIcon>
<span> Edit </span>
</button>
<button
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-white/80 rounded"
@click="deleteSelected"
v-if="selectedTimeEntries.length">
<TrashIcon class="w-3.5"></TrashIcon>
<span> Delete </span>
</button>
</MainContainer>
</template>
<style scoped></style>

View File

@@ -0,0 +1,246 @@
<script setup lang="ts">
import TextInput from '../Input/TextInput.vue';
import SecondaryButton from '../Buttons/SecondaryButton.vue';
import DialogModal from '@/packages/ui/src/DialogModal.vue';
import { computed, nextTick, ref, watch } from 'vue';
import PrimaryButton from '../Buttons/PrimaryButton.vue';
import TimeTrackerProjectTaskDropdown from '@/packages/ui/src/TimeTracker/TimeTrackerProjectTaskDropdown.vue';
import InputLabel from '../Input/InputLabel.vue';
import {
type CreateClientBody,
type CreateProjectBody,
type Project,
type Client,
type TimeEntry,
type UpdateMultipleTimeEntriesChangeset,
} from '@/packages/api/src';
import { Badge, Checkbox } from '@/packages/ui/src';
import SelectDropdown from '../Input/SelectDropdown.vue';
import TagDropdown from '@/packages/ui/src/Tag/TagDropdown.vue';
import type { Tag, Task } from '@/packages/api/src';
const show = defineModel('show', { default: false });
const saving = ref(false);
const props = defineProps<{
timeEntries: TimeEntry[];
projects: Project[];
tasks: Task[];
clients: Client[];
tags: Tag[];
createProject: (project: CreateProjectBody) => Promise<Project | undefined>;
createClient: (client: CreateClientBody) => Promise<Client | undefined>;
createTag: (name: string) => Promise<Tag | undefined>;
updateTimeEntries: (
changeset: UpdateMultipleTimeEntriesChangeset
) => Promise<void>;
currency: string;
enableEstimatedTime: boolean;
}>();
const emit = defineEmits<{
submit: [];
}>();
const descriptionInput = ref<HTMLInputElement | null>(null);
watch(show, (value) => {
if (value) {
nextTick(() => {
descriptionInput.value?.focus();
});
}
});
const description = ref<string>('');
const taskId = ref<string | null | undefined>(undefined);
const projectId = ref<string | null>(null);
const billable = ref<boolean | undefined>(undefined);
const selectedTags = ref<string[]>([]);
const timeEntryBillable = computed({
get: () => {
if (billable.value === undefined) {
return 'do-not-update';
}
return billable.value ? 'billable' : 'non-billable';
},
set: (value) => {
if (value === 'do-not-update') {
billable.value = undefined;
} else if (value === 'billable') {
billable.value = true;
} else {
billable.value = false;
}
},
});
async function submit() {
saving.value = true;
const timeEntryUpdatesBody = {} as UpdateMultipleTimeEntriesChangeset;
if (description.value && description.value !== '') {
timeEntryUpdatesBody.description = description.value;
}
if (projectId.value !== null) {
if (projectId.value === '') {
// "No Project" is selected
timeEntryUpdatesBody.project_id = null;
} else {
timeEntryUpdatesBody.project_id = projectId.value;
}
timeEntryUpdatesBody.task_id = null;
if (taskId.value !== undefined) {
timeEntryUpdatesBody.task_id = taskId.value;
}
}
if (billable.value !== undefined) {
timeEntryUpdatesBody.billable = billable.value;
}
if (selectedTags.value.length > 0) {
timeEntryUpdatesBody.tags = selectedTags.value;
}
if (removeAllTags.value) {
timeEntryUpdatesBody.tags = [];
}
try {
await props.updateTimeEntries({ ...timeEntryUpdatesBody });
show.value = false;
emit('submit');
description.value = '';
projectId.value = null;
taskId.value = undefined;
selectedTags.value = [];
billable.value = undefined;
saving.value = false;
removeAllTags.value = false;
} catch (e) {
saving.value = false;
}
}
const removeAllTags = ref(false);
watch(removeAllTags, () => {
if (removeAllTags.value) {
selectedTags.value = [];
}
});
</script>
<template>
<DialogModal closeable :show="show" @close="show = false">
<template #title>
<div class="flex space-x-2">
<span> Update {{ timeEntries.length }} time entries </span>
</div>
</template>
<template #content>
<div class="space-y-4">
<div class="space-y-2">
<InputLabel for="description" value="Description" />
<TextInput
id="description"
ref="descriptionInput"
v-model="description"
@keydown.enter="submit"
type="text"
class="mt-1 block w-full" />
</div>
<div class="space-y-2">
<InputLabel for="project" value="Project" />
<TimeTrackerProjectTaskDropdown
:clients
:createProject
:createClient
:currency="currency"
class="mt-1"
empty-placeholder="Select project..."
allow-reset
size="xlarge"
:enableEstimatedTime
:projects="projects"
:tasks="tasks"
v-model:project="projectId"
v-model:task="taskId"></TimeTrackerProjectTaskDropdown>
</div>
<div class="space-y-2">
<InputLabel for="project" value="Tag" />
<div class="flex space-x-5">
<TagDropdown
:createTag
v-model="selectedTags"
:tags="tags">
<template #trigger>
<Badge
:disabled="removeAllTags"
tag="button"
size="xlarge">
<span v-if="selectedTags.length > 0">
Set {{ selectedTags.length }} tags
</span>
<span v-else> Select Tags... </span>
</Badge>
</template>
</TagDropdown>
<div class="flex items-center space-x-2">
<Checkbox
v-model:checked="removeAllTags"
id="no_tags"></Checkbox>
<InputLabel for="no_tags" value="Remove all tags" />
</div>
</div>
</div>
<div class="space-y-2">
<InputLabel for="project" value="Billable" />
<div class="flex">
<SelectDropdown
v-model="timeEntryBillable"
:get-key-from-item="(item) => item.value"
:get-name-for-item="(item) => item.label"
:items="[
{
label: 'Keep current billable status',
value: 'do-not-update',
},
{
label: 'Billable',
value: 'billable',
},
{
label: 'Non Billable',
value: 'non-billable',
},
]">
<template v-slot:trigger>
<Badge tag="button" size="xlarge">
<span v-if="billable === undefined">
Set billable status
</span>
<span v-else-if="billable === true">
Billable
</span>
<span v-else> Non Billable </span></Badge
>
</template>
</SelectDropdown>
</div>
</div>
</div>
</template>
<template #footer>
<SecondaryButton @click="show = false"> Cancel</SecondaryButton>
<PrimaryButton
class="ms-3"
:class="{ 'opacity-25': saving }"
:disabled="saving"
@click="submit">
Update Time Entries
</PrimaryButton>
</template>
</DialogModal>
</template>
<style scoped></style>

View File

@@ -38,6 +38,7 @@ const props = defineProps<{
showMember?: boolean;
showDate?: boolean;
selected?: boolean;
enableEstimatedTime: boolean;
}>();
const emit = defineEmits<{ selected: []; unselected: [] }>();
@@ -117,6 +118,7 @@ function onSelectChange(event: Event) {
@changed="updateProjectAndTask"
:project="timeEntry.project_id"
:currency="currency"
:enableEstimatedTime
:task="
timeEntry.task_id
"></TimeTrackerProjectTaskDropdown>