From a8e88d9d657a8b7951ea6b43d3780c0a6f0b2dea Mon Sep 17 00:00:00 2001 From: eric Date: Thu, 4 Apr 2024 07:25:14 -0400 Subject: [PATCH 01/11] Wrap server actions in sentry HOF; add custom global-error files --- .../members/[[...add]]/actions.ts | 312 ++++++++++-------- .../[communitySlug]/stages/manage/actions.ts | 289 ++++++++++------ core/global-error.tsx | 19 ++ core/sentry.client.config.ts | 2 +- .../app/actions/evaluate/actions.ts | 119 ++++--- .../evaluations/app/actions/manage/actions.ts | 254 +++++++------- .../app/actions/respond/actions.ts | 286 +++++++++------- integrations/evaluations/global-error.tsx | 19 ++ .../submissions/app/actions/submit/actions.ts | 65 ++-- integrations/submissions/global-error.tsx | 19 ++ 10 files changed, 821 insertions(+), 563 deletions(-) create mode 100644 core/global-error.tsx create mode 100644 integrations/evaluations/global-error.tsx create mode 100644 integrations/submissions/global-error.tsx diff --git a/core/app/c/[communitySlug]/members/[[...add]]/actions.ts b/core/app/c/[communitySlug]/members/[[...add]]/actions.ts index 4b06445392..8a18db55ba 100644 --- a/core/app/c/[communitySlug]/members/[[...add]]/actions.ts +++ b/core/app/c/[communitySlug]/members/[[...add]]/actions.ts @@ -1,14 +1,14 @@ "use server"; -import { cache } from "react"; -import { revalidatePath, revalidateTag } from "next/cache"; import { Community } from "@prisma/client"; -import { captureException } from "@sentry/nextjs"; +import { captureException, withServerActionInstrumentation } from "@sentry/nextjs"; import { User } from "@supabase/supabase-js"; - -import type { SuggestedUser } from "~/lib/server/members"; +import { revalidatePath, revalidateTag } from "next/cache"; +import { headers } from "next/headers"; +import { cache } from "react"; import { getLoginData } from "~/lib/auth/loginData"; import { env } from "~/lib/env/env.mjs"; +import type { SuggestedUser } from "~/lib/server/members"; import { generateHash, slugifyString } from "~/lib/string"; import { formatSupabaseError } from "~/lib/supabase"; import { getServerSupabase } from "~/lib/supabaseServer"; @@ -16,10 +16,18 @@ import prisma from "~/prisma/db"; import { TableMember } from "./getMemberTableColumns"; export const revalidateMemberPathsAndTags = (community: Community) => { - revalidatePath(`/c/${community.slug}/members`); - revalidateTag(`members_${community.id}`); - // not in use yet, but should be updated here - revalidateTag(`users`); + return withServerActionInstrumentation( + "members/revalidateMemberPathsAndTags", + { + headers: headers(), + }, + async () => { + revalidatePath(`/c/${community.slug}/members`); + revalidateTag(`members_${community.id}`); + // not in use yet, but should be updated here + revalidateTag(`users`); + } + ); }; const isCommunityAdmin = cache(async (community: Community) => { @@ -92,13 +100,19 @@ const addSupabaseUser = async ({ // let's just give up if (!force) { captureException(error); - return { user: null, error: `Failed to invite member.\n ${formatSupabaseError(error)}` }; + return { + user: null, + error: `Failed to invite member.\n ${formatSupabaseError(error)}`, + }; } // 422 = email already exists in supabase if (error.status !== 422) { captureException(error); - return { user: null, error: `Failed to invite member.\n ${formatSupabaseError(error)}` }; + return { + user: null, + error: `Failed to invite member.\n ${formatSupabaseError(error)}`, + }; } // the user already exists in supabase, so we will delete them and try again @@ -149,65 +163,73 @@ export const addMember = async ({ canAdmin?: boolean; community: Community; }) => { - const { error: adminError } = await isCommunityAdmin(community); - if (adminError) { - return { error: adminError }; - } - - try { - const existingMember = await prisma.member.findFirst({ - where: { - userId: user.id, - communityId: community.id, - }, - }); - - if (existingMember) { - return { error: "User is already a member of this community" }; - } - - const member = await prisma.member.create({ - data: { - communityId: community.id, - userId: user.id, - canAdmin: Boolean(canAdmin), - }, - }); - - revalidateMemberPathsAndTags(community); - - if (user.supabaseId) { - return { member }; - } - - // the user exists in our DB, but not in supabase, or is not linked to a supabase user - // most likely they were invited as an evaluator before by the evaluation integration - const { error: supabaseInviteError } = await addSupabaseUser({ - email: user.email, - firstName: user.firstName, - lastName: user.lastName, - community, - canAdmin, - force: true, - }); - - if (supabaseInviteError) { - return { error: supabaseInviteError }; + return withServerActionInstrumentation( + "members/addMember", + { + headers: headers(), + }, + async () => { + const { error: adminError } = await isCommunityAdmin(community); + if (adminError) { + return { error: adminError }; + } + + try { + const existingMember = await prisma.member.findFirst({ + where: { + userId: user.id, + communityId: community.id, + }, + }); + + if (existingMember) { + return { error: "User is already a member of this community" }; + } + + const member = await prisma.member.create({ + data: { + communityId: community.id, + userId: user.id, + canAdmin: Boolean(canAdmin), + }, + }); + + revalidateMemberPathsAndTags(community); + + if (user.supabaseId) { + return { member }; + } + + // the user exists in our DB, but not in supabase, or is not linked to a supabase user + // most likely they were invited as an evaluator before by the evaluation integration + const { error: supabaseInviteError } = await addSupabaseUser({ + email: user.email, + firstName: user.firstName, + lastName: user.lastName, + community, + canAdmin, + force: true, + }); + + if (supabaseInviteError) { + return { error: supabaseInviteError }; + } + + await prisma.user.update({ + where: { + id: user.id, + }, + data: { + supabaseId: user.supabaseId, + }, + }); + + return { member }; + } catch (error) { + return { error: error.message }; + } } - - await prisma.user.update({ - where: { - id: user.id, - }, - data: { - supabaseId: user.supabaseId, - }, - }); - - return { member }; - } catch (error) { - return { error: error.message }; - } + ); }; /** @@ -227,52 +249,60 @@ export const createUserWithMembership = async ({ community: Community; canAdmin: boolean; }) => { - const { error: adminError } = await isCommunityAdmin(community); - if (adminError) { - return { error: adminError }; - } - - const user = await prisma.user.create({ - data: { - email, - firstName, - lastName, - slug: `${slugifyString(firstName)}${ - lastName ? `-${slugifyString(lastName)}` : "" - }-${generateHash(4, "0123456789")}`, - memberships: { - create: { - communityId: community.id, - canAdmin, - }, - }, - }, - }); - - const { error: supabaseError, user: supabaseUser } = await addSupabaseUser({ - email, - firstName, - lastName, - community, - canAdmin, - }); - - if (supabaseError !== null) { - return { error: supabaseError }; - } - - await prisma.user.update({ - where: { - id: user.id, + return withServerActionInstrumentation( + "member/createUserWithMembership", + { + headers: headers(), }, - data: { - supabaseId: supabaseUser.id, - }, - }); + async () => { + const { error: adminError } = await isCommunityAdmin(community); + if (adminError) { + return { error: adminError }; + } + + const user = await prisma.user.create({ + data: { + email, + firstName, + lastName, + slug: `${slugifyString(firstName)}${ + lastName ? `-${slugifyString(lastName)}` : "" + }-${generateHash(4, "0123456789")}`, + memberships: { + create: { + communityId: community.id, + canAdmin, + }, + }, + }, + }); + + const { error: supabaseError, user: supabaseUser } = await addSupabaseUser({ + email, + firstName, + lastName, + community, + canAdmin, + }); + + if (supabaseError !== null) { + return { error: supabaseError }; + } + + await prisma.user.update({ + where: { + id: user.id, + }, + data: { + supabaseId: supabaseUser.id, + }, + }); - revalidateMemberPathsAndTags(community); + revalidateMemberPathsAndTags(community); - return { user }; + return { user }; + } + ); }; export const removeMember = async ({ @@ -282,30 +312,38 @@ export const removeMember = async ({ member: TableMember; community: Community; }) => { - try { - const { loginData, error: adminError } = await isCommunityAdmin(community); - - if (adminError) { - return { error: adminError }; - } - - if (loginData?.memberships.find((m) => m.id === member.id)) { - return { error: "You cannot remove yourself from the community" }; - } - - const deleted = await prisma.member.delete({ - where: { - id: member.id, - }, - }); - - if (!deleted) { - return { error: "Failed to remove member" }; + return withServerActionInstrumentation( + "members/removeMember", + { + headers: headers(), + }, + async () => { + try { + const { loginData, error: adminError } = await isCommunityAdmin(community); + + if (adminError) { + return { error: adminError }; + } + + if (loginData?.memberships.find((m) => m.id === member.id)) { + return { error: "You cannot remove yourself from the community" }; + } + + const deleted = await prisma.member.delete({ + where: { + id: member.id, + }, + }); + + if (!deleted) { + return { error: "Failed to remove member" }; + } + + revalidateMemberPathsAndTags(community); + return { success: true }; + } catch (error) { + return { error: error.message }; + } } - - revalidateMemberPathsAndTags(community); - return { success: true }; - } catch (error) { - return { error: error.message }; - } + ); }; diff --git a/core/app/c/[communitySlug]/stages/manage/actions.ts b/core/app/c/[communitySlug]/stages/manage/actions.ts index 3946ed63df..ab65d13cb8 100644 --- a/core/app/c/[communitySlug]/stages/manage/actions.ts +++ b/core/app/c/[communitySlug]/stages/manage/actions.ts @@ -1,39 +1,56 @@ "use server"; +import { withServerActionInstrumentation } from "@sentry/nextjs"; import { revalidateTag } from "next/cache"; - +import { headers } from "next/headers"; import db from "~/prisma/db"; export async function createStage(communityId: string) { - try { - await db.stage.create({ - data: { - name: "Untitled Stage", - order: "aa", - community: { - connect: { - id: communityId, + return withServerActionInstrumentation( + "stages/manage/createStage", + { + headers: headers(), + }, + async () => { + try { + await db.stage.create({ + data: { + name: "Untitled Stage", + order: "aa", + community: { + connect: { + id: communityId, + }, + }, }, - }, - }, - }); - } finally { - revalidateTag(`community-stages_${communityId}`); - } + }); + } finally { + revalidateTag(`community-stages_${communityId}`); + } + } + ); } export async function deleteStages(communityId: string, stageIds: string[]) { - try { - await db.stage.deleteMany({ - where: { - id: { - in: stageIds, - }, - }, - }); - } finally { - revalidateTag(`community-stages_${communityId}`); - } + return withServerActionInstrumentation( + "stages/manage/deleteStages", + { + headers: headers(), + }, + async () => { + try { + await db.stage.deleteMany({ + where: { + id: { + in: stageIds, + }, + }, + }); + } finally { + revalidateTag(`community-stages_${communityId}`); + } + } + ); } export async function createMoveConstraint( @@ -41,45 +58,61 @@ export async function createMoveConstraint( sourceStageId: string, destinationStageId: string ) { - try { - await db.moveConstraint.create({ - data: { - stage: { - connect: { - id: sourceStageId, - }, - }, - destination: { - connect: { - id: destinationStageId, + return withServerActionInstrumentation( + "stages/manage/createMoveConstraint", + { + headers: headers(), + }, + async () => { + try { + await db.moveConstraint.create({ + data: { + stage: { + connect: { + id: sourceStageId, + }, + }, + destination: { + connect: { + id: destinationStageId, + }, + }, }, - }, - }, - }); - } finally { - revalidateTag(`community-stages_${communityId}`); - } + }); + } finally { + revalidateTag(`community-stages_${communityId}`); + } + } + ); } export async function deleteMoveConstraints( communityId: string, moveConstraintIds: [string, string][] ) { - try { - const ops = moveConstraintIds.map(([stageId, destinationId]) => - db.moveConstraint.delete({ - where: { - move_constraint_id: { - stageId, - destinationId, - }, - }, - }) - ); - await Promise.all(ops); - } finally { - revalidateTag(`community-stages_${communityId}`); - } + return withServerActionInstrumentation( + "stages/manage/deleteMoveConstraints", + { + headers: headers(), + }, + async () => { + try { + const ops = moveConstraintIds.map(([stageId, destinationId]) => + db.moveConstraint.delete({ + where: { + move_constraint_id: { + stageId, + destinationId, + }, + }, + }) + ); + await Promise.all(ops); + } finally { + revalidateTag(`community-stages_${communityId}`); + } + } + ); } export async function deleteStagesAndMoveConstraints( @@ -87,68 +120,108 @@ export async function deleteStagesAndMoveConstraints( stageIds: string[], moveConstraintIds: [string, string][] ) { - try { - // Delete move constraints prior to deleting stages to prevent foreign - // key constraint violations. - if (moveConstraintIds.length > 0) { - await deleteMoveConstraints(communityId, moveConstraintIds); - } - if (stageIds.length > 0) { - await deleteStages(communityId, stageIds); + return withServerActionInstrumentation( + "stages/manage/deleteStagesAndMoveConstraints", + { + headers: headers(), + }, + async () => { + try { + // Delete move constraints prior to deleting stages to prevent foreign + // key constraint violations. + if (moveConstraintIds.length > 0) { + await deleteMoveConstraints(communityId, moveConstraintIds); + } + if (stageIds.length > 0) { + await deleteStages(communityId, stageIds); + } + } finally { + revalidateTag(`community-stages_${communityId}`); + } } - } finally { - revalidateTag(`community-stages_${communityId}`); - } + ); } export async function updateStageName(communityId: string, stageId: string, name: string) { - try { - await db.stage.update({ - where: { - id: stageId, - }, - data: { - name, - }, - }); - } finally { - revalidateTag(`community-stages_${communityId}`); - } + return withServerActionInstrumentation( + "stages/manage/updateStageName", + { + headers: headers(), + }, + async () => { + try { + await db.stage.update({ + where: { + id: stageId, + }, + data: { + name, + }, + }); + } finally { + revalidateTag(`community-stages_${communityId}`); + } + } + ); } export async function revalidateStages(communityId: string) { - revalidateTag(`community-stages_${communityId}`); + return withServerActionInstrumentation( + "stages/manage/revalidateStages", + { + headers: headers(), + }, + async () => { + revalidateTag(`community-stages_${communityId}`); + } + ); } export async function addAction(communityId: string, stageId: string, actionId: string) { - try { - await db.actionInstance.create({ - data: { - action: { - connect: { - id: actionId, - }, - }, - stage: { - connect: { - id: stageId, + return withServerActionInstrumentation( + "stages/manage/addAction", + { + headers: headers(), + }, + async () => { + try { + await db.actionInstance.create({ + data: { + action: { + connect: { + id: actionId, + }, + }, + stage: { + connect: { + id: stageId, + }, + }, }, - }, - }, - }); - } finally { - revalidateTag(`community-stages_${communityId}`); - } + }); + } finally { + revalidateTag(`community-stages_${communityId}`); + } + } + ); } export async function deleteAction(communityId: string, actionId: string) { - try { - await db.actionInstance.delete({ - where: { - id: actionId, - }, - }); - } finally { - revalidateTag(`community-stages_${communityId}`); - } + return withServerActionInstrumentation( + "stages/manage/deleteAction", + { + headers: headers(), + }, + async () => { + try { + await db.actionInstance.delete({ + where: { + id: actionId, + }, + }); + } finally { + revalidateTag(`community-stages_${communityId}`); + } + } + ); } diff --git a/core/global-error.tsx b/core/global-error.tsx new file mode 100644 index 0000000000..7632da0825 --- /dev/null +++ b/core/global-error.tsx @@ -0,0 +1,19 @@ +"use client"; + +import * as Sentry from "@sentry/nextjs"; +import NextError from "next/error"; +import { useEffect } from "react"; + +export default function GlobalError({ error }: { error: Error & { digest?: string } }) { + useEffect(() => { + Sentry.captureException(error); + }, [error]); + + return ( + + + + + + ); +} diff --git a/core/sentry.client.config.ts b/core/sentry.client.config.ts index 3c0a6fd73b..423ce310a3 100644 --- a/core/sentry.client.config.ts +++ b/core/sentry.client.config.ts @@ -24,7 +24,7 @@ if (env.NODE_ENV === "production") { // You can remove this option if you're not planning to use the Sentry Session Replay feature: integrations: [ - new Sentry.Replay({ + Sentry.replayIntegration({ // Additional Replay configuration goes in here, for example: maskAllText: true, blockAllMedia: true, diff --git a/integrations/evaluations/app/actions/evaluate/actions.ts b/integrations/evaluations/app/actions/evaluate/actions.ts index c95c90dd18..e144fafefd 100644 --- a/integrations/evaluations/app/actions/evaluate/actions.ts +++ b/integrations/evaluations/app/actions/evaluate/actions.ts @@ -1,8 +1,9 @@ "use server"; -import { revalidatePath } from "next/cache"; - import { PubValues } from "@pubpub/sdk"; +import { withServerActionInstrumentation } from "@sentry/nextjs"; +import { revalidatePath } from "next/cache"; +import { headers } from "next/headers"; import { expect } from "utils"; import { @@ -16,57 +17,73 @@ import { cookie } from "~/lib/request"; import { assertHasAccepted } from "~/lib/types"; export const submit = async (instanceId: string, submissionPubId: string, values: PubValues) => { - try { - const submissionPub = await client.getPub(instanceId, submissionPubId); - const user = JSON.parse(expect(cookie("user"))); - const instanceConfig = expect( - await getInstanceConfig(instanceId), - "Instance not configured" - ); - const instanceState = (await getInstanceState(instanceId, submissionPubId)) ?? {}; - if (instanceConfig === undefined) { - return { error: "Instance not configured" }; + return withServerActionInstrumentation( + "evaluations/evaluate/submit", + { + headers: headers(), + }, + async () => { + try { + const submissionPub = await client.getPub(instanceId, submissionPubId); + const user = JSON.parse(expect(cookie("user"))); + const instanceConfig = expect( + await getInstanceConfig(instanceId), + "Instance not configured" + ); + const instanceState = (await getInstanceState(instanceId, submissionPubId)) ?? {}; + if (instanceConfig === undefined) { + return { error: "Instance not configured" }; + } + let evaluator = expect( + instanceState[user.id], + `User was not invited to evaluate pub ${submissionPubId}` + ); + assertHasAccepted(evaluator); + const evaluationPub = await client.createPub(instanceId, { + pubTypeId: instanceConfig.pubTypeId, + parentId: submissionPubId, + values: values, + }); + evaluator = instanceState[user.id] = { + ...evaluator, + status: "received", + evaluatedAt: new Date().toString(), + evaluationPubId: evaluationPub.id, + }; + await setInstanceState(instanceId, submissionPubId, instanceState); + // Unschedule no-submit notification email for manager. + await unscheduleNoSubmitNotificationEmail(instanceId, submissionPubId, evaluator); + // unschedule dealine reminder emails. + await unscheduleAllDeadlineReminderEmails(instanceId, submissionPubId, evaluator); + // Immediately send submitted notification email. + await sendSubmittedNotificationEmail( + instanceId, + instanceConfig, + submissionPubId, + evaluator, + submissionPub.assignee + ); + revalidatePath("/"); + return { success: true }; + } catch (error) { + return { error: error.message }; + } } - let evaluator = expect( - instanceState[user.id], - `User was not invited to evaluate pub ${submissionPubId}` - ); - assertHasAccepted(evaluator); - const evaluationPub = await client.createPub(instanceId, { - pubTypeId: instanceConfig.pubTypeId, - parentId: submissionPubId, - values: values, - }); - evaluator = instanceState[user.id] = { - ...evaluator, - status: "received", - evaluatedAt: new Date().toString(), - evaluationPubId: evaluationPub.id, - }; - await setInstanceState(instanceId, submissionPubId, instanceState); - // Unschedule no-submit notification email for manager. - await unscheduleNoSubmitNotificationEmail(instanceId, submissionPubId, evaluator); - // unschedule dealine reminder emails. - await unscheduleAllDeadlineReminderEmails(instanceId, submissionPubId, evaluator); - // Immediately send submitted notification email. - await sendSubmittedNotificationEmail( - instanceId, - instanceConfig, - submissionPubId, - evaluator, - submissionPub.assignee - ); - revalidatePath("/"); - return { success: true }; - } catch (error) { - return { error: error.message }; - } + ); }; export const upload = async (instanceId: string, pubId: string, fileName: string) => { - try { - return await client.generateSignedAssetUploadUrl(instanceId, pubId, fileName); - } catch (error) { - return { error: error.message }; - } + return withServerActionInstrumentation( + "evaluations/evaluate/upload", + { + headers: headers(), + }, + async () => { + try { + return await client.generateSignedAssetUploadUrl(instanceId, pubId, fileName); + } catch (error) { + return { error: error.message }; + } + } + ); }; diff --git a/integrations/evaluations/app/actions/manage/actions.ts b/integrations/evaluations/app/actions/manage/actions.ts index a1d1a5c74b..c5fd7fa03b 100644 --- a/integrations/evaluations/app/actions/manage/actions.ts +++ b/integrations/evaluations/app/actions/manage/actions.ts @@ -23,6 +23,8 @@ import { isInvited, } from "~/lib/types"; import { InviteFormEvaluator } from "./types"; +import { withServerActionInstrumentation } from "@sentry/nextjs"; +import { headers } from "next/headers"; export const save = async ( instanceId: string, @@ -30,129 +32,153 @@ export const save = async ( evaluators: InviteFormEvaluator[], send: boolean ) => { - try { - const submissionPub = await client.getPub(instanceId, submissionPubId); - const user = JSON.parse(expect(cookie("user"))); - const instanceConfig = expect( - await getInstanceConfig(instanceId), - "Instance not configured" - ); - const instanceState = (await getInstanceState(instanceId, submissionPubId)) ?? {}; - const evaluatorUserIds = new Set(); - for (let i = 0; i < evaluators.length; i++) { - let evaluator = evaluators[i]; - // Invited evaluators are read-only and already persisted, so we can skip - // them. - if (isInvited(evaluator)) { - // We still need to add the userId to the set so we can check for - // duplicates. We don't need to check, however, because the invite - // should already be unique. - evaluatorUserIds.add(evaluator.userId); - continue; - } - // Find or create a PubPub user for the evaluator. - const evaluatorUser = await client.getOrCreateUser(instanceId, evaluator); - // If the user has already been saved or invited, halt and notify the - // user. - if (evaluatorUserIds.has(evaluatorUser.id)) { - const { firstName, lastName } = evaluator; - const name = `${firstName}${lastName ? ` ${lastName}` : ""}`; - return { - error: `${name} was added more than once.`, - }; - } - // Update the evaluator to reflect that they have been persisted. - evaluator = { - ...evaluator, - userId: evaluatorUser.id, - firstName: evaluatorUser.firstName, - lastName: evaluatorUser.lastName ?? undefined, - status: "saved", - }; - // If the user intends to invite selected evaluators, send an email to - // the evaluator with the invite link. - if (send && evaluator.selected) { - // Update the evaluator to reflect that they have been invited. - evaluator = { - ...evaluator, - status: "invited", - invitedAt: new Date().toString(), - invitedBy: user.id, - }; - // Immediately send the invite email. - await sendInviteEmail(instanceId, submissionPubId, evaluator); - // Scehdule a reminder email to person who was invited to evaluate. - await scheduleInvitationReminderEmail( - instanceId, - instanceConfig, - submissionPubId, - evaluator - ); - // Schedule no-reply notification email to person who invited the - // evaluator. - await scheduleNoReplyNotificationEmail( - instanceId, - instanceConfig, - submissionPubId, - evaluator, - submissionPub.assignee + return withServerActionInstrumentation( + "evaluations/manage/save", + { + headers: headers(), + }, + async () => { + try { + const submissionPub = await client.getPub(instanceId, submissionPubId); + const user = JSON.parse(expect(cookie("user"))); + const instanceConfig = expect( + await getInstanceConfig(instanceId), + "Instance not configured" ); + const instanceState = (await getInstanceState(instanceId, submissionPubId)) ?? {}; + const evaluatorUserIds = new Set(); + for (let i = 0; i < evaluators.length; i++) { + let evaluator = evaluators[i]; + // Invited evaluators are read-only and already persisted, so we can skip + // them. + if (isInvited(evaluator)) { + // We still need to add the userId to the set so we can check for + // duplicates. We don't need to check, however, because the invite + // should already be unique. + evaluatorUserIds.add(evaluator.userId); + continue; + } + // Find or create a PubPub user for the evaluator. + const evaluatorUser = await client.getOrCreateUser(instanceId, evaluator); + // If the user has already been saved or invited, halt and notify the + // user. + if (evaluatorUserIds.has(evaluatorUser.id)) { + const { firstName, lastName } = evaluator; + const name = `${firstName}${lastName ? ` ${lastName}` : ""}`; + return { + error: `${name} was added more than once.`, + }; + } + // Update the evaluator to reflect that they have been persisted. + evaluator = { + ...evaluator, + userId: evaluatorUser.id, + firstName: evaluatorUser.firstName, + lastName: evaluatorUser.lastName ?? undefined, + status: "saved", + }; + // If the user intends to invite selected evaluators, send an email to + // the evaluator with the invite link. + if (send && evaluator.selected) { + // Update the evaluator to reflect that they have been invited. + evaluator = { + ...evaluator, + status: "invited", + invitedAt: new Date().toString(), + invitedBy: user.id, + }; + // Immediately send the invite email. + await sendInviteEmail(instanceId, submissionPubId, evaluator); + // Scehdule a reminder email to person who was invited to evaluate. + await scheduleInvitationReminderEmail( + instanceId, + instanceConfig, + submissionPubId, + evaluator + ); + // Schedule no-reply notification email to person who invited the + // evaluator. + await scheduleNoReplyNotificationEmail( + instanceId, + instanceConfig, + submissionPubId, + evaluator, + submissionPub.assignee + ); + } + // Remove the form's selected property from the evaluator before + // persisting. + const { selected, ...rest } = evaluator; + instanceState[evaluator.userId] = rest; + // Add the user id to the set so we can check for duplicates. + evaluatorUserIds.add(evaluatorUser.id); + } + // Persist the updated instance state. + await setInstanceState(instanceId, submissionPubId, instanceState); + // Reload the page to reflect the changes. + revalidatePath("/"); + return { success: true }; + } catch (error) { + return { error: error.message }; } - // Remove the form's selected property from the evaluator before - // persisting. - const { selected, ...rest } = evaluator; - instanceState[evaluator.userId] = rest; - // Add the user id to the set so we can check for duplicates. - evaluatorUserIds.add(evaluatorUser.id); } - // Persist the updated instance state. - await setInstanceState(instanceId, submissionPubId, instanceState); - // Reload the page to reflect the changes. - revalidatePath("/"); - return { success: true }; - } catch (error) { - return { error: error.message }; - } + ); }; export const suggest = async (instanceId: string, query: SuggestedMembersQuery) => { - try { - const users = await client.getSuggestedMembers(instanceId, query); - return users; - } catch (error) { - return { error: error.message }; - } + return withServerActionInstrumentation( + "evaluations/manage/suggest", + { + headers: headers(), + }, + async () => { + try { + const users = await client.getSuggestedMembers(instanceId, query); + return users; + } catch (error) { + return { error: error.message }; + } + } + ); }; export const remove = async (instanceId: string, pubId: string, userId: string) => { - try { - const instanceConfig = expect( - await getInstanceConfig(instanceId), - "Instance not configured" - ); - const instanceState = await getInstanceState(instanceId, pubId); - const pub = await client.getPub(instanceId, pubId); - const evaluation = pub.children.find( - (child) => child.values[instanceConfig.evaluatorFieldSlug] === userId - ); + return withServerActionInstrumentation( + "evaluations/manage/remove", + { + headers: headers(), + }, + async () => { + try { + const instanceConfig = expect( + await getInstanceConfig(instanceId), + "Instance not configured" + ); + const instanceState = await getInstanceState(instanceId, pubId); + const pub = await client.getPub(instanceId, pubId); + const evaluation = pub.children.find( + (child) => child.values[instanceConfig.evaluatorFieldSlug] === userId + ); - if (evaluation !== undefined) { - await client.deletePub(instanceId, evaluation.id); - } - if (instanceState !== undefined) { - let evaluator = expect( - instanceState[userId], - `User was not invited to evaluate pub ${pubId}` - ); - assertHasAccepted(evaluator); - await unscheduleAllDeadlineReminderEmails(instanceId, pubId, evaluator); - await unscheduleAllManagerEmails(instanceId, pubId, evaluator); - delete instanceState[userId]; - await setInstanceState(instanceId, pubId, instanceState); - } + if (evaluation !== undefined) { + await client.deletePub(instanceId, evaluation.id); + } + if (instanceState !== undefined) { + let evaluator = expect( + instanceState[userId], + `User was not invited to evaluate pub ${pubId}` + ); + assertHasAccepted(evaluator); + await unscheduleAllDeadlineReminderEmails(instanceId, pubId, evaluator); + await unscheduleAllManagerEmails(instanceId, pubId, evaluator); + delete instanceState[userId]; + await setInstanceState(instanceId, pubId, instanceState); + } - return { success: true }; - } catch (error) { - return { error: error.message }; - } + return { success: true }; + } catch (error) { + return { error: error.message }; + } + } + ); }; diff --git a/integrations/evaluations/app/actions/respond/actions.ts b/integrations/evaluations/app/actions/respond/actions.ts index e4668d8218..42bc842798 100644 --- a/integrations/evaluations/app/actions/respond/actions.ts +++ b/integrations/evaluations/app/actions/respond/actions.ts @@ -1,5 +1,7 @@ "use server"; +import { withServerActionInstrumentation } from "@sentry/nextjs"; +import { headers } from "next/headers"; import { redirect } from "next/navigation"; import { expect } from "utils"; @@ -32,141 +34,169 @@ export const accept = async ( instanceId: string, submissionPubId: string ): Promise => { - const redirectParams = `?token=${cookie( - "token" - )}&instanceId=${instanceId}&pubId=${submissionPubId}`; - try { - const submissionPub = await client.getPub(instanceId, submissionPubId); - const user = JSON.parse(expect(cookie("user"))); - const instanceConfig = expect( - await getInstanceConfig(instanceId), - "Instance not configured" - ); - const instanceState = (await getInstanceState(instanceId, submissionPubId)) ?? {}; - let evaluator = expect( - instanceState[user.id], - `User was not invited to evaluate pub ${submissionPubId}` - ); - // Accepting again is a no-op. - if (evaluator.status === "accepted" || evaluator.status === "received") { + return withServerActionInstrumentation( + "evaluations/respond/accept", + { + headers: headers(), + }, + async () => { + const redirectParams = `?token=${cookie( + "token" + )}&instanceId=${instanceId}&pubId=${submissionPubId}`; + try { + const submissionPub = await client.getPub(instanceId, submissionPubId); + const user = JSON.parse(expect(cookie("user"))); + const instanceConfig = expect( + await getInstanceConfig(instanceId), + "Instance not configured" + ); + const instanceState = (await getInstanceState(instanceId, submissionPubId)) ?? {}; + let evaluator = expect( + instanceState[user.id], + `User was not invited to evaluate pub ${submissionPubId}` + ); + // Accepting again is a no-op. + if (evaluator.status === "accepted" || evaluator.status === "received") { + redirect(`/actions/respond/accepted${redirectParams}`); + } + // Assert the user is invited to evaluate this pub. + assertIsInvited(evaluator); + // Update the evaluator's status to accepted and add recored the time of + // acceptance. + evaluator = instanceState[user.id] = { + ...evaluator, + status: "accepted", + acceptedAt: new Date().toString(), + }; + const deadline = calculateDeadline(instanceConfig, new Date(evaluator.acceptedAt)); + evaluator.deadline = deadline; + await setInstanceState(instanceId, submissionPubId, instanceState); + // Unschedule reminder email to evaluator. + await unscheduleInvitationReminderEmail(instanceId, submissionPubId, evaluator); + // Unschedule no-reply notification email to community manager. + await unscheduleNoReplyNotificationEmail(instanceId, submissionPubId, evaluator); + // Immediately send accepted notification email to community manager. + await sendAcceptedNotificationEmail( + instanceId, + instanceConfig, + submissionPubId, + evaluator, + submissionPub.assignee + ); + // Immediately send accepted email to evaluator. + await sendAcceptedEmail(instanceId, instanceConfig, submissionPubId, evaluator); + // Schedule no-submit notification email to community manager. + await scheduleNoSubmitNotificationEmail( + instanceId, + instanceConfig, + submissionPubId, + evaluator, + submissionPub.assignee + ); + + // schedule prompt evaluation email to evaluator. + await schedulePromptEvalBonusReminderEmail( + instanceId, + instanceConfig, + submissionPubId, + evaluator + ); + //schedule final prompt eval email to evaluator + await scheduleFinalPromptEvalBonusReminderEmail( + instanceId, + instanceConfig, + submissionPubId, + evaluator + ); + //schedule eval reminder email to evaluator + await scheduleEvaluationReminderEmail( + instanceId, + instanceConfig, + submissionPubId, + evaluator + ); + //schedule final eval reminder email to evaluator + await scheduleFinalEvaluationReminderEmail( + instanceId, + instanceConfig, + submissionPubId, + evaluator + ); + //schedule follow up to final eval reminder email to evaluator + await scheduleFollowUpToFinalEvaluationReminderEmail( + instanceId, + instanceConfig, + submissionPubId, + evaluator + ); + // schedule no-submit notification email to evalutaor + await sendNoticeOfNoSubmitEmail( + instanceId, + instanceConfig, + submissionPubId, + evaluator + ); + } catch (error) { + return { error: error.message }; + } redirect(`/actions/respond/accepted${redirectParams}`); } - // Assert the user is invited to evaluate this pub. - assertIsInvited(evaluator); - // Update the evaluator's status to accepted and add recored the time of - // acceptance. - evaluator = instanceState[user.id] = { - ...evaluator, - status: "accepted", - acceptedAt: new Date().toString(), - }; - const deadline = calculateDeadline(instanceConfig, new Date(evaluator.acceptedAt)); - evaluator.deadline = deadline; - await setInstanceState(instanceId, submissionPubId, instanceState); - // Unschedule reminder email to evaluator. - await unscheduleInvitationReminderEmail(instanceId, submissionPubId, evaluator); - // Unschedule no-reply notification email to community manager. - await unscheduleNoReplyNotificationEmail(instanceId, submissionPubId, evaluator); - // Immediately send accepted notification email to community manager. - await sendAcceptedNotificationEmail( - instanceId, - instanceConfig, - submissionPubId, - evaluator, - submissionPub.assignee - ); - // Immediately send accepted email to evaluator. - await sendAcceptedEmail(instanceId, instanceConfig, submissionPubId, evaluator); - // Schedule no-submit notification email to community manager. - await scheduleNoSubmitNotificationEmail( - instanceId, - instanceConfig, - submissionPubId, - evaluator, - submissionPub.assignee - ); - - // schedule prompt evaluation email to evaluator. - await schedulePromptEvalBonusReminderEmail( - instanceId, - instanceConfig, - submissionPubId, - evaluator - ); - //schedule final prompt eval email to evaluator - await scheduleFinalPromptEvalBonusReminderEmail( - instanceId, - instanceConfig, - submissionPubId, - evaluator - ); - //schedule eval reminder email to evaluator - await scheduleEvaluationReminderEmail( - instanceId, - instanceConfig, - submissionPubId, - evaluator - ); - //schedule final eval reminder email to evaluator - await scheduleFinalEvaluationReminderEmail( - instanceId, - instanceConfig, - submissionPubId, - evaluator - ); - //schedule follow up to final eval reminder email to evaluator - await scheduleFollowUpToFinalEvaluationReminderEmail( - instanceId, - instanceConfig, - submissionPubId, - evaluator - ); - // schedule no-submit notification email to evalutaor - await sendNoticeOfNoSubmitEmail(instanceId, instanceConfig, submissionPubId, evaluator); - } catch (error) { - return { error: error.message }; - } - redirect(`/actions/respond/accepted${redirectParams}`); + ); }; export const decline = async ( instanceId: string, submissionPubId: string ): Promise => { - const redirectParams = `?token=${cookie( - "token" - )}&instanceId=${instanceId}&pubId=${submissionPubId}`; - try { - const submissionPub = await client.getPub(instanceId, submissionPubId); - const user = JSON.parse(expect(cookie("user"))); - const instanceConfig = expect( - await getInstanceConfig(instanceId), - "Instance not configured" - ); - const instanceState = (await getInstanceState(instanceId, submissionPubId)) ?? {}; - let evaluator = expect(instanceState[user.id], "User was not invited to evaluate this pub"); - // Declining again is a no-op. - if (evaluator.status !== "declined") { - // Assert the user is invited to evaluate this pub. - assertIsInvited(evaluator); - // Update the evaluator's status to declined. - evaluator = instanceState[user.id] = { ...evaluator, status: "declined" }; - await setInstanceState(instanceId, submissionPubId, instanceState); - // Unschedule reminder email. - await unscheduleInvitationReminderEmail(instanceId, submissionPubId, evaluator); - // Unschedule no-reply notification email. - await unscheduleNoReplyNotificationEmail(instanceId, submissionPubId, evaluator); - // Immediately send declined notification email. - await sendDeclinedNotificationEmail( - instanceId, - instanceConfig, - submissionPubId, - evaluator, - submissionPub.assignee - ); + return withServerActionInstrumentation( + "evaluations/respond/decline", + { + headers: headers(), + }, + async () => { + const redirectParams = `?token=${cookie( + "token" + )}&instanceId=${instanceId}&pubId=${submissionPubId}`; + try { + const submissionPub = await client.getPub(instanceId, submissionPubId); + const user = JSON.parse(expect(cookie("user"))); + const instanceConfig = expect( + await getInstanceConfig(instanceId), + "Instance not configured" + ); + const instanceState = (await getInstanceState(instanceId, submissionPubId)) ?? {}; + let evaluator = expect( + instanceState[user.id], + "User was not invited to evaluate this pub" + ); + // Declining again is a no-op. + if (evaluator.status !== "declined") { + // Assert the user is invited to evaluate this pub. + assertIsInvited(evaluator); + // Update the evaluator's status to declined. + evaluator = instanceState[user.id] = { ...evaluator, status: "declined" }; + await setInstanceState(instanceId, submissionPubId, instanceState); + // Unschedule reminder email. + await unscheduleInvitationReminderEmail(instanceId, submissionPubId, evaluator); + // Unschedule no-reply notification email. + await unscheduleNoReplyNotificationEmail( + instanceId, + submissionPubId, + evaluator + ); + // Immediately send declined notification email. + await sendDeclinedNotificationEmail( + instanceId, + instanceConfig, + submissionPubId, + evaluator, + submissionPub.assignee + ); + } + } catch (error) { + return { error: error.message }; + } + redirect(`/actions/respond/declined${redirectParams}`); } - } catch (error) { - return { error: error.message }; - } - redirect(`/actions/respond/declined${redirectParams}`); + ); }; diff --git a/integrations/evaluations/global-error.tsx b/integrations/evaluations/global-error.tsx new file mode 100644 index 0000000000..7632da0825 --- /dev/null +++ b/integrations/evaluations/global-error.tsx @@ -0,0 +1,19 @@ +"use client"; + +import * as Sentry from "@sentry/nextjs"; +import NextError from "next/error"; +import { useEffect } from "react"; + +export default function GlobalError({ error }: { error: Error & { digest?: string } }) { + useEffect(() => { + Sentry.captureException(error); + }, [error]); + + return ( + + + + + + ); +} diff --git a/integrations/submissions/app/actions/submit/actions.ts b/integrations/submissions/app/actions/submit/actions.ts index e47a24a03d..c9db687168 100644 --- a/integrations/submissions/app/actions/submit/actions.ts +++ b/integrations/submissions/app/actions/submit/actions.ts @@ -1,26 +1,35 @@ "use server"; import { PubValues } from "@pubpub/sdk"; - +import { withServerActionInstrumentation } from "@sentry/nextjs"; +import { headers } from "next/headers"; import { getInstanceConfig } from "~/lib/instance"; import { makePubFromDoi, makePubFromTitle, makePubFromUrl } from "~/lib/metadata"; import { client } from "~/lib/pubpub"; export const submit = async (instanceId: string, values: PubValues, assigneeId: string) => { - try { - const instance = await getInstanceConfig(instanceId); - if (instance === undefined) { - return { error: "Instance not configured" }; + return withServerActionInstrumentation( + "submissions/submit", + { + headers: headers(), + }, + async () => { + try { + const instance = await getInstanceConfig(instanceId); + if (instance === undefined) { + return { error: "Instance not configured" }; + } + const pub = await client.createPub(instanceId, { + assigneeId, + values, + pubTypeId: instance.pubTypeId, + }); + return pub; + } catch (error) { + return { error: error.message }; + } } - const pub = await client.createPub(instanceId, { - assigneeId, - values, - pubTypeId: instance.pubTypeId, - }); - return pub; - } catch (error) { - return { error: error.message }; - } + ); }; const metadataResolvers = { @@ -33,16 +42,24 @@ export const resolveMetadata = async ( identifierName: string, identifierValue: string ): Promise | { error: string }> => { - const resolve = metadataResolvers[identifierName]; - try { - if (resolve !== undefined) { - const pub = await resolve(identifierValue); - if (pub !== null) { - return pub; + return withServerActionInstrumentation( + "submissions/resolveMetadata", + { + headers: headers(), + }, + async () => { + const resolve = metadataResolvers[identifierName]; + try { + if (resolve !== undefined) { + const pub = await resolve(identifierValue); + if (pub !== null) { + return pub; + } + } + } catch (error) { + return { error: "There was an error resolving metadata." }; } + return { error: "No metdata found." }; } - } catch (error) { - return { error: "There was an error resolving metadata." }; - } - return { error: "No metdata found." }; + ); }; diff --git a/integrations/submissions/global-error.tsx b/integrations/submissions/global-error.tsx new file mode 100644 index 0000000000..7632da0825 --- /dev/null +++ b/integrations/submissions/global-error.tsx @@ -0,0 +1,19 @@ +"use client"; + +import * as Sentry from "@sentry/nextjs"; +import NextError from "next/error"; +import { useEffect } from "react"; + +export default function GlobalError({ error }: { error: Error & { digest?: string } }) { + useEffect(() => { + Sentry.captureException(error); + }, [error]); + + return ( + + + + + + ); +} From 1d6e7f5b5ec1dd25bf978c2018850234cb2e8cd5 Mon Sep 17 00:00:00 2001 From: eric Date: Thu, 4 Apr 2024 07:55:28 -0400 Subject: [PATCH 02/11] Put global-error.tsx files in the correct place --- core/app/c/[communitySlug]/stages/manage/actions.ts | 2 +- core/{ => app}/global-error.tsx | 0 integrations/evaluations/app/actions/evaluate/actions.ts | 4 ++-- integrations/evaluations/app/actions/manage/actions.ts | 6 +++--- integrations/evaluations/app/actions/respond/actions.ts | 2 +- integrations/evaluations/{ => app}/global-error.tsx | 0 integrations/submissions/app/actions/submit/actions.ts | 4 ++-- integrations/submissions/{ => app}/global-error.tsx | 0 8 files changed, 9 insertions(+), 9 deletions(-) rename core/{ => app}/global-error.tsx (100%) rename integrations/evaluations/{ => app}/global-error.tsx (100%) rename integrations/submissions/{ => app}/global-error.tsx (100%) diff --git a/core/app/c/[communitySlug]/stages/manage/actions.ts b/core/app/c/[communitySlug]/stages/manage/actions.ts index ab65d13cb8..0bfde5e38a 100644 --- a/core/app/c/[communitySlug]/stages/manage/actions.ts +++ b/core/app/c/[communitySlug]/stages/manage/actions.ts @@ -1,6 +1,6 @@ "use server"; -import { withServerActionInstrumentation } from "@sentry/nextjs"; +import { withServerActionInstrumentation, captureException } from "@sentry/nextjs"; import { revalidateTag } from "next/cache"; import { headers } from "next/headers"; import db from "~/prisma/db"; diff --git a/core/global-error.tsx b/core/app/global-error.tsx similarity index 100% rename from core/global-error.tsx rename to core/app/global-error.tsx diff --git a/integrations/evaluations/app/actions/evaluate/actions.ts b/integrations/evaluations/app/actions/evaluate/actions.ts index e144fafefd..993ee4d4db 100644 --- a/integrations/evaluations/app/actions/evaluate/actions.ts +++ b/integrations/evaluations/app/actions/evaluate/actions.ts @@ -18,7 +18,7 @@ import { assertHasAccepted } from "~/lib/types"; export const submit = async (instanceId: string, submissionPubId: string, values: PubValues) => { return withServerActionInstrumentation( - "evaluations/evaluate/submit", + "evaluate/submit", { headers: headers(), }, @@ -74,7 +74,7 @@ export const submit = async (instanceId: string, submissionPubId: string, values export const upload = async (instanceId: string, pubId: string, fileName: string) => { return withServerActionInstrumentation( - "evaluations/evaluate/upload", + "evaluate/upload", { headers: headers(), }, diff --git a/integrations/evaluations/app/actions/manage/actions.ts b/integrations/evaluations/app/actions/manage/actions.ts index c5fd7fa03b..10e0b1467b 100644 --- a/integrations/evaluations/app/actions/manage/actions.ts +++ b/integrations/evaluations/app/actions/manage/actions.ts @@ -33,7 +33,7 @@ export const save = async ( send: boolean ) => { return withServerActionInstrumentation( - "evaluations/manage/save", + "manage/save", { headers: headers(), }, @@ -127,7 +127,7 @@ export const save = async ( export const suggest = async (instanceId: string, query: SuggestedMembersQuery) => { return withServerActionInstrumentation( - "evaluations/manage/suggest", + "manage/suggest", { headers: headers(), }, @@ -144,7 +144,7 @@ export const suggest = async (instanceId: string, query: SuggestedMembersQuery) export const remove = async (instanceId: string, pubId: string, userId: string) => { return withServerActionInstrumentation( - "evaluations/manage/remove", + "manage/remove", { headers: headers(), }, diff --git a/integrations/evaluations/app/actions/respond/actions.ts b/integrations/evaluations/app/actions/respond/actions.ts index 42bc842798..4202535dc0 100644 --- a/integrations/evaluations/app/actions/respond/actions.ts +++ b/integrations/evaluations/app/actions/respond/actions.ts @@ -35,7 +35,7 @@ export const accept = async ( submissionPubId: string ): Promise => { return withServerActionInstrumentation( - "evaluations/respond/accept", + "respond/accept", { headers: headers(), }, diff --git a/integrations/evaluations/global-error.tsx b/integrations/evaluations/app/global-error.tsx similarity index 100% rename from integrations/evaluations/global-error.tsx rename to integrations/evaluations/app/global-error.tsx diff --git a/integrations/submissions/app/actions/submit/actions.ts b/integrations/submissions/app/actions/submit/actions.ts index c9db687168..db82e663c7 100644 --- a/integrations/submissions/app/actions/submit/actions.ts +++ b/integrations/submissions/app/actions/submit/actions.ts @@ -9,7 +9,7 @@ import { client } from "~/lib/pubpub"; export const submit = async (instanceId: string, values: PubValues, assigneeId: string) => { return withServerActionInstrumentation( - "submissions/submit", + "submit", { headers: headers(), }, @@ -43,7 +43,7 @@ export const resolveMetadata = async ( identifierValue: string ): Promise | { error: string }> => { return withServerActionInstrumentation( - "submissions/resolveMetadata", + "resolveMetadata", { headers: headers(), }, diff --git a/integrations/submissions/global-error.tsx b/integrations/submissions/app/global-error.tsx similarity index 100% rename from integrations/submissions/global-error.tsx rename to integrations/submissions/app/global-error.tsx From c21ccaf26faccf3afb34a97c536651024997e1db Mon Sep 17 00:00:00 2001 From: eric Date: Thu, 4 Apr 2024 09:21:56 -0400 Subject: [PATCH 03/11] wip --- .../members/[[...add]]/MemberInviteForm.tsx | 21 +-- .../members/[[...add]]/actions.ts | 97 ++++++------ .../stages/manage/StagesContext.tsx | 147 ++++++++---------- .../[communitySlug]/stages/manage/actions.ts | 20 +++ .../panel/StagePanelActionCreator.tsx | 14 +- .../panel/StagePanelActionEditor.tsx | 15 +- .../components/panel/StagePanelActions.tsx | 5 +- core/lib/error/UIException.ts | 15 ++ core/lib/error/useDisplayUiException.tsx | 18 +++ .../app/actions/evaluate/actions.ts | 4 +- .../evaluations/app/actions/manage/actions.ts | 5 +- .../app/actions/respond/actions.ts | 4 +- .../evaluations/app/configure/actions.ts | 21 ++- .../submissions/app/actions/submit/actions.ts | 4 +- .../submissions/app/configure/actions.ts | 23 ++- 15 files changed, 251 insertions(+), 162 deletions(-) create mode 100644 core/lib/error/UIException.ts create mode 100644 core/lib/error/useDisplayUiException.tsx diff --git a/core/app/c/[communitySlug]/members/[[...add]]/MemberInviteForm.tsx b/core/app/c/[communitySlug]/members/[[...add]]/MemberInviteForm.tsx index 27aa29ae9c..a0b994c2c1 100644 --- a/core/app/c/[communitySlug]/members/[[...add]]/MemberInviteForm.tsx +++ b/core/app/c/[communitySlug]/members/[[...add]]/MemberInviteForm.tsx @@ -28,6 +28,8 @@ import { toast } from "ui/use-toast"; import * as actions from "./actions"; import { MemberFormState } from "./AddMember"; import { memberInviteFormSchema } from "./memberInviteFormSchema"; +import { isUiException } from "~/lib/error/UIException"; +import { useDisplayUiException } from "~/lib/error/useDisplayUiException"; export const MemberInviteForm = ({ community, @@ -38,6 +40,7 @@ export const MemberInviteForm = ({ state: MemberFormState; email?: string; }) => { + const displayUiException = useDisplayUiException(); const [isPending, startTransition] = useTransition(); const router = useRouter(); @@ -71,7 +74,7 @@ export const MemberInviteForm = ({ return; } - const { error } = await actions.createUserWithMembership({ + const result = await actions.createUserWithMembership({ email: data.email, firstName: data.firstName!, lastName: data.lastName!, @@ -79,12 +82,8 @@ export const MemberInviteForm = ({ canAdmin: Boolean(data.canAdmin), }); - if (error) { - toast({ - title: "Error", - description: error, - variant: "destructive", - }); + if (isUiException(result)) { + displayUiException(result); return; } @@ -103,12 +102,8 @@ export const MemberInviteForm = ({ community, }); - if ("error" in result) { - toast({ - title: "Error", - description: `Failed to add member. ${result.error}`, - variant: "destructive", - }); + if (isUiException(result)) { + displayUiException(result); return; } diff --git a/core/app/c/[communitySlug]/members/[[...add]]/actions.ts b/core/app/c/[communitySlug]/members/[[...add]]/actions.ts index 8a18db55ba..402b84eeea 100644 --- a/core/app/c/[communitySlug]/members/[[...add]]/actions.ts +++ b/core/app/c/[communitySlug]/members/[[...add]]/actions.ts @@ -8,6 +8,7 @@ import { headers } from "next/headers"; import { cache } from "react"; import { getLoginData } from "~/lib/auth/loginData"; import { env } from "~/lib/env/env.mjs"; +import { makeUiException } from "~/lib/error/UIException"; import type { SuggestedUser } from "~/lib/server/members"; import { generateHash, slugifyString } from "~/lib/string"; import { formatSupabaseError } from "~/lib/supabase"; @@ -171,7 +172,9 @@ export const addMember = async ({ async () => { const { error: adminError } = await isCommunityAdmin(community); if (adminError) { - return { error: adminError }; + return makeUiException( + "You do not have permission to invite members to this community" + ); } try { @@ -183,7 +186,7 @@ export const addMember = async ({ }); if (existingMember) { - return { error: "User is already a member of this community" }; + return makeUiException("User is already a member of this community"); } const member = await prisma.member.create({ @@ -212,7 +215,7 @@ export const addMember = async ({ }); if (supabaseInviteError) { - return { error: supabaseInviteError }; + return makeUiException("Failed to add member"); } await prisma.user.update({ @@ -226,7 +229,7 @@ export const addMember = async ({ return { member }; } catch (error) { - return { error: error.message }; + return makeUiException("Failed to add member", captureException(error)); } } ); @@ -255,52 +258,58 @@ export const createUserWithMembership = async ({ headers: headers(), }, async () => { - const { error: adminError } = await isCommunityAdmin(community); - if (adminError) { - return { error: adminError }; - } + try { + const { error: adminError } = await isCommunityAdmin(community); + if (adminError) { + return makeUiException( + "You do not have permission to invite members to this community" + ); + } - const user = await prisma.user.create({ - data: { + const user = await prisma.user.create({ + data: { + email, + firstName, + lastName, + slug: `${slugifyString(firstName)}${ + lastName ? `-${slugifyString(lastName)}` : "" + }-${generateHash(4, "0123456789")}`, + memberships: { + create: { + communityId: community.id, + canAdmin, + }, + }, + }, + }); + + const { error: supabaseError, user: supabaseUser } = await addSupabaseUser({ email, firstName, lastName, - slug: `${slugifyString(firstName)}${ - lastName ? `-${slugifyString(lastName)}` : "" - }-${generateHash(4, "0123456789")}`, - memberships: { - create: { - communityId: community.id, - canAdmin, - }, - }, - }, - }); - - const { error: supabaseError, user: supabaseUser } = await addSupabaseUser({ - email, - firstName, - lastName, - community, - canAdmin, - }); - - if (supabaseError !== null) { - return { error: supabaseError }; - } + community, + canAdmin, + }); + + if (supabaseError !== null) { + return makeUiException("Failed to create user", supabaseError); + } - await prisma.user.update({ - where: { - id: user.id, - }, - data: { - supabaseId: supabaseUser.id, - }, - }); + await prisma.user.update({ + where: { + id: user.id, + }, + data: { + supabaseId: supabaseUser.id, + }, + }); - revalidateMemberPathsAndTags(community); + revalidateMemberPathsAndTags(community); - return { user }; + return { user }; + } catch (error) { + return makeUiException("Failed to create user", captureException(error)); + } } ); }; @@ -342,7 +351,7 @@ export const removeMember = async ({ revalidateMemberPathsAndTags(community); return { success: true }; } catch (error) { - return { error: error.message }; + return makeUiException(error.message, captureException(error)); } } ); diff --git a/core/app/c/[communitySlug]/stages/manage/StagesContext.tsx b/core/app/c/[communitySlug]/stages/manage/StagesContext.tsx index ee22f4e9af..bf7fed4842 100644 --- a/core/app/c/[communitySlug]/stages/manage/StagesContext.tsx +++ b/core/app/c/[communitySlug]/stages/manage/StagesContext.tsx @@ -12,8 +12,8 @@ import { useState, } from "react"; -import { logger } from "logger"; +import { useDisplayUiException } from "~/lib/error/useDisplayUiException"; import { ActionPayload, StagePayload, StagePayloadAction } from "~/lib/types"; import * as actions from "./actions"; @@ -166,6 +166,7 @@ type DeleteBatch = { }; export const StagesProvider = (props: StagesProviderProps) => { + const displayUiException = useDisplayUiException(); const [stages, dispatch] = useOptimistic( props.stages, makeOptimisitcStagesReducer(props.communityId) @@ -176,119 +177,107 @@ export const StagesProvider = (props: StagesProviderProps) => { } as DeleteBatch); const createStage = useCallback(async () => { - try { - startTransition(() => { - dispatch({ type: "stage_created" }); - }); - await actions.createStage(props.communityId); - } catch (e) { - logger.error(e); + startTransition(() => { + dispatch({ type: "stage_created" }); + }); + const result = await actions.createStage(props.communityId); + if (result) { + displayUiException(result); } - }, [dispatch, props.communityId]); + }, [dispatch, props.communityId, displayUiException]); const deleteStages = useCallback( async (stageIds: string[]) => { - try { - startTransition(() => { - dispatch({ type: "stages_deleted", stageIds }); - }); - setDeleteBatch((prev) => ({ ...prev, stageIds: [...prev.stageIds, ...stageIds] })); - } catch (e) { - logger.error(e); - } + startTransition(() => { + dispatch({ type: "stages_deleted", stageIds }); + }); + setDeleteBatch((prev) => ({ ...prev, stageIds: [...prev.stageIds, ...stageIds] })); }, [dispatch, props.communityId] ); const deleteStagesAndMoveConstraints = useCallback( async (stageIds: string[], moveConstraintIds: [string, string][]) => { - try { - if (stageIds.length > 0) { - startTransition(() => { - dispatch({ - type: "stages_deleted", - stageIds, - }); + if (stageIds.length > 0) { + startTransition(() => { + dispatch({ + type: "stages_deleted", + stageIds, }); - } - if (moveConstraintIds.length > 0) { - startTransition(() => { - dispatch({ - type: "move_constraints_deleted", - moveConstraintIds, - }); + }); + } + if (moveConstraintIds.length > 0) { + startTransition(() => { + dispatch({ + type: "move_constraints_deleted", + moveConstraintIds, }); - } - await actions.deleteStagesAndMoveConstraints( - props.communityId, - stageIds, - moveConstraintIds - ); - } catch (e) { - logger.error(e); + }); + } + const result = await actions.deleteStagesAndMoveConstraints( + props.communityId, + stageIds, + moveConstraintIds + ); + if (result) { + displayUiException(result); } }, - [dispatch, props.communityId] + [dispatch, props.communityId, displayUiException] ); const createMoveConstraint = useCallback( async (sourceStageId: string, destinationStageId: string) => { - try { - startTransition(() => { - dispatch({ - type: "move_constraint_created", - sourceStageId, - destinationStageId, - }); - }); - await actions.createMoveConstraint( - props.communityId, + startTransition(() => { + dispatch({ + type: "move_constraint_created", sourceStageId, - destinationStageId - ); - } catch (e) { - logger.error(e); + destinationStageId, + }); + }); + const result = await actions.createMoveConstraint( + props.communityId, + sourceStageId, + destinationStageId + ); + if (result) { + displayUiException(result); } }, - [dispatch, props.communityId] + [dispatch, props.communityId, displayUiException] ); const deleteMoveConstraints = useCallback( async (moveConstraintIds: [string, string][]) => { - try { - startTransition(() => { - dispatch({ - type: "move_constraints_deleted", - moveConstraintIds, - }); + startTransition(() => { + dispatch({ + type: "move_constraints_deleted", + moveConstraintIds, }); - setDeleteBatch((prev) => ({ - ...prev, - moveConstraintIds: [...prev.moveConstraintIds, ...moveConstraintIds], - })); - } catch (e) { - logger.error(e); - } + }); + setDeleteBatch((prev) => ({ + ...prev, + moveConstraintIds: [...prev.moveConstraintIds, ...moveConstraintIds], + })); }, [dispatch, props.communityId] ); const updateStageName = useCallback( async (stageId: string, name: string) => { - try { - startTransition(() => { - dispatch({ - type: "stage_name_updated", - stageId, - name, - }); + startTransition(() => { + dispatch({ + type: "stage_name_updated", + stageId, + name, }); - await actions.updateStageName(props.communityId, stageId, name); - } catch (e) { - logger.error(e); + }); + const result = await actions.updateStageName(props.communityId, stageId, name); + if (result) { + displayUiException(result); } }, - [dispatch, props.communityId] + [dispatch, props.communityId, displayUiException] ); const fetchStages = useCallback(() => { diff --git a/core/app/c/[communitySlug]/stages/manage/actions.ts b/core/app/c/[communitySlug]/stages/manage/actions.ts index 0bfde5e38a..157173a9a0 100644 --- a/core/app/c/[communitySlug]/stages/manage/actions.ts +++ b/core/app/c/[communitySlug]/stages/manage/actions.ts @@ -3,6 +3,7 @@ import { withServerActionInstrumentation, captureException } from "@sentry/nextjs"; import { revalidateTag } from "next/cache"; import { headers } from "next/headers"; +import { makeUiException } from "~/lib/error/UIException"; import db from "~/prisma/db"; export async function createStage(communityId: string) { @@ -24,6 +25,8 @@ export async function createStage(communityId: string) { }, }, }); + } catch (error) { + return makeUiException("Failed to create stage", captureException(error)); } finally { revalidateTag(`community-stages_${communityId}`); } @@ -46,6 +49,8 @@ export async function deleteStages(communityId: string, stageIds: string[]) { }, }, }); + } catch (error) { + return makeUiException("Failed to delete stages", captureException(error)); } finally { revalidateTag(`community-stages_${communityId}`); } @@ -79,6 +84,8 @@ export async function createMoveConstraint( }, }, }); + } catch (error) { + return makeUiException("Failed to connect stages", captureException(error)); } finally { revalidateTag(`community-stages_${communityId}`); } @@ -108,6 +115,11 @@ export async function deleteMoveConstraints( }) ); await Promise.all(ops); + } catch (error) { + return makeUiException( + "Failed to delete stage connections", + captureException(error) + ); } finally { revalidateTag(`community-stages_${communityId}`); } @@ -135,6 +147,8 @@ export async function deleteStagesAndMoveConstraints( if (stageIds.length > 0) { await deleteStages(communityId, stageIds); } + } catch (error) { + return makeUiException("Failed to delete stages", captureException(error)); } finally { revalidateTag(`community-stages_${communityId}`); } @@ -158,6 +172,8 @@ export async function updateStageName(communityId: string, stageId: string, name name, }, }); + } catch (error) { + return makeUiException("Failed to update stage name", captureException(error)); } finally { revalidateTag(`community-stages_${communityId}`); } @@ -199,6 +215,8 @@ export async function addAction(communityId: string, stageId: string, actionId: }, }, }); + } catch (error) { + return makeUiException("Failed to add action", captureException(error)); } finally { revalidateTag(`community-stages_${communityId}`); } @@ -219,6 +237,8 @@ export async function deleteAction(communityId: string, actionId: string) { id: actionId, }, }); + } catch (error) { + return makeUiException("Failed to delete action", captureException(error)); } finally { revalidateTag(`community-stages_${communityId}`); } diff --git a/core/app/c/[communitySlug]/stages/manage/components/panel/StagePanelActionCreator.tsx b/core/app/c/[communitySlug]/stages/manage/components/panel/StagePanelActionCreator.tsx index 36cb3e6858..c7c647f8a0 100644 --- a/core/app/c/[communitySlug]/stages/manage/components/panel/StagePanelActionCreator.tsx +++ b/core/app/c/[communitySlug]/stages/manage/components/panel/StagePanelActionCreator.tsx @@ -13,6 +13,8 @@ import { } from "ui/dialog"; import { FileText, Mail, Terminal } from "ui/icon"; +import { UIException } from "~/lib/error/UIException"; +import { useDisplayUiException } from "~/lib/error/useDisplayUiException"; import { ActionPayload } from "~/lib/types"; type ActionCellProps = { @@ -63,17 +65,21 @@ const ActionCell = (props: ActionCellProps) => { type Props = { actions: ActionPayload[]; - onAdd: (actionId: string) => void; + onAdd: (actionId: string) => Promise; }; export const StagePanelActionCreator = (props: Props) => { + const displayUiException = useDisplayUiException(); const [isOpen, setIsOpen] = useState(false); const onActionSelect = useCallback( - (action: ActionPayload) => { + async (action: ActionPayload) => { setIsOpen(false); - props.onAdd(action.id); + const result = await props.onAdd(action.id); + if (result) { + displayUiException(result); + } }, - [props.onAdd] + [props.onAdd, displayUiException] ); const onOpenChange = useCallback((open: boolean) => { setIsOpen(open); diff --git a/core/app/c/[communitySlug]/stages/manage/components/panel/StagePanelActionEditor.tsx b/core/app/c/[communitySlug]/stages/manage/components/panel/StagePanelActionEditor.tsx index 1bae982284..99909ad717 100644 --- a/core/app/c/[communitySlug]/stages/manage/components/panel/StagePanelActionEditor.tsx +++ b/core/app/c/[communitySlug]/stages/manage/components/panel/StagePanelActionEditor.tsx @@ -1,5 +1,6 @@ "use client"; +import { logger } from "logger"; import { useCallback, useState } from "react"; import { logger } from "logger"; @@ -9,19 +10,25 @@ import { ChevronDown } from "ui/icon"; import { Separator } from "ui/separator"; import { getActionByName } from "~/actions"; +import { UIException } from "~/lib/error/UIException"; +import { useDisplayUiException } from "~/lib/error/useDisplayUiException"; import { StagePayloadActionInstance } from "~/lib/types"; import { StagePanelActionConfig } from "./StagePanelActionConfig"; type Props = { actionInstance: StagePayloadActionInstance; - onDelete: (actionInstanceId: string) => void; + onDelete: (actionInstanceId: string) => Promise; }; export const StagePanelActionEditor = (props: Props) => { + const displayUiException = useDisplayUiException(); const [isOpen, setIsOpen] = useState(false); - const onDeleteClick = useCallback(() => { - props.onDelete(props.actionInstance.id); - }, [props.onDelete, props.actionInstance]); + const onDeleteClick = useCallback(async () => { + const result = await props.onDelete(props.actionInstance.id); + if (result) { + displayUiException(result); + } + }, [props.onDelete, props.actionInstance, displayUiException]); const action = getActionByName(props.actionInstance.action.name); if (!action) { diff --git a/core/app/c/[communitySlug]/stages/manage/components/panel/StagePanelActions.tsx b/core/app/c/[communitySlug]/stages/manage/components/panel/StagePanelActions.tsx index 396cf21335..4538630bb4 100644 --- a/core/app/c/[communitySlug]/stages/manage/components/panel/StagePanelActions.tsx +++ b/core/app/c/[communitySlug]/stages/manage/components/panel/StagePanelActions.tsx @@ -1,13 +1,12 @@ import { Suspense } from "react"; -import { Card, CardContent, CardHeader, CardTitle } from "ui/card"; +import { Card, CardContent } from "ui/card"; import { SkeletonCard } from "~/app/components/skeletons/SkeletonCard"; -import { ActionPayload, StagePayloadActionInstance } from "~/lib/types"; import { addAction, deleteAction } from "../../actions"; -import { getActions, getStage, getStageActions } from "./queries"; import { StagePanelActionCreator } from "./StagePanelActionCreator"; import { StagePanelActionEditor } from "./StagePanelActionEditor"; +import { getActions, getStage, getStageActions } from "./queries"; type PropsInner = { stageId: string; diff --git a/core/lib/error/UIException.ts b/core/lib/error/UIException.ts new file mode 100644 index 0000000000..6ef83a84fe --- /dev/null +++ b/core/lib/error/UIException.ts @@ -0,0 +1,15 @@ +export type UIException = { + isUiException: true; + message: string; + title?: string; + id?: string; +}; + +export const makeUiException = (message: string, id?: string): UIException => ({ + isUiException: true, + message, + id, +}); + +export const isUiException = (error: unknown): error is UIException => + typeof error === "object" && error !== null && "isUiException" in error; diff --git a/core/lib/error/useDisplayUiException.tsx b/core/lib/error/useDisplayUiException.tsx new file mode 100644 index 0000000000..ba1b1dea44 --- /dev/null +++ b/core/lib/error/useDisplayUiException.tsx @@ -0,0 +1,18 @@ +import { useCallback } from "react"; +import { useToast } from "ui/use-toast"; +import { UIException } from "./UIException"; + +export function useDisplayUiException() { + const { toast } = useToast(); + const report = useCallback( + ({ message, id }: UIException) => { + toast({ + title: "Error", + variant: "destructive", + description: `${message}${id ? ` (${id})` : ""}`, + }); + }, + [toast] + ); + return report; +} diff --git a/integrations/evaluations/app/actions/evaluate/actions.ts b/integrations/evaluations/app/actions/evaluate/actions.ts index 993ee4d4db..1a946d0c1f 100644 --- a/integrations/evaluations/app/actions/evaluate/actions.ts +++ b/integrations/evaluations/app/actions/evaluate/actions.ts @@ -1,7 +1,7 @@ "use server"; import { PubValues } from "@pubpub/sdk"; -import { withServerActionInstrumentation } from "@sentry/nextjs"; +import { captureException, withServerActionInstrumentation } from "@sentry/nextjs"; import { revalidatePath } from "next/cache"; import { headers } from "next/headers"; import { expect } from "utils"; @@ -66,6 +66,7 @@ export const submit = async (instanceId: string, submissionPubId: string, values revalidatePath("/"); return { success: true }; } catch (error) { + captureException(error); return { error: error.message }; } } @@ -82,6 +83,7 @@ export const upload = async (instanceId: string, pubId: string, fileName: string try { return await client.generateSignedAssetUploadUrl(instanceId, pubId, fileName); } catch (error) { + captureException(error); return { error: error.message }; } } diff --git a/integrations/evaluations/app/actions/manage/actions.ts b/integrations/evaluations/app/actions/manage/actions.ts index 10e0b1467b..5e18edd512 100644 --- a/integrations/evaluations/app/actions/manage/actions.ts +++ b/integrations/evaluations/app/actions/manage/actions.ts @@ -23,7 +23,7 @@ import { isInvited, } from "~/lib/types"; import { InviteFormEvaluator } from "./types"; -import { withServerActionInstrumentation } from "@sentry/nextjs"; +import { captureException, withServerActionInstrumentation } from "@sentry/nextjs"; import { headers } from "next/headers"; export const save = async ( @@ -119,6 +119,7 @@ export const save = async ( revalidatePath("/"); return { success: true }; } catch (error) { + captureException(error); return { error: error.message }; } } @@ -136,6 +137,7 @@ export const suggest = async (instanceId: string, query: SuggestedMembersQuery) const users = await client.getSuggestedMembers(instanceId, query); return users; } catch (error) { + captureException(error); return { error: error.message }; } } @@ -177,6 +179,7 @@ export const remove = async (instanceId: string, pubId: string, userId: string) return { success: true }; } catch (error) { + captureException(error); return { error: error.message }; } } diff --git a/integrations/evaluations/app/actions/respond/actions.ts b/integrations/evaluations/app/actions/respond/actions.ts index 4202535dc0..8374a5c73c 100644 --- a/integrations/evaluations/app/actions/respond/actions.ts +++ b/integrations/evaluations/app/actions/respond/actions.ts @@ -1,6 +1,6 @@ "use server"; -import { withServerActionInstrumentation } from "@sentry/nextjs"; +import { captureException, withServerActionInstrumentation } from "@sentry/nextjs"; import { headers } from "next/headers"; import { redirect } from "next/navigation"; @@ -137,6 +137,7 @@ export const accept = async ( evaluator ); } catch (error) { + captureException(error); return { error: error.message }; } redirect(`/actions/respond/accepted${redirectParams}`); @@ -194,6 +195,7 @@ export const decline = async ( ); } } catch (error) { + captureException(error); return { error: error.message }; } redirect(`/actions/respond/declined${redirectParams}`); diff --git a/integrations/evaluations/app/configure/actions.ts b/integrations/evaluations/app/configure/actions.ts index 14d15eccc1..634cae5bdb 100644 --- a/integrations/evaluations/app/configure/actions.ts +++ b/integrations/evaluations/app/configure/actions.ts @@ -1,12 +1,23 @@ "use server"; +import { captureException, withServerActionInstrumentation } from "@sentry/nextjs"; +import { headers } from "next/headers"; import { setInstanceConfig } from "~/lib/instance"; import { InstanceConfig } from "~/lib/types"; export const configure = (instanceId: string, instanceConfig: InstanceConfig) => { - try { - return setInstanceConfig(instanceId, instanceConfig); - } catch (error) { - return { error: error.message }; - } + return withServerActionInstrumentation( + "configure", + { + headers: headers(), + }, + async () => { + try { + return setInstanceConfig(instanceId, instanceConfig); + } catch (error) { + captureException(error); + return { error: error.message }; + } + } + ); }; diff --git a/integrations/submissions/app/actions/submit/actions.ts b/integrations/submissions/app/actions/submit/actions.ts index db82e663c7..4a2ce3ff52 100644 --- a/integrations/submissions/app/actions/submit/actions.ts +++ b/integrations/submissions/app/actions/submit/actions.ts @@ -1,7 +1,7 @@ "use server"; import { PubValues } from "@pubpub/sdk"; -import { withServerActionInstrumentation } from "@sentry/nextjs"; +import { captureException, withServerActionInstrumentation } from "@sentry/nextjs"; import { headers } from "next/headers"; import { getInstanceConfig } from "~/lib/instance"; import { makePubFromDoi, makePubFromTitle, makePubFromUrl } from "~/lib/metadata"; @@ -26,6 +26,7 @@ export const submit = async (instanceId: string, values: PubValues, assigneeId: }); return pub; } catch (error) { + captureException(error); return { error: error.message }; } } @@ -57,6 +58,7 @@ export const resolveMetadata = async ( } } } catch (error) { + captureException(error); return { error: "There was an error resolving metadata." }; } return { error: "No metdata found." }; diff --git a/integrations/submissions/app/configure/actions.ts b/integrations/submissions/app/configure/actions.ts index 555b2e5fb4..0f0c7ce3d6 100644 --- a/integrations/submissions/app/configure/actions.ts +++ b/integrations/submissions/app/configure/actions.ts @@ -1,12 +1,23 @@ "use server"; +import { captureException, withServerActionInstrumentation } from "@sentry/nextjs"; +import { headers } from "next/headers"; import { setInstanceConfig } from "~/lib/instance"; export const configure = async (instanceId: string, pubTypeId: string) => { - try { - await setInstanceConfig(instanceId, { pubTypeId }); - return { success: true }; - } catch (error) { - return { error: error.message }; - } + return withServerActionInstrumentation( + "configure", + { + headers: headers(), + }, + async () => { + try { + await setInstanceConfig(instanceId, { pubTypeId }); + return { success: true }; + } catch (error) { + captureException(error); + return { error: error.message }; + } + } + ); }; From b6156626a93b895de94c26eeb1cbe455571f64f5 Mon Sep 17 00:00:00 2001 From: Eric McDaniel Date: Thu, 4 Apr 2024 10:48:57 -0400 Subject: [PATCH 04/11] client exception error --- .../members/[[...add]]/MemberInviteForm.tsx | 14 ++--- .../members/[[...add]]/RemoveMemberButton.tsx | 13 ++-- .../members/[[...add]]/actions.ts | 60 ++++++++++++++----- .../stages/manage/StagesContext.tsx | 31 ++++------ .../[communitySlug]/stages/manage/actions.ts | 18 +++--- .../components/editor/StageEditorNode.tsx | 1 - .../panel/StagePanelActionCreator.tsx | 12 ++-- .../panel/StagePanelActionEditor.tsx | 12 ++-- core/lib/error/ClientException.ts | 23 +++++++ core/lib/error/UIException.ts | 15 ----- core/lib/error/useDisplayUiException.tsx | 18 ------ core/lib/error/useShowClientException.tsx | 20 +++++++ 12 files changed, 132 insertions(+), 105 deletions(-) create mode 100644 core/lib/error/ClientException.ts delete mode 100644 core/lib/error/UIException.ts delete mode 100644 core/lib/error/useDisplayUiException.tsx create mode 100644 core/lib/error/useShowClientException.tsx diff --git a/core/app/c/[communitySlug]/members/[[...add]]/MemberInviteForm.tsx b/core/app/c/[communitySlug]/members/[[...add]]/MemberInviteForm.tsx index a0b994c2c1..d1fad2cc7e 100644 --- a/core/app/c/[communitySlug]/members/[[...add]]/MemberInviteForm.tsx +++ b/core/app/c/[communitySlug]/members/[[...add]]/MemberInviteForm.tsx @@ -28,8 +28,8 @@ import { toast } from "ui/use-toast"; import * as actions from "./actions"; import { MemberFormState } from "./AddMember"; import { memberInviteFormSchema } from "./memberInviteFormSchema"; -import { isUiException } from "~/lib/error/UIException"; -import { useDisplayUiException } from "~/lib/error/useDisplayUiException"; +import { isClientException } from "~/lib/error/ClientException"; +import { useShowClientException } from "~/lib/error/useShowClientException"; export const MemberInviteForm = ({ community, @@ -40,7 +40,7 @@ export const MemberInviteForm = ({ state: MemberFormState; email?: string; }) => { - const displayUiException = useDisplayUiException(); + const showClientException = useShowClientException(); const [isPending, startTransition] = useTransition(); const router = useRouter(); @@ -82,8 +82,8 @@ export const MemberInviteForm = ({ canAdmin: Boolean(data.canAdmin), }); - if (isUiException(result)) { - displayUiException(result); + if (isClientException(result)) { + showClientException(result); return; } @@ -102,8 +102,8 @@ export const MemberInviteForm = ({ community, }); - if (isUiException(result)) { - displayUiException(result); + if (isClientException(result)) { + showClientException(result); return; } diff --git a/core/app/c/[communitySlug]/members/[[...add]]/RemoveMemberButton.tsx b/core/app/c/[communitySlug]/members/[[...add]]/RemoveMemberButton.tsx index a8701f98bb..ffb2efec41 100644 --- a/core/app/c/[communitySlug]/members/[[...add]]/RemoveMemberButton.tsx +++ b/core/app/c/[communitySlug]/members/[[...add]]/RemoveMemberButton.tsx @@ -1,6 +1,6 @@ "use client"; -import type { Community, Member, User } from "@prisma/client"; +import type { Community } from "@prisma/client"; import { AlertDialog, @@ -16,6 +16,8 @@ import { Trash } from "ui/icon"; import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "ui/tooltip"; import { toast } from "ui/use-toast"; +import { isClientException } from "~/lib/error/ClientException"; +import { useShowClientException } from "~/lib/error/useShowClientException"; import * as actions from "./actions"; import { TableMember } from "./getMemberTableColumns"; @@ -26,6 +28,7 @@ export const RemoveMemberButton = ({ member: TableMember; community: Community; }) => { + const showClientException = useShowClientException(); return ( @@ -57,12 +60,8 @@ export const RemoveMemberButton = ({ { const response = await actions.removeMember({ member, community }); - if ("error" in response) { - toast({ - title: "Error", - description: response.error, - variant: "destructive", - }); + if (isClientException(response)) { + showClientException(response); return; } toast({ diff --git a/core/app/c/[communitySlug]/members/[[...add]]/actions.ts b/core/app/c/[communitySlug]/members/[[...add]]/actions.ts index 402b84eeea..ac24e29f39 100644 --- a/core/app/c/[communitySlug]/members/[[...add]]/actions.ts +++ b/core/app/c/[communitySlug]/members/[[...add]]/actions.ts @@ -8,7 +8,7 @@ import { headers } from "next/headers"; import { cache } from "react"; import { getLoginData } from "~/lib/auth/loginData"; import { env } from "~/lib/env/env.mjs"; -import { makeUiException } from "~/lib/error/UIException"; +import { makeClientException } from "~/lib/error/ClientException"; import type { SuggestedUser } from "~/lib/server/members"; import { generateHash, slugifyString } from "~/lib/string"; import { formatSupabaseError } from "~/lib/supabase"; @@ -172,9 +172,10 @@ export const addMember = async ({ async () => { const { error: adminError } = await isCommunityAdmin(community); if (adminError) { - return makeUiException( - "You do not have permission to invite members to this community" - ); + return makeClientException({ + title: "Failed to add member", + message: "You do not have permission to invite members to this community", + }); } try { @@ -186,7 +187,10 @@ export const addMember = async ({ }); if (existingMember) { - return makeUiException("User is already a member of this community"); + return makeClientException({ + title: "Failed to add member", + message: "User is already a member of this community", + }); } const member = await prisma.member.create({ @@ -215,7 +219,10 @@ export const addMember = async ({ }); if (supabaseInviteError) { - return makeUiException("Failed to add member"); + return makeClientException({ + title: "Failed to add member", + message: "We encounted a problem with our authentication provider.", + }); } await prisma.user.update({ @@ -229,7 +236,10 @@ export const addMember = async ({ return { member }; } catch (error) { - return makeUiException("Failed to add member", captureException(error)); + return makeClientException({ + title: "Failed to add member", + id: captureException(error), + }); } } ); @@ -261,9 +271,10 @@ export const createUserWithMembership = async ({ try { const { error: adminError } = await isCommunityAdmin(community); if (adminError) { - return makeUiException( - "You do not have permission to invite members to this community" - ); + return makeClientException({ + title: "Failed to add member", + message: "You do not have permission to invite members to this community", + }); } const user = await prisma.user.create({ @@ -292,7 +303,10 @@ export const createUserWithMembership = async ({ }); if (supabaseError !== null) { - return makeUiException("Failed to create user", supabaseError); + return makeClientException({ + title: "Failed to add member", + message: "We encounted a problem with our authentication provider.", + }); } await prisma.user.update({ @@ -308,7 +322,10 @@ export const createUserWithMembership = async ({ return { user }; } catch (error) { - return makeUiException("Failed to create user", captureException(error)); + return makeClientException({ + title: "Failed to add member", + id: captureException(error), + }); } } ); @@ -331,11 +348,17 @@ export const removeMember = async ({ const { loginData, error: adminError } = await isCommunityAdmin(community); if (adminError) { - return { error: adminError }; + return makeClientException({ + title: "Failed to remove member", + message: adminError, + }); } if (loginData?.memberships.find((m) => m.id === member.id)) { - return { error: "You cannot remove yourself from the community" }; + return makeClientException({ + title: "Failed to remove member", + message: "You cannot remove yourself from the community", + }); } const deleted = await prisma.member.delete({ @@ -345,13 +368,18 @@ export const removeMember = async ({ }); if (!deleted) { - return { error: "Failed to remove member" }; + return makeClientException({ + title: "Failed to remove member", + }); } revalidateMemberPathsAndTags(community); return { success: true }; } catch (error) { - return makeUiException(error.message, captureException(error)); + return makeClientException({ + title: "Failed to remove member", + id: captureException(error), + }); } } ); diff --git a/core/app/c/[communitySlug]/stages/manage/StagesContext.tsx b/core/app/c/[communitySlug]/stages/manage/StagesContext.tsx index bf7fed4842..8615697709 100644 --- a/core/app/c/[communitySlug]/stages/manage/StagesContext.tsx +++ b/core/app/c/[communitySlug]/stages/manage/StagesContext.tsx @@ -13,8 +13,8 @@ import { } from "react"; -import { useDisplayUiException } from "~/lib/error/useDisplayUiException"; -import { ActionPayload, StagePayload, StagePayloadAction } from "~/lib/types"; +import { useShowClientException } from "~/lib/error/useShowClientException"; +import { ActionPayload, StagePayload } from "~/lib/types"; import * as actions from "./actions"; export type StagesContext = { @@ -82,15 +82,6 @@ const makeOptimisticMoveConstraint = (source: StagePayload, destination: StagePa updatedAt: new Date(), }); -const makeOptimisticActionInstance = (action: StagePayloadAction, stageId: string) => ({ - id: "new", - action, - actionId: action.id, - stageId, - createdAt: new Date(), - updatedAt: new Date(), -}); - const makeOptimisitcStagesReducer = (communityId: string) => (state: StagePayload[], action: Action): StagePayload[] => { @@ -166,7 +157,7 @@ type DeleteBatch = { }; export const StagesProvider = (props: StagesProviderProps) => { - const displayUiException = useDisplayUiException(); + const showClientException = useShowClientException(); const [stages, dispatch] = useOptimistic( props.stages, makeOptimisitcStagesReducer(props.communityId) @@ -182,9 +173,9 @@ export const StagesProvider = (props: StagesProviderProps) => { }); const result = await actions.createStage(props.communityId); if (result) { - displayUiException(result); + showClientException(result); } - }, [dispatch, props.communityId, displayUiException]); + }, [dispatch, props.communityId, showClientException]); const deleteStages = useCallback( async (stageIds: string[]) => { @@ -220,10 +211,10 @@ export const StagesProvider = (props: StagesProviderProps) => { moveConstraintIds ); if (result) { - displayUiException(result); + showClientException(result); } }, - [dispatch, props.communityId, displayUiException] + [dispatch, props.communityId, showClientException] ); const createMoveConstraint = useCallback( @@ -241,10 +232,10 @@ export const StagesProvider = (props: StagesProviderProps) => { destinationStageId ); if (result) { - displayUiException(result); + showClientException(result); } }, - [dispatch, props.communityId, displayUiException] + [dispatch, props.communityId, showClientException] ); const deleteMoveConstraints = useCallback( @@ -274,10 +265,10 @@ export const StagesProvider = (props: StagesProviderProps) => { }); const result = await actions.updateStageName(props.communityId, stageId, name); if (result) { - displayUiException(result); + showClientException(result); } }, - [dispatch, props.communityId, displayUiException] + [dispatch, props.communityId, showClientException] ); const fetchStages = useCallback(() => { diff --git a/core/app/c/[communitySlug]/stages/manage/actions.ts b/core/app/c/[communitySlug]/stages/manage/actions.ts index 157173a9a0..3f7a99e7a9 100644 --- a/core/app/c/[communitySlug]/stages/manage/actions.ts +++ b/core/app/c/[communitySlug]/stages/manage/actions.ts @@ -3,7 +3,7 @@ import { withServerActionInstrumentation, captureException } from "@sentry/nextjs"; import { revalidateTag } from "next/cache"; import { headers } from "next/headers"; -import { makeUiException } from "~/lib/error/UIException"; +import { makeClientException } from "~/lib/error/ClientException"; import db from "~/prisma/db"; export async function createStage(communityId: string) { @@ -26,7 +26,7 @@ export async function createStage(communityId: string) { }, }); } catch (error) { - return makeUiException("Failed to create stage", captureException(error)); + return makeClientException("Failed to create stage", captureException(error)); } finally { revalidateTag(`community-stages_${communityId}`); } @@ -50,7 +50,7 @@ export async function deleteStages(communityId: string, stageIds: string[]) { }, }); } catch (error) { - return makeUiException("Failed to delete stages", captureException(error)); + return makeClientException("Failed to delete stages", captureException(error)); } finally { revalidateTag(`community-stages_${communityId}`); } @@ -85,7 +85,7 @@ export async function createMoveConstraint( }, }); } catch (error) { - return makeUiException("Failed to connect stages", captureException(error)); + return makeClientException("Failed to connect stages", captureException(error)); } finally { revalidateTag(`community-stages_${communityId}`); } @@ -116,7 +116,7 @@ export async function deleteMoveConstraints( ); await Promise.all(ops); } catch (error) { - return makeUiException( + return makeClientException( "Failed to delete stage connections", captureException(error) ); @@ -148,7 +148,7 @@ export async function deleteStagesAndMoveConstraints( await deleteStages(communityId, stageIds); } } catch (error) { - return makeUiException("Failed to delete stages", captureException(error)); + return makeClientException("Failed to delete stages", captureException(error)); } finally { revalidateTag(`community-stages_${communityId}`); } @@ -173,7 +173,7 @@ export async function updateStageName(communityId: string, stageId: string, name }, }); } catch (error) { - return makeUiException("Failed to update stage name", captureException(error)); + return makeClientException("Failed to update stage name", captureException(error)); } finally { revalidateTag(`community-stages_${communityId}`); } @@ -216,7 +216,7 @@ export async function addAction(communityId: string, stageId: string, actionId: }, }); } catch (error) { - return makeUiException("Failed to add action", captureException(error)); + return makeClientException("Failed to add action", captureException(error)); } finally { revalidateTag(`community-stages_${communityId}`); } @@ -238,7 +238,7 @@ export async function deleteAction(communityId: string, actionId: string) { }, }); } catch (error) { - return makeUiException("Failed to delete action", captureException(error)); + return makeClientException("Failed to delete action", captureException(error)); } finally { revalidateTag(`community-stages_${communityId}`); } diff --git a/core/app/c/[communitySlug]/stages/manage/components/editor/StageEditorNode.tsx b/core/app/c/[communitySlug]/stages/manage/components/editor/StageEditorNode.tsx index 8a6af08d5b..58bcc2b632 100644 --- a/core/app/c/[communitySlug]/stages/manage/components/editor/StageEditorNode.tsx +++ b/core/app/c/[communitySlug]/stages/manage/components/editor/StageEditorNode.tsx @@ -9,7 +9,6 @@ import { cn, expect } from "utils"; import { StagePayload } from "~/lib/types"; import { useStages } from "../../StagesContext"; -import { useStageEditor } from "./StageEditorContext"; export const STAGE_NODE_WIDTH = 250; export const STAGE_NODE_HEIGHT = 50; diff --git a/core/app/c/[communitySlug]/stages/manage/components/panel/StagePanelActionCreator.tsx b/core/app/c/[communitySlug]/stages/manage/components/panel/StagePanelActionCreator.tsx index c7c647f8a0..2dc81d90e1 100644 --- a/core/app/c/[communitySlug]/stages/manage/components/panel/StagePanelActionCreator.tsx +++ b/core/app/c/[communitySlug]/stages/manage/components/panel/StagePanelActionCreator.tsx @@ -13,8 +13,8 @@ import { } from "ui/dialog"; import { FileText, Mail, Terminal } from "ui/icon"; -import { UIException } from "~/lib/error/UIException"; -import { useDisplayUiException } from "~/lib/error/useDisplayUiException"; +import { ClientException } from "~/lib/error/ClientException"; +import { useShowClientException } from "~/lib/error/useShowClientException"; import { ActionPayload } from "~/lib/types"; type ActionCellProps = { @@ -65,21 +65,21 @@ const ActionCell = (props: ActionCellProps) => { type Props = { actions: ActionPayload[]; - onAdd: (actionId: string) => Promise; + onAdd: (actionId: string) => Promise; }; export const StagePanelActionCreator = (props: Props) => { - const displayUiException = useDisplayUiException(); + const showClientException = useShowClientException(); const [isOpen, setIsOpen] = useState(false); const onActionSelect = useCallback( async (action: ActionPayload) => { setIsOpen(false); const result = await props.onAdd(action.id); if (result) { - displayUiException(result); + showClientException(result); } }, - [props.onAdd, displayUiException] + [props.onAdd, showClientException] ); const onOpenChange = useCallback((open: boolean) => { setIsOpen(open); diff --git a/core/app/c/[communitySlug]/stages/manage/components/panel/StagePanelActionEditor.tsx b/core/app/c/[communitySlug]/stages/manage/components/panel/StagePanelActionEditor.tsx index 99909ad717..eed9ba758e 100644 --- a/core/app/c/[communitySlug]/stages/manage/components/panel/StagePanelActionEditor.tsx +++ b/core/app/c/[communitySlug]/stages/manage/components/panel/StagePanelActionEditor.tsx @@ -10,25 +10,25 @@ import { ChevronDown } from "ui/icon"; import { Separator } from "ui/separator"; import { getActionByName } from "~/actions"; -import { UIException } from "~/lib/error/UIException"; -import { useDisplayUiException } from "~/lib/error/useDisplayUiException"; +import { ClientException } from "~/lib/error/ClientException"; +import { useShowClientException } from "~/lib/error/useShowClientException"; import { StagePayloadActionInstance } from "~/lib/types"; import { StagePanelActionConfig } from "./StagePanelActionConfig"; type Props = { actionInstance: StagePayloadActionInstance; - onDelete: (actionInstanceId: string) => Promise; + onDelete: (actionInstanceId: string) => Promise; }; export const StagePanelActionEditor = (props: Props) => { - const displayUiException = useDisplayUiException(); + const showClientException = useShowClientException(); const [isOpen, setIsOpen] = useState(false); const onDeleteClick = useCallback(async () => { const result = await props.onDelete(props.actionInstance.id); if (result) { - displayUiException(result); + showClientException(result); } - }, [props.onDelete, props.actionInstance, displayUiException]); + }, [props.onDelete, props.actionInstance, showClientException]); const action = getActionByName(props.actionInstance.action.name); if (!action) { diff --git a/core/lib/error/ClientException.ts b/core/lib/error/ClientException.ts new file mode 100644 index 0000000000..2d786eaae7 --- /dev/null +++ b/core/lib/error/ClientException.ts @@ -0,0 +1,23 @@ +export type ClientException = { + isClientException: true; + message?: string; + title?: string; + id?: string; +}; + +type ClientExceptionOptions = Omit; + +export function makeClientException(options: ClientExceptionOptions): ClientException; +export function makeClientException(message: string, id?: string): ClientException; +export function makeClientException( + message: string | ClientExceptionOptions, + id?: string +): ClientException { + if (typeof message === "object") { + return { ...message, isClientException: true }; + } + return { isClientException: true, message, id }; +} + +export const isClientException = (error: unknown): error is ClientException => + typeof error === "object" && error !== null && "isClientException" in error; diff --git a/core/lib/error/UIException.ts b/core/lib/error/UIException.ts deleted file mode 100644 index 6ef83a84fe..0000000000 --- a/core/lib/error/UIException.ts +++ /dev/null @@ -1,15 +0,0 @@ -export type UIException = { - isUiException: true; - message: string; - title?: string; - id?: string; -}; - -export const makeUiException = (message: string, id?: string): UIException => ({ - isUiException: true, - message, - id, -}); - -export const isUiException = (error: unknown): error is UIException => - typeof error === "object" && error !== null && "isUiException" in error; diff --git a/core/lib/error/useDisplayUiException.tsx b/core/lib/error/useDisplayUiException.tsx deleted file mode 100644 index ba1b1dea44..0000000000 --- a/core/lib/error/useDisplayUiException.tsx +++ /dev/null @@ -1,18 +0,0 @@ -import { useCallback } from "react"; -import { useToast } from "ui/use-toast"; -import { UIException } from "./UIException"; - -export function useDisplayUiException() { - const { toast } = useToast(); - const report = useCallback( - ({ message, id }: UIException) => { - toast({ - title: "Error", - variant: "destructive", - description: `${message}${id ? ` (${id})` : ""}`, - }); - }, - [toast] - ); - return report; -} diff --git a/core/lib/error/useShowClientException.tsx b/core/lib/error/useShowClientException.tsx new file mode 100644 index 0000000000..77b43c33ca --- /dev/null +++ b/core/lib/error/useShowClientException.tsx @@ -0,0 +1,20 @@ +import { useCallback } from "react"; +import { useToast } from "ui/use-toast"; +import { ClientException } from "./ClientException"; + +export function useShowClientException() { + const { toast } = useToast(); + const report = useCallback( + ({ message, id, title }: ClientException) => { + toast({ + title: title ?? "Error", + variant: "destructive", + description: `${message ?? "An unexpected error occurred"}${ + id ? ` (Error ID: ${id})` : "" + }`, + }); + }, + [toast] + ); + return report; +} From 80f479fdf987614268ecbedbad2c7153a8f67e37 Mon Sep 17 00:00:00 2001 From: Eric McDaniel Date: Thu, 4 Apr 2024 10:52:49 -0400 Subject: [PATCH 05/11] Use isClientException instead of truthiness check --- .../c/[communitySlug]/stages/manage/StagesContext.tsx | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/core/app/c/[communitySlug]/stages/manage/StagesContext.tsx b/core/app/c/[communitySlug]/stages/manage/StagesContext.tsx index 8615697709..bc416a9e88 100644 --- a/core/app/c/[communitySlug]/stages/manage/StagesContext.tsx +++ b/core/app/c/[communitySlug]/stages/manage/StagesContext.tsx @@ -16,6 +16,7 @@ import { import { useShowClientException } from "~/lib/error/useShowClientException"; import { ActionPayload, StagePayload } from "~/lib/types"; import * as actions from "./actions"; +import { isClientException } from "~/lib/error/ClientException"; export type StagesContext = { actions: ActionPayload[]; @@ -172,7 +173,7 @@ export const StagesProvider = (props: StagesProviderProps) => { dispatch({ type: "stage_created" }); }); const result = await actions.createStage(props.communityId); - if (result) { + if (isClientException(result)) { showClientException(result); } }, [dispatch, props.communityId, showClientException]); @@ -210,7 +211,7 @@ export const StagesProvider = (props: StagesProviderProps) => { stageIds, moveConstraintIds ); - if (result) { + if (isClientException(result)) { showClientException(result); } }, @@ -231,7 +232,7 @@ export const StagesProvider = (props: StagesProviderProps) => { sourceStageId, destinationStageId ); - if (result) { + if (isClientException(result)) { showClientException(result); } }, @@ -264,7 +265,7 @@ export const StagesProvider = (props: StagesProviderProps) => { }); }); const result = await actions.updateStageName(props.communityId, stageId, name); - if (result) { + if (isClientException(result)) { showClientException(result); } }, From b7f18c22bc235cf6e6e78b02fccd85659d7c18c2 Mon Sep 17 00:00:00 2001 From: Eric McDaniel Date: Thu, 4 Apr 2024 11:39:37 -0400 Subject: [PATCH 06/11] redirect outside of try/catch block --- .../app/actions/respond/actions.ts | 178 +++++++++--------- 1 file changed, 94 insertions(+), 84 deletions(-) diff --git a/integrations/evaluations/app/actions/respond/actions.ts b/integrations/evaluations/app/actions/respond/actions.ts index 8374a5c73c..89c0fbe0dd 100644 --- a/integrations/evaluations/app/actions/respond/actions.ts +++ b/integrations/evaluations/app/actions/respond/actions.ts @@ -1,6 +1,7 @@ "use server"; import { captureException, withServerActionInstrumentation } from "@sentry/nextjs"; +import { isRedirectError } from "next/dist/client/components/redirect"; import { headers } from "next/headers"; import { redirect } from "next/navigation"; @@ -40,9 +41,6 @@ export const accept = async ( headers: headers(), }, async () => { - const redirectParams = `?token=${cookie( - "token" - )}&instanceId=${instanceId}&pubId=${submissionPubId}`; try { const submissionPub = await client.getPub(instanceId, submissionPubId); const user = JSON.parse(expect(cookie("user"))); @@ -55,92 +53,104 @@ export const accept = async ( instanceState[user.id], `User was not invited to evaluate pub ${submissionPubId}` ); - // Accepting again is a no-op. - if (evaluator.status === "accepted" || evaluator.status === "received") { - redirect(`/actions/respond/accepted${redirectParams}`); - } - // Assert the user is invited to evaluate this pub. - assertIsInvited(evaluator); - // Update the evaluator's status to accepted and add recored the time of - // acceptance. - evaluator = instanceState[user.id] = { - ...evaluator, - status: "accepted", - acceptedAt: new Date().toString(), - }; - const deadline = calculateDeadline(instanceConfig, new Date(evaluator.acceptedAt)); - evaluator.deadline = deadline; - await setInstanceState(instanceId, submissionPubId, instanceState); - // Unschedule reminder email to evaluator. - await unscheduleInvitationReminderEmail(instanceId, submissionPubId, evaluator); - // Unschedule no-reply notification email to community manager. - await unscheduleNoReplyNotificationEmail(instanceId, submissionPubId, evaluator); - // Immediately send accepted notification email to community manager. - await sendAcceptedNotificationEmail( - instanceId, - instanceConfig, - submissionPubId, - evaluator, - submissionPub.assignee - ); - // Immediately send accepted email to evaluator. - await sendAcceptedEmail(instanceId, instanceConfig, submissionPubId, evaluator); - // Schedule no-submit notification email to community manager. - await scheduleNoSubmitNotificationEmail( - instanceId, - instanceConfig, - submissionPubId, - evaluator, - submissionPub.assignee - ); + const hasAlreadyAccepted = + evaluator.status === "accepted" || evaluator.status === "received"; + // Accepting more than one time is a no-op. + if (!hasAlreadyAccepted) { + // Assert the user is invited to evaluate this pub. + assertIsInvited(evaluator); + // Update the evaluator's status to accepted and add recored the time of + // acceptance. + evaluator = instanceState[user.id] = { + ...evaluator, + status: "accepted", + acceptedAt: new Date().toString(), + }; + const deadline = calculateDeadline( + instanceConfig, + new Date(evaluator.acceptedAt) + ); + evaluator.deadline = deadline; + await setInstanceState(instanceId, submissionPubId, instanceState); + // Unschedule reminder email to evaluator. + await unscheduleInvitationReminderEmail(instanceId, submissionPubId, evaluator); + // Unschedule no-reply notification email to community manager. + await unscheduleNoReplyNotificationEmail( + instanceId, + submissionPubId, + evaluator + ); + // Immediately send accepted notification email to community manager. + await sendAcceptedNotificationEmail( + instanceId, + instanceConfig, + submissionPubId, + evaluator, + submissionPub.assignee + ); + // Immediately send accepted email to evaluator. + await sendAcceptedEmail(instanceId, instanceConfig, submissionPubId, evaluator); + // Schedule no-submit notification email to community manager. + await scheduleNoSubmitNotificationEmail( + instanceId, + instanceConfig, + submissionPubId, + evaluator, + submissionPub.assignee + ); - // schedule prompt evaluation email to evaluator. - await schedulePromptEvalBonusReminderEmail( - instanceId, - instanceConfig, - submissionPubId, - evaluator - ); - //schedule final prompt eval email to evaluator - await scheduleFinalPromptEvalBonusReminderEmail( - instanceId, - instanceConfig, - submissionPubId, - evaluator - ); - //schedule eval reminder email to evaluator - await scheduleEvaluationReminderEmail( - instanceId, - instanceConfig, - submissionPubId, - evaluator - ); - //schedule final eval reminder email to evaluator - await scheduleFinalEvaluationReminderEmail( - instanceId, - instanceConfig, - submissionPubId, - evaluator - ); - //schedule follow up to final eval reminder email to evaluator - await scheduleFollowUpToFinalEvaluationReminderEmail( - instanceId, - instanceConfig, - submissionPubId, - evaluator - ); - // schedule no-submit notification email to evalutaor - await sendNoticeOfNoSubmitEmail( - instanceId, - instanceConfig, - submissionPubId, - evaluator - ); + // schedule prompt evaluation email to evaluator. + await schedulePromptEvalBonusReminderEmail( + instanceId, + instanceConfig, + submissionPubId, + evaluator + ); + //schedule final prompt eval email to evaluator + await scheduleFinalPromptEvalBonusReminderEmail( + instanceId, + instanceConfig, + submissionPubId, + evaluator + ); + //schedule eval reminder email to evaluator + await scheduleEvaluationReminderEmail( + instanceId, + instanceConfig, + submissionPubId, + evaluator + ); + //schedule final eval reminder email to evaluator + await scheduleFinalEvaluationReminderEmail( + instanceId, + instanceConfig, + submissionPubId, + evaluator + ); + //schedule follow up to final eval reminder email to evaluator + await scheduleFollowUpToFinalEvaluationReminderEmail( + instanceId, + instanceConfig, + submissionPubId, + evaluator + ); + // schedule no-submit notification email to evalutaor + await sendNoticeOfNoSubmitEmail( + instanceId, + instanceConfig, + submissionPubId, + evaluator + ); + } } catch (error) { captureException(error); return { error: error.message }; } - redirect(`/actions/respond/accepted${redirectParams}`); + redirect( + `/actions/respond/accepted?token=${cookie( + "token" + )}&instanceId=${instanceId}&pubId=${submissionPubId}` + ); } ); }; From 965c490a074d64974a6ca1af554e13da9c9386ca Mon Sep 17 00:00:00 2001 From: Eric McDaniel Date: Thu, 4 Apr 2024 14:34:51 -0400 Subject: [PATCH 07/11] Sentry upgrade; further abstract client exceptions and sentry instrumentation --- .../members/[[...add]]/MemberInviteForm.tsx | 50 +-- .../members/[[...add]]/RemoveMemberButton.tsx | 21 +- .../members/[[...add]]/actions.ts | 389 ++++++++--------- .../stages/manage/StagesContext.tsx | 48 +-- .../[communitySlug]/stages/manage/actions.ts | 401 ++++++++---------- .../panel/StagePanelActionCreator.tsx | 14 +- .../panel/StagePanelActionEditor.tsx | 14 +- .../components/panel/StagePanelOverview.tsx | 6 +- core/lib/error/ClientException.ts | 23 - core/lib/error/useShowClientException.tsx | 20 - core/lib/server/defineServerAction.ts | 12 + core/lib/serverActions.ts | 56 +++ core/package.json | 2 +- integrations/evaluations/package.json | 2 +- integrations/submissions/package.json | 2 +- pnpm-lock.yaml | 167 ++++---- 16 files changed, 576 insertions(+), 651 deletions(-) delete mode 100644 core/lib/error/ClientException.ts delete mode 100644 core/lib/error/useShowClientException.tsx create mode 100644 core/lib/server/defineServerAction.ts create mode 100644 core/lib/serverActions.ts diff --git a/core/app/c/[communitySlug]/members/[[...add]]/MemberInviteForm.tsx b/core/app/c/[communitySlug]/members/[[...add]]/MemberInviteForm.tsx index d1fad2cc7e..1665296ec0 100644 --- a/core/app/c/[communitySlug]/members/[[...add]]/MemberInviteForm.tsx +++ b/core/app/c/[communitySlug]/members/[[...add]]/MemberInviteForm.tsx @@ -1,9 +1,9 @@ "use client"; -import { useCallback, useEffect, useTransition } from "react"; -import { useRouter } from "next/navigation"; import { zodResolver } from "@hookform/resolvers/zod"; import { Community } from "@prisma/client"; +import { useRouter } from "next/navigation"; +import { useCallback, useEffect, useTransition } from "react"; import { useForm } from "react-hook-form"; import { useDebouncedCallback } from "use-debounce"; import { z } from "zod"; @@ -25,11 +25,10 @@ import { Loader2, Mail, UserPlus } from "ui/icon"; import { Input } from "ui/input"; import { toast } from "ui/use-toast"; -import * as actions from "./actions"; +import { didSucceed, useServerAction } from "~/lib/serverActions"; import { MemberFormState } from "./AddMember"; +import * as actions from "./actions"; import { memberInviteFormSchema } from "./memberInviteFormSchema"; -import { isClientException } from "~/lib/error/ClientException"; -import { useShowClientException } from "~/lib/error/useShowClientException"; export const MemberInviteForm = ({ community, @@ -40,7 +39,8 @@ export const MemberInviteForm = ({ state: MemberFormState; email?: string; }) => { - const showClientException = useShowClientException(); + const runCreateUserWithMembership = useServerAction(actions.createUserWithMembership); + const runAddMember = useServerAction(actions.addMember); const [isPending, startTransition] = useTransition(); const router = useRouter(); @@ -74,7 +74,7 @@ export const MemberInviteForm = ({ return; } - const result = await actions.createUserWithMembership({ + const result = await runCreateUserWithMembership({ email: data.email, firstName: data.firstName!, lastName: data.lastName!, @@ -82,38 +82,32 @@ export const MemberInviteForm = ({ canAdmin: Boolean(data.canAdmin), }); - if (isClientException(result)) { - showClientException(result); - return; + if (didSucceed(result)) { + toast({ + title: "Success", + description: "User successfully invited", + }); + closeForm(); } - toast({ - title: "Success", - description: "User successfully invited", - }); - closeForm(); - return; } - const result = await actions.addMember({ + const result = await runAddMember({ user: state.user, canAdmin: data.canAdmin, community, }); - if (isClientException(result)) { - showClientException(result); - return; - } - - toast({ - title: "Success", - description: "Member added successfully", - }); + if (didSucceed(result)) { + toast({ + title: "Success", + description: "Member added successfully", + }); - // navigate away from the add page to the normal member page - closeForm(); + // navigate away from the add page to the normal member page + closeForm(); + } } const debouncedEmailCheck = useDebouncedCallback(async (email: string) => { diff --git a/core/app/c/[communitySlug]/members/[[...add]]/RemoveMemberButton.tsx b/core/app/c/[communitySlug]/members/[[...add]]/RemoveMemberButton.tsx index ffb2efec41..01abb52747 100644 --- a/core/app/c/[communitySlug]/members/[[...add]]/RemoveMemberButton.tsx +++ b/core/app/c/[communitySlug]/members/[[...add]]/RemoveMemberButton.tsx @@ -16,8 +16,7 @@ import { Trash } from "ui/icon"; import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "ui/tooltip"; import { toast } from "ui/use-toast"; -import { isClientException } from "~/lib/error/ClientException"; -import { useShowClientException } from "~/lib/error/useShowClientException"; +import { didSucceed, useServerAction } from "~/lib/serverActions"; import * as actions from "./actions"; import { TableMember } from "./getMemberTableColumns"; @@ -28,7 +27,7 @@ export const RemoveMemberButton = ({ member: TableMember; community: Community; }) => { - const showClientException = useShowClientException(); + const runRemoveMember = useServerAction(actions.removeMember); return ( @@ -59,16 +58,14 @@ export const RemoveMemberButton = ({ + {url &&

URL: {url}

} + + ); +} +``` diff --git a/core/lib/serverActions.ts b/core/lib/serverActions.ts index 4098b11c71..ce10162e54 100644 --- a/core/lib/serverActions.ts +++ b/core/lib/serverActions.ts @@ -9,7 +9,7 @@ export type ClientException = { id?: string; }; -type ClientExceptionOptions = Omit; +type ClientExceptionOptions = Omit & { cause?: unknown }; export function makeClientException(options: ClientExceptionOptions): ClientException; export function makeClientException(message: string, id?: string): ClientException; From 67441b648d7f5b16a3bec2daf764f58b85b878bd Mon Sep 17 00:00:00 2001 From: Eric McDaniel Date: Thu, 4 Apr 2024 16:51:15 -0400 Subject: [PATCH 09/11] Format and lint --- .../members/[[...add]]/MemberInviteForm.tsx | 6 +++--- .../c/[communitySlug]/members/[[...add]]/actions.ts | 10 +++++----- .../[communitySlug]/stages/manage/StagesContext.tsx | 1 - core/app/c/[communitySlug]/stages/manage/actions.ts | 1 + .../components/panel/StagePanelActionEditor.tsx | 1 - .../manage/components/panel/StagePanelActions.tsx | 2 +- .../manage/components/panel/StagePanelOverview.tsx | 2 +- core/app/global-error.tsx | 6 +++--- core/lib/server/defineServerAction.ts | 5 +++-- core/lib/serverActions.ts | 3 ++- .../evaluations/app/actions/evaluate/actions.ts | 5 +++-- .../evaluations/app/actions/manage/actions.ts | 4 ++-- .../evaluations/app/actions/respond/actions.ts | 2 +- integrations/evaluations/app/configure/actions.ts | 3 ++- integrations/evaluations/app/global-error.tsx | 6 +++--- .../submissions/app/actions/submit/actions.ts | 6 ++++-- integrations/submissions/app/configure/actions.ts | 3 ++- integrations/submissions/app/global-error.tsx | 6 +++--- packages/ui/src/data-table.tsx | 3 ++- packages/ui/src/form.tsx | 13 ++++--------- packages/ui/src/pagination.tsx | 3 ++- 21 files changed, 47 insertions(+), 44 deletions(-) diff --git a/core/app/c/[communitySlug]/members/[[...add]]/MemberInviteForm.tsx b/core/app/c/[communitySlug]/members/[[...add]]/MemberInviteForm.tsx index 1665296ec0..219da6270f 100644 --- a/core/app/c/[communitySlug]/members/[[...add]]/MemberInviteForm.tsx +++ b/core/app/c/[communitySlug]/members/[[...add]]/MemberInviteForm.tsx @@ -1,9 +1,9 @@ "use client"; +import { useCallback, useEffect, useTransition } from "react"; +import { useRouter } from "next/navigation"; import { zodResolver } from "@hookform/resolvers/zod"; import { Community } from "@prisma/client"; -import { useRouter } from "next/navigation"; -import { useCallback, useEffect, useTransition } from "react"; import { useForm } from "react-hook-form"; import { useDebouncedCallback } from "use-debounce"; import { z } from "zod"; @@ -26,8 +26,8 @@ import { Input } from "ui/input"; import { toast } from "ui/use-toast"; import { didSucceed, useServerAction } from "~/lib/serverActions"; -import { MemberFormState } from "./AddMember"; import * as actions from "./actions"; +import { MemberFormState } from "./AddMember"; import { memberInviteFormSchema } from "./memberInviteFormSchema"; export const MemberInviteForm = ({ diff --git a/core/app/c/[communitySlug]/members/[[...add]]/actions.ts b/core/app/c/[communitySlug]/members/[[...add]]/actions.ts index 955a57c7e0..319c16694c 100644 --- a/core/app/c/[communitySlug]/members/[[...add]]/actions.ts +++ b/core/app/c/[communitySlug]/members/[[...add]]/actions.ts @@ -1,15 +1,15 @@ "use server"; +import { cache } from "react"; +import { revalidatePath, revalidateTag } from "next/cache"; +import { headers } from "next/headers"; import { Community } from "@prisma/client"; import { captureException, withServerActionInstrumentation } from "@sentry/nextjs"; import { User } from "@supabase/supabase-js"; -import { revalidatePath, revalidateTag } from "next/cache"; -import { headers } from "next/headers"; -import { cache } from "react"; + +import type { SuggestedUser } from "~/lib/server/members"; import { getLoginData } from "~/lib/auth/loginData"; import { env } from "~/lib/env/env.mjs"; -import type { SuggestedUser } from "~/lib/server/members"; - import { defineServerAction } from "~/lib/server/defineServerAction"; import { generateHash, slugifyString } from "~/lib/string"; import { formatSupabaseError } from "~/lib/supabase"; diff --git a/core/app/c/[communitySlug]/stages/manage/StagesContext.tsx b/core/app/c/[communitySlug]/stages/manage/StagesContext.tsx index 7c9f000f77..bcf7659450 100644 --- a/core/app/c/[communitySlug]/stages/manage/StagesContext.tsx +++ b/core/app/c/[communitySlug]/stages/manage/StagesContext.tsx @@ -12,7 +12,6 @@ import { useState, } from "react"; - import { useServerAction } from "~/lib/serverActions"; import { ActionPayload, StagePayload } from "~/lib/types"; import * as actions from "./actions"; diff --git a/core/app/c/[communitySlug]/stages/manage/actions.ts b/core/app/c/[communitySlug]/stages/manage/actions.ts index 0584dc147c..8557dc56da 100644 --- a/core/app/c/[communitySlug]/stages/manage/actions.ts +++ b/core/app/c/[communitySlug]/stages/manage/actions.ts @@ -1,6 +1,7 @@ "use server"; import { revalidateTag } from "next/cache"; + import { defineServerAction } from "~/lib/server/defineServerAction"; import db from "~/prisma/db"; diff --git a/core/app/c/[communitySlug]/stages/manage/components/panel/StagePanelActionEditor.tsx b/core/app/c/[communitySlug]/stages/manage/components/panel/StagePanelActionEditor.tsx index ed9883e54e..db8262e717 100644 --- a/core/app/c/[communitySlug]/stages/manage/components/panel/StagePanelActionEditor.tsx +++ b/core/app/c/[communitySlug]/stages/manage/components/panel/StagePanelActionEditor.tsx @@ -1,6 +1,5 @@ "use client"; -import { logger } from "logger"; import { useCallback, useState } from "react"; import { logger } from "logger"; diff --git a/core/app/c/[communitySlug]/stages/manage/components/panel/StagePanelActions.tsx b/core/app/c/[communitySlug]/stages/manage/components/panel/StagePanelActions.tsx index 4538630bb4..d8644fb567 100644 --- a/core/app/c/[communitySlug]/stages/manage/components/panel/StagePanelActions.tsx +++ b/core/app/c/[communitySlug]/stages/manage/components/panel/StagePanelActions.tsx @@ -4,9 +4,9 @@ import { Card, CardContent } from "ui/card"; import { SkeletonCard } from "~/app/components/skeletons/SkeletonCard"; import { addAction, deleteAction } from "../../actions"; +import { getActions, getStage, getStageActions } from "./queries"; import { StagePanelActionCreator } from "./StagePanelActionCreator"; import { StagePanelActionEditor } from "./StagePanelActionEditor"; -import { getActions, getStage, getStageActions } from "./queries"; type PropsInner = { stageId: string; diff --git a/core/app/c/[communitySlug]/stages/manage/components/panel/StagePanelOverview.tsx b/core/app/c/[communitySlug]/stages/manage/components/panel/StagePanelOverview.tsx index 96c6c8ec1d..beb92aba93 100644 --- a/core/app/c/[communitySlug]/stages/manage/components/panel/StagePanelOverview.tsx +++ b/core/app/c/[communitySlug]/stages/manage/components/panel/StagePanelOverview.tsx @@ -5,9 +5,9 @@ import { Separator } from "ui/separator"; import { SkeletonCard } from "~/app/components/skeletons/SkeletonCard"; import { deleteStage, updateStageName } from "../../actions"; +import { getStage } from "./queries"; import { StageNameInput } from "./StageNameInput"; import { StagePanelOverviewManagement } from "./StagePanelOverviewManagement"; -import { getStage } from "./queries"; type PropsInner = { stageId: string; diff --git a/core/app/global-error.tsx b/core/app/global-error.tsx index 7632da0825..3d36a71758 100644 --- a/core/app/global-error.tsx +++ b/core/app/global-error.tsx @@ -1,8 +1,8 @@ "use client"; -import * as Sentry from "@sentry/nextjs"; -import NextError from "next/error"; import { useEffect } from "react"; +import NextError from "next/error"; +import * as Sentry from "@sentry/nextjs"; export default function GlobalError({ error }: { error: Error & { digest?: string } }) { useEffect(() => { @@ -10,7 +10,7 @@ export default function GlobalError({ error }: { error: Error & { digest?: strin }, [error]); return ( - + diff --git a/core/lib/server/defineServerAction.ts b/core/lib/server/defineServerAction.ts index b8c25cb4fa..e5934dbc7b 100644 --- a/core/lib/server/defineServerAction.ts +++ b/core/lib/server/defineServerAction.ts @@ -1,5 +1,6 @@ -import { withServerActionInstrumentation } from "@sentry/nextjs"; import { headers } from "next/headers"; +import { withServerActionInstrumentation } from "@sentry/nextjs"; + import { isClientExceptionOptions, makeClientException } from "../serverActions"; /** @@ -27,7 +28,7 @@ export const defineServerAction = unknown>( // server action result as-is. return isClientExceptionOptions(serverActionResult) ? // Create a client exception and send its cause (if any) to Sentry. - makeClientException(serverActionResult) + makeClientException(serverActionResult) : serverActionResult; } catch (error) { // https://github.com/vercel/next.js/discussions/49426#discussioncomment-8176059 diff --git a/core/lib/serverActions.ts b/core/lib/serverActions.ts index ce10162e54..4bdd9ca8f6 100644 --- a/core/lib/serverActions.ts +++ b/core/lib/serverActions.ts @@ -1,5 +1,6 @@ -import { captureException } from "@sentry/nextjs"; import { useCallback } from "react"; +import { captureException } from "@sentry/nextjs"; + import { toast } from "ui/use-toast"; export type ClientException = { diff --git a/integrations/evaluations/app/actions/evaluate/actions.ts b/integrations/evaluations/app/actions/evaluate/actions.ts index 1a946d0c1f..c57ae733c9 100644 --- a/integrations/evaluations/app/actions/evaluate/actions.ts +++ b/integrations/evaluations/app/actions/evaluate/actions.ts @@ -1,9 +1,10 @@ "use server"; -import { PubValues } from "@pubpub/sdk"; -import { captureException, withServerActionInstrumentation } from "@sentry/nextjs"; import { revalidatePath } from "next/cache"; import { headers } from "next/headers"; +import { captureException, withServerActionInstrumentation } from "@sentry/nextjs"; + +import { PubValues } from "@pubpub/sdk"; import { expect } from "utils"; import { diff --git a/integrations/evaluations/app/actions/manage/actions.ts b/integrations/evaluations/app/actions/manage/actions.ts index 5e18edd512..68d3204758 100644 --- a/integrations/evaluations/app/actions/manage/actions.ts +++ b/integrations/evaluations/app/actions/manage/actions.ts @@ -1,6 +1,8 @@ "use server"; import { revalidatePath } from "next/cache"; +import { headers } from "next/headers"; +import { captureException, withServerActionInstrumentation } from "@sentry/nextjs"; import { SuggestedMembersQuery } from "@pubpub/sdk"; import { expect } from "utils"; @@ -23,8 +25,6 @@ import { isInvited, } from "~/lib/types"; import { InviteFormEvaluator } from "./types"; -import { captureException, withServerActionInstrumentation } from "@sentry/nextjs"; -import { headers } from "next/headers"; export const save = async ( instanceId: string, diff --git a/integrations/evaluations/app/actions/respond/actions.ts b/integrations/evaluations/app/actions/respond/actions.ts index 89c0fbe0dd..d59cafe129 100644 --- a/integrations/evaluations/app/actions/respond/actions.ts +++ b/integrations/evaluations/app/actions/respond/actions.ts @@ -1,9 +1,9 @@ "use server"; -import { captureException, withServerActionInstrumentation } from "@sentry/nextjs"; import { isRedirectError } from "next/dist/client/components/redirect"; import { headers } from "next/headers"; import { redirect } from "next/navigation"; +import { captureException, withServerActionInstrumentation } from "@sentry/nextjs"; import { expect } from "utils"; diff --git a/integrations/evaluations/app/configure/actions.ts b/integrations/evaluations/app/configure/actions.ts index 634cae5bdb..a4f99afefe 100644 --- a/integrations/evaluations/app/configure/actions.ts +++ b/integrations/evaluations/app/configure/actions.ts @@ -1,7 +1,8 @@ "use server"; -import { captureException, withServerActionInstrumentation } from "@sentry/nextjs"; import { headers } from "next/headers"; +import { captureException, withServerActionInstrumentation } from "@sentry/nextjs"; + import { setInstanceConfig } from "~/lib/instance"; import { InstanceConfig } from "~/lib/types"; diff --git a/integrations/evaluations/app/global-error.tsx b/integrations/evaluations/app/global-error.tsx index 7632da0825..3d36a71758 100644 --- a/integrations/evaluations/app/global-error.tsx +++ b/integrations/evaluations/app/global-error.tsx @@ -1,8 +1,8 @@ "use client"; -import * as Sentry from "@sentry/nextjs"; -import NextError from "next/error"; import { useEffect } from "react"; +import NextError from "next/error"; +import * as Sentry from "@sentry/nextjs"; export default function GlobalError({ error }: { error: Error & { digest?: string } }) { useEffect(() => { @@ -10,7 +10,7 @@ export default function GlobalError({ error }: { error: Error & { digest?: strin }, [error]); return ( - + diff --git a/integrations/submissions/app/actions/submit/actions.ts b/integrations/submissions/app/actions/submit/actions.ts index 4a2ce3ff52..bca314c65f 100644 --- a/integrations/submissions/app/actions/submit/actions.ts +++ b/integrations/submissions/app/actions/submit/actions.ts @@ -1,8 +1,10 @@ "use server"; -import { PubValues } from "@pubpub/sdk"; -import { captureException, withServerActionInstrumentation } from "@sentry/nextjs"; import { headers } from "next/headers"; +import { captureException, withServerActionInstrumentation } from "@sentry/nextjs"; + +import { PubValues } from "@pubpub/sdk"; + import { getInstanceConfig } from "~/lib/instance"; import { makePubFromDoi, makePubFromTitle, makePubFromUrl } from "~/lib/metadata"; import { client } from "~/lib/pubpub"; diff --git a/integrations/submissions/app/configure/actions.ts b/integrations/submissions/app/configure/actions.ts index 0f0c7ce3d6..055d798c5f 100644 --- a/integrations/submissions/app/configure/actions.ts +++ b/integrations/submissions/app/configure/actions.ts @@ -1,7 +1,8 @@ "use server"; -import { captureException, withServerActionInstrumentation } from "@sentry/nextjs"; import { headers } from "next/headers"; +import { captureException, withServerActionInstrumentation } from "@sentry/nextjs"; + import { setInstanceConfig } from "~/lib/instance"; export const configure = async (instanceId: string, pubTypeId: string) => { diff --git a/integrations/submissions/app/global-error.tsx b/integrations/submissions/app/global-error.tsx index 7632da0825..3d36a71758 100644 --- a/integrations/submissions/app/global-error.tsx +++ b/integrations/submissions/app/global-error.tsx @@ -1,8 +1,8 @@ "use client"; -import * as Sentry from "@sentry/nextjs"; -import NextError from "next/error"; import { useEffect } from "react"; +import NextError from "next/error"; +import * as Sentry from "@sentry/nextjs"; export default function GlobalError({ error }: { error: Error & { digest?: string } }) { useEffect(() => { @@ -10,7 +10,7 @@ export default function GlobalError({ error }: { error: Error & { digest?: strin }, [error]); return ( - + diff --git a/packages/ui/src/data-table.tsx b/packages/ui/src/data-table.tsx index 120ebda914..97ea03cad1 100644 --- a/packages/ui/src/data-table.tsx +++ b/packages/ui/src/data-table.tsx @@ -1,3 +1,5 @@ +import type { Column, Table } from "@tanstack/react-table"; + import * as React from "react"; import { ArrowDownIcon, @@ -9,7 +11,6 @@ import { DoubleArrowRightIcon, EyeNoneIcon, } from "@radix-ui/react-icons"; -import { Column, Table } from "@tanstack/react-table"; import { cn } from "utils"; diff --git a/packages/ui/src/form.tsx b/packages/ui/src/form.tsx index 7adeaeb9ed..bcc6f5ac84 100644 --- a/packages/ui/src/form.tsx +++ b/packages/ui/src/form.tsx @@ -1,16 +1,11 @@ "use client"; +import type * as LabelPrimitive from "@radix-ui/react-label"; +import type { ControllerProps, FieldPath, FieldValues } from "react-hook-form"; + import * as React from "react"; -import * as LabelPrimitive from "@radix-ui/react-label"; import { Slot } from "@radix-ui/react-slot"; -import { - Controller, - ControllerProps, - FieldPath, - FieldValues, - FormProvider, - useFormContext, -} from "react-hook-form"; +import { Controller, FormProvider, useFormContext } from "react-hook-form"; import { cn } from "utils"; diff --git a/packages/ui/src/pagination.tsx b/packages/ui/src/pagination.tsx index ae54d7fc2c..e7445da923 100644 --- a/packages/ui/src/pagination.tsx +++ b/packages/ui/src/pagination.tsx @@ -4,7 +4,8 @@ import { ChevronLeftIcon, ChevronRightIcon, DotsHorizontalIcon } from "@radix-ui import { cn } from "utils"; -import { ButtonProps, buttonVariants } from "./button"; +import type { ButtonProps } from "./button"; +import { buttonVariants } from "./button"; const Pagination = ({ className, ...props }: React.ComponentProps<"nav">) => (