mirror of
https://github.com/solidtime-io/solidtime.git
synced 2026-08-16 04:02:15 +01:00
add pagination to client and project table
This commit is contained in:
@@ -2,11 +2,12 @@
|
||||
import SecondaryButton from '@/packages/ui/src/Buttons/SecondaryButton.vue';
|
||||
import { UserCircleIcon } from '@heroicons/vue/24/solid';
|
||||
import { PlusIcon } from '@heroicons/vue/16/solid';
|
||||
import { type Component, computed, ref } from 'vue';
|
||||
import { type Component, computed, ref, watch } from 'vue';
|
||||
import { type Client } from '@/packages/api/src';
|
||||
import ClientTableRow from '@/Components/Common/Client/ClientTableRow.vue';
|
||||
import ClientCreateModal from '@/Components/Common/Client/ClientCreateModal.vue';
|
||||
import ClientTableHeading from '@/Components/Common/Client/ClientTableHeading.vue';
|
||||
import Pagination from '@/Components/Common/Pagination.vue';
|
||||
import { canCreateClients } from '@/utils/permissions';
|
||||
import { useProjectsQuery } from '@/utils/useProjectsQuery';
|
||||
import {
|
||||
@@ -100,6 +101,19 @@ const table = useVueTable({
|
||||
const sortedClients = computed(() => {
|
||||
return table.getRowModel().rows.map((row) => row.original);
|
||||
});
|
||||
|
||||
// Client-side pagination: the full list is in memory, only one page is mounted at a time.
|
||||
const PAGE_SIZE = 15;
|
||||
const currentPage = ref(1);
|
||||
|
||||
watch([() => props.sortColumn, () => props.sortDirection, () => props.clients], () => {
|
||||
currentPage.value = 1;
|
||||
});
|
||||
|
||||
const paginatedClients = computed(() => {
|
||||
const start = (currentPage.value - 1) * PAGE_SIZE;
|
||||
return sortedClients.value.slice(start, start + PAGE_SIZE);
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -126,10 +140,14 @@ const sortedClients = computed(() => {
|
||||
>Create your First Client
|
||||
</SecondaryButton>
|
||||
</div>
|
||||
<template v-for="client in sortedClients" :key="client.id">
|
||||
<template v-for="client in paginatedClients" :key="client.id">
|
||||
<ClientTableRow :client="client"></ClientTableRow>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<Pagination
|
||||
v-model:page="currentPage"
|
||||
:total="sortedClients.length"
|
||||
:items-per-page="PAGE_SIZE"></Pagination>
|
||||
</template>
|
||||
|
||||
104
resources/js/Components/Common/Pagination.vue
Normal file
104
resources/js/Components/Common/Pagination.vue
Normal file
@@ -0,0 +1,104 @@
|
||||
<script setup lang="ts">
|
||||
import {
|
||||
PaginationEllipsis,
|
||||
PaginationFirst,
|
||||
PaginationLast,
|
||||
PaginationList,
|
||||
PaginationListItem,
|
||||
PaginationNext,
|
||||
PaginationPrev,
|
||||
PaginationRoot,
|
||||
} from 'radix-vue';
|
||||
import {
|
||||
ChevronDoubleLeftIcon,
|
||||
ChevronDoubleRightIcon,
|
||||
ChevronLeftIcon,
|
||||
ChevronRightIcon,
|
||||
EllipsisHorizontalIcon,
|
||||
} from '@heroicons/vue/20/solid';
|
||||
import { buttonVariants } from '@/packages/ui/src';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { computed, watch } from 'vue';
|
||||
|
||||
const page = defineModel<number>('page', { default: 1 });
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
total: number;
|
||||
itemsPerPage?: number;
|
||||
siblingCount?: number;
|
||||
showEdges?: boolean;
|
||||
}>(),
|
||||
{
|
||||
itemsPerPage: 15,
|
||||
siblingCount: 1,
|
||||
showEdges: true,
|
||||
}
|
||||
);
|
||||
|
||||
const pageCount = computed(() => Math.max(1, Math.ceil(props.total / props.itemsPerPage)));
|
||||
|
||||
watch(page, (value) => {
|
||||
if (value > pageCount.value) {
|
||||
page.value = pageCount.value;
|
||||
}
|
||||
});
|
||||
|
||||
watch(pageCount, (value) => {
|
||||
if (page.value > value) {
|
||||
page.value = value;
|
||||
}
|
||||
});
|
||||
|
||||
// The shared buttonVariants ghost/outline hover is `bg-white/5`, which is invisible in light
|
||||
// mode. Override it with a theme-aware hover that shows in both light and dark mode.
|
||||
const hoverClass = 'hover:bg-black/5 dark:hover:bg-white/5';
|
||||
const navButtonClass = cn(buttonVariants({ variant: 'ghost', size: 'icon' }), hoverClass);
|
||||
|
||||
function pageButtonClass(isActive: boolean): string {
|
||||
return cn(
|
||||
buttonVariants({ variant: isActive ? 'outline' : 'ghost', size: 'icon' }),
|
||||
hoverClass
|
||||
);
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<PaginationRoot
|
||||
v-if="pageCount > 1"
|
||||
v-model:page="page"
|
||||
:total="props.total"
|
||||
:items-per-page="props.itemsPerPage"
|
||||
:sibling-count="props.siblingCount"
|
||||
:show-edges="props.showEdges"
|
||||
class="mx-auto flex w-full justify-center py-8">
|
||||
<PaginationList v-slot="{ items }" class="flex items-center gap-1">
|
||||
<PaginationFirst :class="navButtonClass">
|
||||
<ChevronDoubleLeftIcon class="size-4" />
|
||||
</PaginationFirst>
|
||||
<PaginationPrev :class="navButtonClass">
|
||||
<ChevronLeftIcon class="size-4" />
|
||||
</PaginationPrev>
|
||||
<template v-for="(item, index) in items" :key="index">
|
||||
<PaginationListItem
|
||||
v-if="item.type === 'page'"
|
||||
:value="item.value"
|
||||
:class="pageButtonClass(item.value === page)">
|
||||
{{ item.value }}
|
||||
</PaginationListItem>
|
||||
<PaginationEllipsis
|
||||
v-else
|
||||
:index="index"
|
||||
class="flex size-9 items-center justify-center text-text-tertiary">
|
||||
<EllipsisHorizontalIcon class="size-4" />
|
||||
</PaginationEllipsis>
|
||||
</template>
|
||||
<PaginationNext :class="navButtonClass">
|
||||
<ChevronRightIcon class="size-4" />
|
||||
</PaginationNext>
|
||||
<PaginationLast :class="navButtonClass">
|
||||
<ChevronDoubleRightIcon class="size-4" />
|
||||
</PaginationLast>
|
||||
</PaginationList>
|
||||
</PaginationRoot>
|
||||
</template>
|
||||
@@ -2,10 +2,11 @@
|
||||
import SecondaryButton from '@/packages/ui/src/Buttons/SecondaryButton.vue';
|
||||
import { FolderPlusIcon } from '@heroicons/vue/24/solid';
|
||||
import { PlusIcon } from '@heroicons/vue/16/solid';
|
||||
import { computed, ref } from 'vue';
|
||||
import { computed, ref, watch } from 'vue';
|
||||
import ProjectCreateModal from '@/packages/ui/src/Project/ProjectCreateModal.vue';
|
||||
import ProjectTableHeading from '@/Components/Common/Project/ProjectTableHeading.vue';
|
||||
import ProjectTableRow from '@/Components/Common/Project/ProjectTableRow.vue';
|
||||
import Pagination from '@/Components/Common/Pagination.vue';
|
||||
|
||||
export type SortColumn =
|
||||
| 'name'
|
||||
@@ -143,6 +144,19 @@ const sortedProjects = computed(() => {
|
||||
return table.getRowModel().rows.map((row) => row.original);
|
||||
});
|
||||
|
||||
// Client-side pagination: the full list is in memory, only one page is mounted at a time.
|
||||
const PAGE_SIZE = 15;
|
||||
const currentPage = ref(1);
|
||||
|
||||
watch([() => props.sortColumn, () => props.sortDirection, () => props.projects], () => {
|
||||
currentPage.value = 1;
|
||||
});
|
||||
|
||||
const paginatedProjects = computed(() => {
|
||||
const start = (currentPage.value - 1) * PAGE_SIZE;
|
||||
return sortedProjects.value.slice(start, start + PAGE_SIZE);
|
||||
});
|
||||
|
||||
const showCreateProjectModal = ref(false);
|
||||
|
||||
async function createProject(project: CreateProjectBody): Promise<Project | undefined> {
|
||||
@@ -199,7 +213,7 @@ const gridTemplate = computed(() => {
|
||||
>Create your First Project
|
||||
</SecondaryButton>
|
||||
</div>
|
||||
<template v-for="project in sortedProjects" :key="project.id">
|
||||
<template v-for="project in paginatedProjects" :key="project.id">
|
||||
<ProjectTableRow
|
||||
:show-billable-rate="props.showBillableRate"
|
||||
:project="project"></ProjectTableRow>
|
||||
@@ -207,4 +221,8 @@ const gridTemplate = computed(() => {
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<Pagination
|
||||
v-model:page="currentPage"
|
||||
:total="sortedProjects.length"
|
||||
:items-per-page="PAGE_SIZE"></Pagination>
|
||||
</template>
|
||||
|
||||
Reference in New Issue
Block a user