diff --git a/src/__mocks__/handlers/user/createUser.mock.ts b/src/__mocks__/handlers/user/createUser.mock.ts deleted file mode 100644 index 0ff2368c..00000000 --- a/src/__mocks__/handlers/user/createUser.mock.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { HttpResponse } from "msw"; - -import { mockCreateUserMutation } from "generated/graphql.mock"; - -/** - * Create user mutation (success) mock. - */ -export const mockCreateUserMutationSuccess = mockCreateUserMutation( - ({ variables }) => { - const { hidraId, username, firstName, lastName } = variables; - - return HttpResponse.json({ - data: { - createUser: { - id: "1dc43c0f-5140-43e9-a646-b144305d7787", - hidraId, - firstName, - lastName, - username, - }, - }, - }); - }, -); - -// TODO mock error as well -// export const mockCreateUserMutationError = mockCreateUserQuery(({ variables }) => {}); diff --git a/src/__mocks__/handlers/user/index.ts b/src/__mocks__/handlers/user/index.ts index cb260caf..a71370e7 100644 --- a/src/__mocks__/handlers/user/index.ts +++ b/src/__mocks__/handlers/user/index.ts @@ -1,2 +1 @@ -export * from "./createUser.mock"; export * from "./user.mock"; diff --git a/src/app/organizations/[organizationSlug]/(manage)/layout.tsx b/src/app/organizations/[organizationSlug]/(manage)/layout.tsx index 501a00e2..d912ef6a 100644 --- a/src/app/organizations/[organizationSlug]/(manage)/layout.tsx +++ b/src/app/organizations/[organizationSlug]/(manage)/layout.tsx @@ -25,8 +25,6 @@ const ManageOrganizationLayout = async ({ params, children }: Props) => { const session = await auth(); - if (!session) notFound(); - const organization = await getOrganization({ organizationSlug }); if (!organization) notFound(); @@ -38,22 +36,26 @@ const ManageOrganizationLayout = async ({ params, children }: Props) => { queryKey: useOrganizationQuery.getKey({ slug: organizationSlug }), queryFn: useOrganizationQuery.fetcher({ slug: organizationSlug }), }), - queryClient.prefetchQuery({ - queryKey: useOrganizationRoleQuery.getKey({ - userId: session.user.rowId!, - organizationId: organization.rowId, - }), - queryFn: useOrganizationRoleQuery.fetcher({ - userId: session.user.rowId!, - organizationId: organization.rowId, - }), - }), + ...(session + ? [ + queryClient.prefetchQuery({ + queryKey: useOrganizationRoleQuery.getKey({ + userId: session.user.rowId!, + organizationId: organization.rowId, + }), + queryFn: useOrganizationRoleQuery.fetcher({ + userId: session.user.rowId!, + organizationId: organization.rowId, + }), + }), + ] + : []), ]); return ( - {children} + {children} ); diff --git a/src/app/organizations/[organizationSlug]/(manage)/members/page.tsx b/src/app/organizations/[organizationSlug]/(manage)/members/page.tsx index 9d402d74..1b6e755a 100644 --- a/src/app/organizations/[organizationSlug]/(manage)/members/page.tsx +++ b/src/app/organizations/[organizationSlug]/(manage)/members/page.tsx @@ -21,6 +21,7 @@ import { getSdk } from "lib/graphql"; import { getQueryClient, getSearchParams } from "lib/util"; import { DialogType } from "store"; +import type { Member } from "generated/graphql"; import type { SearchParams } from "nuqs/server"; export const generateMetadata = async ({ params }: Props) => { @@ -50,8 +51,6 @@ const OrganizationMembersPage = async ({ params, searchParams }: Props) => { const session = await auth(); - if (!session) notFound(); - const organization = await getOrganization({ organizationSlug, }); @@ -60,12 +59,17 @@ const OrganizationMembersPage = async ({ params, searchParams }: Props) => { const sdk = getSdk({ session }); - const { memberByUserIdAndOrganizationId: member } = - await sdk.OrganizationRole({ - userId: session.user.rowId!, + let member: Partial | null = null; + + if (session) { + const { memberByUserIdAndOrganizationId } = await sdk.OrganizationRole({ + userId: session?.user.rowId!, organizationId: organization.rowId, }); + member = memberByUserIdAndOrganizationId ?? null; + } + const queryClient = getQueryClient(); const { search, roles } = await getSearchParams.parse(searchParams); @@ -95,16 +99,20 @@ const OrganizationMembersPage = async ({ params, searchParams }: Props) => { excludeRoles: [Role.Owner], }), }), - queryClient.prefetchQuery({ - queryKey: useOrganizationRoleQuery.getKey({ - organizationId: organization.rowId, - userId: session.user.rowId!, - }), - queryFn: useOrganizationRoleQuery.fetcher({ - organizationId: organization.rowId, - userId: session.user.rowId!, - }), - }), + ...(session + ? [ + queryClient.prefetchQuery({ + queryKey: useOrganizationRoleQuery.getKey({ + organizationId: organization.rowId, + userId: session.user.rowId!, + }), + queryFn: useOrganizationRoleQuery.fetcher({ + organizationId: organization.rowId, + userId: session.user.rowId!, + }), + }), + ] + : []), ]); return ( @@ -132,7 +140,7 @@ const OrganizationMembersPage = async ({ params, searchParams }: Props) => { - + {/* dialogs */} {/* TODO: allow adding owners when transferring ownership is resolved. Restricting to single ownership for now. */} diff --git a/src/app/organizations/[organizationSlug]/(manage)/settings/page.tsx b/src/app/organizations/[organizationSlug]/(manage)/settings/page.tsx index fc8f8893..4542d0de 100644 --- a/src/app/organizations/[organizationSlug]/(manage)/settings/page.tsx +++ b/src/app/organizations/[organizationSlug]/(manage)/settings/page.tsx @@ -37,13 +37,12 @@ interface Props { const OrganizationSettingsPage = async ({ params }: Props) => { const { organizationSlug } = await params; - const session = await auth(); - - if (!session) notFound(); - - const organization = await getOrganization({ organizationSlug }); + const [session, organization] = await Promise.all([ + auth(), + getOrganization({ organizationSlug }), + ]); - if (!organization) notFound(); + if (!session || !organization) notFound(); const sdk = getSdk({ session }); @@ -58,23 +57,23 @@ const OrganizationSettingsPage = async ({ params }: Props) => { await Promise.all([ queryClient.prefetchQuery({ - queryKey: useOrganizationRoleQuery.getKey({ - userId: session.user.rowId!, + queryKey: useMembersQuery.getKey({ organizationId: organization.rowId, + roles: [Role.Owner], }), - queryFn: useOrganizationRoleQuery.fetcher({ - userId: session.user.rowId!, + queryFn: useMembersQuery.fetcher({ organizationId: organization.rowId, + roles: [Role.Owner], }), }), queryClient.prefetchQuery({ - queryKey: useMembersQuery.getKey({ + queryKey: useOrganizationRoleQuery.getKey({ + userId: session.user.rowId!, organizationId: organization.rowId, - roles: [Role.Owner], }), - queryFn: useMembersQuery.fetcher({ + queryFn: useOrganizationRoleQuery.fetcher({ + userId: session.user.rowId!, organizationId: organization.rowId, - roles: [Role.Owner], }), }), ]); diff --git a/src/app/organizations/[organizationSlug]/page.tsx b/src/app/organizations/[organizationSlug]/page.tsx index c896ad5e..3788a356 100644 --- a/src/app/organizations/[organizationSlug]/page.tsx +++ b/src/app/organizations/[organizationSlug]/page.tsx @@ -26,6 +26,7 @@ import { getQueryClient } from "lib/util"; import { DialogType } from "store"; import type { BreadcrumbRecord } from "components/core"; +import type { Member } from "generated/graphql"; export const generateMetadata = async ({ params }: Props) => { const { organizationSlug } = await params; @@ -52,8 +53,6 @@ const OrganizationPage = async ({ params }: Props) => { const session = await auth(); - if (!session) notFound(); - const [ organization, { isOwnerSubscribed, hasBasicTierPrivileges, hasTeamTierPrivileges }, @@ -66,12 +65,17 @@ const OrganizationPage = async ({ params }: Props) => { const sdk = getSdk({ session }); - const { memberByUserIdAndOrganizationId: member } = - await sdk.OrganizationRole({ - userId: session.user.rowId!, + let member: Partial | null = null; + + if (session) { + const { memberByUserIdAndOrganizationId } = await sdk.OrganizationRole({ + userId: session?.user.rowId!, organizationId: organization.rowId, }); + member = memberByUserIdAndOrganizationId ?? null; + } + const hasAdminPrivileges = member?.role === Role.Admin || member?.role === Role.Owner; @@ -109,16 +113,20 @@ const OrganizationPage = async ({ params }: Props) => { organizationId: organization.rowId, }), }), - queryClient.prefetchQuery({ - queryKey: useOrganizationRoleQuery.getKey({ - organizationId: organization.rowId, - userId: session.user.rowId!, - }), - queryFn: useOrganizationRoleQuery.fetcher({ - organizationId: organization.rowId, - userId: session.user.rowId!, - }), - }), + ...(session + ? [ + queryClient.prefetchQuery({ + queryKey: useOrganizationRoleQuery.getKey({ + organizationId: organization.rowId, + userId: session.user.rowId!, + }), + queryFn: useOrganizationRoleQuery.fetcher({ + organizationId: organization.rowId, + userId: session.user.rowId!, + }), + }), + ] + : []), ]); return ( @@ -162,7 +170,7 @@ const OrganizationPage = async ({ params }: Props) => { diff --git a/src/app/organizations/[organizationSlug]/projects/[projectSlug]/[feedbackId]/page.tsx b/src/app/organizations/[organizationSlug]/projects/[projectSlug]/[feedbackId]/page.tsx index d0b85e53..ad35bfc0 100644 --- a/src/app/organizations/[organizationSlug]/projects/[projectSlug]/[feedbackId]/page.tsx +++ b/src/app/organizations/[organizationSlug]/projects/[projectSlug]/[feedbackId]/page.tsx @@ -5,17 +5,20 @@ import { auth } from "auth"; import { Comments, FeedbackDetails } from "components/feedback"; import { Page } from "components/layout"; import { + Role, useCommentsQuery, - useFeedbackByIdQuery, useInfiniteCommentsQuery, useOrganizationRoleQuery, + useProjectStatusesQuery, } from "generated/graphql"; import { getFeedback } from "lib/actions"; import { app } from "lib/config"; -import { freeTierCommentsOptions } from "lib/options"; +import { getSdk } from "lib/graphql"; +import { feedbackByIdOptions, freeTierCommentsOptions } from "lib/options"; import { getQueryClient } from "lib/util"; import type { BreadcrumbRecord } from "components/core"; +import type { Member } from "generated/graphql"; export const metadata = { title: app.feedbackPage.breadcrumb, @@ -38,12 +41,25 @@ const FeedbackPage = async ({ params }: Props) => { const session = await auth(); - if (!session) notFound(); - const feedback = await getFeedback({ feedbackId }); if (!feedback) notFound(); + let member: Partial | null = null; + + if (session) { + const sdk = getSdk({ session }); + + const { memberByUserIdAndOrganizationId } = await sdk.OrganizationRole({ + userId: session?.user?.rowId!, + organizationId: feedback.project?.organization?.rowId!, + }); + + member = memberByUserIdAndOrganizationId ?? null; + } + + const isAdmin = member?.role === Role.Admin || member?.role === Role.Owner; + const queryClient = getQueryClient(); const breadcrumbs: BreadcrumbRecord[] = [ @@ -69,43 +85,52 @@ const FeedbackPage = async ({ params }: Props) => { ]; await Promise.all([ - queryClient.prefetchQuery({ - queryKey: useFeedbackByIdQuery.getKey({ - rowId: feedbackId, - userId: session.user.rowId, - }), - queryFn: useFeedbackByIdQuery.fetcher({ + queryClient.prefetchQuery( + feedbackByIdOptions({ rowId: feedbackId, - userId: session.user.rowId, + userId: session?.user.rowId, }), - }), - queryClient.prefetchQuery( - freeTierCommentsOptions({ projectSlug, organizationSlug, feedbackId }), ), - queryClient.prefetchQuery({ - queryKey: useOrganizationRoleQuery.getKey({ - userId: session.user.rowId!, - organizationId: feedback.project?.organization?.rowId!, - }), - queryFn: useOrganizationRoleQuery.fetcher({ - userId: session.user.rowId!, - organizationId: feedback.project?.organization?.rowId!, - }), - }), queryClient.prefetchInfiniteQuery({ queryKey: useInfiniteCommentsQuery.getKey({ feedbackId }), queryFn: useCommentsQuery.fetcher({ feedbackId }), initialPageParam: undefined, }), + queryClient.prefetchQuery( + freeTierCommentsOptions({ projectSlug, organizationSlug, feedbackId }), + ), + // ! NB: only prefetch the project statuses if the user is an admin + ...(isAdmin + ? [ + queryClient.prefetchQuery({ + queryKey: useProjectStatusesQuery.getKey({ + projectId: feedback.project?.rowId!, + }), + queryFn: useProjectStatusesQuery.fetcher({ + projectId: feedback.project?.rowId!, + }), + }), + queryClient.prefetchQuery({ + queryKey: useOrganizationRoleQuery.getKey({ + userId: session?.user.rowId!, + organizationId: feedback.project?.organization?.rowId!, + }), + queryFn: useOrganizationRoleQuery.fetcher({ + userId: session?.user.rowId!, + organizationId: feedback.project?.organization?.rowId!, + }), + }), + ] + : []), ]); return ( - + diff --git a/src/app/organizations/[organizationSlug]/projects/[projectSlug]/page.tsx b/src/app/organizations/[organizationSlug]/projects/[projectSlug]/page.tsx index 31cefad2..38767cf7 100644 --- a/src/app/organizations/[organizationSlug]/projects/[projectSlug]/page.tsx +++ b/src/app/organizations/[organizationSlug]/projects/[projectSlug]/page.tsx @@ -7,23 +7,24 @@ import { auth } from "auth"; import { Page } from "components/layout"; import { ProjectLinks, ProjectOverview } from "components/project"; import { - PostOrderBy, Role, - useInfinitePostsQuery, useOrganizationRoleQuery, - usePostsQuery, useProjectMetricsQuery, - useProjectQuery, useProjectStatusesQuery, useStatusBreakdownQuery, } from "generated/graphql"; import { getProject } from "lib/actions"; import { app } from "lib/config"; import { getSdk } from "lib/graphql"; -import { freeTierFeedbackOptions } from "lib/options"; +import { + freeTierFeedbackOptions, + infinitePostsOptions, + projectOptions, +} from "lib/options"; import { getQueryClient, getSearchParams } from "lib/util"; import type { BreadcrumbRecord } from "components/core"; +import type { Member } from "generated/graphql"; import type { SearchParams } from "nuqs/server"; export const generateMetadata = async ({ params }: Props) => { @@ -54,18 +55,22 @@ const ProjectPage = async ({ params, searchParams }: Props) => { const session = await auth(); - if (!session) notFound(); - const project = await getProject({ organizationSlug, projectSlug }); if (!project) notFound(); const sdk = getSdk({ session }); - const { memberByUserIdAndOrganizationId } = await sdk.OrganizationRole({ - userId: session.user?.rowId!, - organizationId: project.organization?.rowId!, - }); + let member: Partial | null = null; + + if (session) { + const { memberByUserIdAndOrganizationId } = await sdk.OrganizationRole({ + userId: session?.user.rowId!, + organizationId: project.organization?.rowId!, + }); + + member = memberByUserIdAndOrganizationId ?? null; + } const { excludedStatuses, orderBy, search } = await getSearchParams.parse(searchParams); @@ -91,50 +96,39 @@ const ProjectPage = async ({ params, searchParams }: Props) => { ]; await Promise.all([ - queryClient.prefetchQuery({ - queryKey: useProjectQuery.getKey({ - projectSlug, - organizationSlug, - }), - queryFn: useProjectQuery.fetcher({ + queryClient.prefetchQuery( + projectOptions({ projectSlug, organizationSlug, + userId: session?.user.rowId, }), - }), + ), queryClient.prefetchQuery( freeTierFeedbackOptions({ organizationSlug, projectSlug }), ), - queryClient.prefetchInfiniteQuery({ - queryKey: useInfinitePostsQuery.getKey({ - projectId: project.rowId, - excludedStatuses, - orderBy: orderBy - ? [orderBy as PostOrderBy, PostOrderBy.CreatedAtDesc] - : undefined, - search, - userId: session.user.rowId, - }), - queryFn: usePostsQuery.fetcher({ + queryClient.prefetchInfiniteQuery( + infinitePostsOptions({ projectId: project.rowId, + userId: session?.user.rowId, excludedStatuses, - orderBy: orderBy - ? [orderBy as PostOrderBy, PostOrderBy.CreatedAtDesc] - : undefined, + orderBy, search, - userId: session.user.rowId, }), - initialPageParam: undefined, - }), - queryClient.prefetchQuery({ - queryKey: useOrganizationRoleQuery.getKey({ - userId: session.user.rowId!, - organizationId: project.organization?.rowId!, - }), - queryFn: useOrganizationRoleQuery.fetcher({ - userId: session.user.rowId!, - organizationId: project.organization?.rowId!, - }), - }), + ), + ...(session + ? [ + queryClient.prefetchQuery({ + queryKey: useOrganizationRoleQuery.getKey({ + userId: session.user.rowId!, + organizationId: project.organization?.rowId!, + }), + queryFn: useOrganizationRoleQuery.fetcher({ + userId: session.user.rowId!, + organizationId: project.organization?.rowId!, + }), + }), + ] + : []), queryClient.prefetchQuery({ queryKey: useProjectMetricsQuery.getKey({ projectId: project.rowId }), queryFn: useProjectMetricsQuery.fetcher({ projectId: project.rowId }), @@ -149,9 +143,7 @@ const ProjectPage = async ({ params, searchParams }: Props) => { }), ]); - const hasAdminPrivileges = - memberByUserIdAndOrganizationId && - memberByUserIdAndOrganizationId.role !== Role.Member; + const hasAdminPrivileges = member && member.role !== Role.Member; return ( @@ -184,7 +176,7 @@ const ProjectPage = async ({ params, searchParams }: Props) => { ], }} > - + ); diff --git a/src/app/organizations/[organizationSlug]/projects/[projectSlug]/settings/page.tsx b/src/app/organizations/[organizationSlug]/projects/[projectSlug]/settings/page.tsx index 003c80f1..ff133a49 100644 --- a/src/app/organizations/[organizationSlug]/projects/[projectSlug]/settings/page.tsx +++ b/src/app/organizations/[organizationSlug]/projects/[projectSlug]/settings/page.tsx @@ -4,14 +4,11 @@ import { notFound } from "next/navigation"; import { auth } from "auth"; import { Page } from "components/layout"; import { ProjectSettings } from "components/project"; -import { - Role, - useProjectQuery, - useProjectStatusesQuery, -} from "generated/graphql"; +import { Role, useProjectStatusesQuery } from "generated/graphql"; import { getProject } from "lib/actions"; import { app, isDevEnv } from "lib/config"; import { getSdk } from "lib/graphql"; +import { projectOptions } from "lib/options"; import { getQueryClient } from "lib/util"; import type { BreadcrumbRecord } from "components/core"; @@ -86,10 +83,13 @@ const ProjectSettingsPage = async ({ params }: Props) => { ]; await Promise.all([ - queryClient.prefetchQuery({ - queryKey: useProjectQuery.getKey({ projectSlug, organizationSlug }), - queryFn: useProjectQuery.fetcher({ projectSlug, organizationSlug }), - }), + queryClient.prefetchQuery( + projectOptions({ + projectSlug, + organizationSlug, + userId: session?.user.rowId, + }), + ), // TODO: when ready to implement for production, remove the development environment check ...(isDevEnv ? [ diff --git a/src/app/organizations/[organizationSlug]/projects/page.tsx b/src/app/organizations/[organizationSlug]/projects/page.tsx index 58ae18ad..b82c4a8c 100644 --- a/src/app/organizations/[organizationSlug]/projects/page.tsx +++ b/src/app/organizations/[organizationSlug]/projects/page.tsx @@ -14,7 +14,7 @@ import { getQueryClient, getSearchParams } from "lib/util"; import { DialogType } from "store"; import type { BreadcrumbRecord } from "components/core"; -import type { ProjectsQueryVariables } from "generated/graphql"; +import type { Member, ProjectsQueryVariables } from "generated/graphql"; import type { SearchParams } from "nuqs/server"; export const generateMetadata = async ({ params }: Props) => { @@ -44,8 +44,6 @@ const ProjectsPage = async ({ params, searchParams }: Props) => { const session = await auth(); - if (!session) notFound(); - const [ organization, { isOwnerSubscribed, hasBasicTierPrivileges, hasTeamTierPrivileges }, @@ -58,12 +56,17 @@ const ProjectsPage = async ({ params, searchParams }: Props) => { const sdk = getSdk({ session }); - const { memberByUserIdAndOrganizationId: member } = - await sdk.OrganizationRole({ - userId: session.user.rowId!, + let member: Partial | null = null; + + if (session) { + const { memberByUserIdAndOrganizationId } = await sdk.OrganizationRole({ + userId: session?.user.rowId!, organizationId: organization.rowId, }); + member = memberByUserIdAndOrganizationId ?? null; + } + const hasAdminPrivileges = member?.role === Role.Admin || member?.role === Role.Owner; @@ -101,12 +104,10 @@ const ProjectsPage = async ({ params, searchParams }: Props) => { search, }; - await Promise.all([ - queryClient.prefetchQuery({ - queryKey: useProjectsQuery.getKey(variables), - queryFn: useProjectsQuery.fetcher(variables), - }), - ]); + await queryClient.prefetchQuery({ + queryKey: useProjectsQuery.getKey(variables), + queryFn: useProjectsQuery.fetcher(variables), + }); return ( @@ -131,7 +132,7 @@ const ProjectsPage = async ({ params, searchParams }: Props) => { diff --git a/src/app/organizations/page.tsx b/src/app/organizations/page.tsx index c0af362e..c033c434 100644 --- a/src/app/organizations/page.tsx +++ b/src/app/organizations/page.tsx @@ -1,5 +1,4 @@ import { HydrationBoundary, dehydrate } from "@tanstack/react-query"; -import { notFound } from "next/navigation"; import { auth } from "auth"; import { OrganizationsOverview } from "components/organization"; @@ -32,8 +31,6 @@ interface Props { const OrganizationsPage = async ({ searchParams }: Props) => { const session = await auth(); - if (!session) notFound(); - const queryClient = getQueryClient(); const { page, pageSize, search } = await getSearchParams.parse(searchParams); @@ -51,27 +48,31 @@ const OrganizationsPage = async ({ searchParams }: Props) => { queryKey: useOrganizationsQuery.getKey(organizationsQueryVariables), queryFn: useOrganizationsQuery.fetcher(organizationsQueryVariables), }), - queryClient.prefetchQuery({ - queryKey: useOrganizationsQuery.getKey({ - pageSize: 1, - userId: organizationsQueryVariables.userId, - excludeRoles: [Role.Member, Role.Admin], - }), - queryFn: useOrganizationsQuery.fetcher({ - pageSize: 1, - userId: organizationsQueryVariables.userId, - excludeRoles: [Role.Member, Role.Admin], - }), - }), - queryClient.prefetchQuery({ - queryKey: useUserQuery.getKey({ hidraId: session.user.hidraId! }), - queryFn: useUserQuery.fetcher({ hidraId: session.user.hidraId! }), - }), + ...(session + ? [ + queryClient.prefetchQuery({ + queryKey: useOrganizationsQuery.getKey({ + pageSize: 1, + userId: session?.user.rowId, + excludeRoles: [Role.Member, Role.Admin], + }), + queryFn: useOrganizationsQuery.fetcher({ + pageSize: 1, + userId: session?.user.rowId, + excludeRoles: [Role.Member, Role.Admin], + }), + }), + queryClient.prefetchQuery({ + queryKey: useUserQuery.getKey({ hidraId: session?.user.hidraId! }), + queryFn: useUserQuery.fetcher({ hidraId: session?.user.hidraId! }), + }), + ] + : []), ]); return ( - + ); }; diff --git a/src/components/dashboard/FeedbackOverview/FeedbackOverview.tsx b/src/components/dashboard/FeedbackOverview/FeedbackOverview.tsx index dfbd20c6..96b52d5a 100644 --- a/src/components/dashboard/FeedbackOverview/FeedbackOverview.tsx +++ b/src/components/dashboard/FeedbackOverview/FeedbackOverview.tsx @@ -76,7 +76,6 @@ const FeedbackOverview = ({ user, oneWeekAgo }: Props) => { return ( ( ( top={0} w="full" backgroundColor="background.subtle" + borderTopRadius="lg" fontSize="2xl" fontWeight="semibold" boxShadow="xs" diff --git a/src/components/dashboard/RecentFeedback/RecentFeedback.tsx b/src/components/dashboard/RecentFeedback/RecentFeedback.tsx index f2458000..3cb9aa56 100644 --- a/src/components/dashboard/RecentFeedback/RecentFeedback.tsx +++ b/src/components/dashboard/RecentFeedback/RecentFeedback.tsx @@ -55,10 +55,10 @@ const RecentFeedback = ({ user }: Props) => { {isError ? ( @@ -69,12 +69,19 @@ const RecentFeedback = ({ user }: Props) => { w="full" /> ) : ( - // NB: the margin is necessary to prevent clipping of the card borders/box shadows - + {isLoading ? ( ) : recentFeedback?.length ? ( - + {recentFeedback?.map((feedback) => ( { /> )} - {!!recentFeedback?.length && } + {!!recentFeedback?.length && ( + // NB: the width override is necessary to prevent the mask from clipping box shadows + + )} )} diff --git a/src/components/feedback/CommentCard/CommentCard.tsx b/src/components/feedback/CommentCard/CommentCard.tsx index df231bee..186051e2 100644 --- a/src/components/feedback/CommentCard/CommentCard.tsx +++ b/src/components/feedback/CommentCard/CommentCard.tsx @@ -23,11 +23,11 @@ import { DestructiveAction } from "components/core"; import { CreateReply, Replies } from "components/feedback"; import { useDeleteCommentMutation, - useFeedbackByIdQuery, useInfiniteCommentsQuery, } from "generated/graphql"; import { app } from "lib/config"; import { useOrganizationMembership } from "lib/hooks"; +import { feedbackByIdOptions } from "lib/options"; import { setSingularOrPlural } from "lib/util"; import type { StackProps } from "@omnidev/sigil"; @@ -87,12 +87,12 @@ const CommentCard = ({ }), }), queryClient.invalidateQueries({ queryKey: ["Posts.infinite"] }), - queryClient.invalidateQueries({ - queryKey: useFeedbackByIdQuery.getKey({ + queryClient.invalidateQueries( + feedbackByIdOptions({ rowId: feedbackId, userId: user?.rowId, }), - }), + ), ]), }); diff --git a/src/components/feedback/Comments/Comments.tsx b/src/components/feedback/Comments/Comments.tsx index fa0a1695..b280a769 100644 --- a/src/components/feedback/Comments/Comments.tsx +++ b/src/components/feedback/Comments/Comments.tsx @@ -26,7 +26,7 @@ import type { Session } from "next-auth"; interface Props { /** Authenticated user. */ - user: Session["user"]; + user: Session["user"] | undefined; /** Organization ID. */ organizationId: Organization["rowId"]; /** Feedback ID. */ @@ -139,7 +139,7 @@ const Comments = ({ user, organizationId, feedbackId }: Props) => { user={user} comment={comment!} organizationId={organizationId} - canReply={canCreateComment ?? false} + canReply={!!user && !!canCreateComment} w="full" minH={21} /> diff --git a/src/components/feedback/CreateComment/CreateComment.tsx b/src/components/feedback/CreateComment/CreateComment.tsx index bfef2372..853b7135 100644 --- a/src/components/feedback/CreateComment/CreateComment.tsx +++ b/src/components/feedback/CreateComment/CreateComment.tsx @@ -9,13 +9,12 @@ import { z } from "zod"; import { CharacterLimit } from "components/core"; import { useCreateCommentMutation, - useFeedbackByIdQuery, useInfiniteCommentsQuery, } from "generated/graphql"; import { app } from "lib/config"; import { DEBOUNCE_TIME, uuidSchema } from "lib/constants"; import { useForm } from "lib/hooks"; -import { freeTierCommentsOptions } from "lib/options"; +import { feedbackByIdOptions, freeTierCommentsOptions } from "lib/options"; import { toaster } from "lib/util"; import type { Session } from "next-auth"; @@ -68,12 +67,12 @@ const CreateComment = ({ user, canCreateComment }: Props) => { feedbackId, }), ), - queryClient.invalidateQueries({ - queryKey: useFeedbackByIdQuery.getKey({ + queryClient.invalidateQueries( + feedbackByIdOptions({ rowId: feedbackId, userId: user?.rowId, }), - }), + ), ]); return queryClient.invalidateQueries({ @@ -145,7 +144,11 @@ const CreateComment = ({ user, canCreateComment }: Props) => { fontSize="sm" minH={16} disabled={!user || !canCreateComment} - tooltip={app.feedbackPage.comments.disabled} + tooltip={ + user + ? app.feedbackPage.comments.disabled.signedIn + : app.feedbackPage.comments.disabled.signedOut + } maxLength={MAX_COMMENT_LENGTH} errorProps={{ top: -6, diff --git a/src/components/feedback/CreateFeedback/CreateFeedback.tsx b/src/components/feedback/CreateFeedback/CreateFeedback.tsx index e4c347e5..89a77fc9 100644 --- a/src/components/feedback/CreateFeedback/CreateFeedback.tsx +++ b/src/components/feedback/CreateFeedback/CreateFeedback.tsx @@ -10,15 +10,14 @@ import { CharacterLimit } from "components/core"; import { useCreateFeedbackMutation, useProjectMetricsQuery, - useProjectQuery, useProjectStatusesQuery, useStatusBreakdownQuery, } from "generated/graphql"; import { app } from "lib/config"; import { DEBOUNCE_TIME, uuidSchema } from "lib/constants"; import { useForm } from "lib/hooks"; +import { freeTierFeedbackOptions, projectOptions } from "lib/options"; import { useDialogStore } from "lib/hooks/store"; -import { freeTierFeedbackOptions } from "lib/options"; import { toaster } from "lib/util"; import { DialogType } from "store"; @@ -72,16 +71,15 @@ const CreateFeedback = ({ user }: Props) => { freeTierFeedbackOptions({ organizationSlug, projectSlug }), ); - const { data: projectId } = useProjectQuery( - { + const { data: projectId } = useQuery({ + ...projectOptions({ projectSlug, organizationSlug, - }, - { - enabled: !!projectSlug && !!organizationSlug, - select: (data) => data?.projects?.nodes?.[0]?.rowId, - }, - ); + userId: user?.rowId, + }), + enabled: !!projectSlug && !!organizationSlug, + select: (data) => data?.projects?.nodes?.[0]?.rowId, + }); const { data: defaultStatusId } = useProjectStatusesQuery( { diff --git a/src/components/feedback/CreateReply/CreateReply.tsx b/src/components/feedback/CreateReply/CreateReply.tsx index e22ea010..75741598 100644 --- a/src/components/feedback/CreateReply/CreateReply.tsx +++ b/src/components/feedback/CreateReply/CreateReply.tsx @@ -9,7 +9,6 @@ import { z } from "zod"; import { CharacterLimit } from "components/core"; import { useCreateCommentMutation, - useFeedbackByIdQuery, useInfiniteCommentsQuery, useInfiniteRepliesQuery, } from "generated/graphql"; @@ -17,7 +16,7 @@ import { token } from "generated/panda/tokens"; import { app } from "lib/config"; import { DEBOUNCE_TIME, uuidSchema } from "lib/constants"; import { useAuth, useForm } from "lib/hooks"; -import { freeTierCommentsOptions } from "lib/options"; +import { feedbackByIdOptions, freeTierCommentsOptions } from "lib/options"; import { toaster } from "lib/util"; import type { CollapsibleProps } from "@omnidev/sigil"; @@ -75,12 +74,12 @@ const CreateReply = ({ commentId, canReply, onReply, ...rest }: Props) => { feedbackId, }), }), - queryClient.invalidateQueries({ - queryKey: useFeedbackByIdQuery.getKey({ + queryClient.invalidateQueries( + feedbackByIdOptions({ rowId: feedbackId, userId: user?.rowId, }), - }), + ), queryClient.invalidateQueries( freeTierCommentsOptions({ organizationSlug, diff --git a/src/components/feedback/FeedbackCard/FeedbackCard.tsx b/src/components/feedback/FeedbackCard/FeedbackCard.tsx index 29131677..52ac8a79 100644 --- a/src/components/feedback/FeedbackCard/FeedbackCard.tsx +++ b/src/components/feedback/FeedbackCard/FeedbackCard.tsx @@ -21,13 +21,10 @@ import { DestructiveAction, StatusBadge } from "components/core"; import { UpdateFeedback, VotingButtons } from "components/feedback"; import { useDeletePostMutation, - useFeedbackByIdQuery, - useInfinitePostsQuery, useProjectMetricsQuery, useStatusBreakdownQuery, useUpdatePostMutation, } from "generated/graphql"; -import { app } from "lib/config"; import { useSearchParams } from "lib/hooks"; import { useStatusMenuStore } from "lib/hooks/store"; @@ -36,10 +33,12 @@ import type { InfiniteData } from "@tanstack/react-query"; import type { FeedbackByIdQuery, FeedbackFragment, - PostOrderBy, PostStatus, PostsQuery, } from "generated/graphql"; +import { app } from "lib/config"; +import { feedbackByIdOptions, infinitePostsOptions } from "lib/options"; + import type { Session } from "next-auth"; interface ProjectStatus { @@ -130,76 +129,78 @@ const FeedbackCard = ({ const { mutate: updateStatus, isPending: isUpdateStatusPending } = useUpdatePostMutation({ onMutate: (variables) => { + const feedbackKey = feedbackByIdOptions({ + rowId: feedback.rowId!, + userId: user?.rowId, + }).queryKey; + const feedbackSnapshot = queryClient.getQueryData( - useFeedbackByIdQuery.getKey({ - rowId: feedback.rowId!, - userId: user?.rowId, - }), + feedbackKey, ) as FeedbackByIdQuery; - const postsQueryKey = useInfinitePostsQuery.getKey({ + const postsQueryKey = infinitePostsOptions({ projectId: feedback.project?.rowId!, + userId: user?.rowId, excludedStatuses, - orderBy: orderBy ? (orderBy as PostOrderBy) : undefined, + orderBy, search, - userId: user?.rowId, - }); + }).queryKey; - const postsSnapshot = queryClient.getQueryData( - postsQueryKey, - ) as InfiniteData; + const postsSnapshot = + queryClient.getQueryData>(postsQueryKey); const updatedStatus = projectStatuses?.find( (status) => status.rowId === variables.patch.statusId, ); if (feedbackSnapshot) { - queryClient.setQueryData( - useFeedbackByIdQuery.getKey({ - rowId: feedback.rowId!, - userId: user?.rowId, - }), - { - post: { - ...feedbackSnapshot.post, - statusId: variables.patch.statusId, - statusUpdatedAt: variables.patch.statusUpdatedAt, - status: { - ...feedbackSnapshot.post?.status, - status: updatedStatus?.status, - color: updatedStatus?.color, - }, + queryClient.setQueryData(feedbackKey, { + post: { + ...feedbackSnapshot.post, + statusId: variables.patch.statusId, + statusUpdatedAt: variables.patch.statusUpdatedAt, + status: { + ...feedbackSnapshot.post?.status, + status: updatedStatus?.status, + color: updatedStatus?.color, }, }, - ); + } as FeedbackByIdQuery); } if (postsSnapshot) { - queryClient.setQueryData(postsQueryKey, { - ...postsSnapshot, - pages: postsSnapshot.pages.map((page) => ({ - ...page, - posts: { - ...page.posts, - nodes: page.posts?.nodes?.map((post) => { - if (post?.rowId === variables.rowId) { - return { - ...post, - statusUpdatedAt: variables.patch.statusUpdatedAt, - status: { - ...post?.status, - rowId: variables.patch.statusId, - status: updatedStatus?.status, - color: updatedStatus?.color, - }, - }; - } - - return post; - }), - }, - })), - }); + queryClient.setQueryData>( + postsQueryKey, + (snapshot) => { + const updatedPosts = snapshot?.pages.map((page) => ({ + ...page, + posts: { + ...page.posts, + nodes: page.posts?.nodes?.map((post) => { + if (post?.rowId === variables.rowId) { + return { + ...post, + statusUpdatedAt: variables.patch.statusUpdatedAt, + status: { + ...post?.status, + rowId: variables.patch.statusId, + status: updatedStatus?.status, + color: updatedStatus?.color, + }, + }; + } + + return post; + }), + }, + })); + + return { + ...snapshot, + pages: updatedPosts, + } as InfiniteData; + }, + ); } }, onSettled: async () => @@ -212,12 +213,12 @@ const FeedbackCard = ({ }), }), - queryClient.invalidateQueries({ - queryKey: useFeedbackByIdQuery.getKey({ + queryClient.invalidateQueries( + feedbackByIdOptions({ rowId: feedback.rowId!, userId: user?.rowId, }), - }), + ), ]), }); @@ -281,6 +282,7 @@ const FeedbackCard = ({ { - const { data: feedback } = useFeedbackByIdQuery( - { - rowId: feedbackId, - userId: user?.rowId, - }, - { - select: (data) => data?.post, - }, + const { data: feedback } = useQuery( + feedbackByIdOptions({ rowId: feedbackId, userId: user?.rowId }), ); const { isAdmin } = useOrganizationMembership({ diff --git a/src/components/feedback/UpdateFeedback/UpdateFeedback.tsx b/src/components/feedback/UpdateFeedback/UpdateFeedback.tsx index b5599991..23bd98e5 100644 --- a/src/components/feedback/UpdateFeedback/UpdateFeedback.tsx +++ b/src/components/feedback/UpdateFeedback/UpdateFeedback.tsx @@ -15,12 +15,13 @@ import { useIsClient } from "usehooks-ts"; import { z } from "zod"; import { CharacterLimit } from "components/core"; -import { useFeedbackByIdQuery, useUpdatePostMutation } from "generated/graphql"; +import { useUpdatePostMutation } from "generated/graphql"; import { token } from "generated/panda/tokens"; import { app } from "lib/config"; import { DEBOUNCE_TIME } from "lib/constants"; import { useForm, useViewportSize } from "lib/hooks"; import { toaster } from "lib/util"; +import { feedbackByIdOptions } from "lib/options"; import type { DialogProps } from "@omnidev/sigil"; import type { FeedbackFragment } from "generated/graphql"; @@ -78,12 +79,12 @@ const UpdateFeedback = ({ user, feedback, ...rest }: Props) => { queryClient.invalidateQueries({ queryKey: ["Posts.infinite"], }), - queryClient.invalidateQueries({ - queryKey: useFeedbackByIdQuery.getKey({ + queryClient.invalidateQueries( + feedbackByIdOptions({ rowId: feedback.rowId!, userId: user?.rowId, }), - }), + ), ]); }, onSuccess: () => { diff --git a/src/components/feedback/VotingButtons/VotingButtons.tsx b/src/components/feedback/VotingButtons/VotingButtons.tsx index aee4937b..5f7ebc6d 100644 --- a/src/components/feedback/VotingButtons/VotingButtons.tsx +++ b/src/components/feedback/VotingButtons/VotingButtons.tsx @@ -14,8 +14,11 @@ import { } from "lib/hooks/mutations"; import type { Downvote, Post, Project, Upvote } from "generated/graphql"; +import type { Session } from "next-auth"; interface Props { + /** Authenticated user. */ + user: Session["user"] | undefined; /** Feedback ID. */ feedbackId: Post["rowId"]; /** Project ID. */ @@ -33,6 +36,7 @@ interface Props { } const VotingButtons = ({ + user, feedbackId, projectId, upvote, @@ -101,16 +105,20 @@ const VotingButtons = ({ e.stopPropagation(); handleUpvote(); }, - disabled: isVotePending || isOptimistic, + disabled: !user || isVotePending || isOptimistic, + opacity: !user ? 0.5 : 1, }} > - {app.feedbackPage.details.upvote} + {!user + ? app.feedbackPage.details.signedOut + : app.feedbackPage.details.upvote} {`${netTotalVotes > 0 ? "+" : ""}${netTotalVotes}`} @@ -132,10 +140,13 @@ const VotingButtons = ({ e.stopPropagation(); handleDownvote(); }, - disabled: isVotePending || isOptimistic, + disabled: !user || isVotePending || isOptimistic, + opacity: !user ? 0.5 : 1, }} > - {app.feedbackPage.details.downvote} + {!user + ? app.feedbackPage.details.signedOut + : app.feedbackPage.details.downvote} ); diff --git a/src/components/layout/Header/Header.tsx b/src/components/layout/Header/Header.tsx index 7aae8f64..01cd42a3 100644 --- a/src/components/layout/Header/Header.tsx +++ b/src/components/layout/Header/Header.tsx @@ -28,6 +28,8 @@ const Header = () => { const showPricingLink = !isLoading && (!isAuthenticated || subscriptionNotFound); + const showOrganizationsLink = !isLoading && !isAuthenticated; + return ( { {showPricingLink && ( - + { )} + {showOrganizationsLink && ( + + + + {app.header.routes.organizations.label} + + + + )} + { - const isSmallViewport = useViewportSize({ - minWidth: token("breakpoints.sm"), + const isMediumViewport = useViewportSize({ + minWidth: token("breakpoints.md"), }); const { isAuthenticated, isLoading } = useAuth(), @@ -46,14 +46,14 @@ const HeaderActions = () => { }; useEffect(() => { - if (isSmallViewport) { + if (isMediumViewport) { setIsMobileSidebarOpen(false); } - }, [isSmallViewport, setIsMobileSidebarOpen]); + }, [isMediumViewport, setIsMobileSidebarOpen]); if (isLoading) return null; - if (isSmallViewport) { + if (isMediumViewport) { return ( diff --git a/src/components/layout/SidebarNavigation/SidebarNavigation.tsx b/src/components/layout/SidebarNavigation/SidebarNavigation.tsx index c81ca034..88626534 100644 --- a/src/components/layout/SidebarNavigation/SidebarNavigation.tsx +++ b/src/components/layout/SidebarNavigation/SidebarNavigation.tsx @@ -55,7 +55,7 @@ const SidebarNavigation = () => {