From 732ec8462cdf6406c93fccc28018fefcf7ccef7d Mon Sep 17 00:00:00 2001 From: The Doom Lab Date: Mon, 15 Dec 2025 20:26:15 -0600 Subject: [PATCH 1/7] allow for more than 100 rows fix pagination so when you have many notifications/tasks/etc you only grab a few at a time but also can mark them all --- src/comments/queries/getComments.ts | 9 +- src/core/components/Table.tsx | 41 +++++-- .../components/fields/MultiSelectCheckbox.tsx | 6 +- .../components/fields/MultiSelectContext.tsx | 30 ++++- .../components/fields/SelectAllCheckbox.tsx | 17 ++- src/milestones/queries/getMilestones.ts | 11 +- .../components/DeleteNotificationButton.tsx | 34 +++++- .../components/MultiReadToggleButton.tsx | 51 +++++--- .../mutations/deleteNotification.ts | 24 +++- .../updateNotificationReadStatusBulk.ts | 20 ++++ src/notifications/queries/getNotifications.ts | 16 ++- .../tables/columns/NotificationColumns.tsx | 2 +- .../ProjectNotificationTableColumns.tsx | 2 +- src/pages/notifications/index.tsx | 103 +++++++++++++++-- .../[projectId]/notifications/index.tsx | 109 ++++++++++++++++-- .../queries/getProjectMembers.ts | 11 +- src/projects/queries/getProjects.ts | 16 ++- src/roles/queries/getRoles.ts | 16 ++- src/tasks/queries/getTasks.ts | 12 +- 19 files changed, 455 insertions(+), 75 deletions(-) create mode 100644 src/notifications/mutations/updateNotificationReadStatusBulk.ts diff --git a/src/comments/queries/getComments.ts b/src/comments/queries/getComments.ts index 723af6ca..e014cc05 100644 --- a/src/comments/queries/getComments.ts +++ b/src/comments/queries/getComments.ts @@ -8,17 +8,12 @@ interface GetCommentsInput export default resolver.pipe( resolver.authorize(), - async ({ - where, - orderBy, - skip = 0, - take = 100, - }: GetCommentsInput): Promise => { + async ({ where, orderBy, skip = 0, take }: GetCommentsInput): Promise => { const comments = await db.comment.findMany({ where, orderBy: orderBy || { createdAt: "asc" }, // Default ordering by creation date skip, - take, + ...(typeof take === "number" ? { take } : {}), include: { author: { include: { diff --git a/src/core/components/Table.tsx b/src/core/components/Table.tsx index 51359e3e..6ee0d6f5 100644 --- a/src/core/components/Table.tsx +++ b/src/core/components/Table.tsx @@ -9,6 +9,8 @@ import { useReactTable, getPaginationRowModel, getFacetedMinMaxValues, + PaginationState, + OnChangeFn, } from "@tanstack/react-table" import React from "react" @@ -131,6 +133,11 @@ type TableProps = { enableGlobalSearch?: boolean globalSearchPlaceholder?: string addPagination?: boolean + manualPagination?: boolean + paginationState?: PaginationState + onPaginationChange?: OnChangeFn + pageCount?: number + pageSizeOptions?: number[] classNames?: { table?: string thead?: string @@ -179,9 +186,26 @@ const Table = ({ enableGlobalSearch = true, globalSearchPlaceholder = "Search...", addPagination = false, + manualPagination = false, + paginationState, + onPaginationChange, + pageCount: controlledPageCount, + pageSizeOptions = [5, 10, 20, 30, 40, 50], }: TableProps) => { const [sorting, setSorting] = React.useState([]) const [globalFilter, setGlobalFilter] = React.useState("") + const [internalPagination, setInternalPagination] = React.useState({ + pageIndex: 0, + pageSize: 5, + }) + + const resolvedPaginationState = manualPagination + ? paginationState ?? { pageIndex: 0, pageSize: 5 } + : internalPagination + + const handlePaginationChange: OnChangeFn = manualPagination + ? onPaginationChange ?? (() => {}) + : setInternalPagination const table = useReactTable({ data, @@ -192,19 +216,18 @@ const Table = ({ getSortedRowModel: getSortedRowModel(), getFilteredRowModel: getFilteredRowModel(), getFacetedUniqueValues: getFacetedUniqueValues(), - getPaginationRowModel: getPaginationRowModel(), + ...(manualPagination ? {} : { getPaginationRowModel: getPaginationRowModel() }), getFacetedMinMaxValues: getFacetedMinMaxValues(), + manualPagination, + pageCount: manualPagination ? controlledPageCount : undefined, state: { sorting: sorting, globalFilter: globalFilter, - }, - initialState: { - pagination: { - pageSize: 5, - }, + pagination: resolvedPaginationState, }, onSortingChange: setSorting, onGlobalFilterChange: setGlobalFilter, + onPaginationChange: handlePaginationChange, globalFilterFn: defaultGlobalFilterFn, autoResetPageIndex: false, }) @@ -220,10 +243,10 @@ const Table = ({ return } - if (pageCount > 0 && pageIndex >= pageCount) { + if (!manualPagination && pageCount > 0 && pageIndex >= pageCount) { table.setPageIndex(0) } - }, [addPagination, pageCount, pageIndex, table]) + }, [addPagination, pageCount, pageIndex, table, manualPagination]) return ( <> @@ -386,7 +409,7 @@ const Table = ({ classNames?.pageSizeSelect || "" }`} > - {[5, 10, 20, 30, 40, 50].map((pageSize) => ( + {pageSizeOptions.map((pageSize) => ( diff --git a/src/core/components/fields/MultiSelectCheckbox.tsx b/src/core/components/fields/MultiSelectCheckbox.tsx index 764af3e8..5a9a4216 100644 --- a/src/core/components/fields/MultiSelectCheckbox.tsx +++ b/src/core/components/fields/MultiSelectCheckbox.tsx @@ -6,7 +6,8 @@ type MultiSelectCheckboxProps = { } export const MultiSelectCheckbox = React.memo(({ id }: MultiSelectCheckboxProps) => { - const { selectedIds, toggleSelection } = useMultiSelect() + const { selectedIds, toggleSelection, isGlobalSelection } = useMultiSelect() + const isChecked = isGlobalSelection || selectedIds.includes(id) return (
@@ -15,7 +16,8 @@ export const MultiSelectCheckbox = React.memo(({ id }: MultiSelectCheckboxProps) toggleSelection(id)} /> diff --git a/src/core/components/fields/MultiSelectContext.tsx b/src/core/components/fields/MultiSelectContext.tsx index 0584167f..1145e3c6 100644 --- a/src/core/components/fields/MultiSelectContext.tsx +++ b/src/core/components/fields/MultiSelectContext.tsx @@ -6,6 +6,9 @@ interface MultiSelectContextType { toggleSelection: (selectedId: number) => void resetSelection: () => void handleBulkSelection: (selectedIds: number[], isSelectAll: boolean) => void + isGlobalSelection: boolean + enableGlobalSelection: () => void + disableGlobalSelection: () => void } // Create the context @@ -14,9 +17,11 @@ const MultiSelectContext = createContext(und // Context provider component export const MultiSelectProvider = ({ children }: { children?: ReactNode }) => { const [selectedIds, setSelectedIds] = useState([]) + const [isGlobalSelection, setIsGlobalSelection] = useState(false) // Toggle individual selection const toggleSelection = (selectedId: number) => { + setIsGlobalSelection(false) setSelectedIds((prev) => prev.includes(selectedId) ? prev.filter((item) => item !== selectedId) : [...prev, selectedId] ) @@ -24,16 +29,37 @@ export const MultiSelectProvider = ({ children }: { children?: ReactNode }) => { // Handle bulk selection (select/deselect all) const handleBulkSelection = (selectedIds: number[], isSelectAll: boolean) => { + setIsGlobalSelection(false) setSelectedIds((prev) => (isSelectAll ? [...new Set([...prev, ...selectedIds])] : [])) } // Add resetSelection to clear all selected IDs - const resetSelection = () => setSelectedIds([]) + const resetSelection = () => { + setIsGlobalSelection(false) + setSelectedIds([]) + } + + const enableGlobalSelection = () => { + setIsGlobalSelection(true) + setSelectedIds([]) + } + + const disableGlobalSelection = () => { + setIsGlobalSelection(false) + } // Provide the selectedIds and the handler to children components return ( {children} diff --git a/src/core/components/fields/SelectAllCheckbox.tsx b/src/core/components/fields/SelectAllCheckbox.tsx index ae7bf464..43b568cd 100644 --- a/src/core/components/fields/SelectAllCheckbox.tsx +++ b/src/core/components/fields/SelectAllCheckbox.tsx @@ -1,10 +1,12 @@ import { useMultiSelect } from "./MultiSelectContext" export const SelectAllCheckbox = ({ allIds }: { allIds: number[] }) => { - const { selectedIds, handleBulkSelection } = useMultiSelect() + const { selectedIds, handleBulkSelection, isGlobalSelection, disableGlobalSelection } = + useMultiSelect() - const isAllSelected = allIds.length > 0 && allIds.every((id) => selectedIds.includes(id)) - const isIndeterminate = selectedIds.length > 0 && !isAllSelected + const isPageSelected = allIds.length > 0 && allIds.every((id) => selectedIds.includes(id)) + const isAllSelected = isGlobalSelection || isPageSelected + const isIndeterminate = !isGlobalSelection && selectedIds.length > 0 && !isPageSelected return ( ) diff --git a/src/milestones/queries/getMilestones.ts b/src/milestones/queries/getMilestones.ts index 1ffc68bd..7d87bf79 100644 --- a/src/milestones/queries/getMilestones.ts +++ b/src/milestones/queries/getMilestones.ts @@ -7,7 +7,16 @@ interface GetMilestonesInput export default resolver.pipe( resolver.authorize(), - async ({ where, orderBy, include, skip = 0, take = 100 }: GetMilestonesInput) => { + async ({ where, orderBy, include, skip = 0, take }: GetMilestonesInput) => { + if (typeof take !== "number") { + const [milestones, count] = await Promise.all([ + db.milestone.findMany({ where, orderBy, include, skip }), + db.milestone.count({ where }), + ]) + + return { milestones, nextPage: null, hasMore: false, count } + } + // TODO: in multi-tenant app, you must add validation to ensure correct tenant const { items: milestones, diff --git a/src/notifications/components/DeleteNotificationButton.tsx b/src/notifications/components/DeleteNotificationButton.tsx index afc896ac..5bdacc68 100644 --- a/src/notifications/components/DeleteNotificationButton.tsx +++ b/src/notifications/components/DeleteNotificationButton.tsx @@ -1,23 +1,42 @@ import { useMutation } from "@blitzjs/rpc" import deleteNotification from "../mutations/deleteNotification" import toast from "react-hot-toast" +import { Prisma } from "db" interface DeleteNotificationButtonProps { ids: number[] + where: Prisma.NotificationWhereInput + selectionMode: "ids" | "all" + totalSelectedCount: number + onCompleted?: () => void } -export const DeleteNotificationButton = ({ ids }: DeleteNotificationButtonProps) => { +export const DeleteNotificationButton = ({ + ids, + where, + selectionMode, + totalSelectedCount, + onCompleted, +}: DeleteNotificationButtonProps) => { const [deleteNotificationMutation] = useMutation(deleteNotification) const handleDelete = async () => { + const countLabel = + selectionMode === "all" ? totalSelectedCount : Math.max(ids.length, totalSelectedCount) + if ( window.confirm( - `The selected ${ids.length} notification(s) will be permanently deleted. Are you sure you want to continue?` + `The selected ${countLabel} notification(s) will be permanently deleted. Are you sure you want to continue?` ) ) { try { - await deleteNotificationMutation({ ids }) // Send array of IDs + if (selectionMode === "all") { + await deleteNotificationMutation({ selectAll: true, where }) + } else { + await deleteNotificationMutation({ ids }) + } toast.success("Notifications deleted successfully!") + onCompleted?.() } catch (error) { console.error("Error deleting notifications:", error) toast.error("Failed to delete notifications.") @@ -25,9 +44,14 @@ export const DeleteNotificationButton = ({ ids }: DeleteNotificationButtonProps) } } + const isDisabled = + selectionMode === "all" + ? totalSelectedCount === 0 + : ids.length === 0 || totalSelectedCount === 0 + return ( - ) } diff --git a/src/notifications/components/MultiReadToggleButton.tsx b/src/notifications/components/MultiReadToggleButton.tsx index c1b09167..bc90d436 100644 --- a/src/notifications/components/MultiReadToggleButton.tsx +++ b/src/notifications/components/MultiReadToggleButton.tsx @@ -2,35 +2,49 @@ import { useMutation } from "@blitzjs/rpc" import updateNotifications from "../mutations/updateNotifications" import { useNotificationMenuData } from "../hooks/useNotificationMenuData" import toast from "react-hot-toast" +import { Prisma } from "db" +import updateNotificationReadStatusBulk from "../mutations/updateNotificationReadStatusBulk" interface MultiReadToggleButtonProps { notifications: { id: number; read: boolean }[] refetch: () => void resetSelection: () => void + selectionMode: "ids" | "all" + totalSelectedCount: number + where: Prisma.NotificationWhereInput } export const MultiReadToggleButton = ({ notifications, refetch, resetSelection, + selectionMode, + totalSelectedCount, + where, }: MultiReadToggleButtonProps) => { const [updateNotificationMutation] = useMutation(updateNotifications) + const [updateAllNotificationMutation] = useMutation(updateNotificationReadStatusBulk) const { updateNotificationMenuData } = useNotificationMenuData() - const allRead = notifications.every((n) => n.read) - const allUnread = notifications.every((n) => !n.read) - const mixedStatus = !allRead && !allUnread + const canInferStatus = selectionMode === "ids" + const allRead = canInferStatus && notifications.every((n) => n.read) + const allUnread = canInferStatus && notifications.every((n) => !n.read) + const mixedStatus = canInferStatus && !allRead && !allUnread const handleToggle = async (markAsRead: boolean) => { - if (notifications.length === 0) return // Prevent unnecessary calls when no notifications are selected + if (totalSelectedCount === 0) return try { - await Promise.all( - notifications.map((n) => updateNotificationMutation({ id: n.id, read: markAsRead })) - ) + if (selectionMode === "all") { + await updateAllNotificationMutation({ where, read: markAsRead }) + } else { + await Promise.all( + notifications.map((n) => updateNotificationMutation({ id: n.id, read: markAsRead })) + ) + } toast.success( - `${notifications.length} notification${notifications.length > 1 ? "s" : ""} marked as ${ + `${totalSelectedCount} notification${totalSelectedCount > 1 ? "s" : ""} marked as ${ markAsRead ? "read" : "unread" }.` ) @@ -44,23 +58,34 @@ export const MultiReadToggleButton = ({ } } - const noSelection = notifications.length === 0 + const noSelection = totalSelectedCount === 0 + + const showMarkAsReadButton = selectionMode === "all" || allUnread || mixedStatus + const showMarkAsUnreadButton = selectionMode === "all" || allRead || mixedStatus return (
- {(allUnread || mixedStatus) && ( + {showMarkAsReadButton && ( )} - {(allRead || mixedStatus) && ( + {showMarkAsUnreadButton && ( )}
diff --git a/src/notifications/mutations/deleteNotification.ts b/src/notifications/mutations/deleteNotification.ts index d58b594c..cd0aa09e 100644 --- a/src/notifications/mutations/deleteNotification.ts +++ b/src/notifications/mutations/deleteNotification.ts @@ -1,18 +1,32 @@ import { resolver } from "@blitzjs/rpc" -import db from "db" +import db, { Prisma } from "db" import { z } from "zod" -const DeleteNotificationSchema = z.object({ - ids: z.array(z.number()), // Expecting an array of IDs +const deleteByIdsSchema = z.object({ + ids: z.array(z.number()).min(1), }) +const deleteAllSchema = z.object({ + selectAll: z.literal(true), + where: z.custom((value) => typeof value === "object"), +}) + +const DeleteNotificationSchema = z.union([deleteByIdsSchema, deleteAllSchema]) + export default resolver.pipe( resolver.zod(DeleteNotificationSchema), resolver.authorize(), - async ({ ids }) => { + async (input) => { + if ("selectAll" in input) { + await db.notification.deleteMany({ + where: input.where, + }) + return { success: true } + } + await db.notification.deleteMany({ where: { - id: { in: ids }, + id: { in: input.ids }, }, }) return { success: true } diff --git a/src/notifications/mutations/updateNotificationReadStatusBulk.ts b/src/notifications/mutations/updateNotificationReadStatusBulk.ts new file mode 100644 index 00000000..ad52c8ca --- /dev/null +++ b/src/notifications/mutations/updateNotificationReadStatusBulk.ts @@ -0,0 +1,20 @@ +import { resolver } from "@blitzjs/rpc" +import db, { Prisma } from "db" +import { z } from "zod" + +const updateAllSchema = z.object({ + where: z.custom((value) => typeof value === "object"), + read: z.boolean(), +}) + +export default resolver.pipe( + resolver.zod(updateAllSchema), + resolver.authorize(), + async ({ where, read }) => { + await db.notification.updateMany({ + where, + data: { read }, + }) + return { success: true } + } +) diff --git a/src/notifications/queries/getNotifications.ts b/src/notifications/queries/getNotifications.ts index 442dd68e..ebf963a0 100644 --- a/src/notifications/queries/getNotifications.ts +++ b/src/notifications/queries/getNotifications.ts @@ -10,7 +10,21 @@ interface GetNotificationsInput export default resolver.pipe( resolver.authorize(), - async ({ where, orderBy, include, skip = 0, take = 100 }: GetNotificationsInput) => { + async ({ where, orderBy, include, skip = 0, take }: GetNotificationsInput) => { + if (typeof take !== "number") { + const [notifications, count] = await Promise.all([ + db.notification.findMany({ where, orderBy, include, skip }), + db.notification.count({ where }), + ]) + + return { + notifications, + nextPage: null, + hasMore: false, + count, + } + } + const { items: notifications, hasMore, diff --git a/src/notifications/tables/columns/NotificationColumns.tsx b/src/notifications/tables/columns/NotificationColumns.tsx index 1ddd7a66..71713465 100644 --- a/src/notifications/tables/columns/NotificationColumns.tsx +++ b/src/notifications/tables/columns/NotificationColumns.tsx @@ -94,7 +94,7 @@ export const useNotificationTableColumns = (refetch: () => void, data: Notificat
diff --git a/src/notifications/tables/columns/ProjectNotificationTableColumns.tsx b/src/notifications/tables/columns/ProjectNotificationTableColumns.tsx index 0796b1e9..ee9ecb4f 100644 --- a/src/notifications/tables/columns/ProjectNotificationTableColumns.tsx +++ b/src/notifications/tables/columns/ProjectNotificationTableColumns.tsx @@ -99,7 +99,7 @@ export const useProjectNotificationTableColumns = ( diff --git a/src/pages/notifications/index.tsx b/src/pages/notifications/index.tsx index 6a2b7c54..0f57a612 100644 --- a/src/pages/notifications/index.tsx +++ b/src/pages/notifications/index.tsx @@ -1,4 +1,4 @@ -import { Suspense, useMemo } from "react" +import { Suspense, useMemo, useState } from "react" import Layout from "src/core/layouts/Layout" import Table from "src/core/components/Table" import { usePaginatedQuery } from "@blitzjs/rpc" @@ -15,14 +15,25 @@ import { MultiReadToggleButton } from "src/notifications/components/MultiReadTog import { InformationCircleIcon } from "@heroicons/react/24/outline" import { Tooltip } from "react-tooltip" import Card from "src/core/components/Card" +import { PaginationState } from "@tanstack/react-table" +import { Prisma } from "db" const NotificationContent = () => { const currentUser = useCurrentUser() - const { selectedIds, resetSelection } = useMultiSelect() + const { + selectedIds, + resetSelection, + isGlobalSelection, + enableGlobalSelection, + disableGlobalSelection, + } = useMultiSelect() + const [pagination, setPagination] = useState({ + pageIndex: 0, + pageSize: 10, + }) - // Get notifications - const [{ notifications }, { refetch }] = usePaginatedQuery(getNotifications, { - where: { + const baseWhere = useMemo( + () => ({ recipients: { some: { id: currentUser!.id, @@ -40,9 +51,24 @@ const NotificationContent = () => { }, }, }, - }, + }), + [currentUser?.id] + ) + + const paginationArgs = useMemo( + () => ({ + skip: pagination.pageIndex * pagination.pageSize, + take: pagination.pageSize, + }), + [pagination] + ) + + // Get notifications + const [{ notifications, count }, { refetch }] = usePaginatedQuery(getNotifications, { + where: baseWhere, orderBy: { createdAt: "desc" }, include: { project: true }, + ...paginationArgs, }) const extendedNotifications = notifications as unknown as ExtendedNotification[] @@ -57,6 +83,30 @@ const NotificationContent = () => { const columns = useNotificationTableColumns(refetch, notificationTableData) const selectedNotifications = extendedNotifications.filter((n) => selectedIds.includes(n.id)) + const allPageIds = useMemo( + () => notificationTableData.map((item) => item.id), + [notificationTableData] + ) + + const isPageFullySelected = + !isGlobalSelection && + allPageIds.length > 0 && + allPageIds.every((id) => selectedIds.includes(id)) + const totalSelectedCount = isGlobalSelection ? count : selectedIds.length + const selectionMode: "ids" | "all" = isGlobalSelection ? "all" : "ids" + const pageCount = Math.max(1, Math.ceil(count / pagination.pageSize)) + + const handlePaginationChange = ( + updater: PaginationState | ((state: PaginationState) => PaginationState) + ) => { + setPagination((prev) => (typeof updater === "function" ? updater(prev) : updater)) + disableGlobalSelection() + } + + const handleActionCompleted = async () => { + await refetch() + resetSelection() + } return (
@@ -74,15 +124,52 @@ const NotificationContent = () => {
- +
- + {isPageFullySelected && count > allPageIds.length && ( +
+ + All {allPageIds.length} notifications on this page are selected. You can select every + notification in your inbox. + + +
+ )} + {isGlobalSelection && count > 0 && ( +
+ All {count} notifications are selected. + +
+ )} +
) diff --git a/src/pages/projects/[projectId]/notifications/index.tsx b/src/pages/projects/[projectId]/notifications/index.tsx index 5a1b1dd3..1a55a997 100644 --- a/src/pages/projects/[projectId]/notifications/index.tsx +++ b/src/pages/projects/[projectId]/notifications/index.tsx @@ -1,8 +1,8 @@ -import { Suspense, useMemo } from "react" +import { Suspense, useMemo, useState } from "react" import Layout from "src/core/layouts/Layout" import Table from "src/core/components/Table" import { useParam } from "@blitzjs/next" -import { useQuery } from "@blitzjs/rpc" +import { usePaginatedQuery } from "@blitzjs/rpc" import getNotifications from "src/notifications/queries/getNotifications" import { useCurrentUser } from "src/users/hooks/useCurrentUser" import { useProjectNotificationTableColumns } from "src/notifications/tables/columns/ProjectNotificationTableColumns" @@ -14,24 +14,50 @@ import { MultiReadToggleButton } from "src/notifications/components/MultiReadTog import Card from "src/core/components/Card" import { InformationCircleIcon } from "@heroicons/react/24/outline" import { Tooltip } from "react-tooltip" +import { PaginationState } from "@tanstack/react-table" +import { Prisma } from "db" const NotificationContent = () => { const projectId = useParam("projectId", "number") const currentUser = useCurrentUser() - const { selectedIds, resetSelection } = useMultiSelect() + const { + selectedIds, + resetSelection, + isGlobalSelection, + enableGlobalSelection, + disableGlobalSelection, + } = useMultiSelect() + const [pagination, setPagination] = useState({ + pageIndex: 0, + pageSize: 10, + }) - // Get notifications - const [{ notifications }, { refetch }] = useQuery(getNotifications, { - where: { + const baseWhere = useMemo( + () => ({ recipients: { some: { - id: currentUser?.id, + id: currentUser!.id, }, }, - projectId: projectId, - }, + projectId: projectId ?? undefined, + }), + [currentUser?.id, projectId] + ) + + const paginationArgs = useMemo( + () => ({ + skip: pagination.pageIndex * pagination.pageSize, + take: pagination.pageSize, + }), + [pagination] + ) + + // Get notifications + const [{ notifications, count }, { refetch }] = usePaginatedQuery(getNotifications, { + where: baseWhere, orderBy: { createdAt: "desc" }, include: { project: true }, + ...paginationArgs, }) const extendedNotifications = notifications as unknown as ExtendedNotification[] @@ -47,6 +73,30 @@ const NotificationContent = () => { const columns = useProjectNotificationTableColumns(refetch, projectNotificationTableData) const selectedNotifications = extendedNotifications.filter((n) => selectedIds.includes(n.id)) + const allPageIds = useMemo( + () => projectNotificationTableData.map((item) => item.id), + [projectNotificationTableData] + ) + + const isPageFullySelected = + !isGlobalSelection && + allPageIds.length > 0 && + allPageIds.every((id) => selectedIds.includes(id)) + const totalSelectedCount = isGlobalSelection ? count : selectedIds.length + const selectionMode: "ids" | "all" = isGlobalSelection ? "all" : "ids" + const pageCount = Math.max(1, Math.ceil(count / pagination.pageSize)) + + const handlePaginationChange = ( + updater: PaginationState | ((state: PaginationState) => PaginationState) + ) => { + setPagination((prev) => (typeof updater === "function" ? updater(prev) : updater)) + disableGlobalSelection() + } + + const handleActionCompleted = async () => { + await refetch() + resetSelection() + } return (
@@ -63,15 +113,52 @@ const NotificationContent = () => { />
- +
-
+ {isPageFullySelected && count > allPageIds.length && ( +
+ + All {allPageIds.length} notifications on this page are selected. You can select every + notification for this project. + + +
+ )} + {isGlobalSelection && count > 0 && ( +
+ All {count} project notifications are selected. + +
+ )} +
) diff --git a/src/projectmembers/queries/getProjectMembers.ts b/src/projectmembers/queries/getProjectMembers.ts index fb9b4888..24ea1d1b 100644 --- a/src/projectmembers/queries/getProjectMembers.ts +++ b/src/projectmembers/queries/getProjectMembers.ts @@ -10,7 +10,16 @@ interface GetProjectMembersInput export default resolver.pipe( resolver.authorize(), - async ({ where, orderBy, skip = 0, take = 100, include }: GetProjectMembersInput) => { + async ({ where, orderBy, skip = 0, take, include }: GetProjectMembersInput) => { + if (typeof take !== "number") { + const [projectMembers, count] = await Promise.all([ + db.projectMember.findMany({ where, orderBy, include, skip }), + db.projectMember.count({ where }), + ]) + + return { projectMembers, nextPage: null, hasMore: false, count } + } + // TODO: in multi-tenant app, you must add validation to ensure correct tenant const { items: projectMembers, diff --git a/src/projects/queries/getProjects.ts b/src/projects/queries/getProjects.ts index 7ef1cd17..79d9e040 100644 --- a/src/projects/queries/getProjects.ts +++ b/src/projects/queries/getProjects.ts @@ -7,7 +7,21 @@ interface GetProjectsInput export default resolver.pipe( resolver.authorize(), - async ({ where, orderBy, skip = 0, take = 100, include }: GetProjectsInput) => { + async ({ where, orderBy, skip = 0, take, include }: GetProjectsInput) => { + if (typeof take !== "number") { + const [projects, count] = await Promise.all([ + db.project.findMany({ + where, + orderBy, + skip, + ...(include ? { include } : {}), + }), + db.project.count({ where }), + ]) + + return { projects, nextPage: null, hasMore: false, count } + } + const { items: projects, hasMore, diff --git a/src/roles/queries/getRoles.ts b/src/roles/queries/getRoles.ts index 3005f108..abb8f5e9 100644 --- a/src/roles/queries/getRoles.ts +++ b/src/roles/queries/getRoles.ts @@ -7,7 +7,21 @@ interface GetRolesInput export default resolver.pipe( resolver.authorize(), - async ({ where, orderBy, skip = 0, take = 100, include }: GetRolesInput) => { + async ({ where, orderBy, skip = 0, take, include }: GetRolesInput) => { + if (typeof take !== "number") { + const [roles, count] = await Promise.all([ + db.role.findMany({ + where, + orderBy, + include, + skip, + }), + db.role.count({ where }), + ]) + + return { roles, nextPage: null, hasMore: false, count } + } + // TODO: in multi-tenant app, you must add validation to ensure correct tenant const { items: roles, diff --git a/src/tasks/queries/getTasks.ts b/src/tasks/queries/getTasks.ts index 314f1fc0..5f0215f6 100644 --- a/src/tasks/queries/getTasks.ts +++ b/src/tasks/queries/getTasks.ts @@ -7,8 +7,16 @@ export interface GetTasksInput export default resolver.pipe( resolver.authorize(), - async ({ where, orderBy, include, skip = 0, take = 100 }: GetTasksInput) => { - // TODO: in multi-tenant app, you must add validation to ensure correct tenant + async ({ where, orderBy, include, skip = 0, take }: GetTasksInput) => { + if (typeof take !== "number") { + const [tasks, count] = await Promise.all([ + db.task.findMany({ where, orderBy, include, skip }), + db.task.count({ where }), + ]) + + return { tasks, nextPage: null, hasMore: false, count } + } + const { items: tasks, hasMore, From d8a7868bce7bda81ae17479973ade48ba7fca959 Mon Sep 17 00:00:00 2001 From: The Doom Lab Date: Mon, 15 Dec 2025 20:41:46 -0600 Subject: [PATCH 2/7] update tasks to use new pagination --- .../components/ContributorInformation.tsx | 2 +- .../components/MilestoneSummary.tsx | 2 +- .../components/ProjectMemberTaskList.tsx | 2 +- src/tasklogs/queries/getTaskLogs.ts | 51 ++++++++++++++++--- src/tasks/components/AllTaskList.tsx | 43 +++++++++++++--- src/tasks/components/ProjectTasksList.tsx | 26 +++++++++- src/tasks/hooks/useProjectTasksListData.ts | 27 +++++++--- src/teams/components/TeamStatistics.tsx | 2 +- .../components/widgets/MainTotalTask.tsx | 2 +- .../widgets/ProjectOverdueTasks.tsx | 2 +- .../widgets/ProjectUpcomingTasks.tsx | 2 +- 11 files changed, 131 insertions(+), 30 deletions(-) diff --git a/src/contributors/components/ContributorInformation.tsx b/src/contributors/components/ContributorInformation.tsx index 71c23217..2dc3ba06 100644 --- a/src/contributors/components/ContributorInformation.tsx +++ b/src/contributors/components/ContributorInformation.tsx @@ -47,7 +47,7 @@ const ContributorInformation = ({ }) // get taskLogs for those tasks - const [fetchedTaskLogs, { refetch: refetchTaskLogs }] = useQuery(getTaskLogs, { + const [{ taskLogs: fetchedTaskLogs }, { refetch: refetchTaskLogs }] = useQuery(getTaskLogs, { where: { taskId: { in: tasks.map((task) => task.id) }, assignedToId: contributorId, diff --git a/src/milestones/components/MilestoneSummary.tsx b/src/milestones/components/MilestoneSummary.tsx index fbccd608..5600eac5 100644 --- a/src/milestones/components/MilestoneSummary.tsx +++ b/src/milestones/components/MilestoneSummary.tsx @@ -33,7 +33,7 @@ export const MilestoneSummary: React.FC = ({ milestone, p }, include: { task: true }, }) - const [fetchedTaskLogs, { refetch: refetchLogs }] = taskLogsQuery as any + const [{ taskLogs: fetchedTaskLogs }, { refetch: refetchLogs }] = taskLogsQuery as any // Cast and handle the possibility of `undefined` const taskLogs: TaskLogWithTask[] = (fetchedTaskLogs ?? []) as TaskLogWithTask[] diff --git a/src/projectmembers/components/ProjectMemberTaskList.tsx b/src/projectmembers/components/ProjectMemberTaskList.tsx index 4704366d..cee9a9a0 100644 --- a/src/projectmembers/components/ProjectMemberTaskList.tsx +++ b/src/projectmembers/components/ProjectMemberTaskList.tsx @@ -19,7 +19,7 @@ const ProjectMemberTaskList = ({ tableColumns, currentContributor, }: ProjectMemberTaskListProps) => { - const [taskLogs, { refetch: refetchTaskLogs }] = useQuery(getTaskLogs, { + const [{ taskLogs }, { refetch: refetchTaskLogs }] = useQuery(getTaskLogs, { where: { assignedToId: projectMemberId }, orderBy: { createdAt: "desc" }, include: { diff --git a/src/tasklogs/queries/getTaskLogs.ts b/src/tasklogs/queries/getTaskLogs.ts index c4471a66..f6e2ed82 100644 --- a/src/tasklogs/queries/getTaskLogs.ts +++ b/src/tasklogs/queries/getTaskLogs.ts @@ -1,19 +1,56 @@ import { resolver } from "@blitzjs/rpc" import db, { Prisma } from "db" +import { paginate } from "blitz" // Define input types for the query interface GetTaskLogsInput - extends Pick {} + extends Pick {} export default resolver.pipe( resolver.authorize(), // Automatically handles authorization - async ({ where, orderBy, include }: GetTaskLogsInput) => { - const taskLogs = await db.taskLog.findMany({ - where, - orderBy, - include, + async ({ where, orderBy, include, skip = 0, take }: GetTaskLogsInput) => { + if (typeof take !== "number") { + const [taskLogs, count] = await Promise.all([ + db.taskLog.findMany({ + where, + orderBy, + include, + skip, + }), + db.taskLog.count({ where }), + ]) + + return { + taskLogs, + nextPage: null, + hasMore: false, + count, + } + } + + const { + items: taskLogs, + hasMore, + nextPage, + count, + } = await paginate({ + skip, + take, + count: () => db.taskLog.count({ where }), + query: (paginateArgs) => + db.taskLog.findMany({ + ...paginateArgs, + where, + orderBy, + include, + }), }) - return taskLogs || [] + return { + taskLogs, + nextPage, + hasMore, + count, + } } ) diff --git a/src/tasks/components/AllTaskList.tsx b/src/tasks/components/AllTaskList.tsx index ec5b85f4..4ecd1e79 100644 --- a/src/tasks/components/AllTaskList.tsx +++ b/src/tasks/components/AllTaskList.tsx @@ -1,5 +1,5 @@ import { useCurrentUser } from "src/users/hooks/useCurrentUser" -import { useQuery } from "@blitzjs/rpc" +import { usePaginatedQuery } from "@blitzjs/rpc" import getTaskLogs from "src/tasklogs/queries/getTaskLogs" import getLatestTaskLogs from "src/tasklogs/hooks/getLatestTaskLogs" import { processAllTasks } from "../tables/processing/processAllTasks" @@ -7,12 +7,26 @@ import Table from "src/core/components/Table" import { AllTasksColumns } from "../tables/columns/AllTasksColumns" import { TaskLogWithTaskProjectAndComments } from "src/core/types" import Card from "src/core/components/Card" +import { useMemo, useState } from "react" +import { PaginationState } from "@tanstack/react-table" export const AllTasksList = () => { const currentUser = useCurrentUser() + const [pagination, setPagination] = useState({ + pageIndex: 0, + pageSize: 10, + }) + + const paginationArgs = useMemo( + () => ({ + skip: pagination.pageIndex * pagination.pageSize, + take: pagination.pageSize, + }), + [pagination] + ) // Get latest logs that this user is involved in - const [fetchedTaskLogs] = useQuery(getTaskLogs, { + const [{ taskLogs: fetchedTaskLogs = [], count }] = usePaginatedQuery(getTaskLogs, { where: { assignedTo: { users: { some: { id: currentUser?.id } }, @@ -42,11 +56,11 @@ export const AllTasksList = () => { }, }, orderBy: { id: "asc" }, + ...paginationArgs, }) - // Cast and handle the possibility of `undefined` - const taskLogs: TaskLogWithTaskProjectAndComments[] = (fetchedTaskLogs ?? - []) as TaskLogWithTaskProjectAndComments[] + const taskLogs: TaskLogWithTaskProjectAndComments[] = + fetchedTaskLogs as TaskLogWithTaskProjectAndComments[] // process those logs to get the latest one for each task-projectmemberId const latestLogs = getLatestTaskLogs(taskLogs) @@ -54,10 +68,27 @@ export const AllTasksList = () => { // process both sets so that comment counts use original taskLogs (first log for each person-task combo) const processedTasks = processAllTasks(latestLogs, taskLogs) + const pageCount = Math.max(1, Math.ceil((count ?? 0) / pagination.pageSize)) + + const handlePaginationChange = ( + updater: PaginationState | ((state: PaginationState) => PaginationState) + ) => { + setPagination((prev) => (typeof updater === "function" ? updater(prev) : updater)) + } + return (
-
+
Note: This list only shows comment notifications for tasks that are explicitly assigned to you. If you are a project manager but not assigned to a task, you will not see its comment diff --git a/src/tasks/components/ProjectTasksList.tsx b/src/tasks/components/ProjectTasksList.tsx index 024bda76..ea959fb6 100644 --- a/src/tasks/components/ProjectTasksList.tsx +++ b/src/tasks/components/ProjectTasksList.tsx @@ -2,16 +2,38 @@ import { useParam } from "@blitzjs/next" import { ProjectTasksColumns } from "src/tasks/tables/columns/ProjectTasksColumns" import Table from "src/core/components/Table" import useProjecTasksListData from "../hooks/useProjectTasksListData" +import { useState } from "react" +import { PaginationState } from "@tanstack/react-table" export const ProjectTasksList = () => { const projectId = useParam("projectId", "number") + const [pagination, setPagination] = useState({ + pageIndex: 0, + pageSize: 10, + }) - const { tasks } = useProjecTasksListData(projectId) + const { tasks, count } = useProjecTasksListData(projectId, pagination) + const pageCount = Math.max(1, Math.ceil((count ?? 0) / pagination.pageSize)) + + const handlePaginationChange = ( + updater: PaginationState | ((state: PaginationState) => PaginationState) + ) => { + setPagination((prev) => (typeof updater === "function" ? updater(prev) : updater)) + } return (
-
+
) diff --git a/src/tasks/hooks/useProjectTasksListData.ts b/src/tasks/hooks/useProjectTasksListData.ts index 51742c32..0bb616d5 100644 --- a/src/tasks/hooks/useProjectTasksListData.ts +++ b/src/tasks/hooks/useProjectTasksListData.ts @@ -1,13 +1,17 @@ -import { useEffect, useState } from "react" -import { useQuery } from "@blitzjs/rpc" +import { useEffect, useMemo, useState } from "react" +import { usePaginatedQuery, useQuery } from "@blitzjs/rpc" import { useCurrentUser } from "src/users/hooks/useCurrentUser" import getTasks, { GetTasksInput } from "../queries/getTasks" import { MemberPrivileges } from "@prisma/client" import { useMemberPrivileges } from "src/projectprivileges/components/MemberPrivilegesContext" import { processProjectTasks } from "../tables/processing/processProjectTasks" import getUserProjectMemberIds from "src/tasks/queries/getUserProjectMemberIds" +import { PaginationState } from "@tanstack/react-table" -export default function useProjectTasksListData(projectId: number | undefined) { +export default function useProjectTasksListData( + projectId: number | undefined, + pagination: PaginationState +) { const currentUser = useCurrentUser() const { privilege } = useMemberPrivileges() const [queryParams, setQueryParams] = useState(null) @@ -102,16 +106,23 @@ export default function useProjectTasksListData(projectId: number | undefined) { setQueryParams(baseParams) }, [privilege, currentUser, projectId, userMemberIds]) - const [{ tasks: fetchedTasks }, { refetch }] = useQuery( - getTasks, - queryParams ?? { + const queryInput = useMemo(() => { + const base = queryParams ?? { where: { project: { id: -1 } }, // dummy query until params are ready orderBy: [{ id: "asc" }], } - ) + + return { + ...base, + skip: pagination.pageIndex * pagination.pageSize, + take: pagination.pageSize, + } + }, [queryParams, pagination]) + + const [{ tasks: fetchedTasks = [], count }, { refetch }] = usePaginatedQuery(getTasks, queryInput) const tasks = processProjectTasks(fetchedTasks, async () => { await refetch() }) - return { tasks, refetchTasks: refetch } + return { tasks, refetchTasks: refetch, count } } diff --git a/src/teams/components/TeamStatistics.tsx b/src/teams/components/TeamStatistics.tsx index 7a1f0278..32bd2a8b 100644 --- a/src/teams/components/TeamStatistics.tsx +++ b/src/teams/components/TeamStatistics.tsx @@ -39,7 +39,7 @@ export const TeamStatistics = ({ teamId, projectId }) => { }) // get taskLogs for those tasks - const [fetchedTaskLogs, { refetch: refetchTaskLogs }] = useQuery(getTaskLogs, { + const [{ taskLogs: fetchedTaskLogs }, { refetch: refetchTaskLogs }] = useQuery(getTaskLogs, { where: { taskId: { in: tasks.map((task) => task.id) }, assignedToId: teamId, diff --git a/src/widgets/components/widgets/MainTotalTask.tsx b/src/widgets/components/widgets/MainTotalTask.tsx index e1b283a5..d329c3c0 100644 --- a/src/widgets/components/widgets/MainTotalTask.tsx +++ b/src/widgets/components/widgets/MainTotalTask.tsx @@ -19,7 +19,7 @@ const AllTaskTotal: React.FC<{ size: "SMALL" | "MEDIUM" | "LARGE" }> = ({ size } // Get latest logs that this user is involved in // Fetch all tasks // Get latest logs that this user is involved in - const [fetchedTaskLogs] = useQuery(getTaskLogs, { + const [{ taskLogs: fetchedTaskLogs }] = useQuery(getTaskLogs, { where: { assignedTo: { users: { some: { id: currentUser?.id } }, diff --git a/src/widgets/components/widgets/ProjectOverdueTasks.tsx b/src/widgets/components/widgets/ProjectOverdueTasks.tsx index 5457a23f..ceaa2d02 100644 --- a/src/widgets/components/widgets/ProjectOverdueTasks.tsx +++ b/src/widgets/components/widgets/ProjectOverdueTasks.tsx @@ -19,7 +19,7 @@ const ProjectOverdueTasks: React.FC<{ size: "SMALL" | "MEDIUM" | "LARGE" }> = ({ const currentUser = useCurrentUser() // get TaskLogs for this project and user - const [taskLogs] = useQuery(getTaskLogs, { + const [{ taskLogs }] = useQuery(getTaskLogs, { where: { task: { projectId: projectId }, assignedTo: { users: { some: { id: currentUser?.id } } }, diff --git a/src/widgets/components/widgets/ProjectUpcomingTasks.tsx b/src/widgets/components/widgets/ProjectUpcomingTasks.tsx index 00d3cf3c..d2e15a52 100644 --- a/src/widgets/components/widgets/ProjectUpcomingTasks.tsx +++ b/src/widgets/components/widgets/ProjectUpcomingTasks.tsx @@ -18,7 +18,7 @@ const ProjectUpcomingTasks: React.FC<{ size: "SMALL" | "MEDIUM" | "LARGE" }> = ( const currentUser = useCurrentUser() // get TaskLogs for this project and user - const [taskLogs] = useQuery(getTaskLogs, { + const [{ taskLogs }] = useQuery(getTaskLogs, { where: { task: { projectId: projectId }, assignedTo: { users: { some: { id: currentUser?.id } } }, From 7771930de8fda8500308d475881e7e6daaa3316c Mon Sep 17 00:00:00 2001 From: The Doom Lab Date: Mon, 15 Dec 2025 20:45:23 -0600 Subject: [PATCH 3/7] update forms and roles to allow for more than 100 --- src/forms/components/FormsList.tsx | 27 ++++++- src/forms/queries/getForms.ts | 75 ++++++++++++++----- src/pages/forms/index.tsx | 38 +++++++++- src/pages/roles/index.tsx | 62 +++++++++++++-- .../components/ProjectSchemaInput.tsx | 8 +- src/roles/components/AllRolesList.tsx | 28 ++++++- src/tasks/components/TaskSchemaInput.tsx | 2 +- .../components/widgets/MainTotalForms.tsx | 2 +- 8 files changed, 202 insertions(+), 40 deletions(-) diff --git a/src/forms/components/FormsList.tsx b/src/forms/components/FormsList.tsx index 493c2ba5..76ff6cce 100644 --- a/src/forms/components/FormsList.tsx +++ b/src/forms/components/FormsList.tsx @@ -2,18 +2,39 @@ import Table from "src/core/components/Table" import { FormsColumns } from "src/forms/tables/columns/FormsColumns" import { processForms } from "../tables/processing/processForms" import { FormWithFormVersion } from "../queries/getForms" +import { PaginationState, OnChangeFn } from "@tanstack/react-table" type FormsListProps = { forms: FormWithFormVersion[] - addPagination: Boolean + manualPagination?: boolean + paginationState?: PaginationState + onPaginationChange?: OnChangeFn + pageCount?: number + pageSizeOptions?: number[] } -export const FormsList = ({ forms }: FormsListProps) => { +export const FormsList = ({ + forms, + manualPagination = false, + paginationState, + onPaginationChange, + pageCount, + pageSizeOptions, +}: FormsListProps) => { const formsTableData = processForms(forms) return (
-
+
) } diff --git a/src/forms/queries/getForms.ts b/src/forms/queries/getForms.ts index f4032c3b..8ebc4fe0 100644 --- a/src/forms/queries/getForms.ts +++ b/src/forms/queries/getForms.ts @@ -1,36 +1,75 @@ import { resolver } from "@blitzjs/rpc" import db, { Prisma } from "db" import { Form, FormVersion } from "db" +import { paginate } from "blitz" export interface FormWithFormVersion extends Form { formVersion: FormVersion | null } -interface GetFormInput extends Pick {} +interface GetFormInput + extends Pick {} + +const includeLatestVersion = (include?: Prisma.FormInclude) => ({ + ...include, + versions: { + orderBy: { version: "desc" }, + take: 1, + }, +}) + +const mapForms = (fetchedForms: (Form & { versions: FormVersion[] })[]): FormWithFormVersion[] => { + return fetchedForms.map((form) => ({ + ...form, + formVersion: form.versions[0] || null, + })) +} export default resolver.pipe( resolver.authorize(), - async ({ where, orderBy, include }: GetFormInput): Promise => { - // TODO: in multi-tenant app, you must add validation to ensure correct tenant - const fetchedForms = await db.form.findMany({ - where, - orderBy, - include: { - ...include, - versions: { - orderBy: { version: "desc" }, - take: 1, - }, - }, - }) + async ({ where, orderBy, include, skip = 0, take }: GetFormInput) => { + if (typeof take !== "number") { + const [fetchedForms, count] = await Promise.all([ + db.form.findMany({ + where, + orderBy, + include: includeLatestVersion(include), + skip, + }), + db.form.count({ where }), + ]) - const forms: FormWithFormVersion[] = fetchedForms.map((form) => { return { - ...form, - formVersion: form.versions[0] || null, + forms: mapForms(fetchedForms), + nextPage: null, + hasMore: false, + count, } + } + + const { + items: fetchedForms, + hasMore, + nextPage, + count, + } = await paginate({ + skip, + take, + count: () => db.form.count({ where }), + query: (paginateArgs) => + db.form.findMany({ + ...paginateArgs, + where, + orderBy, + include: includeLatestVersion(include), + }), }) - return forms + return { + forms: mapForms(fetchedForms), + nextPage, + hasMore, + count, + } } ) diff --git a/src/pages/forms/index.tsx b/src/pages/forms/index.tsx index 665256b7..6fcb3d18 100644 --- a/src/pages/forms/index.tsx +++ b/src/pages/forms/index.tsx @@ -1,15 +1,16 @@ -import { Suspense, useState } from "react" +import { Suspense, useMemo, useState } from "react" import Layout from "src/core/layouts/Layout" import Link from "next/link" import { Routes } from "@blitzjs/next" import { FormsList } from "src/forms/components/FormsList" import AddFormTemplates from "src/forms/components/AddFormTemplates" import { useCurrentUser } from "src/users/hooks/useCurrentUser" -import { useQuery } from "@blitzjs/rpc" +import { usePaginatedQuery } from "@blitzjs/rpc" import getForms from "src/forms/queries/getForms" import Card from "src/core/components/Card" import { InformationCircleIcon } from "@heroicons/react/24/outline" import { Tooltip } from "react-tooltip" +import { PaginationState } from "@tanstack/react-table" const AllFormsPage = () => { // AddFormTemplate modal settings @@ -20,15 +21,37 @@ const AllFormsPage = () => { // Get user const currentUser = useCurrentUser() + const [pagination, setPagination] = useState({ + pageIndex: 0, + pageSize: 10, + }) + + const paginationArgs = useMemo( + () => ({ + skip: pagination.pageIndex * pagination.pageSize, + take: pagination.pageSize, + }), + [pagination] + ) + // Get forms - const [forms, { refetch }] = useQuery(getForms, { + const [{ forms, count }, { refetch }] = usePaginatedQuery(getForms, { where: { user: { id: currentUser?.id }, archived: false, }, orderBy: { id: "desc" }, + ...paginationArgs, }) + const pageCount = Math.max(1, Math.ceil((count ?? 0) / pagination.pageSize)) + + const handlePaginationChange = ( + updater: PaginationState | ((state: PaginationState) => PaginationState) + ) => { + setPagination((prev) => (typeof updater === "function" ? updater(prev) : updater)) + } + return ( // @ts-expect-error children are clearly passed below @@ -61,7 +84,14 @@ const AllFormsPage = () => { /> - + diff --git a/src/pages/roles/index.tsx b/src/pages/roles/index.tsx index bb21c2c0..40102354 100644 --- a/src/pages/roles/index.tsx +++ b/src/pages/roles/index.tsx @@ -1,5 +1,5 @@ -import { Suspense } from "react" -import { useQuery } from "@blitzjs/rpc" +import { Suspense, useMemo, useState } from "react" +import { usePaginatedQuery, useQuery } from "@blitzjs/rpc" import Layout from "src/core/layouts/Layout" import { useCurrentUser } from "src/users/hooks/useCurrentUser" import getRoles from "src/roles/queries/getRoles" @@ -9,19 +9,56 @@ import { InformationCircleIcon } from "@heroicons/react/24/outline" import { Tooltip } from "react-tooltip" import Card from "src/core/components/Card" import { DefaultRoles } from "src/roles/components/DefaultRoles" +import { PaginationState } from "@tanstack/react-table" const RoleBuilderPage = () => { const currentUser = useCurrentUser() + const [pagination, setPagination] = useState({ + pageIndex: 0, + pageSize: 10, + }) + + const paginationArgs = useMemo( + () => ({ + skip: pagination.pageIndex * pagination.pageSize, + take: pagination.pageSize, + }), + [pagination] + ) + + const [{ roles, count }, { refetch: refetchPagedRoles }] = usePaginatedQuery(getRoles, { + where: { user: { id: currentUser?.id } }, + orderBy: { id: "asc" }, + ...paginationArgs, + }) - const [{ roles }, { refetch }] = useQuery(getRoles, { + const [{ roles: allRoles }, { refetch: refetchAllRoles }] = useQuery(getRoles, { where: { user: { id: currentUser?.id } }, orderBy: { id: "asc" }, }) - const taxonomyList = Array.from( - new Set(roles.map((role) => (role.taxonomy || "").trim()).filter((taxonomy) => taxonomy !== "")) + const taxonomyList = useMemo( + () => + Array.from( + new Set( + allRoles.map((role) => (role.taxonomy || "").trim()).filter((taxonomy) => taxonomy !== "") + ) + ), + [allRoles] ) + const handleRolesChanged = async () => { + await Promise.all([refetchPagedRoles(), refetchAllRoles()]) + } + + const pageCount = Math.max(1, Math.ceil((count ?? 0) / pagination.pageSize)) + + const handlePaginationChange = ( + updater: PaginationState | ((state: PaginationState) => PaginationState) + ) => { + setPagination((prev) => (typeof updater === "function" ? updater(prev) : updater)) + } + return ( // @ts-expect-error children are clearly passed below @@ -40,12 +77,21 @@ const RoleBuilderPage = () => {
- - + +
Loading...}> - + diff --git a/src/projects/components/ProjectSchemaInput.tsx b/src/projects/components/ProjectSchemaInput.tsx index 9bb4f92a..dd0d0f8e 100644 --- a/src/projects/components/ProjectSchemaInput.tsx +++ b/src/projects/components/ProjectSchemaInput.tsx @@ -44,9 +44,11 @@ export const ProjectSchemaInput = ({ toast.success("Default form has been successfully created!") // Ensure versions exists and has at least one item - const { data: updatedForms } = await refetchForms() + const { data: updatedFormsResult } = await refetchForms() - const allVersions = (updatedForms ?? []).flatMap((form) => form.formVersion ?? []) + const allVersions = (updatedFormsResult?.forms ?? []).flatMap( + (form) => form.formVersion ?? [] + ) const sortedVersions = allVersions.sort( (a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime() ) @@ -71,7 +73,7 @@ export const ProjectSchemaInput = ({ } } - const [userForms, { refetch: refetchForms }] = useQuery(getForms, { + const [{ forms: userForms }, { refetch: refetchForms }] = useQuery(getForms, { where: { userId: { in: userId }, archived: false }, }) diff --git a/src/roles/components/AllRolesList.tsx b/src/roles/components/AllRolesList.tsx index d9dd368b..91461668 100644 --- a/src/roles/components/AllRolesList.tsx +++ b/src/roles/components/AllRolesList.tsx @@ -2,20 +2,44 @@ import Table from "src/core/components/Table" import { RoleTableColumns } from "../tables/columns/RoleTableColumns" import { processRoleTableData } from "../tables/processing/processRoleTableData" import { Role } from "db" +import { PaginationState, OnChangeFn } from "@tanstack/react-table" interface AllRolesListProps { roles: Role[] onRolesChanged?: () => void taxonomyList: string[] + manualPagination?: boolean + paginationState?: PaginationState + onPaginationChange?: OnChangeFn + pageCount?: number + pageSizeOptions?: number[] } -export const AllRolesList = ({ roles, onRolesChanged, taxonomyList }: AllRolesListProps) => { +export const AllRolesList = ({ + roles, + onRolesChanged, + taxonomyList, + manualPagination = false, + paginationState, + onPaginationChange, + pageCount, + pageSizeOptions, +}: AllRolesListProps) => { // Process table data const roleTableData = processRoleTableData(roles, onRolesChanged, taxonomyList) return (
-
+
) } diff --git a/src/tasks/components/TaskSchemaInput.tsx b/src/tasks/components/TaskSchemaInput.tsx index c6249a47..993b78d4 100644 --- a/src/tasks/components/TaskSchemaInput.tsx +++ b/src/tasks/components/TaskSchemaInput.tsx @@ -24,7 +24,7 @@ export const TaskSchemaInput = ({ const [openSchemaModal, setOpenSchemaModal] = useState(false) const handleToggleSchemaUpload = () => setOpenSchemaModal((prev) => !prev) - const [pmForms] = useQuery(getForms, { + const [{ forms: pmForms }] = useQuery(getForms, { where: { userId: { in: projectManagerIds }, archived: false }, include: { user: true }, }) diff --git a/src/widgets/components/widgets/MainTotalForms.tsx b/src/widgets/components/widgets/MainTotalForms.tsx index 6172fa13..25a5159a 100644 --- a/src/widgets/components/widgets/MainTotalForms.tsx +++ b/src/widgets/components/widgets/MainTotalForms.tsx @@ -12,7 +12,7 @@ import { useTranslation } from "react-i18next" const TotalForms: React.FC<{ size: "SMALL" | "MEDIUM" | "LARGE" }> = ({ size }) => { const currentUser = useCurrentUser() // Get forms - const [forms] = useQuery(getForms, { + const [{ forms }] = useQuery(getForms, { where: { user: { id: currentUser?.id }, archived: false, From 5a41b9ed58f85df328cc016a26235f2472808b0d Mon Sep 17 00:00:00 2001 From: The Doom Lab Date: Mon, 15 Dec 2025 20:54:06 -0600 Subject: [PATCH 4/7] update contributor pagination --- src/contributors/hooks/useContributorsData.ts | 56 +++++++++++-- src/contributors/queries/getContributors.ts | 84 ++++++++++++++----- .../[projectId]/contributors/index.tsx | 38 ++++++++- .../projects/[projectId]/teams/index.tsx | 32 ++++++- src/roles/components/ContributorsTab.tsx | 53 ++++++++---- src/roles/components/RoleContributorTable.tsx | 32 ++++++- src/teams/components/AssignTeamMembers.tsx | 2 +- 7 files changed, 239 insertions(+), 58 deletions(-) diff --git a/src/contributors/hooks/useContributorsData.ts b/src/contributors/hooks/useContributorsData.ts index a0216c58..17194c09 100644 --- a/src/contributors/hooks/useContributorsData.ts +++ b/src/contributors/hooks/useContributorsData.ts @@ -1,4 +1,4 @@ -import { useQuery } from "@blitzjs/rpc" +import { usePaginatedQuery, useQuery } from "@blitzjs/rpc" import getContributors from "src/contributors/queries/getContributors" import { processContributor, @@ -8,26 +8,64 @@ import { MemberPrivileges } from "@prisma/client" import { useMemo } from "react" import { CurrentUser } from "src/users/queries/getCurrentUser" import { ProjectMemberWithUsers } from "src/core/types" +import { PaginationState } from "@tanstack/react-table" + +type UseContributorsDataResult = { + data: ContributorTableData[] + count: number + refetch: () => Promise +} export function useContributorsData( privilege: MemberPrivileges, currentUser: CurrentUser, - projectId: number -): ContributorTableData[] { - // Fetch - const [contributors] = useQuery(getContributors, { projectId: projectId, deleted: false }) + projectId: number, + pagination?: PaginationState +): UseContributorsDataResult { + const shouldPaginate = privilege !== MemberPrivileges.CONTRIBUTOR && Boolean(pagination) + + const baseArgs = { + projectId, + deleted: false, + orderBy: { id: "asc" as const }, + } + + let contributors: ProjectMemberWithUsers[] = [] + let count = 0 + let refetchFn: () => Promise + + if (shouldPaginate) { + const [{ contributors: pagedContributors, count: total }, { refetch }] = usePaginatedQuery( + getContributors, + { + ...baseArgs, + skip: pagination!.pageIndex * pagination!.pageSize, + take: pagination!.pageSize, + } + ) + contributors = pagedContributors + count = total + refetchFn = refetch + } else { + const [{ contributors: allContributors }, { refetch }] = useQuery(getContributors, baseArgs) + contributors = allContributors + count = contributors.length + refetchFn = refetch + } - // Filter based on privilege const filteredContributors = useMemo(() => { if (privilege === MemberPrivileges.CONTRIBUTOR) { return contributors.filter( - (contributor: ProjectMemberWithUsers) => + (contributor) => contributor.users.length === 1 && contributor.users[0]?.id === currentUser.id ) } return contributors }, [contributors, privilege, currentUser.id]) - // Process the data for table rendering - return processContributor(filteredContributors, projectId) + return { + data: processContributor(filteredContributors, projectId), + count, + refetch: refetchFn, + } } diff --git a/src/contributors/queries/getContributors.ts b/src/contributors/queries/getContributors.ts index 6006f12b..55040522 100644 --- a/src/contributors/queries/getContributors.ts +++ b/src/contributors/queries/getContributors.ts @@ -1,38 +1,76 @@ import { resolver } from "@blitzjs/rpc" -import db from "db" +import db, { Prisma } from "db" import { ProjectMemberWithUsers } from "src/core/types" import { anonymizeNestedUsers } from "src/core/utils/anonymizeNestedUsers" +import { paginate } from "blitz" -interface GetContributorsInput { +interface GetContributorsInput + extends Pick { projectId: number deleted?: boolean } +const validateContributors = (contributors: ProjectMemberWithUsers[]) => { + contributors.forEach((contributor) => { + if (contributor.users.length !== 1) { + throw new Error( + `Contributor with ID ${contributor.id} has ${contributor.users.length} users! Expected exactly 1.` + ) + } + }) +} + export default resolver.pipe( resolver.authorize(), - async ({ projectId, deleted }: GetContributorsInput): Promise => { - // Directly query the database for contributors - const contributors = await db.projectMember.findMany({ - where: { - projectId: projectId, - deleted: deleted, - name: null, // Ensures we're only getting contributors (where name is null) - }, - orderBy: { id: "asc" }, - include: { - users: true, // Include the related user - }, - }) + async ({ projectId, deleted, skip = 0, take, orderBy = { id: "asc" } }: GetContributorsInput) => { + const baseWhere: Prisma.ProjectMemberWhereInput = { + projectId, + deleted, + name: null, + } + + if (typeof take !== "number") { + const [contributors, count] = await Promise.all([ + db.projectMember.findMany({ + where: baseWhere, + orderBy, + include: { + users: true, + }, + skip, + }), + db.projectMember.count({ where: baseWhere }), + ]) - // Check if any contributor has more than one user and throw an error - contributors.forEach((contributor) => { - if (contributor.users.length !== 1) { - throw new Error( - `Contributor with ID ${contributor.id} has ${contributor.users.length} users! Expected exactly 1.` - ) - } + const processed = anonymizeNestedUsers(contributors) as ProjectMemberWithUsers[] + validateContributors(processed) + + return { contributors: processed, nextPage: null, hasMore: false, count } + } + + const { + items: contributors, + hasMore, + nextPage, + count, + } = await paginate({ + skip, + take, + orderBy, + count: () => db.projectMember.count({ where: baseWhere }), + query: (paginateArgs) => + db.projectMember.findMany({ + ...paginateArgs, + where: baseWhere, + include: { + users: true, + }, + }), }) - return anonymizeNestedUsers(contributors) // This is automatically typed as ProjectMemberWithUsers[] + const processed = anonymizeNestedUsers(contributors) as ProjectMemberWithUsers[] + validateContributors(processed) + + return { contributors: processed, hasMore, nextPage, count } } ) diff --git a/src/pages/projects/[projectId]/contributors/index.tsx b/src/pages/projects/[projectId]/contributors/index.tsx index 00cb230a..40d63e55 100644 --- a/src/pages/projects/[projectId]/contributors/index.tsx +++ b/src/pages/projects/[projectId]/contributors/index.tsx @@ -1,4 +1,4 @@ -import { Suspense } from "react" +import { Suspense, useState } from "react" import { Routes } from "@blitzjs/next" import Link from "next/link" import { useParam } from "@blitzjs/next" @@ -17,6 +17,7 @@ import { CurrentUser } from "src/users/queries/getCurrentUser" import { InformationCircleIcon } from "@heroicons/react/24/outline" import { Tooltip } from "react-tooltip" import Card from "src/core/components/Card" +import { PaginationState } from "@tanstack/react-table" interface ContributorListProps { privilege: MemberPrivileges @@ -25,17 +26,48 @@ interface ContributorListProps { } export const ContributorList = ({ privilege, currentUser, projectId }: ContributorListProps) => { - const contributorTableData = useContributorsData(privilege, currentUser, projectId) + const [pagination, setPagination] = useState({ + pageIndex: 0, + pageSize: 10, + }) + const shouldPaginate = privilege !== MemberPrivileges.CONTRIBUTOR + + const { data: contributorTableData, count } = useContributorsData( + privilege, + currentUser, + projectId, + shouldPaginate ? pagination : undefined + ) const tableColumns = privilege === MemberPrivileges.CONTRIBUTOR ? StandardContributorColumns : ProjectManagerContributorColumns + const pageCount = shouldPaginate ? Math.max(1, Math.ceil(count / pagination.pageSize)) : 1 + + const handlePaginationChange = ( + updater: PaginationState | ((state: PaginationState) => PaginationState) + ) => { + if (!shouldPaginate) { + return + } + setPagination((prev) => (typeof updater === "function" ? updater(prev) : updater)) + } + return ( }> -
+
) diff --git a/src/pages/projects/[projectId]/teams/index.tsx b/src/pages/projects/[projectId]/teams/index.tsx index 81a2f6e7..9139ac00 100644 --- a/src/pages/projects/[projectId]/teams/index.tsx +++ b/src/pages/projects/[projectId]/teams/index.tsx @@ -1,7 +1,7 @@ -import { Suspense } from "react" +import { Suspense, useState } from "react" import { Routes } from "@blitzjs/next" import Link from "next/link" -import { useQuery } from "@blitzjs/rpc" +import { usePaginatedQuery } from "@blitzjs/rpc" import { useParam } from "@blitzjs/next" import Layout from "src/core/layouts/Layout" import { ContributorTeamColumns } from "src/teams/tables/columns/ContributorTeamColumns" @@ -16,6 +16,7 @@ import { ProjectMemberWithUsers } from "src/core/types" import Card from "src/core/components/Card" import { InformationCircleIcon } from "@heroicons/react/24/outline" import { Tooltip } from "react-tooltip" +import { PaginationState } from "@tanstack/react-table" interface AllTeamListProps { privilege: MemberPrivileges @@ -24,8 +25,12 @@ interface AllTeamListProps { export const AllTeamList = ({ privilege, projectId }: AllTeamListProps) => { const currentUser = useCurrentUser() + const [pagination, setPagination] = useState({ + pageIndex: 0, + pageSize: 10, + }) - const [{ projectMembers }] = useQuery(getProjectMembers, { + const [{ projectMembers, count }] = usePaginatedQuery(getProjectMembers, { where: { projectId: projectId, name: { not: null }, // Ensures the name in ProjectMember is non-null @@ -38,6 +43,8 @@ export const AllTeamList = ({ privilege, projectId }: AllTeamListProps) => { include: { users: true, // Ensure that projectMembers are included }, + skip: pagination.pageIndex * pagination.pageSize, + take: pagination.pageSize, }) // Filter teams if the privilege is CONTRIBUTOR @@ -52,9 +59,26 @@ export const AllTeamList = ({ privilege, projectId }: AllTeamListProps) => { const tableColumnsTeams = privilege === MemberPrivileges.CONTRIBUTOR ? ContributorTeamColumns : PmTeamColumns + const pageCount = Math.max(1, Math.ceil((count ?? 0) / pagination.pageSize)) + + const handlePaginationChange = ( + updater: PaginationState | ((state: PaginationState) => PaginationState) + ) => { + setPagination((prev) => (typeof updater === "function" ? updater(prev) : updater)) + } + return (
-
+
) } diff --git a/src/roles/components/ContributorsTab.tsx b/src/roles/components/ContributorsTab.tsx index 937d2b07..bb8ebbd2 100644 --- a/src/roles/components/ContributorsTab.tsx +++ b/src/roles/components/ContributorsTab.tsx @@ -1,6 +1,5 @@ -import { Suspense } from "react" -import { useQuery } from "@blitzjs/rpc" -import React from "react" +import { Suspense, useState } from "react" +import { usePaginatedQuery } from "@blitzjs/rpc" import { Routes, useParam } from "@blitzjs/next" import getProjectMembers from "src/projectmembers/queries/getProjectMembers" import { MultiSelectProvider } from "../../core/components/fields/MultiSelectContext" @@ -9,31 +8,53 @@ import { AddRoleModal } from "./AddRoleModal" import { ProjectMemberWithUsersAndRoles } from "src/core/types" import Link from "next/link" import { Tooltip } from "react-tooltip" +import { PaginationState } from "@tanstack/react-table" const ContributorsTab = () => { const projectId = useParam("projectId", "number") + const [pagination, setPagination] = useState({ pageIndex: 0, pageSize: 10 }) - const [{ projectMembers: contributors }, { refetch }] = useQuery(getProjectMembers, { - where: { - projectId: projectId, - users: { - every: { - id: { not: undefined }, // Ensures there's at least one user + const [{ projectMembers: contributors, count }, { refetch }] = usePaginatedQuery( + getProjectMembers, + { + where: { + projectId: projectId, + users: { + every: { + id: { not: undefined }, // Ensures there's at least one user + }, }, + deleted: undefined, + name: { equals: null }, // Ensures ProjectMember is contributor and not team }, - deleted: undefined, - name: { equals: null }, // Ensures ProjectMember is contributor and not team - }, - include: { users: true, roles: true }, - orderBy: { id: "asc" }, - }) as unknown as [{ projectMembers: ProjectMemberWithUsersAndRoles[] }, any] + include: { users: true, roles: true }, + orderBy: { id: "asc" }, + skip: pagination.pageIndex * pagination.pageSize, + take: pagination.pageSize, + } + ) as [{ projectMembers: ProjectMemberWithUsersAndRoles[]; count: number }, any] + + const pageCount = Math.max(1, Math.ceil((count ?? 0) / pagination.pageSize)) + + const handlePaginationChange = ( + updater: PaginationState | ((state: PaginationState) => PaginationState) + ) => { + setPagination((prev) => (typeof updater === "function" ? updater(prev) : updater)) + } return (
Loading...}>
- +
{ +type RoleContributorTableProps = { + contributors: any[] + manualPagination?: boolean + paginationState?: PaginationState + onPaginationChange?: OnChangeFn + pageCount?: number + pageSizeOptions?: number[] +} + +export const RoleContributorTable = ({ + contributors, + manualPagination = false, + paginationState, + onPaginationChange, + pageCount, + pageSizeOptions, +}: RoleContributorTableProps) => { const processedData = contributors.map((contributor) => ({ username: contributor.users[0].username, firstname: contributor.users[0].firstName, @@ -12,5 +29,16 @@ export const RoleContributorTable = ({ contributors }) => { const columns = useRoleContributorTableColumns(processedData) - return
+ return ( +
+ ) } diff --git a/src/teams/components/AssignTeamMembers.tsx b/src/teams/components/AssignTeamMembers.tsx index 59e9bffb..32fb5183 100644 --- a/src/teams/components/AssignTeamMembers.tsx +++ b/src/teams/components/AssignTeamMembers.tsx @@ -9,7 +9,7 @@ interface AssignTeamMembersProps { } const AssignTeamMembers: React.FC = ({ projectId }) => { - const [contributors] = useQuery(getContributors, { projectId, deleted: false }) + const [{ contributors }] = useQuery(getContributors, { projectId, deleted: false }) const options = contributors.map((contributor) => ({ id: contributor.users[0]!.id, From 41fa47d624a6b770bf652c3e0159a367511b02f9 Mon Sep 17 00:00:00 2001 From: The Doom Lab Date: Mon, 15 Dec 2025 21:17:18 -0600 Subject: [PATCH 5/7] fix type errors --- .../components/ContributorInformation.tsx | 6 +-- src/contributors/hooks/useContributorsData.ts | 38 +++++++------------ src/contributors/queries/getContributors.ts | 2 +- src/forms/queries/getForms.ts | 21 +++++----- .../mutations/deleteNotification.ts | 35 ++++++++++------- src/pages/notifications/index.tsx | 2 +- .../[projectId]/notifications/index.tsx | 2 +- 7 files changed, 53 insertions(+), 53 deletions(-) diff --git a/src/contributors/components/ContributorInformation.tsx b/src/contributors/components/ContributorInformation.tsx index 2dc3ba06..c29ac070 100644 --- a/src/contributors/components/ContributorInformation.tsx +++ b/src/contributors/components/ContributorInformation.tsx @@ -47,7 +47,7 @@ const ContributorInformation = ({ }) // get taskLogs for those tasks - const [{ taskLogs: fetchedTaskLogs }, { refetch: refetchTaskLogs }] = useQuery(getTaskLogs, { + const [{ taskLogs: fetchedTaskLogs = [] }, { refetch: refetchTaskLogs }] = useQuery(getTaskLogs, { where: { taskId: { in: tasks.map((task) => task.id) }, assignedToId: contributorId, @@ -55,7 +55,7 @@ const ContributorInformation = ({ include: { task: true, }, - }) as unknown as [TaskLogWithTask[], { refetch: () => Promise }] + }) useEffect(() => { const handleUpdate = () => { @@ -67,7 +67,7 @@ const ContributorInformation = ({ }, [refetchTasks, refetchTaskLogs]) // Cast and handle the possibility of `undefined` - const taskLogs: TaskLogWithTask[] = (fetchedTaskLogs ?? []) as TaskLogWithTask[] + const taskLogs: TaskLogWithTask[] = fetchedTaskLogs as TaskLogWithTask[] // only the latest task log const allTaskLogs = getLatestTaskLogs(taskLogs) diff --git a/src/contributors/hooks/useContributorsData.ts b/src/contributors/hooks/useContributorsData.ts index 17194c09..91a5a43b 100644 --- a/src/contributors/hooks/useContributorsData.ts +++ b/src/contributors/hooks/useContributorsData.ts @@ -1,4 +1,4 @@ -import { usePaginatedQuery, useQuery } from "@blitzjs/rpc" +import { usePaginatedQuery } from "@blitzjs/rpc" import getContributors from "src/contributors/queries/getContributors" import { processContributor, @@ -7,7 +7,6 @@ import { import { MemberPrivileges } from "@prisma/client" import { useMemo } from "react" import { CurrentUser } from "src/users/queries/getCurrentUser" -import { ProjectMemberWithUsers } from "src/core/types" import { PaginationState } from "@tanstack/react-table" type UseContributorsDataResult = { @@ -30,28 +29,14 @@ export function useContributorsData( orderBy: { id: "asc" as const }, } - let contributors: ProjectMemberWithUsers[] = [] - let count = 0 - let refetchFn: () => Promise + const skip = pagination ? pagination.pageIndex * pagination.pageSize : 0 + const take = pagination ? pagination.pageSize : undefined - if (shouldPaginate) { - const [{ contributors: pagedContributors, count: total }, { refetch }] = usePaginatedQuery( - getContributors, - { - ...baseArgs, - skip: pagination!.pageIndex * pagination!.pageSize, - take: pagination!.pageSize, - } - ) - contributors = pagedContributors - count = total - refetchFn = refetch - } else { - const [{ contributors: allContributors }, { refetch }] = useQuery(getContributors, baseArgs) - contributors = allContributors - count = contributors.length - refetchFn = refetch - } + const [{ contributors, count }, { refetch }] = usePaginatedQuery(getContributors, { + ...baseArgs, + skip, + take, + }) const filteredContributors = useMemo(() => { if (privilege === MemberPrivileges.CONTRIBUTOR) { @@ -63,9 +48,12 @@ export function useContributorsData( return contributors }, [contributors, privilege, currentUser.id]) + const resultCount = + privilege === MemberPrivileges.CONTRIBUTOR ? filteredContributors.length : count + return { data: processContributor(filteredContributors, projectId), - count, - refetch: refetchFn, + count: resultCount, + refetch, } } diff --git a/src/contributors/queries/getContributors.ts b/src/contributors/queries/getContributors.ts index 55040522..11a4e1a3 100644 --- a/src/contributors/queries/getContributors.ts +++ b/src/contributors/queries/getContributors.ts @@ -56,12 +56,12 @@ export default resolver.pipe( } = await paginate({ skip, take, - orderBy, count: () => db.projectMember.count({ where: baseWhere }), query: (paginateArgs) => db.projectMember.findMany({ ...paginateArgs, where: baseWhere, + orderBy, include: { users: true, }, diff --git a/src/forms/queries/getForms.ts b/src/forms/queries/getForms.ts index 8ebc4fe0..a296fa6a 100644 --- a/src/forms/queries/getForms.ts +++ b/src/forms/queries/getForms.ts @@ -10,18 +10,21 @@ export interface FormWithFormVersion extends Form { interface GetFormInput extends Pick {} -const includeLatestVersion = (include?: Prisma.FormInclude) => ({ - ...include, - versions: { - orderBy: { version: "desc" }, - take: 1, - }, -}) +const includeLatestVersion = (include?: Prisma.FormInclude | null) => + include === null + ? undefined + : { + ...(include ?? {}), + versions: { + orderBy: [{ version: "desc" as const }], + take: 1, + }, + } -const mapForms = (fetchedForms: (Form & { versions: FormVersion[] })[]): FormWithFormVersion[] => { +const mapForms = (fetchedForms: (Form & { versions?: FormVersion[] })[]): FormWithFormVersion[] => { return fetchedForms.map((form) => ({ ...form, - formVersion: form.versions[0] || null, + formVersion: form.versions?.[0] || null, })) } diff --git a/src/notifications/mutations/deleteNotification.ts b/src/notifications/mutations/deleteNotification.ts index cd0aa09e..9f510fac 100644 --- a/src/notifications/mutations/deleteNotification.ts +++ b/src/notifications/mutations/deleteNotification.ts @@ -2,31 +2,40 @@ import { resolver } from "@blitzjs/rpc" import db, { Prisma } from "db" import { z } from "zod" -const deleteByIdsSchema = z.object({ - ids: z.array(z.number()).min(1), -}) - -const deleteAllSchema = z.object({ - selectAll: z.literal(true), - where: z.custom((value) => typeof value === "object"), -}) +const DeleteNotificationSchema = z + .object({ + ids: z.array(z.number()).optional(), + selectAll: z.boolean().optional(), + where: z.custom((value) => typeof value === "object").optional(), + }) + .refine( + (data) => { + if (data.selectAll) { + return Boolean(data.where) + } + return Array.isArray(data.ids) && data.ids.length > 0 + }, + { + message: "Provide either ids to delete or set selectAll with a where clause.", + } + ) -const DeleteNotificationSchema = z.union([deleteByIdsSchema, deleteAllSchema]) +type DeleteNotificationInput = z.infer export default resolver.pipe( resolver.zod(DeleteNotificationSchema), resolver.authorize(), - async (input) => { - if ("selectAll" in input) { + async (input: DeleteNotificationInput) => { + if (input.selectAll) { await db.notification.deleteMany({ - where: input.where, + where: input.where!, }) return { success: true } } await db.notification.deleteMany({ where: { - id: { in: input.ids }, + id: { in: input.ids! }, }, }) return { success: true } diff --git a/src/pages/notifications/index.tsx b/src/pages/notifications/index.tsx index 0f57a612..af240f53 100644 --- a/src/pages/notifications/index.tsx +++ b/src/pages/notifications/index.tsx @@ -52,7 +52,7 @@ const NotificationContent = () => { }, }, }), - [currentUser?.id] + [currentUser] ) const paginationArgs = useMemo( diff --git a/src/pages/projects/[projectId]/notifications/index.tsx b/src/pages/projects/[projectId]/notifications/index.tsx index 1a55a997..f1a22586 100644 --- a/src/pages/projects/[projectId]/notifications/index.tsx +++ b/src/pages/projects/[projectId]/notifications/index.tsx @@ -41,7 +41,7 @@ const NotificationContent = () => { }, projectId: projectId ?? undefined, }), - [currentUser?.id, projectId] + [currentUser, projectId] ) const paginationArgs = useMemo( From d14a34fbe2a9550cb049e9a772811497d1203a17 Mon Sep 17 00:00:00 2001 From: The Doom Lab Date: Mon, 15 Dec 2025 21:25:20 -0600 Subject: [PATCH 6/7] fixed mismatch of number shown in tasks --- src/tasks/components/AllTaskList.tsx | 70 ++++++++++++++++------------ 1 file changed, 41 insertions(+), 29 deletions(-) diff --git a/src/tasks/components/AllTaskList.tsx b/src/tasks/components/AllTaskList.tsx index 4ecd1e79..21d43c01 100644 --- a/src/tasks/components/AllTaskList.tsx +++ b/src/tasks/components/AllTaskList.tsx @@ -1,13 +1,13 @@ import { useCurrentUser } from "src/users/hooks/useCurrentUser" import { usePaginatedQuery } from "@blitzjs/rpc" -import getTaskLogs from "src/tasklogs/queries/getTaskLogs" +import getTasks from "src/tasks/queries/getTasks" import getLatestTaskLogs from "src/tasklogs/hooks/getLatestTaskLogs" import { processAllTasks } from "../tables/processing/processAllTasks" import Table from "src/core/components/Table" import { AllTasksColumns } from "../tables/columns/AllTasksColumns" import { TaskLogWithTaskProjectAndComments } from "src/core/types" import Card from "src/core/components/Card" -import { useMemo, useState } from "react" +import { useState } from "react" import { PaginationState } from "@tanstack/react-table" export const AllTasksList = () => { @@ -17,36 +17,42 @@ export const AllTasksList = () => { pageSize: 10, }) - const paginationArgs = useMemo( - () => ({ - skip: pagination.pageIndex * pagination.pageSize, - take: pagination.pageSize, - }), - [pagination] - ) - - // Get latest logs that this user is involved in - const [{ taskLogs: fetchedTaskLogs = [], count }] = usePaginatedQuery(getTaskLogs, { + const [{ tasks, count }] = usePaginatedQuery(getTasks, { where: { - assignedTo: { - users: { some: { id: currentUser?.id } }, - deleted: false, + taskLogs: { + some: { + assignedTo: { + users: { + some: { id: currentUser?.id }, + }, + }, + }, }, }, include: { - task: { - include: { - project: true, // Include the project linked to the task + project: true, + taskLogs: { + where: { + assignedTo: { + users: { + some: { id: currentUser?.id }, + }, + }, }, - }, - comments: { include: { - commentReadStatus: { - where: { - projectMember: { - users: { - some: { - id: currentUser?.id, + assignedTo: { + include: { + users: true, + }, + }, + comments: { + include: { + commentReadStatus: { + include: { + projectMember: { + include: { + users: true, + }, }, }, }, @@ -56,11 +62,17 @@ export const AllTasksList = () => { }, }, orderBy: { id: "asc" }, - ...paginationArgs, + skip: pagination.pageIndex * pagination.pageSize, + take: pagination.pageSize, }) - const taskLogs: TaskLogWithTaskProjectAndComments[] = - fetchedTaskLogs as TaskLogWithTaskProjectAndComments[] + const taskLogs: TaskLogWithTaskProjectAndComments[] = tasks.flatMap((task) => { + const { taskLogs: taskLogsForTask, ...taskWithoutLogs } = task + return taskLogsForTask.map((log) => ({ + ...log, + task: taskWithoutLogs, + })) as TaskLogWithTaskProjectAndComments[] + }) // process those logs to get the latest one for each task-projectmemberId const latestLogs = getLatestTaskLogs(taskLogs) From b0cbe02b660df074879ce6e56e4f902e272b2689 Mon Sep 17 00:00:00 2001 From: The Doom Lab Date: Mon, 15 Dec 2025 21:30:11 -0600 Subject: [PATCH 7/7] fixed typescript error --- src/tasks/components/AllTaskList.tsx | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/tasks/components/AllTaskList.tsx b/src/tasks/components/AllTaskList.tsx index 21d43c01..e3158fbe 100644 --- a/src/tasks/components/AllTaskList.tsx +++ b/src/tasks/components/AllTaskList.tsx @@ -10,6 +10,10 @@ import Card from "src/core/components/Card" import { useState } from "react" import { PaginationState } from "@tanstack/react-table" +type TaskWithLogs = TaskLogWithTaskProjectAndComments["task"] & { + taskLogs: TaskLogWithTaskProjectAndComments[] +} + export const AllTasksList = () => { const currentUser = useCurrentUser() const [pagination, setPagination] = useState({ @@ -66,12 +70,13 @@ export const AllTasksList = () => { take: pagination.pageSize, }) - const taskLogs: TaskLogWithTaskProjectAndComments[] = tasks.flatMap((task) => { + const typedTasks = tasks as TaskWithLogs[] + const taskLogs: TaskLogWithTaskProjectAndComments[] = typedTasks.flatMap((task) => { const { taskLogs: taskLogsForTask, ...taskWithoutLogs } = task return taskLogsForTask.map((log) => ({ ...log, task: taskWithoutLogs, - })) as TaskLogWithTaskProjectAndComments[] + })) }) // process those logs to get the latest one for each task-projectmemberId