diff --git a/apps/web/app/(ee)/admin.dub.co/(dashboard)/partners/network/network-partner-application-sheet.tsx b/apps/web/app/(ee)/admin.dub.co/(dashboard)/partners/network/network-partner-application-sheet.tsx index 3e30a8f8e39..3931c0e0120 100644 --- a/apps/web/app/(ee)/admin.dub.co/(dashboard)/partners/network/network-partner-application-sheet.tsx +++ b/apps/web/app/(ee)/admin.dub.co/(dashboard)/partners/network/network-partner-application-sheet.tsx @@ -19,6 +19,7 @@ import { Trophy, useKeyboardShortcut, User, + useRouterStuff, Users, } from "@dub/ui"; import { cn, COUNTRIES, currencyFormatter, OG_AVATAR_URL } from "@dub/utils"; @@ -29,6 +30,24 @@ import { NetworkPartnerChangeHistory } from "./network-partner-change-history"; type NetworkPartnerSheetTabId = "about" | "programs" | "duplicates"; +function isDirectDraftToApprovedNetworkApproval( + partner: AdminNetworkPartner, +): boolean { + if (partner.networkStatus !== "approved") { + return false; + } + + const networkLogs = (partner.changeHistoryLog ?? []).filter( + (entry) => entry.field === "networkStatus", + ); + + return ( + networkLogs.length === 1 && + networkLogs[0].from === "draft" && + networkLogs[0].to === "approved" + ); +} + export function NetworkPartnerApplicationSheet({ isOpen, setIsOpen, @@ -44,11 +63,15 @@ export function NetworkPartnerApplicationSheet({ onNext?: () => void; onReview: ( partner: AdminNetworkPartner, - status: "approved" | "rejected", + status: "approved" | "rejected" | "draft", ) => Promise; }) { const [currentTabId, setCurrentTabId] = useState("about"); + const { queryParams } = useRouterStuff(); + + const showRevertToDraftInsteadOfReject = + isDirectDraftToApprovedNetworkApproval(partner); const PartnerDetails = (
@@ -93,6 +116,21 @@ export function NetworkPartnerApplicationSheet({ confirmShortcutOptions: { sheet: true, modal: true }, }); + const { + setShowConfirmModal: setShowRevertToDraftConfirm, + confirmModal: revertToDraftModal, + } = useConfirmModal({ + title: "Revert network profile to draft", + description: PartnerDetails, + confirmText: "Revert to draft", + confirmVariant: "danger", + onConfirm: async () => { + await onReview(partner, "draft"); + }, + confirmShortcut: "r", + confirmShortcutOptions: { sheet: true, modal: true }, + }); + useKeyboardShortcut( "ArrowRight", () => { @@ -120,19 +158,28 @@ export function NetworkPartnerApplicationSheet({ useKeyboardShortcut("r", () => setShowRejectConfirm(true), { sheet: true, - enabled: !["rejected", "trusted"].includes(partner.networkStatus), + enabled: + !showRevertToDraftInsteadOfReject && + !["rejected", "trusted"].includes(partner.networkStatus), + }); + + useKeyboardShortcut("r", () => setShowRevertToDraftConfirm(true), { + sheet: true, + enabled: showRevertToDraftInsteadOfReject, }); return ( queryParams({ del: "partnerId", scroll: false })} contentProps={{ className: "md:w-[max(min(calc(100vw-334px),1170px),540px)]", }} > {approveModal} {rejectModal} + {revertToDraftModal}
@@ -217,19 +264,30 @@ export function NetworkPartnerApplicationSheet({
-
+ ); + })() + : requirementsNotMet; + const statusBadge = programEnrollment ? { ...PartnerStatusBadges, @@ -69,8 +189,6 @@ export function ProgramSidebar({ }[programEnrollment.status] : null; - const [justApplied, setJustApplied] = useState(false); - const buttonText = useMemo(() => { if (justApplied) return "Applied"; if (!programEnrollment) return "Apply"; @@ -98,6 +216,7 @@ export function ProgramSidebar({ return (
+ {confirmModal} {programApplicationSheet}
: undefined} disabled={ - !!programEnrollment || justApplied || !!requirementsNotMet + !applyDisabledTooltip && + (!!programEnrollment || justApplied) ? true : undefined } - disabledTooltip={requirementsNotMet} + disabledTooltip={applyDisabledTooltip} onClick={() => setIsApplicationSheetOpen(true)} />
diff --git a/apps/web/app/(ee)/partners.dub.co/(dashboard)/programs/marketplace/[programSlug]/header-controls.tsx b/apps/web/app/(ee)/partners.dub.co/(dashboard)/programs/marketplace/[programSlug]/header-controls.tsx index 6e3fc6f957a..24889715a7f 100644 --- a/apps/web/app/(ee)/partners.dub.co/(dashboard)/programs/marketplace/[programSlug]/header-controls.tsx +++ b/apps/web/app/(ee)/partners.dub.co/(dashboard)/programs/marketplace/[programSlug]/header-controls.tsx @@ -152,7 +152,9 @@ function ApplyButton({ program }: { program: NetworkProgramProps }) { href="/profile" className={cn( "flex items-center justify-center gap-2 rounded-lg p-2", - className, + "ctaClassName" in partnerTooltip + ? partnerTooltip.ctaClassName + : className, )} > diff --git a/apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/partners/partners-table.tsx b/apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/partners/partners-table.tsx index b3a70120b09..f9fc936bcd0 100644 --- a/apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/partners/partners-table.tsx +++ b/apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/partners/partners-table.tsx @@ -761,51 +761,32 @@ function RowMenuButton({ content={ - {row.original.status === "invited" ? ( + {["invited", "declined"].includes(row.original.status) ? ( - { - setShowChangeGroupModal(true); - setIsOpen(false); - }} - /> - - { - setShowUpdatePartnerTagsModal(true); - setIsOpen(false); - }} - /> - - { - if (row.original.status !== "invited") { - return; + {row.original.status === "invited" && ( + { + if (row.original.status !== "invited") { + return; + } - await resendInvite({ - workspaceId, - partnerId: row.original.id, - }); - }} - /> + await resendInvite({ + workspaceId, + partnerId: row.original.id, + }); + }} + /> + )} { - if (row.original.status !== "invited") { - return; - } if ( !window.confirm( "Are you sure you want to delete this invite? This action cannot be undone.", diff --git a/apps/web/lib/actions/partners/delete-program-invite.ts b/apps/web/lib/actions/partners/delete-program-invite.ts index 820fbe802ad..39c7b7dd752 100644 --- a/apps/web/lib/actions/partners/delete-program-invite.ts +++ b/apps/web/lib/actions/partners/delete-program-invite.ts @@ -34,7 +34,9 @@ export const deleteProgramInviteAction = authActionClient partnerId, programId, }, - status: "invited", // can only delete invited enrollments + status: { + in: ["invited", "declined"], + }, }, include: { program: true, diff --git a/apps/web/lib/api/partners/bulk-delete-partners.ts b/apps/web/lib/api/partners/bulk-delete-partners.ts index 0000e22236b..b0e6f947b03 100644 --- a/apps/web/lib/api/partners/bulk-delete-partners.ts +++ b/apps/web/lib/api/partners/bulk-delete-partners.ts @@ -147,10 +147,9 @@ export async function bulkDeletePartners({ // Delete the messages const deletedMessages = await prisma.message.deleteMany({ where: { - programEnrollment: { - id: { - in: programEnrollmentIds, - }, + programId: ACME_PROGRAM_ID, + partnerId: { + in: partnerIds, }, }, }); diff --git a/apps/web/ui/partners/partner-network/network-status-badges.ts b/apps/web/ui/partners/partner-network/network-status-badges.ts index 9ee854af2e1..33a916d9cef 100644 --- a/apps/web/ui/partners/partner-network/network-status-badges.ts +++ b/apps/web/ui/partners/partner-network/network-status-badges.ts @@ -22,6 +22,7 @@ export const NetworkStatusBadges = { content: "Your Dub Partner Network application is under review. You'll be able to apply to this program once approved.", cta: "Pending approval", + ctaClassName: "bg-bg-attention text-content-attention", }, label: "Submitted", variant: "new", diff --git a/packages/email/src/templates/network-partner-application-approved.tsx b/packages/email/src/templates/network-partner-application-approved.tsx index cb06a6025e4..f38b72fdcd3 100644 --- a/packages/email/src/templates/network-partner-application-approved.tsx +++ b/packages/email/src/templates/network-partner-application-approved.tsx @@ -77,10 +77,9 @@ export default function NetworkPartnerApplicationApproved({ -