upgrade inertia v2; add prefetching; migrate queries to tanstack query

vue
This commit is contained in:
Gregor Vostrak
2026-01-09 03:15:32 +01:00
parent 5a05ee35e0
commit 47c2d8e6de
59 changed files with 712 additions and 392 deletions

View File

@@ -1,11 +1,9 @@
<script setup lang="ts">
import MultiselectDropdown from '@/packages/ui/src/Input/MultiselectDropdown.vue';
import { storeToRefs } from 'pinia';
import type { Client } from '@/packages/api/src';
import { useClientsStore } from '@/utils/useClients';
import { useClientsQuery } from '@/utils/useClientsQuery';
const clientsStore = useClientsStore();
const { clients } = storeToRefs(clientsStore);
const { clients } = useClientsQuery();
function getKeyFromItem(item: Client) {
return item.id;

View File

@@ -1,15 +1,13 @@
<script setup lang="ts">
import { computed, ref } from 'vue';
import { storeToRefs } from 'pinia';
import { useMembersStore } from '@/utils/useMembers';
import { useMembersQuery } from '@/utils/useMembersQuery';
import { UserIcon, ChevronDownIcon } from '@heroicons/vue/24/solid';
import { useFocus } from '@vueuse/core';
import type { ProjectMember } from '@/packages/api/src';
import { Badge, SelectDropdown } from '@/packages/ui/src';
import type { Member } from '@/packages/api/src';
const membersStore = useMembersStore();
const { members } = storeToRefs(membersStore);
const { members } = useMembersQuery();
const model = defineModel<string>({
default: '',

View File

@@ -2,7 +2,7 @@
import type { Member } from '@/packages/api/src';
import { api } from '@/packages/api/src';
import { useForm } from '@tanstack/vue-form';
import { useMutation } from '@tanstack/vue-query';
import { useMutation, useQueryClient } from '@tanstack/vue-query';
import Modal from '@/packages/ui/src/Modal.vue';
import DangerButton from '@/packages/ui/src/Buttons/DangerButton.vue';
import SecondaryButton from '@/packages/ui/src/Buttons/SecondaryButton.vue';
@@ -11,7 +11,6 @@ import { useNotificationsStore } from '@/utils/notification';
import { getCurrentOrganizationId } from '@/utils/useUser';
import InputLabel from '@/packages/ui/src/Input/InputLabel.vue';
import InputError from '@/packages/ui/src/Input/InputError.vue';
import { useMembersStore } from '@/utils/useMembers';
const props = defineProps<{
show: boolean;
@@ -23,6 +22,7 @@ const emit = defineEmits<{
}>();
const { handleApiRequestNotifications } = useNotificationsStore();
const queryClient = useQueryClient();
const deleteMutation = useMutation({
mutationFn: async () => {
@@ -43,7 +43,7 @@ const deleteMutation = useMutation({
},
onSuccess: () => {
close();
useMembersStore().fetchMembers();
queryClient.invalidateQueries({ queryKey: ['members'] });
},
});

View File

@@ -4,12 +4,12 @@ import DialogModal from '@/packages/ui/src/DialogModal.vue';
import { ref } from 'vue';
import { api, type Member } from '@/packages/api/src';
import PrimaryButton from '@/packages/ui/src/Buttons/PrimaryButton.vue';
import { useMutation } from '@tanstack/vue-query';
import { useMutation, useQueryClient } from '@tanstack/vue-query';
import { getCurrentOrganizationId } from '@/utils/useUser';
import { useNotificationsStore } from '@/utils/notification';
import { useMembersStore } from '@/utils/useMembers';
const { handleApiRequestNotifications } = useNotificationsStore();
const queryClient = useQueryClient();
const show = defineModel('show', { default: false });
const saving = ref(false);
@@ -41,7 +41,7 @@ async function submit() {
'There was an error deactivating the user.',
() => {
show.value = false;
useMembersStore().fetchMembers();
queryClient.invalidateQueries({ queryKey: ['members'] });
}
);
}

View File

@@ -1,11 +1,9 @@
<script setup lang="ts">
import MultiselectDropdown from '@/packages/ui/src/Input/MultiselectDropdown.vue';
import { useMembersStore } from '@/utils/useMembers';
import { storeToRefs } from 'pinia';
import { useMembersQuery } from '@/utils/useMembersQuery';
import type { Member } from '@/packages/api/src';
const membersStore = useMembersStore();
const { members } = storeToRefs(membersStore);
const { members } = useMembersQuery();
function getKeyFromItem(item: Member) {
return item.id;

View File

@@ -1,15 +1,9 @@
<script setup lang="ts">
import { onMounted } from 'vue';
import { storeToRefs } from 'pinia';
import MemberTableHeading from '@/Components/Common/Member/MemberTableHeading.vue';
import MemberTableRow from '@/Components/Common/Member/MemberTableRow.vue';
import { useMembersStore } from '@/utils/useMembers';
import { useMembersQuery } from '@/utils/useMembersQuery';
const { members } = storeToRefs(useMembersStore());
onMounted(async () => {
await useMembersStore().fetchMembers();
});
const { members } = useMembersQuery();
</script>
<template>

View File

@@ -15,14 +15,12 @@ import MemberMakePlaceholderModal from '@/Components/Common/Member/MemberMakePla
import MemberDeleteModal from '@/Components/Common/Member/MemberDeleteModal.vue';
import { capitalizeFirstLetter } from '../../../utils/format';
import { formatCents } from '../../../packages/ui/src/utils/money';
import { useMembersStore } from '@/utils/useMembers';
const props = defineProps<{
member: Member;
}>();
const organization = inject<ComputedRef<Organization>>('organization');
const memberStore = useMembersStore();
const showEditMemberModal = ref(false);
const showMergeMemberModal = ref(false);
@@ -31,7 +29,6 @@ const showDeleteMemberModal = ref(false);
function removeMember() {
showDeleteMemberModal.value = true;
memberStore.fetchMembers();
}
async function invitePlaceholder(id: string) {

View File

@@ -10,7 +10,7 @@ defineProps<{
<template>
<h3
class="text-text-primary font-semibold text-sm sm:text-base flex items-center space-x-2 sm:space-x-2.5">
<component :is="icon" class="w-5 sm:w-6 text-icon-default"></component>
<component :is="icon" class="w-5 text-icon-default"></component>
<span> {{ title }} </span>
</h3>
</template>

View File

@@ -6,11 +6,11 @@ import { computed, ref } from 'vue';
import type { CreateClientBody, CreateProjectBody, Project } from '@/packages/api/src';
import PrimaryButton from '@/packages/ui/src/Buttons/PrimaryButton.vue';
import { useProjectsStore } from '@/utils/useProjects';
import { useClientsStore } from '@/utils/useClients';
import { useFocus } from '@vueuse/core';
import ClientDropdown from '@/packages/ui/src/Client/ClientDropdown.vue';
import Badge from '@/packages/ui/src/Badge.vue';
import { useClientsStore } from '@/utils/useClients';
import { storeToRefs } from 'pinia';
import { useClientsQuery } from '@/utils/useClientsQuery';
import ProjectColorSelector from '@/packages/ui/src/Project/ProjectColorSelector.vue';
import { UserCircleIcon } from '@heroicons/vue/20/solid';
import EstimatedTimeSection from '@/packages/ui/src/EstimatedTimeSection.vue';
@@ -21,7 +21,7 @@ import ProjectEditBillableSection from '@/packages/ui/src/Project/ProjectEditBil
import { isAllowedToPerformPremiumAction } from '@/utils/billing';
const { updateProject } = useProjectsStore();
const { clients } = storeToRefs(useClientsStore());
const { clients } = useClientsQuery();
const show = defineModel('show', { default: false });
const saving = ref(false);
const showBillableRateModal = ref(false);

View File

@@ -13,7 +13,7 @@ import { canCreateProjects } from '@/utils/permissions';
import type { CreateProjectBody, Project, Client, CreateClientBody } from '@/packages/api/src';
import { useProjectsStore } from '@/utils/useProjects';
import { useClientsStore } from '@/utils/useClients';
import { storeToRefs } from 'pinia';
import { useClientsQuery } from '@/utils/useClientsQuery';
import { getOrganizationCurrencyString } from '@/utils/money';
import { isAllowedToPerformPremiumAction } from '@/utils/billing';
import {
@@ -34,7 +34,7 @@ const emit = defineEmits<{
sort: [column: SortColumn];
}>();
const { clients } = storeToRefs(useClientsStore());
const { clients } = useClientsQuery();
// Create a map of client names for sorting
const clientNameMap = computed(() => {

View File

@@ -3,9 +3,8 @@ import ProjectMoreOptionsDropdown from '@/Components/Common/Project/ProjectMoreO
import type { Project } from '@/packages/api/src';
import { computed, ref, inject, type ComputedRef } from 'vue';
import { CheckCircleIcon, ArchiveBoxIcon } from '@heroicons/vue/24/outline';
import { useClientsStore } from '@/utils/useClients';
import { storeToRefs } from 'pinia';
import { useTasksStore } from '@/utils/useTasks';
import { useClientsQuery } from '@/utils/useClientsQuery';
import { useTasksQuery } from '@/utils/useTasksQuery';
import { useProjectsStore } from '@/utils/useProjects';
import TableRow from '@/Components/TableRow.vue';
import ProjectEditModal from '@/Components/Common/Project/ProjectEditModal.vue';
@@ -17,8 +16,8 @@ import { formatHumanReadableDuration } from '../../../packages/ui/src/utils/time
import { isAllowedToPerformPremiumAction } from '@/utils/billing';
import type { Organization } from '@/packages/api/src';
const { clients } = storeToRefs(useClientsStore());
const { tasks } = storeToRefs(useTasksStore());
const { clients } = useClientsQuery();
const { tasks } = useTasksQuery();
const props = defineProps<{
project: Project;

View File

@@ -1,8 +1,7 @@
<script setup lang="ts">
import { TrashIcon, PencilSquareIcon } from '@heroicons/vue/20/solid';
import type { ProjectMember } from '@/packages/api/src';
import { useMembersStore } from '@/utils/useMembers';
import { storeToRefs } from 'pinia';
import { useMembersQuery } from '@/utils/useMembersQuery';
import { computed } from 'vue';
import {
DropdownMenu,
@@ -19,7 +18,7 @@ const props = defineProps<{
projectMember: ProjectMember;
}>();
const { members } = storeToRefs(useMembersStore());
const { members } = useMembersQuery();
const currentMember = computed(() => {
return members.value.find((member) => member.id === props.projectMember.user_id);

View File

@@ -1,9 +1,8 @@
<script setup lang="ts">
import type { ProjectMember } from '@/packages/api/src';
import { computed, ref, inject, type ComputedRef } from 'vue';
import { storeToRefs } from 'pinia';
import TableRow from '@/Components/TableRow.vue';
import { useMembersStore } from '@/utils/useMembers';
import { useMembersQuery } from '@/utils/useMembersQuery';
import { useProjectMembersStore } from '@/utils/useProjectMembers';
import ProjectMemberMoreOptionsDropdown from '@/Components/Common/ProjectMember/ProjectMemberMoreOptionsDropdown.vue';
import { formatCents } from '@/packages/ui/src/utils/money';
@@ -29,7 +28,7 @@ function editProjectMember() {
showEditModal.value = true;
}
const { members } = storeToRefs(useMembersStore());
const { members } = useMembersQuery();
const member = computed(() => {
return members.value.find((member) => member.id === props.projectMember.member_id);
});

View File

@@ -39,12 +39,13 @@ import {
type Organization,
} from '@/packages/api/src';
import { getCurrentMembershipId, getCurrentOrganizationId, getCurrentRole } from '@/utils/useUser';
import { useTagsQuery } from '@/utils/useTagsQuery';
import { useTagsStore } from '@/utils/useTags';
import { useSessionStorage, useStorage } from '@vueuse/core';
import { useNotificationsStore } from '@/utils/notification';
import type { ExportFormat } from '@/types/reporting';
import { getRandomColorWithSeed } from '@/packages/ui/src/utils/color';
import { useProjectsStore } from '@/utils/useProjects';
import { useProjectsQuery } from '@/utils/useProjectsQuery';
// TimeEntryRoundingType is now defined in ReportingRoundingControls component
type TimeEntryRoundingType = 'up' | 'down' | 'nearest';
@@ -154,7 +155,7 @@ onMounted(() => {
updateTableReporting();
});
const { tags } = storeToRefs(useTagsStore());
const { tags } = useTagsQuery();
async function createTag(tag: string) {
return await useTagsStore().createTag(tag);
@@ -199,8 +200,7 @@ async function downloadExport(format: ExportFormat) {
const { getNameForReportingRowEntry, emptyPlaceholder } = useReportingStore();
const projectsStore = useProjectsStore();
const { projects } = storeToRefs(projectsStore);
const { projects } = useProjectsQuery();
const showExportModal = ref(false);
const exportUrl = ref<string | null>(null);
@@ -375,7 +375,7 @@ const tableData = computed(() => {
</MainContainer>
<MainContainer>
<div class="sm:grid grid-cols-4 pt-6 items-start">
<div class="col-span-3 bg-card-background rounded-lg border border-card-border pt-3">
<div class="col-span-3 bg-secondary rounded-lg border border-card-border pt-3">
<div
class="text-sm flex text-text-primary items-center space-x-3 font-medium px-6 border-b border-card-background-separator pb-3">
<span>Group by</span>
@@ -391,7 +391,7 @@ const tableData = computed(() => {
</div>
<div class="grid items-center" style="grid-template-columns: 1fr 100px 150px">
<div
class="contents [&>*]:border-card-background-separator [&>*]:border-b [&>*]:bg-tertiary [&>*]:pb-1.5 [&>*]:pt-1 text-text-secondary text-sm">
class="contents [&>*]:border-card-background-separator [&>*]:border-b [&>*]:bg-secondary [&>*]:pb-1.5 [&>*]:pt-1 text-text-tertiary text-sm">
<div class="pl-6">Name</div>
<div class="text-right">Duration</div>
<div class="text-right pr-6">Cost</div>

View File

@@ -3,8 +3,7 @@ import SecondaryButton from '@/packages/ui/src/Buttons/SecondaryButton.vue';
import { FolderPlusIcon } from '@heroicons/vue/24/solid';
import { PlusIcon } from '@heroicons/vue/16/solid';
import { ref } from 'vue';
import { storeToRefs } from 'pinia';
import { useTagsStore } from '@/utils/useTags';
import { useTagsQuery } from '@/utils/useTagsQuery';
import TagTableRow from '@/Components/Common/Tag/TagTableRow.vue';
import TagCreateModal from '@/packages/ui/src/Tag/TagCreateModal.vue';
import TagTableHeading from '@/Components/Common/Tag/TagTableHeading.vue';
@@ -13,7 +12,7 @@ import type { Tag } from '@/packages/api/src';
defineProps<{
createTag: (name: string) => Promise<Tag | undefined>;
}>();
const { tags } = storeToRefs(useTagsStore());
const { tags } = useTagsQuery();
const showCreateTagModal = ref(false);
</script>

View File

@@ -1,11 +1,9 @@
<script setup lang="ts">
import MultiselectDropdown from '@/packages/ui/src/Input/MultiselectDropdown.vue';
import { storeToRefs } from 'pinia';
import type { Task } from '@/packages/api/src';
import { useTasksStore } from '@/utils/useTasks';
import { useTasksQuery } from '@/utils/useTasksQuery';
const tasksStore = useTasksStore();
const { tasks } = storeToRefs(tasksStore);
const { tasks } = useTasksQuery();
function getKeyFromItem(item: Task) {
return item.id;

View File

@@ -1,26 +1,26 @@
<script setup lang="ts">
import ProjectBadge from '@/packages/ui/src/Project/ProjectBadge.vue';
import TimeTrackerStartStop from '@/packages/ui/src/TimeTrackerStartStop.vue';
import { useProjectsStore } from '@/utils/useProjects';
import { storeToRefs } from 'pinia';
import { useProjectsQuery } from '@/utils/useProjectsQuery';
import { computed } from 'vue';
import { useCurrentTimeEntryStore } from '@/utils/useCurrentTimeEntry';
import { storeToRefs } from 'pinia';
import { getDayJsInstance } from '@/packages/ui/src/utils/time';
import type { TimeEntry } from '@/packages/api/src';
import { useTasksStore } from '@/utils/useTasks';
import { useTasksQuery } from '@/utils/useTasksQuery';
import { ChevronRightIcon } from '@heroicons/vue/16/solid';
const props = defineProps<{
timeEntry: TimeEntry;
}>();
const { projects } = storeToRefs(useProjectsStore());
const { projects } = useProjectsQuery();
const project = computed(() => {
return projects.value.find((project) => project.id === props.timeEntry.project_id);
});
const { tasks } = storeToRefs(useTasksStore());
const { tasks } = useTasksQuery();
const task = computed(() => {
return tasks.value.find((task) => task.id === props.timeEntry.task_id);

View File

@@ -26,6 +26,7 @@ defineProps<{
<Link
v-else
:href="href ?? ''"
prefetch
class="block px-4 py-2 text-sm leading-5 text-text-primary hover:bg-card-background-active focus:outline-none focus:bg-card-background-active transition duration-150 ease-in-out">
<slot />
</Link>

View File

@@ -15,7 +15,7 @@ const classes = computed(() => {
</script>
<template>
<Link :href="href ?? ''" :class="classes">
<Link :href="href ?? ''" :class="classes" prefetch>
<slot />
</Link>
</template>

View File

@@ -10,7 +10,7 @@ defineProps<{
</script>
<template>
<Link :href="href" class="block group">
<Link :href="href" class="block group" prefetch>
<div
:class="[
current

View File

@@ -21,7 +21,7 @@ const classes = computed(() => {
<slot />
</button>
<Link v-else :href="href ?? ''" :class="classes">
<Link v-else :href="href ?? ''" :class="classes" prefetch>
<slot />
</Link>
</div>

View File

@@ -12,9 +12,12 @@ import { useCurrentTimeEntryStore } from '@/utils/useCurrentTimeEntry';
import { storeToRefs } from 'pinia';
import { getCurrentOrganizationId } from '@/utils/useUser';
import { switchOrganization } from '@/utils/useOrganization';
import { useProjectsStore } from '@/utils/useProjects';
import { useTasksStore } from '@/utils/useTasks';
import { useProjectsQuery } from '@/utils/useProjectsQuery';
import { useTasksQuery } from '@/utils/useTasksQuery';
import { useTagsQuery } from '@/utils/useTagsQuery';
import { useClientsQuery } from '@/utils/useClientsQuery';
import { useTagsStore } from '@/utils/useTags';
import { useProjectsStore } from '@/utils/useProjects';
import TimeTrackerControls from '@/packages/ui/src/TimeTracker/TimeTrackerControls.vue';
import type {
CreateClientBody,
@@ -49,12 +52,9 @@ const currentTimeEntryStore = useCurrentTimeEntryStore();
const { currentTimeEntry, isActive, now } = storeToRefs(currentTimeEntryStore);
const { startLiveTimer, stopLiveTimer, setActiveState } = currentTimeEntryStore;
const projectStore = useProjectsStore();
const { projects } = storeToRefs(projectStore);
const taskStore = useTasksStore();
const { tasks } = storeToRefs(taskStore);
const clientStore = useClientsStore();
const { clients } = storeToRefs(clientStore);
const { projects } = useProjectsQuery();
const { tasks } = useTasksQuery();
const { clients } = useClientsQuery();
const emit = defineEmits<{
change: [];
@@ -157,7 +157,7 @@ async function discardCurrentTimeEntry() {
}
}
const { tags } = storeToRefs(useTagsStore());
const { tags } = useTagsQuery();
const { timeEntries } = storeToRefs(useTimeEntriesStore());
</script>