add dynamic loading of paginated endpoints above page_limit

add request classes and fix collection typing for clients, tasks and tags
This commit is contained in:
Gregor Vostrak
2026-02-18 22:32:56 +01:00
parent eed638d0aa
commit 556bbedeca
23 changed files with 386 additions and 185 deletions

View File

@@ -6,6 +6,7 @@ namespace App\Http\Controllers\Api\V1;
use App\Exceptions\Api\InactiveUserCanNotBeUsedApiException;
use App\Exceptions\Api\UserIsAlreadyMemberOfProjectApiException;
use App\Http\Requests\V1\ProjectMember\ProjectMemberIndexRequest;
use App\Http\Requests\V1\ProjectMember\ProjectMemberStoreRequest;
use App\Http\Requests\V1\ProjectMember\ProjectMemberUpdateRequest;
use App\Http\Resources\V1\ProjectMember\ProjectMemberCollection;
@@ -41,7 +42,7 @@ class ProjectMemberController extends Controller
*
* @operationId getProjectMembers
*/
public function index(Organization $organization, Project $project): ProjectMemberCollection
public function index(Organization $organization, Project $project, ProjectMemberIndexRequest $request): ProjectMemberCollection
{
$this->checkPermission($organization, 'project-members:view', $project);

View File

@@ -5,6 +5,7 @@ declare(strict_types=1);
namespace App\Http\Controllers\Api\V1;
use App\Enums\Weekday;
use App\Http\Requests\V1\Report\ReportIndexRequest;
use App\Http\Requests\V1\Report\ReportStoreRequest;
use App\Http\Requests\V1\Report\ReportUpdateRequest;
use App\Http\Resources\V1\Report\DetailedReportResource;
@@ -40,7 +41,7 @@ class ReportController extends Controller
*
* @operationId getReports
*/
public function index(Organization $organization): ReportCollection
public function index(Organization $organization, ReportIndexRequest $request): ReportCollection
{
$this->checkPermission($organization, 'reports:view');

View File

@@ -5,6 +5,7 @@ declare(strict_types=1);
namespace App\Http\Controllers\Api\V1;
use App\Exceptions\Api\EntityStillInUseApiException;
use App\Http\Requests\V1\Tag\TagIndexRequest;
use App\Http\Requests\V1\Tag\TagStoreRequest;
use App\Http\Requests\V1\Tag\TagUpdateRequest;
use App\Http\Resources\V1\Tag\TagCollection;
@@ -34,7 +35,7 @@ class TagController extends Controller
*
* @throws AuthorizationException
*/
public function index(Organization $organization): TagCollection
public function index(Organization $organization, TagIndexRequest $request): TagCollection
{
$this->checkPermission($organization, 'tags:view');

View File

@@ -21,6 +21,11 @@ class InvitationIndexRequest extends BaseFormRequest
public function rules(): array
{
return [
'page' => [
'integer',
'min:1',
'max:2147483647',
],
];
}
}

View File

@@ -21,6 +21,11 @@ class MemberIndexRequest extends BaseFormRequest
public function rules(): array
{
return [
'page' => [
'integer',
'min:1',
'max:2147483647',
],
];
}
}

View File

@@ -0,0 +1,27 @@
<?php
declare(strict_types=1);
namespace App\Http\Requests\V1\ProjectMember;
use App\Http\Requests\V1\BaseFormRequest;
use Illuminate\Contracts\Validation\ValidationRule;
class ProjectMemberIndexRequest extends BaseFormRequest
{
/**
* Get the validation rules that apply to the request.
*
* @return array<string, array<string|ValidationRule>>
*/
public function rules(): array
{
return [
'page' => [
'integer',
'min:1',
'max:2147483647',
],
];
}
}

View File

@@ -0,0 +1,27 @@
<?php
declare(strict_types=1);
namespace App\Http\Requests\V1\Report;
use App\Http\Requests\V1\BaseFormRequest;
use Illuminate\Contracts\Validation\ValidationRule;
class ReportIndexRequest extends BaseFormRequest
{
/**
* Get the validation rules that apply to the request.
*
* @return array<string, array<string|ValidationRule>>
*/
public function rules(): array
{
return [
'page' => [
'integer',
'min:1',
'max:2147483647',
],
];
}
}

View File

@@ -0,0 +1,27 @@
<?php
declare(strict_types=1);
namespace App\Http\Requests\V1\Tag;
use App\Http\Requests\V1\BaseFormRequest;
use Illuminate\Contracts\Validation\ValidationRule;
class TagIndexRequest extends BaseFormRequest
{
/**
* Get the validation rules that apply to the request.
*
* @return array<string, array<string|ValidationRule>>
*/
public function rules(): array
{
return [
'page' => [
'integer',
'min:1',
'max:2147483647',
],
];
}
}

View File

@@ -26,6 +26,11 @@ class TaskIndexRequest extends BaseFormRequest
public function rules(): array
{
return [
'page' => [
'integer',
'min:1',
'max:2147483647',
],
'project_id' => [
ExistsEloquent::make(Project::class, null, function (Builder $builder): Builder {
/** @var Builder<Project> $builder */

View File

@@ -4,9 +4,10 @@ declare(strict_types=1);
namespace App\Http\Resources\V1\Client;
use App\Http\Resources\PaginatedResourceCollection;
use Illuminate\Http\Resources\Json\ResourceCollection;
class ClientCollection extends ResourceCollection
class ClientCollection extends ResourceCollection implements PaginatedResourceCollection
{
/**
* The resource that this resource collects.

View File

@@ -4,9 +4,10 @@ declare(strict_types=1);
namespace App\Http\Resources\V1\Tag;
use App\Http\Resources\PaginatedResourceCollection;
use Illuminate\Http\Resources\Json\ResourceCollection;
class TagCollection extends ResourceCollection
class TagCollection extends ResourceCollection implements PaginatedResourceCollection
{
/**
* The resource that this resource collects.

View File

@@ -2,29 +2,10 @@
import MainContainer from '@/packages/ui/src/MainContainer.vue';
import AppLayout from '@/Layouts/AppLayout.vue';
import PageTitle from '@/Components/Common/PageTitle.vue';
import {
ChartBarIcon,
ChevronLeftIcon,
ChevronDoubleLeftIcon,
ChevronRightIcon,
ChevronDoubleRightIcon,
CreditCardIcon,
UserGroupIcon,
} from '@heroicons/vue/20/solid';
import { computed, ref, watch } from 'vue';
import { ChartBarIcon, CreditCardIcon, UserGroupIcon } from '@heroicons/vue/20/solid';
import { computed } from 'vue';
import { api, type ReportIndexResponse } from '@/packages/api/src';
import {
PaginationEllipsis,
PaginationFirst,
PaginationLast,
PaginationList,
PaginationListItem,
PaginationNext,
PaginationPrev,
PaginationRoot,
} from 'radix-vue';
import { useQuery, useQueryClient } from '@tanstack/vue-query';
import { useQuery } from '@tanstack/vue-query';
import { getCurrentOrganizationId } from '@/utils/useUser';
import ReportingTabNavbar from '@/Components/Common/Reporting/ReportingTabNavbar.vue';
import ReportTable from '@/Components/Common/Report/ReportTable.vue';
@@ -32,37 +13,22 @@ import { isAllowedToPerformPremiumAction, isBillingActivated } from '@/utils/bil
import { canManageBilling, canUpdateOrganization } from '@/utils/permissions';
import PrimaryButton from '../packages/ui/src/Buttons/PrimaryButton.vue';
import { Link } from '@inertiajs/vue3';
import { fetchAllReports } from '@/utils/useReportsQuery';
const pageLimit = 15;
const currentPage = ref(1);
const { data: reportsResponse } = useQuery<ReportIndexResponse>({
queryKey: computed(() => ['reports', getCurrentOrganizationId(), currentPage.value]),
const { data: reportsData } = useQuery({
queryKey: computed(() => ['reports', getCurrentOrganizationId()]),
enabled: !!getCurrentOrganizationId(),
queryFn: () =>
api.getReports({
params: {
organization: getCurrentOrganizationId() || '',
},
}),
queryFn: async () => {
const organizationId = getCurrentOrganizationId();
if (!organizationId) throw new Error('No organization');
const data = await fetchAllReports(organizationId);
return { data };
},
staleTime: 1000 * 30,
});
const reports = computed(() => {
return reportsResponse.value?.data ?? [];
});
const totalPages = computed(() => {
return 1;
});
const queryClient = useQueryClient();
async function updateFilteredTimeEntries() {
await queryClient.invalidateQueries({
queryKey: ['reports'],
});
}
watch(currentPage, () => {
updateFilteredTimeEntries();
return reportsData.value?.data ?? [];
});
</script>
@@ -110,64 +76,5 @@ watch(currentPage, () => {
<ReportTable
v-if="reports.length > 0 || isAllowedToPerformPremiumAction()"
:reports="reports"></ReportTable>
<PaginationRoot
v-if="reports.length > 0 || isAllowedToPerformPremiumAction()"
v-model:page="currentPage"
:total="totalPages"
:items-per-page="pageLimit"
class="flex justify-center items-center py-8"
:sibling-count="1"
show-edges>
<PaginationList v-slot="{ items }" class="flex items-center space-x-1 relative">
<div class="pr-2 flex items-center space-x-1 border-r border-border-primary mr-1">
<PaginationFirst class="navigation-item">
<ChevronDoubleLeftIcon class="w-4"> </ChevronDoubleLeftIcon>
</PaginationFirst>
<PaginationPrev class="mr-4 navigation-item">
<ChevronLeftIcon class="w-4 text-text-tertiary hover:text-text-primary">
</ChevronLeftIcon>
</PaginationPrev>
</div>
<template v-for="(page, index) in items">
<PaginationListItem
v-if="page.type === 'page'"
:key="index"
class="pagination-item"
:value="page.value">
{{ page.value }}
</PaginationListItem>
<PaginationEllipsis
v-else
:key="page.type"
:index="index"
class="PaginationEllipsis">
<div class="px-2">&#8230;</div>
</PaginationEllipsis>
</template>
<div class="!ml-2 pl-2 flex items-center space-x-1 border-l border-border-primary">
<PaginationNext class="navigation-item">
<ChevronRightIcon
class="w-4 text-text-tertiary hover:text-text-primary"></ChevronRightIcon>
</PaginationNext>
<PaginationLast class="navigation-item">
<ChevronDoubleRightIcon
class="w-4 text-text-tertiary hover:text-text-primary"></ChevronDoubleRightIcon>
</PaginationLast>
</div>
</PaginationList>
</PaginationRoot>
</AppLayout>
</template>
<style lang="postcss">
.navigation-item {
@apply bg-quaternary h-8 w-8 flex items-center justify-center rounded border border-border-primary text-text-tertiary hover:text-text-primary transition cursor-pointer hover:border-border-secondary hover:bg-secondary focus-visible:text-text-primary focus-visible:outline-0 focus-visible:ring-2 focus-visible:ring-ring;
}
.pagination-item {
@apply bg-secondary h-8 w-8 flex items-center justify-center rounded border border-border-tertiary text-text-secondary hover:text-text-primary transition cursor-pointer hover:border-border-secondary hover:bg-secondary focus-visible:text-text-primary focus-visible:outline-0 focus-visible:ring-2 focus-visible:ring-ring;
}
.pagination-item[data-selected] {
@apply text-text-primary bg-accent-300/10 border border-accent-300/20 rounded-md font-medium hover:bg-accent-300/20 active:bg-accent-300/20 outline-0 focus-visible:ring-2 focus:ring-ring transition ease-in-out duration-150;
}
</style>

View File

@@ -33,7 +33,6 @@ const ClientResource = z
updated_at: z.string(),
})
.passthrough();
const ClientCollection = z.array(ClientResource);
const ClientStoreRequest = z.object({ name: z.string().min(1).max(255) }).passthrough();
const ClientUpdateRequest = z
.object({ name: z.string().min(1).max(255), is_archived: z.boolean().optional() })
@@ -598,7 +597,6 @@ const DetailedWithDataReportResource = z
const TagResource = z
.object({ id: z.string(), name: z.string(), created_at: z.string(), updated_at: z.string() })
.passthrough();
const TagCollection = z.array(TagResource);
const TagStoreRequest = z.object({ name: z.string().min(1).max(255) }).passthrough();
const TagUpdateRequest = z.object({ name: z.string().min(1).max(255) }).passthrough();
const TaskResource = z
@@ -711,7 +709,6 @@ export const schemas = {
ApiTokenStoreRequest,
ApiTokenWithAccessTokenResource,
ClientResource,
ClientCollection,
ClientStoreRequest,
ClientUpdateRequest,
ImportRequest,
@@ -755,7 +752,6 @@ export const schemas = {
ReportUpdateRequest,
DetailedWithDataReportResource,
TagResource,
TagCollection,
TagStoreRequest,
TagUpdateRequest,
TaskResource,
@@ -1201,7 +1197,39 @@ const endpoints = makeApi([
schema: z.enum(['true', 'false', 'all']).optional(),
},
],
response: z.object({ data: ClientCollection }).passthrough(),
response: z
.object({
data: z.array(ClientResource),
links: z
.object({
first: z.union([z.string(), z.null()]),
last: z.union([z.string(), z.null()]),
prev: z.union([z.string(), z.null()]),
next: z.union([z.string(), z.null()]),
})
.passthrough(),
meta: z
.object({
current_page: z.number().int(),
from: z.union([z.number(), z.null()]),
last_page: z.number().int(),
links: z.array(
z
.object({
url: z.union([z.string(), z.null()]),
label: z.string(),
active: z.boolean(),
})
.passthrough()
),
path: z.union([z.string(), z.null()]),
per_page: z.number().int(),
to: z.union([z.number(), z.null()]),
total: z.number().int(),
})
.passthrough(),
})
.passthrough(),
errors: [
{
status: 401,
@@ -1512,6 +1540,11 @@ const endpoints = makeApi([
type: 'Path',
schema: z.string(),
},
{
name: 'page',
type: 'Query',
schema: z.number().int().gte(1).lte(2147483647).optional(),
},
],
response: z
.object({
@@ -2137,6 +2170,11 @@ const endpoints = makeApi([
type: 'Path',
schema: z.string(),
},
{
name: 'page',
type: 'Query',
schema: z.number().int().gte(1).lte(2147483647).optional(),
},
],
response: z
.object({
@@ -2742,6 +2780,11 @@ const endpoints = makeApi([
type: 'Path',
schema: z.string(),
},
{
name: 'page',
type: 'Query',
schema: z.number().int().gte(1).lte(2147483647).optional(),
},
],
response: z
.object({
@@ -2792,6 +2835,13 @@ const endpoints = makeApi([
description: `Not found`,
schema: z.object({ message: z.string() }).passthrough(),
},
{
status: 422,
description: `Validation error`,
schema: z
.object({ message: z.string(), errors: z.record(z.array(z.string())) })
.passthrough(),
},
],
},
{
@@ -2860,6 +2910,11 @@ const endpoints = makeApi([
type: 'Path',
schema: z.string(),
},
{
name: 'page',
type: 'Query',
schema: z.number().int().gte(1).lte(2147483647).optional(),
},
],
response: z
.object({
@@ -2910,6 +2965,13 @@ const endpoints = makeApi([
description: `Not found`,
schema: z.object({ message: z.string() }).passthrough(),
},
{
status: 422,
description: `Validation error`,
schema: z
.object({ message: z.string(), errors: z.record(z.array(z.string())) })
.passthrough(),
},
],
},
{
@@ -3086,8 +3148,45 @@ const endpoints = makeApi([
type: 'Path',
schema: z.string(),
},
{
name: 'page',
type: 'Query',
schema: z.number().int().gte(1).lte(2147483647).optional(),
},
],
response: z.object({ data: TagCollection }).passthrough(),
response: z
.object({
data: z.array(TagResource),
links: z
.object({
first: z.union([z.string(), z.null()]),
last: z.union([z.string(), z.null()]),
prev: z.union([z.string(), z.null()]),
next: z.union([z.string(), z.null()]),
})
.passthrough(),
meta: z
.object({
current_page: z.number().int(),
from: z.union([z.number(), z.null()]),
last_page: z.number().int(),
links: z.array(
z
.object({
url: z.union([z.string(), z.null()]),
label: z.string(),
active: z.boolean(),
})
.passthrough()
),
path: z.union([z.string(), z.null()]),
per_page: z.number().int(),
to: z.union([z.number(), z.null()]),
total: z.number().int(),
})
.passthrough(),
})
.passthrough(),
errors: [
{
status: 401,
@@ -3104,6 +3203,13 @@ const endpoints = makeApi([
description: `Not found`,
schema: z.object({ message: z.string() }).passthrough(),
},
{
status: 422,
description: `Validation error`,
schema: z
.object({ message: z.string(), errors: z.record(z.array(z.string())) })
.passthrough(),
},
],
},
{
@@ -3251,6 +3357,11 @@ const endpoints = makeApi([
type: 'Path',
schema: z.string(),
},
{
name: 'page',
type: 'Query',
schema: z.number().int().gte(1).lte(2147483647).optional(),
},
{
name: 'project_id',
type: 'Query',
@@ -4230,6 +4341,11 @@ If the group parameters are all set to &#x60;null&#x60; or are all missing, the
type: 'Query',
schema: z.array(z.string().uuid()).min(1).optional(),
},
{
name: 'client_ids',
type: 'Query',
schema: z.array(z.string()).min(1).optional(),
},
{
name: 'project_ids',
type: 'Query',

View File

@@ -0,0 +1,22 @@
/**
* Fetches all pages from a paginated Laravel API endpoint.
* Uses `meta.last_page` to determine the total number of pages,
* so only a single request is made when all data fits on one page.
*/
export async function fetchAllPages<T>(
fetchPage: (page: number) => Promise<{
data: T[];
meta: { per_page: number; last_page: number };
}>
): Promise<T[]> {
const firstResponse = await fetchPage(1);
const allItems: T[] = [...firstResponse.data];
const { last_page } = firstResponse.meta;
for (let page = 2; page <= last_page; page++) {
const response = await fetchPage(page);
allItems.push(...response.data);
}
return allItems;
}

View File

@@ -8,6 +8,13 @@ import {
createCalendarQueryKey,
fetchAllCalendarEntries,
} from '@/utils/useTimeEntriesCalendarQuery';
import { fetchAllProjects } from '@/utils/useProjectsQuery';
import { fetchAllTasks } from '@/utils/useTasksQuery';
import { fetchAllTags } from '@/utils/useTagsQuery';
import { fetchAllClients } from '@/utils/useClientsQuery';
import { fetchAllMembers } from '@/utils/useMembersQuery';
import { fetchAllReports } from '@/utils/useReportsQuery';
import { fetchAllProjectMembers } from '@/utils/useProjectMembersQuery';
/**
* Route patterns mapped to their prefetch functions.
@@ -152,12 +159,8 @@ function prefetchProjects(queryClient: QueryClient) {
queryClient.prefetchQuery({
queryKey: ['projects', organizationId],
queryFn: () =>
api.getProjects({
params: { organization: organizationId },
queries: { archived: 'all' },
}),
staleTime: 30000, // Consider fresh for 30 seconds
queryFn: async () => ({ data: await fetchAllProjects(organizationId) }),
staleTime: 30000,
});
}
@@ -167,11 +170,7 @@ function prefetchTasks(queryClient: QueryClient) {
queryClient.prefetchQuery({
queryKey: ['tasks', organizationId],
queryFn: () =>
api.getTasks({
params: { organization: organizationId },
queries: { done: 'all' },
}),
queryFn: async () => ({ data: await fetchAllTasks(organizationId) }),
staleTime: 30000,
});
}
@@ -182,10 +181,7 @@ function prefetchTags(queryClient: QueryClient) {
queryClient.prefetchQuery({
queryKey: ['tags', organizationId],
queryFn: () =>
api.getTags({
params: { organization: organizationId },
}),
queryFn: async () => ({ data: await fetchAllTags(organizationId) }),
staleTime: 30000,
});
}
@@ -196,11 +192,7 @@ function prefetchClients(queryClient: QueryClient) {
queryClient.prefetchQuery({
queryKey: ['clients', organizationId],
queryFn: () =>
api.getClients({
params: { organization: organizationId },
queries: { archived: 'all' },
}),
queryFn: async () => ({ data: await fetchAllClients(organizationId) }),
staleTime: 30000,
});
}
@@ -211,10 +203,7 @@ function prefetchMembers(queryClient: QueryClient) {
queryClient.prefetchQuery({
queryKey: ['members', organizationId],
queryFn: () =>
api.getMembers({
params: { organization: organizationId },
}),
queryFn: async () => ({ data: await fetchAllMembers(organizationId) }),
staleTime: 30000,
});
}
@@ -225,10 +214,7 @@ function prefetchReports(queryClient: QueryClient) {
queryClient.prefetchQuery({
queryKey: ['reports', organizationId],
queryFn: () =>
api.getReports({
params: { organization: organizationId },
}),
queryFn: async () => ({ data: await fetchAllReports(organizationId) }),
staleTime: 30000,
});
}
@@ -277,10 +263,9 @@ function prefetchProjectMembers(queryClient: QueryClient, projectId: string) {
queryClient.prefetchQuery({
queryKey: ['projectMembers', organizationId, projectId],
queryFn: () =>
api.getProjectMembers({
params: { organization: organizationId, project: projectId },
}),
queryFn: async () => ({
data: await fetchAllProjectMembers(organizationId, projectId),
}),
staleTime: 30000,
});
}

View File

@@ -3,6 +3,16 @@ import { api } from '@/packages/api/src';
import { getCurrentOrganizationId } from '@/utils/useUser';
import type { Client } from '@/packages/api/src';
import { computed } from 'vue';
import { fetchAllPages } from '@/utils/fetchAllPages';
export async function fetchAllClients(organizationId: string): Promise<Client[]> {
return fetchAllPages((page) =>
api.getClients({
params: { organization: organizationId },
queries: { archived: 'all', page },
})
);
}
export function useClientsQuery() {
const queryClient = useQueryClient();
@@ -12,10 +22,8 @@ export function useClientsQuery() {
queryFn: async () => {
const organizationId = getCurrentOrganizationId();
if (!organizationId) throw new Error('No organization');
return api.getClients({
params: { organization: organizationId },
queries: { archived: 'all' },
});
const data = await fetchAllClients(organizationId);
return { data };
},
enabled: () => !!getCurrentOrganizationId(),
staleTime: 1000 * 30, // 30 seconds

View File

@@ -1,31 +1,35 @@
import { defineStore } from 'pinia';
import { api } from '@/packages/api/src';
import { computed, ref } from 'vue';
import type {
InvitationsIndexResponse,
CreateInvitationBody,
Invitation,
} from '@/packages/api/src';
import type { CreateInvitationBody, Invitation } from '@/packages/api/src';
import { getCurrentOrganizationId } from '@/utils/useUser';
import { useNotificationsStore } from '@/utils/notification';
import { fetchAllPages } from '@/utils/fetchAllPages';
export async function fetchAllInvitations(organizationId: string): Promise<Invitation[]> {
return fetchAllPages((page) =>
api.getInvitations({
params: { organization: organizationId },
queries: { page },
})
);
}
export const useInvitationsStore = defineStore('invitations', () => {
const invitationsResponse = ref<InvitationsIndexResponse | null>(null);
const invitationsData = ref<Invitation[]>([]);
const { handleApiRequestNotifications } = useNotificationsStore();
async function fetchInvitations() {
const organization = getCurrentOrganizationId();
if (organization) {
invitationsResponse.value = await handleApiRequestNotifications(
() =>
api.getInvitations({
params: {
organization: organization,
},
}),
const data = await handleApiRequestNotifications(
() => fetchAllInvitations(organization),
undefined,
'Failed to fetch invitations'
);
if (data) {
invitationsData.value = data;
}
}
}
@@ -47,7 +51,7 @@ export const useInvitationsStore = defineStore('invitations', () => {
}
const invitations = computed<Invitation[]>(() => {
return invitationsResponse.value?.data || [];
return invitationsData.value;
});
return { invitations, fetchInvitations, createInvitation };

View File

@@ -3,6 +3,16 @@ import { api } from '@/packages/api/src';
import { getCurrentOrganizationId } from '@/utils/useUser';
import type { Member } from '@/packages/api/src';
import { computed } from 'vue';
import { fetchAllPages } from '@/utils/fetchAllPages';
export async function fetchAllMembers(organizationId: string): Promise<Member[]> {
return fetchAllPages((page) =>
api.getMembers({
params: { organization: organizationId },
queries: { page },
})
);
}
export function useMembersQuery() {
const queryClient = useQueryClient();
@@ -12,9 +22,8 @@ export function useMembersQuery() {
queryFn: async () => {
const organizationId = getCurrentOrganizationId();
if (!organizationId) throw new Error('No organization');
return api.getMembers({
params: { organization: organizationId },
});
const data = await fetchAllMembers(organizationId);
return { data };
},
enabled: () => !!getCurrentOrganizationId(),
staleTime: 1000 * 30, // 30 seconds

View File

@@ -3,6 +3,19 @@ import { api } from '@/packages/api/src';
import { getCurrentOrganizationId } from '@/utils/useUser';
import type { ProjectMember } from '@/packages/api/src';
import { computed, type Ref } from 'vue';
import { fetchAllPages } from '@/utils/fetchAllPages';
export async function fetchAllProjectMembers(
organizationId: string,
projectId: string
): Promise<ProjectMember[]> {
return fetchAllPages((page) =>
api.getProjectMembers({
params: { organization: organizationId, project: projectId },
queries: { page },
})
);
}
export function useProjectMembersQuery(projectId: Ref<string | null> | string) {
const queryClient = useQueryClient();
@@ -21,9 +34,8 @@ export function useProjectMembersQuery(projectId: Ref<string | null> | string) {
const organizationId = getCurrentOrganizationId();
const pid = projectIdValue.value;
if (!organizationId || !pid) throw new Error('No organization or project');
return api.getProjectMembers({
params: { organization: organizationId, project: pid },
});
const data = await fetchAllProjectMembers(organizationId, pid);
return { data };
},
enabled: () => !!getCurrentOrganizationId() && !!projectIdValue.value,
staleTime: 1000 * 30, // 30 seconds

View File

@@ -3,6 +3,16 @@ import { api } from '@/packages/api/src';
import { getCurrentOrganizationId } from '@/utils/useUser';
import type { Project } from '@/packages/api/src';
import { computed } from 'vue';
import { fetchAllPages } from '@/utils/fetchAllPages';
export async function fetchAllProjects(organizationId: string): Promise<Project[]> {
return fetchAllPages((page) =>
api.getProjects({
params: { organization: organizationId },
queries: { archived: 'all', page },
})
);
}
export function useProjectsQuery() {
const queryClient = useQueryClient();
@@ -12,10 +22,8 @@ export function useProjectsQuery() {
queryFn: async () => {
const organizationId = getCurrentOrganizationId();
if (!organizationId) throw new Error('No organization');
return api.getProjects({
params: { organization: organizationId },
queries: { archived: 'all' },
});
const data = await fetchAllProjects(organizationId);
return { data };
},
enabled: () => !!getCurrentOrganizationId(),
staleTime: 1000 * 30, // 30 seconds

View File

@@ -0,0 +1,12 @@
import { api } from '@/packages/api/src';
import type { Report } from '@/packages/api/src';
import { fetchAllPages } from '@/utils/fetchAllPages';
export async function fetchAllReports(organizationId: string): Promise<Report[]> {
return fetchAllPages((page) =>
api.getReports({
params: { organization: organizationId },
queries: { page },
})
);
}

View File

@@ -3,6 +3,16 @@ import { api } from '@/packages/api/src';
import { getCurrentOrganizationId } from '@/utils/useUser';
import type { Tag } from '@/packages/api/src';
import { computed } from 'vue';
import { fetchAllPages } from '@/utils/fetchAllPages';
export async function fetchAllTags(organizationId: string): Promise<Tag[]> {
return fetchAllPages((page) =>
api.getTags({
params: { organization: organizationId },
queries: { page },
})
);
}
export function useTagsQuery() {
const queryClient = useQueryClient();
@@ -12,9 +22,8 @@ export function useTagsQuery() {
queryFn: async () => {
const organizationId = getCurrentOrganizationId();
if (!organizationId) throw new Error('No organization');
return api.getTags({
params: { organization: organizationId },
});
const data = await fetchAllTags(organizationId);
return { data };
},
enabled: () => !!getCurrentOrganizationId(),
staleTime: 1000 * 30, // 30 seconds

View File

@@ -3,6 +3,16 @@ import { api } from '@/packages/api/src';
import { getCurrentOrganizationId } from '@/utils/useUser';
import type { Task } from '@/packages/api/src';
import { computed } from 'vue';
import { fetchAllPages } from '@/utils/fetchAllPages';
export async function fetchAllTasks(organizationId: string): Promise<Task[]> {
return fetchAllPages((page) =>
api.getTasks({
params: { organization: organizationId },
queries: { done: 'all', page },
})
);
}
export function useTasksQuery() {
const queryClient = useQueryClient();
@@ -12,10 +22,8 @@ export function useTasksQuery() {
queryFn: async () => {
const organizationId = getCurrentOrganizationId();
if (!organizationId) throw new Error('No organization');
return api.getTasks({
params: { organization: organizationId },
queries: { done: 'all' },
});
const data = await fetchAllTasks(organizationId);
return { data };
},
enabled: () => !!getCurrentOrganizationId(),
staleTime: 1000 * 30, // 30 seconds