Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 2 additions & 7 deletions src/comments/queries/getComments.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,17 +8,12 @@ interface GetCommentsInput

export default resolver.pipe(
resolver.authorize(),
async ({
where,
orderBy,
skip = 0,
take = 100,
}: GetCommentsInput): Promise<CommentWithAuthor[]> => {
async ({ where, orderBy, skip = 0, take }: GetCommentsInput): Promise<CommentWithAuthor[]> => {
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: {
Expand Down
6 changes: 3 additions & 3 deletions src/contributors/components/ContributorInformation.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -47,15 +47,15 @@ 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,
},
include: {
task: true,
},
}) as unknown as [TaskLogWithTask[], { refetch: () => Promise<any> }]
})

useEffect(() => {
const handleUpdate = () => {
Expand All @@ -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<TaskLogWithTask>(taskLogs)
Expand Down
46 changes: 36 additions & 10 deletions src/contributors/hooks/useContributorsData.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { useQuery } from "@blitzjs/rpc"
import { usePaginatedQuery } from "@blitzjs/rpc"
import getContributors from "src/contributors/queries/getContributors"
import {
processContributor,
Expand All @@ -7,27 +7,53 @@ 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 = {
data: ContributorTableData[]
count: number
refetch: () => Promise<any>
}

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 },
}

const skip = pagination ? pagination.pageIndex * pagination.pageSize : 0
const take = pagination ? pagination.pageSize : undefined

const [{ contributors, count }, { refetch }] = usePaginatedQuery(getContributors, {
...baseArgs,
skip,
take,
})

// 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)
const resultCount =
privilege === MemberPrivileges.CONTRIBUTOR ? filteredContributors.length : count

return {
data: processContributor(filteredContributors, projectId),
count: resultCount,
refetch,
}
}
84 changes: 61 additions & 23 deletions src/contributors/queries/getContributors.ts
Original file line number Diff line number Diff line change
@@ -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<Prisma.ProjectMemberFindManyArgs, "skip" | "take" | "orderBy"> {
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<ProjectMemberWithUsers[]> => {
// 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,
count: () => db.projectMember.count({ where: baseWhere }),
query: (paginateArgs) =>
db.projectMember.findMany({
...paginateArgs,
where: baseWhere,
orderBy,
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 }
}
)
41 changes: 32 additions & 9 deletions src/core/components/Table.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ import {
useReactTable,
getPaginationRowModel,
getFacetedMinMaxValues,
PaginationState,
OnChangeFn,
} from "@tanstack/react-table"
import React from "react"

Expand Down Expand Up @@ -131,6 +133,11 @@ type TableProps<TData> = {
enableGlobalSearch?: boolean
globalSearchPlaceholder?: string
addPagination?: boolean
manualPagination?: boolean
paginationState?: PaginationState
onPaginationChange?: OnChangeFn<PaginationState>
pageCount?: number
pageSizeOptions?: number[]
classNames?: {
table?: string
thead?: string
Expand Down Expand Up @@ -179,9 +186,26 @@ const Table = <TData,>({
enableGlobalSearch = true,
globalSearchPlaceholder = "Search...",
addPagination = false,
manualPagination = false,
paginationState,
onPaginationChange,
pageCount: controlledPageCount,
pageSizeOptions = [5, 10, 20, 30, 40, 50],
}: TableProps<TData>) => {
const [sorting, setSorting] = React.useState([])
const [globalFilter, setGlobalFilter] = React.useState("")
const [internalPagination, setInternalPagination] = React.useState<PaginationState>({
pageIndex: 0,
pageSize: 5,
})

const resolvedPaginationState = manualPagination
? paginationState ?? { pageIndex: 0, pageSize: 5 }
: internalPagination

const handlePaginationChange: OnChangeFn<PaginationState> = manualPagination
? onPaginationChange ?? (() => {})
: setInternalPagination

const table = useReactTable({
data,
Expand All @@ -192,19 +216,18 @@ const Table = <TData,>({
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,
})
Expand All @@ -220,10 +243,10 @@ const Table = <TData,>({
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 (
<>
Expand Down Expand Up @@ -386,7 +409,7 @@ const Table = <TData,>({
classNames?.pageSizeSelect || ""
}`}
>
{[5, 10, 20, 30, 40, 50].map((pageSize) => (
{pageSizeOptions.map((pageSize) => (
<option key={pageSize} value={pageSize}>
Show {pageSize}
</option>
Expand Down
6 changes: 4 additions & 2 deletions src/core/components/fields/MultiSelectCheckbox.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
<div>
Expand All @@ -15,7 +16,8 @@ export const MultiSelectCheckbox = React.memo(({ id }: MultiSelectCheckboxProps)
<input
type="checkbox"
className="checkbox checkbox-primary border-2"
checked={selectedIds.includes(id)}
checked={isChecked}
disabled={isGlobalSelection}
onChange={() => toggleSelection(id)}
/>
</label>
Expand Down
30 changes: 28 additions & 2 deletions src/core/components/fields/MultiSelectContext.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -14,26 +17,49 @@ const MultiSelectContext = createContext<MultiSelectContextType | undefined>(und
// Context provider component
export const MultiSelectProvider = ({ children }: { children?: ReactNode }) => {
const [selectedIds, setSelectedIds] = useState<number[]>([])
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]
)
}

// 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 (
<MultiSelectContext.Provider
value={{ selectedIds, toggleSelection, resetSelection, handleBulkSelection }}
value={{
selectedIds,
toggleSelection,
resetSelection,
handleBulkSelection,
isGlobalSelection,
enableGlobalSelection,
disableGlobalSelection,
}}
>
{children}
</MultiSelectContext.Provider>
Expand Down
Loading