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 = () => {