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 { clients } = storeToRefs(useClientsStore());
const gridTemplate = computed(() => {
return `grid-template-columns: minmax(300px, 1fr) minmax(150px, auto) minmax(140px, auto) minmax(130px, auto) ${props.showBillableRate ? 'minmax(130px, auto)' : ''} minmax(120px, auto) 80px;`;
});
import { isAllowedToPerformPremiumAction } from '@/utils/billing';
</script>
<template>
@@ -47,6 +48,7 @@ const gridTemplate = computed(() => {
:createClient
:currency="getOrganizationCurrencyString()"
:clients="clients"
:enableEstimatedTime="isAllowedToPerformPremiumAction"
v-model:show="showCreateProjectModal"></ProjectCreateModal>
<div class="flow-root max-w-[100vw] overflow-x-auto">
<div class="inline-block min-w-full align-middle">

View File

@@ -39,6 +39,10 @@ const { createTimeEntry } = useTimeEntriesStore();
const show = defineModel('show', { default: false });
const saving = ref(false);
defineProps<{
enableEstimatedTime: boolean;
}>();
async function createProject(
project: CreateProjectBody
): Promise<Project | undefined> {
@@ -135,6 +139,7 @@ async function createTag(tag: string) {
size="xlarge"
:projects="projects"
:tasks="tasks"
:enableEstimatedTime="enableEstimatedTime"
v-model:project="timeEntry.project_id"
v-model:task="
timeEntry.task_id

View File

@@ -24,6 +24,7 @@ import type {
import TimeTrackerRunningInDifferentOrganizationOverlay from '@/packages/ui/src/TimeTracker/TimeTrackerRunningInDifferentOrganizationOverlay.vue';
import { useClientsStore } from '@/utils/useClients';
import { getOrganizationCurrencyString } from '@/utils/money';
import { isAllowedToPerformPremiumAction } from '@/utils/billing';
const page = usePage<{
auth: {
@@ -111,6 +112,7 @@ const { tags } = storeToRefs(useTagsStore());
<TimeTrackerControls
:createProject
:enableEstimatedTime="isAllowedToPerformPremiumAction()"
:createClient
:clients
:tags

View File

@@ -22,6 +22,7 @@ import type {
import { getOrganizationCurrencyString } from '@/utils/money';
import { getCurrentRole } from '@/utils/useUser';
import { useOrganizationStore } from '@/utils/useOrganization';
import { isAllowedToPerformPremiumAction } from '@/utils/billing';
onMounted(() => {
useProjectsStore().fetchProjects();
@@ -94,6 +95,7 @@ const showBillableRate = computed(() => {
</SecondaryButton>
<ProjectCreateModal
:createProject
:enableEstimatedTime="isAllowedToPerformPremiumAction"
:createClient
:currency="getOrganizationCurrencyString()"
:clients="clients"

View File

@@ -64,7 +64,8 @@ import {
import { useQuery, useQueryClient } from '@tanstack/vue-query';
import { getCurrentOrganizationId } from '@/utils/useUser';
import { useTimeEntriesStore } from '@/utils/useTimeEntries';
import TimeEntryMassActionRow from '@/Components/Common/TimeEntry/TimeEntryMassActionRow.vue';
import TimeEntryMassActionRow from '@/packages/ui/src/TimeEntry/TimeEntryMassActionRow.vue';
import { isAllowedToPerformPremiumAction } from '@/utils/billing';
const startDate = useSessionStorage<string>(
'reporting-start-date',
@@ -118,7 +119,8 @@ function getFilterAttributes() {
const currentTimeEntryStore = useCurrentTimeEntryStore();
const { currentTimeEntry } = storeToRefs(currentTimeEntryStore);
const { setActiveState, startLiveTimer } = currentTimeEntryStore;
const { createTimeEntry, updateTimeEntry } = useTimeEntriesStore();
const { createTimeEntry, updateTimeEntry, updateTimeEntries } =
useTimeEntriesStore();
const { tags } = storeToRefs(useTagsStore());
@@ -338,13 +340,27 @@ async function clearSelectionAndState() {
</div>
<TimeEntryMassActionRow
:selected-time-entries="selectedTimeEntries"
:enableEstimatedTime="isAllowedToPerformPremiumAction()"
@submit="clearSelectionAndState"
:delete-selected="deleteSelected"
@select-all="selectedTimeEntries = [...timeEntries]"
@unselect-all="selectedTimeEntries = []"
:all-selected="
selectedTimeEntries.length === timeEntries.length
"></TimeEntryMassActionRow>
:all-selected="selectedTimeEntries.length === timeEntries.length"
:projects="projects"
:tasks="tasks"
:tags="tags"
:currency="getOrganizationCurrencyString()"
:clients="clients"
:update-time-entries="
(args) =>
updateTimeEntries(
selectedTimeEntries.map((timeEntry) => timeEntry.id),
args
)
"
:create-project="createProject"
:create-client="createClient"
:createTag="createTag"></TimeEntryMassActionRow>
<div class="w-full relative">
<div v-for="entry in timeEntries" :key="entry.id">
<TimeEntryRow
@@ -357,6 +373,7 @@ async function clearSelectionAndState() {
"
:createClient
:createProject
:enableEstimatedTime="isAllowedToPerformPremiumAction()"
:projects="projects"
:tasks="tasks"
:tags="tags"

View File

@@ -26,14 +26,19 @@ import { useTagsStore } from '@/utils/useTags';
import { useClientsStore } from '@/utils/useClients';
import TimeEntryCreateModal from '@/Components/Common/TimeEntry/TimeEntryCreateModal.vue';
import { getOrganizationCurrencyString } from '@/utils/money';
import TimeEntryMassActionRow from '@/Components/Common/TimeEntry/TimeEntryMassActionRow.vue';
import TimeEntryMassActionRow from '@/packages/ui/src/TimeEntry/TimeEntryMassActionRow.vue';
import type { UpdateMultipleTimeEntriesChangeset } from '@/packages/api/src';
import { isAllowedToPerformPremiumAction } from '@/utils/billing';
const timeEntriesStore = useTimeEntriesStore();
const { timeEntries, allTimeEntriesLoaded } = storeToRefs(timeEntriesStore);
const { updateTimeEntry, fetchTimeEntries, createTimeEntry } =
useTimeEntriesStore();
async function updateTimeEntries(ids: string[], changes: Partial<TimeEntry>) {
async function updateTimeEntries(
ids: string[],
changes: UpdateMultipleTimeEntriesChangeset
) {
await useTimeEntriesStore().updateTimeEntries(ids, changes);
fetchTimeEntries();
}
@@ -114,6 +119,7 @@ function deleteSelected() {
<template>
<TimeEntryCreateModal
:enableEstimatedTime="isAllowedToPerformPremiumAction()"
v-model:show="showManualTimeEntryModal"></TimeEntryCreateModal>
<AppLayout title="Dashboard" data-testid="time_view">
<MainContainer
@@ -135,14 +141,31 @@ function deleteSelected() {
</MainContainer>
<TimeEntryMassActionRow
:selected-time-entries="selectedTimeEntries"
:enableEstimatedTime="isAllowedToPerformPremiumAction()"
@submit="clearSelectionAndState"
:all-selected="selectedTimeEntries.length === timeEntries.length"
@select-all="selectedTimeEntries = [...timeEntries]"
@unselect-all="selectedTimeEntries = []"
:delete-selected="deleteSelected"></TimeEntryMassActionRow>
:delete-selected="deleteSelected"
:projects="projects"
:tasks="tasks"
:tags="tags"
:currency="getOrganizationCurrencyString()"
:clients="clients"
:update-time-entries="
(args) =>
updateTimeEntries(
selectedTimeEntries.map((timeEntry) => timeEntry.id),
args
)
"
:create-project="createProject"
:create-client="createClient"
:createTag="createTag"></TimeEntryMassActionRow>
<TimeEntryGroupedTable
v-model:selected="selectedTimeEntries"
:createProject
:enableEstimatedTime="isAllowedToPerformPremiumAction()"
:clients
:createClient
:updateTimeEntry

View File

@@ -19,7 +19,6 @@ import EstimatedTimeSection from '@/packages/ui/src/EstimatedTimeSection.vue';
import InputLabel from '@/packages/ui/src/Input/InputLabel.vue';
import ProjectEditBillableSection from '@/packages/ui/src/Project/ProjectEditBillableSection.vue';
import type { Client } from '@/packages/api/src';
import { isAllowedToPerformPremiumAction } from '@/utils/billing';
const show = defineModel('show', { default: false });
const saving = ref(false);
@@ -29,6 +28,7 @@ const props = defineProps<{
createProject: (project: CreateProjectBody) => Promise<Project | undefined>;
createClient: (client: CreateClientBody) => Promise<Client | undefined>;
currency: string;
enableEstimatedTime: boolean;
}>();
const activeClients = computed(() => {
@@ -138,7 +138,7 @@ const currentClientName = computed(() => {
</div>
<div>
<EstimatedTimeSection
v-if="isAllowedToPerformPremiumAction()"
v-if="enableEstimatedTime"
@submit="submit()"
v-model="project.estimated_time"></EstimatedTimeSection>
</div>

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

@@ -1,8 +1,17 @@
<script setup lang="ts">
import MainContainer from '@/packages/ui/src/MainContainer.vue';
import { PencilSquareIcon, TrashIcon } from '@heroicons/vue/20/solid';
import TimeEntryMassUpdateModal from '@/Components/Common/TimeEntry/TimeEntryMassUpdateModal.vue';
import type { TimeEntry } from '@/packages/api/src';
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';
@@ -12,6 +21,18 @@ const props = defineProps<{
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<{
@@ -25,6 +46,16 @@ const showMassUpdateModal = ref(false);
<template>
<TimeEntryMassUpdateModal
:projects
:tasks
:tags
:clients
:createTag
:createProject
:createClient
:updateTimeEntries
:enableEstimatedTime
:currency
:time-entries="selectedTimeEntries"
@submit="emit('submit')"
v-model:show="showMassUpdateModal"></TimeEntryMassUpdateModal>

View File

@@ -1,66 +1,49 @@
<script setup lang="ts">
import TextInput from '@/packages/ui/src/Input/TextInput.vue';
import SecondaryButton from '@/packages/ui/src/Buttons/SecondaryButton.vue';
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 '@/packages/ui/src/Buttons/PrimaryButton.vue';
import PrimaryButton from '../Buttons/PrimaryButton.vue';
import TimeTrackerProjectTaskDropdown from '@/packages/ui/src/TimeTracker/TimeTrackerProjectTaskDropdown.vue';
import InputLabel from '@/packages/ui/src/Input/InputLabel.vue';
import { storeToRefs } from 'pinia';
import { useTasksStore } from '@/utils/useTasks';
import { useProjectsStore } from '@/utils/useProjects';
import { useTagsStore } from '@/utils/useTags';
import InputLabel from '../Input/InputLabel.vue';
import {
type CreateClientBody,
type CreateProjectBody,
type Project,
type Client,
api,
type TimeEntry,
type UpdateMultipleTimeEntriesChangeset,
} from '@/packages/api/src';
import { useClientsStore } from '@/utils/useClients';
import { getOrganizationCurrencyString } from '@/utils/money';
import { Badge, Checkbox } from '@/packages/ui/src';
import SelectDropdown from '../../../packages/ui/src/Input/SelectDropdown.vue';
import { getCurrentOrganizationId } from '@/utils/useUser';
import { useNotificationsStore } from '@/utils/notification';
import SelectDropdown from '../Input/SelectDropdown.vue';
import TagDropdown from '@/packages/ui/src/Tag/TagDropdown.vue';
const projectStore = useProjectsStore();
const { projects } = storeToRefs(projectStore);
const taskStore = useTasksStore();
const { tasks } = storeToRefs(taskStore);
const clientStore = useClientsStore();
const { clients } = storeToRefs(clientStore);
import type { Tag, Task } from '@/packages/api/src';
const show = defineModel('show', { default: false });
const saving = ref(false);
async function createProject(
project: CreateProjectBody
): Promise<Project | undefined> {
return await useProjectsStore().createProject(project);
}
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: [];
}>();
async function createClient(
body: CreateClientBody
): Promise<Client | undefined> {
return await useClientsStore().createClient(body);
}
const descriptionInput = ref<HTMLInputElement | null>(null);
const { handleApiRequestNotifications } = useNotificationsStore();
watch(show, (value) => {
if (value) {
nextTick(() => {
@@ -75,11 +58,6 @@ const projectId = ref<string | null>(null);
const billable = ref<boolean | undefined>(undefined);
const selectedTags = ref<string[]>([]);
const { tags } = storeToRefs(useTagsStore());
async function createTag(tag: string) {
return await useTagsStore().createTag(tag);
}
const timeEntryBillable = computed({
get: () => {
if (billable.value === undefined) {
@@ -99,71 +77,48 @@ const timeEntryBillable = computed({
});
async function submit() {
const organizationId = getCurrentOrganizationId();
saving.value = true;
if (organizationId) {
const timeEntryUpdatesBody = {} as UpdateMultipleTimeEntriesChangeset;
if (description.value && description.value !== '') {
timeEntryUpdatesBody.description = description.value;
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;
}
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;
}
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 = [];
}
if (billable.value !== undefined) {
timeEntryUpdatesBody.billable = billable.value;
}
if (selectedTags.value.length > 0) {
timeEntryUpdatesBody.tags = selectedTags.value;
}
if (removeAllTags.value) {
timeEntryUpdatesBody.tags = [];
}
try {
await handleApiRequestNotifications(
() =>
api.updateMultipleTimeEntries(
{
ids: props.timeEntries.map(
(timeEntry) => timeEntry.id
),
changes: {
...timeEntryUpdatesBody,
},
},
{
params: {
organization: organizationId,
},
}
),
'Time entries updated',
'Failed to update time entries',
() => {
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;
}
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);
@@ -200,11 +155,12 @@ watch(removeAllTags, () => {
:clients
:createProject
:createClient
:currency="getOrganizationCurrencyString()"
:currency="currency"
class="mt-1"
empty-placeholder="Select project..."
allow-reset
size="xlarge"
:enableEstimatedTime
:projects="projects"
:tasks="tasks"
v-model:project="projectId"

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>

View File

@@ -33,6 +33,7 @@ const props = defineProps<{
createClient: (client: CreateClientBody) => Promise<Client | undefined>;
isActive: boolean;
currency: string;
enableEstimatedTime: boolean;
}>();
const emit = defineEmits<{
@@ -92,7 +93,7 @@ function updateTimeEntryDescription() {
class="flex items-center relative @container"
data-testid="dashboard_timer">
<div
class="flex flex-col lg:flex-row w-full justify-between rounded-lg bg-card-background border-card-border border transition shadow-card">
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?"
@@ -101,12 +102,12 @@ function updateTimeEntryDescription() {
v-model="tempDescription"
@keydown.enter="startTimerIfNotActive"
@blur="updateTimeEntryDescription"
class="w-full rounded-l-lg py-4 sm:py-2.5 px-3.5 border-b border-b-card-background-separator lg:px-4 text-base @4xl:text-lg text-white font-medium bg-transparent border-none placeholder-muted focus:ring-0 transition"
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" />
</div>
<div class="flex items-center justify-between pl-2 shrink min-w-0">
<div
class="flex items-center w-[130px] sm:w-auto shrink min-w-0">
class="flex items-center w-[130px] @2xl:w-auto shrink min-w-0">
<TimeTrackerProjectTaskDropdown
:createClient
:clients
@@ -115,12 +116,13 @@ function updateTimeEntryDescription() {
:projects="projects"
:tasks="tasks"
@changed="updateProject"
:enableEstimatedTime="enableEstimatedTime"
v-model:project="currentTimeEntry.project_id"
v-model:task="
currentTimeEntry.task_id
"></TimeTrackerProjectTaskDropdown>
</div>
<div class="flex items-center lg:space-x-2 px-2 lg:px-4">
<div class="flex items-center @2xl:space-x-2 px-2 @2xl:px-4">
<TimeTrackerTagDropdown
@changed="$emit('updateTimeEntry')"
:createTag
@@ -149,7 +151,7 @@ function updateTimeEntryDescription() {
</div>
</div>
<div
class="pl-4 lg:pl-6 pr-3 absolute sm:relative top-[6px] sm:top-0 right-0">
class="pl-4 @2xl:pl-6 pr-3 absolute sm:relative top-[6px] sm:top-0 right-0">
<TimeTrackerStartStop
:active="isActive"
@changed="onToggleButtonPress"

View File

@@ -63,6 +63,7 @@ const props = withDefaults(
currency: string;
emptyPlaceholder: string;
allowReset: boolean;
enableEstimatedTime: boolean;
}>(),
{
showBadgeBorder: true,
@@ -704,6 +705,7 @@ const showCreateProject = ref(false);
</Dropdown>
<ProjectCreateModal
:createClient
:enableEstimatedTime="enableEstimatedTime"
:currency="currency"
:clients="clients"
:createProject

View File

@@ -25,6 +25,7 @@ import SelectDropdown from './Input/SelectDropdown.vue';
import Badge from './Badge.vue';
import Checkbox from './Input/Checkbox.vue';
import TimeEntryGroupedTable from './TimeEntry/TimeEntryGroupedTable.vue';
import TimeEntryMassActionRow from './TimeEntry/TimeEntryMassActionRow.vue';
export {
money,
@@ -46,4 +47,5 @@ export {
Badge,
Checkbox,
TimeEntryGroupedTable,
TimeEntryMassActionRow,
};

View File

@@ -13,6 +13,7 @@ import {
} from '@/packages/api/src';
import dayjs from 'dayjs';
import { useNotificationsStore } from '@/utils/notification';
import type { UpdateMultipleTimeEntriesChangeset } from '@/packages/api/src';
export const useTimeEntriesStore = defineStore('timeEntries', () => {
const timeEntries = ref<TimeEntry[]>(reactive([]));
@@ -83,7 +84,7 @@ export const useTimeEntriesStore = defineStore('timeEntries', () => {
async function updateTimeEntries(
ids: string[],
changes: Partial<TimeEntry>
changes: UpdateMultipleTimeEntriesChangeset
) {
const organizationId = getCurrentOrganizationId();
if (organizationId) {