chore: import upstream snapshot with attribution

This commit is contained in:
wehub-resource-sync
2026-07-13 13:32:57 +08:00
commit cd420f9332
4811 changed files with 884702 additions and 0 deletions
@@ -0,0 +1,873 @@
import { getFormProps, getInputProps, useForm } from "@conform-to/react";
import { parseWithZod } from "@conform-to/zod";
import { ArrowUpCircleIcon, CheckIcon, EnvelopeIcon, PlusIcon } from "@heroicons/react/20/solid";
import { BookOpenIcon } from "@heroicons/react/24/solid";
import { DialogClose } from "@radix-ui/react-dialog";
import { useFetcher, useSearchParams } from "@remix-run/react";
import { type ActionFunctionArgs, json, type LoaderFunctionArgs } from "@remix-run/server-runtime";
import { tryCatch } from "@trigger.dev/core/v3";
import { useCallback, useEffect, useState } from "react";
import { SearchInput } from "~/components/primitives/SearchInput";
import { typedjson, useTypedLoaderData } from "remix-typedjson";
import { z } from "zod";
import { BranchEnvironmentIconSmall } from "~/assets/icons/EnvironmentIcons";
import { BranchesNoBranchableEnvironment, BranchesNoBranches } from "~/components/BlankStatePanels";
import { Feedback } from "~/components/Feedback";
import { GitMetadata } from "~/components/GitMetadata";
import { V4Title } from "~/components/V4Badge";
import { AdminDebugTooltip } from "~/components/admin/debugTooltip";
import { MainCenteredContainer, PageBody, PageContainer } from "~/components/layout/AppLayout";
import { Badge } from "~/components/primitives/Badge";
import { Button, LinkButton } from "~/components/primitives/Buttons";
import { CopyableText } from "~/components/primitives/CopyableText";
import { DateTime } from "~/components/primitives/DateTime";
import {
Dialog,
DialogContent,
DialogFooter,
DialogHeader,
DialogTrigger,
} from "~/components/primitives/Dialog";
import { Fieldset } from "~/components/primitives/Fieldset";
import { FormButtons } from "~/components/primitives/FormButtons";
import { FormError } from "~/components/primitives/FormError";
import { Header3 } from "~/components/primitives/Headers";
import { InputGroup } from "~/components/primitives/InputGroup";
import { InputNumberStepper } from "~/components/primitives/InputNumberStepper";
import { Label } from "~/components/primitives/Label";
import { NavBar, PageAccessories, PageTitle } from "~/components/primitives/PageHeader";
import { PaginationControls } from "~/components/primitives/Pagination";
import { Paragraph } from "~/components/primitives/Paragraph";
import { PopoverMenuItem } from "~/components/primitives/Popover";
import * as Property from "~/components/primitives/PropertyTable";
import { SpinnerWhite } from "~/components/primitives/Spinner";
import { Switch } from "~/components/primitives/Switch";
import {
Table,
TableBlankRow,
TableBody,
TableCell,
TableCellMenu,
TableHeader,
TableHeaderCell,
TableRow,
} from "~/components/primitives/Table";
import { InfoIconTooltip, SimpleTooltip } from "~/components/primitives/Tooltip";
import { useEnvironment } from "~/hooks/useEnvironment";
import { useShowSelfServe } from "~/hooks/useShowSelfServe";
import { useOrganization } from "~/hooks/useOrganizations";
import { useProject } from "~/hooks/useProject";
import { findProjectBySlug } from "~/models/project.server";
import { redirectWithErrorMessage } from "~/models/message.server";
import { BranchesPresenter } from "~/presenters/v3/BranchesPresenter.server";
import { logger } from "~/services/logger.server";
import { getCurrentPlan, getSelfServePurchaseBlockReason } from "~/services/platform.v3.server";
import { requireUserId } from "~/services/session.server";
import { cn } from "~/utils/cn";
import {
branchesPath,
docsPath,
EnvironmentParamSchema,
ProjectParamSchema,
v3BillingPath,
} from "~/utils/pathBuilder";
import { formatCurrency, formatNumber } from "~/utils/numberFormatter";
import { SetBranchesAddOnService } from "~/v3/services/setBranchesAddOn.server";
import { useCurrentPlan } from "../_app.orgs.$organizationSlug/route";
import { ArchiveButton } from "../resources.branches.archive";
import { NewBranchPanel } from "~/routes/resources.branches.create";
import { BranchesOptions } from "~/utils/branches";
import { IconArrowBearRight2 } from "@tabler/icons-react";
const PurchaseSchema = z.discriminatedUnion("action", [
z.object({
action: z.literal("purchase"),
amount: z.coerce.number().int("Must be a whole number").min(0, "Amount must be 0 or more"),
}),
z.object({
action: z.literal("quota-increase"),
amount: z.coerce.number().int("Must be a whole number").min(1, "Amount must be greater than 0"),
}),
]);
export const loader = async ({ request, params }: LoaderFunctionArgs) => {
const userId = await requireUserId(request);
const { projectParam } = ProjectParamSchema.parse(params);
const searchParams = new URL(request.url).searchParams;
const parsedSearchParams = BranchesOptions.safeParse(Object.fromEntries(searchParams));
const options = parsedSearchParams.success ? parsedSearchParams.data : {};
try {
const presenter = new BranchesPresenter();
const result = await presenter.call({
userId,
projectSlug: projectParam,
env: "preview",
...options,
});
return typedjson(result);
} catch (error) {
logger.error("Error loading preview branches page", { error });
throw new Response(undefined, {
status: 400,
statusText: "Something went wrong, if this problem persists please contact support.",
});
}
};
export async function action({ request, params }: ActionFunctionArgs) {
const userId = await requireUserId(request);
const formData = await request.formData();
const formType = formData.get("_formType");
if (formType === "purchase-branches") {
const { organizationSlug, projectParam, envParam } = EnvironmentParamSchema.parse(params);
const project = await findProjectBySlug(organizationSlug, projectParam, userId);
const redirectPath = branchesPath(
{ slug: organizationSlug },
{ slug: projectParam },
{ slug: envParam }
);
if (!project) {
throw await redirectWithErrorMessage(redirectPath, request, "Project not found");
}
const currentPlan = await getCurrentPlan(project.organizationId);
const purchaseBlockReason = getSelfServePurchaseBlockReason(currentPlan);
if (purchaseBlockReason === "plan_unavailable") {
return json(
{ ok: false, error: "Unable to verify billing status. Please try again." } as const,
{ status: 503 }
);
}
if (purchaseBlockReason === "managed_billing") {
return json({ ok: false, error: "Contact us to request more branches." } as const, {
status: 403,
});
}
const submission = parseWithZod(formData, { schema: PurchaseSchema });
if (submission.status !== "success") {
return json(submission.reply());
}
const service = new SetBranchesAddOnService();
const [error, result] = await tryCatch(
service.call({
userId,
organizationId: project.organizationId,
action: submission.value.action,
amount: submission.value.amount,
})
);
if (error) {
return json(
submission.reply({
fieldErrors: { amount: [error instanceof Error ? error.message : "Unknown error"] },
})
);
}
if (!result.success) {
return json(submission.reply({ fieldErrors: { amount: [result.error] } }));
}
return json({ ok: true } as const);
}
// Branch creation is handled by the `resources.branches.create` resource
// route; this action only services the purchase flow above.
return json({ ok: false, error: "Unsupported action" } as const, { status: 400 });
}
export default function Page() {
const {
branchableEnvironment,
branches,
hasFilters: _hasFilters,
limits,
currentPage,
totalPages,
hasBranches,
canPurchaseBranches,
extraBranches,
branchPricing,
maxBranchQuota,
planBranchLimit,
} = useTypedLoaderData<typeof loader>();
const organization = useOrganization();
const project = useProject();
const environment = useEnvironment();
const plan = useCurrentPlan();
const showSelfServe = useShowSelfServe();
const requiresUpgrade =
plan?.v3Subscription?.plan &&
limits.used >= plan.v3Subscription.plan.limits.branches.number &&
!plan.v3Subscription.plan.limits.branches.canExceed;
const canUpgrade =
plan?.v3Subscription?.plan && !plan.v3Subscription.plan.limits.branches.canExceed;
const atBranchLimit = limits.used >= limits.limit;
const usageRatio = limits.limit > 0 ? Math.min(limits.used / limits.limit, 1) : 0;
if (!branchableEnvironment) {
return (
<PageContainer>
<NavBar>
<PageTitle title={<V4Title>Preview branches</V4Title>} />
</NavBar>
<PageBody>
<MainCenteredContainer className="max-w-md">
<BranchesNoBranchableEnvironment showSelfServe={showSelfServe} />
</MainCenteredContainer>
</PageBody>
</PageContainer>
);
}
return (
<PageContainer>
<NavBar>
<PageTitle title={<V4Title>Preview branches</V4Title>} />
<PageAccessories>
<AdminDebugTooltip>
<Property.Table>
{branches.map((branch) => (
<Property.Item key={branch.id}>
<Property.Label>{branch.branchName}</Property.Label>
<Property.Value>{branch.id}</Property.Value>
</Property.Item>
))}
</Property.Table>
</AdminDebugTooltip>
<LinkButton
variant={"docs/small"}
LeadingIcon={BookOpenIcon}
to={docsPath("deployment/preview-branches")}
>
Branches docs
</LinkButton>
{limits.isAtLimit ? (
<UpgradePanel
limits={limits}
canUpgrade={canUpgrade ?? false}
canPurchaseBranches={canPurchaseBranches}
branchPricing={branchPricing}
extraBranches={extraBranches}
maxBranchQuota={maxBranchQuota}
planBranchLimit={planBranchLimit}
/>
) : (
<NewBranchPanel
button={
<Button
variant="primary/small"
shortcut={{ key: "n" }}
LeadingIcon={PlusIcon}
leadingIconClassName="text-white"
fullWidth
textAlignLeft
>
New branch
</Button>
}
env="preview"
/>
)}
</PageAccessories>
</NavBar>
<PageBody scrollable={false}>
<div className="grid max-h-full min-h-full grid-rows-[auto_1fr_auto]">
{!hasBranches ? (
<MainCenteredContainer className="max-w-md">
<BranchesNoBranches
env="preview"
limits={limits}
canUpgrade={canUpgrade ?? false}
showSelfServe={showSelfServe}
/>
</MainCenteredContainer>
) : (
<>
<div className="flex items-center justify-between gap-x-1.5 p-2">
<BranchFilters />
<PaginationControls
currentPage={currentPage}
totalPages={totalPages}
showPageNumbers={false}
/>
</div>
<div className="grid max-h-full min-h-full grid-rows-[1fr] overflow-x-auto">
<Table>
<TableHeader>
<TableRow>
<TableHeaderCell>Branch</TableHeaderCell>
<TableHeaderCell>Created</TableHeaderCell>
<TableHeaderCell>Git</TableHeaderCell>
<TableHeaderCell>Archived</TableHeaderCell>
<TableHeaderCell>
<span className="sr-only">Actions</span>
</TableHeaderCell>
</TableRow>
</TableHeader>
<TableBody>
{branches.length === 0 ? (
<TableBlankRow colSpan={5}>
<Paragraph>There are no matches for your filters</Paragraph>
</TableBlankRow>
) : (
branches.map((branch) => {
const path = branchesPath(organization, project, branch);
const cellClass = branch.archivedAt ? "opacity-50" : "";
const isSelected = branch.id === environment.id;
return (
<TableRow key={branch.id}>
<TableCell isTabbableCell className={cellClass}>
<div className="flex items-center gap-1">
<BranchEnvironmentIconSmall
className={cn("size-4", isSelected && "text-preview")}
/>
<CopyableText
value={branch.branchName ?? ""}
className={cn(isSelected && "text-preview")}
/>
{isSelected && <Badge variant="extra-small">Current</Badge>}
</div>
</TableCell>
<TableCell className={cellClass}>
<DateTime date={branch.createdAt} />
</TableCell>
<TableCell className={cellClass}>
<div className="-ml-1 flex items-center">
<GitMetadata git={branch.git} />
</div>
</TableCell>
<TableCell className={cellClass}>
{branch.archivedAt ? (
<CheckIcon className="size-4 text-text-dimmed" />
) : (
""
)}
</TableCell>
<TableCellMenu
className="pl-32"
isSticky
hiddenButtons={
isSelected ? null : (
<LinkButton
to={path}
variant="secondary/small"
LeadingIcon={IconArrowBearRight2}
leadingIconClassName="text-blue-500 -mr-2"
className="pl-1.5"
>
Switch to branch
</LinkButton>
)
}
popoverContent={
!isSelected || !branch.archivedAt ? (
<>
{isSelected ? null : (
<PopoverMenuItem
to={path}
icon={IconArrowBearRight2}
leadingIconClassName="text-blue-500 -mr-0.5 -ml-1"
title="Switch to branch"
/>
)}
{!branch.archivedAt ? (
<ArchiveButton environment={branch} />
) : null}
</>
) : null
}
/>
</TableRow>
);
})
)}
</TableBody>
</Table>
</div>
<div className="flex w-full items-start justify-between">
<div className="flex h-fit w-full items-center gap-4 border-t border-grid-bright bg-background-bright p-[0.86rem] pl-4">
<SimpleTooltip
button={
<div className="size-6">
<svg className="h-full w-full -rotate-90 overflow-visible">
<circle
className="fill-none stroke-grid-bright"
strokeWidth="4"
r="10"
cx="12"
cy="12"
/>
<circle
className={`fill-none ${
atBranchLimit ? "stroke-error" : "stroke-success"
}`}
strokeWidth="4"
r="10"
cx="12"
cy="12"
strokeDasharray={`${usageRatio * 62.8} 62.8`}
strokeDashoffset="0"
strokeLinecap="round"
/>
</svg>
</div>
}
content={`${Math.round(usageRatio * 100)}%`}
/>
<div className="flex w-full items-center justify-between gap-6">
{requiresUpgrade ? (
<Header3 className="text-error">
You've used all {limits.limit} of your branches. Archive one or upgrade your
plan to enable more.
</Header3>
) : (
<div className="flex items-center gap-1">
<Header3 className={atBranchLimit ? "text-error" : undefined}>
You've used {limits.used}/{limits.limit} of your branches
</Header3>
<InfoIconTooltip content="Archived branches don't count towards your limit." />
</div>
)}
{canPurchaseBranches && branchPricing ? (
<PurchaseBranchesModal
branchPricing={branchPricing}
extraBranches={extraBranches}
activeBranches={limits.used}
maxQuota={maxBranchQuota}
planBranchLimit={planBranchLimit}
/>
) : canUpgrade ? (
showSelfServe ? (
<div className="flex items-center gap-3">
<Paragraph variant="small" className="whitespace-nowrap text-text-dimmed">
Upgrade plan for more Preview Branches
</Paragraph>
<LinkButton
to={v3BillingPath(organization)}
variant="secondary/small"
LeadingIcon={ArrowUpCircleIcon}
leadingIconClassName="text-indigo-500"
>
Upgrade
</LinkButton>
</div>
) : (
<Feedback
defaultValue="enterprise"
button={<Button variant="secondary/small">Request more</Button>}
/>
)
) : null}
</div>
</div>
</div>
</>
)}
</div>
</PageBody>
</PageContainer>
);
}
export function BranchFilters() {
const [searchParams, setSearchParams] = useSearchParams();
const { showArchived } = BranchesOptions.parse(Object.fromEntries(searchParams.entries()));
const handleArchivedChange = useCallback((checked: boolean) => {
setSearchParams((s) => {
if (checked) {
s.set("showArchived", "true");
} else {
s.delete("showArchived");
}
s.delete("page");
return s;
});
}, []);
return (
<div className="flex w-full items-center justify-between gap-2">
<SearchInput placeholder="Search branch name…" resetParams={["page"]} />
<Switch
checked={showArchived ?? false}
onCheckedChange={handleArchivedChange}
label="Show archived"
variant="secondary/small"
/>
</div>
);
}
function UpgradePanel({
limits,
canUpgrade,
canPurchaseBranches,
branchPricing,
extraBranches,
maxBranchQuota,
planBranchLimit,
}: {
limits: {
used: number;
limit: number;
};
canUpgrade: boolean;
canPurchaseBranches: boolean;
branchPricing: { stepSize: number; centsPerStep: number } | null;
extraBranches: number;
maxBranchQuota: number;
planBranchLimit: number;
}) {
const organization = useOrganization();
const showSelfServe = useShowSelfServe();
if (canPurchaseBranches && branchPricing) {
return (
<PurchaseBranchesModal
branchPricing={branchPricing}
extraBranches={extraBranches}
activeBranches={limits.used}
maxQuota={maxBranchQuota}
planBranchLimit={planBranchLimit}
triggerButton={
<Button
LeadingIcon={PlusIcon}
leadingIconClassName="text-white"
variant="primary/small"
shortcut={{ key: "n" }}
>
Purchase more
</Button>
}
/>
);
}
return (
<Dialog>
<DialogTrigger asChild>
<Button
LeadingIcon={PlusIcon}
leadingIconClassName="text-white"
variant="primary/small"
shortcut={{ key: "n" }}
>
New branch
</Button>
</DialogTrigger>
<DialogContent>
<DialogHeader>You've exceeded your limit</DialogHeader>
<div className="mt-2">
<Paragraph spacing>
You've used {limits.used}/{limits.limit} of your branches.
</Paragraph>
<Paragraph>You can archive one or upgrade your plan for more.</Paragraph>
</div>
<DialogFooter>
{canUpgrade ? (
showSelfServe ? (
<LinkButton variant="primary/small" to={v3BillingPath(organization)}>
Upgrade
</LinkButton>
) : (
<Feedback
defaultValue="enterprise"
button={<Button variant="secondary/small">Request more</Button>}
/>
)
) : null}
</DialogFooter>
</DialogContent>
</Dialog>
);
}
function PurchaseBranchesModal({
branchPricing,
extraBranches,
activeBranches,
maxQuota,
planBranchLimit,
triggerButton,
}: {
branchPricing: {
stepSize: number;
centsPerStep: number;
};
extraBranches: number;
activeBranches: number;
maxQuota: number;
planBranchLimit: number;
triggerButton?: React.ReactNode;
}) {
const showSelfServe = useShowSelfServe();
const fetcher = useFetcher();
const lastSubmission =
fetcher.data && typeof fetcher.data === "object" && "status" in fetcher.data
? fetcher.data
: undefined;
const [form, { amount }] = useForm({
id: "purchase-branches",
lastResult: lastSubmission as any,
onValidate({ formData }) {
return parseWithZod(formData, { schema: PurchaseSchema });
},
shouldRevalidate: "onSubmit",
});
const [amountValue, setAmountValue] = useState(extraBranches);
useEffect(() => {
setAmountValue(extraBranches);
}, [extraBranches]);
const isLoading = fetcher.state !== "idle";
const [open, setOpen] = useState(false);
useEffect(() => {
const data = fetcher.data;
if (
fetcher.state === "idle" &&
data !== null &&
typeof data === "object" &&
"ok" in data &&
data.ok
) {
setOpen(false);
}
}, [fetcher.state, fetcher.data]);
const state = updateBranchState({
value: amountValue,
existingValue: extraBranches,
quota: maxQuota,
activeBranches,
planBranchLimit,
});
const changeClassName =
state === "decrease" ? "text-error" : state === "increase" ? "text-success" : undefined;
const pricePerBranch = branchPricing.centsPerStep / branchPricing.stepSize / 100;
const title = extraBranches === 0 ? "Purchase extra branches…" : "Add/remove extra branches…";
if (!showSelfServe) {
return (
<Feedback
defaultValue="enterprise"
button={<Button variant="secondary/small">Request more</Button>}
/>
);
}
return (
<Dialog open={open} onOpenChange={setOpen}>
<DialogTrigger asChild>
{triggerButton ?? (
<Button variant="primary/small" onClick={() => setOpen(true)}>
{title}
</Button>
)}
</DialogTrigger>
<DialogContent>
<DialogHeader>{title}</DialogHeader>
<fetcher.Form method="post" {...getFormProps(form)}>
<input type="hidden" name="_formType" value="purchase-branches" />
<div className="flex flex-col gap-4 pt-2">
<div className="flex flex-col gap-1">
<Paragraph variant="small/bright">
Purchase extra preview branches at {formatCurrency(pricePerBranch, false)}/month per
branch. Reducing the number of branches will take effect at the start of the next
billing cycle (1st of the month).
</Paragraph>
</div>
<Fieldset>
<InputGroup fullWidth>
<Label htmlFor="amount" className="text-text-dimmed">
Total extra branches
</Label>
<InputNumberStepper
{...getInputProps(amount, { type: "number" })}
step={branchPricing.stepSize}
min={0}
max={undefined}
value={amountValue}
onChange={(e) => setAmountValue(Number(e.target.value))}
disabled={isLoading}
/>
<FormError id={amount.errorId}>{amount.errors}</FormError>
<FormError>{form.errors}</FormError>
</InputGroup>
</Fieldset>
{state === "need_to_archive" ? (
<div className="flex flex-col pb-3">
<Paragraph variant="small" className="text-warning" spacing>
You need to archive{" "}
{formatNumber(activeBranches - (planBranchLimit + amountValue))} more{" "}
{activeBranches - (planBranchLimit + amountValue) === 1 ? "branch" : "branches"}{" "}
before you can reduce to this level.
</Paragraph>
</div>
) : state === "above_quota" ? (
<div className="flex flex-col pb-3">
<Paragraph variant="small" className="text-warning" spacing>
Currently you can only have up to {maxQuota} extra preview branches. Send a
request below to lift your current limit. We'll get back to you soon.
</Paragraph>
</div>
) : (
<div className="flex flex-col pb-3 tabular-nums">
<div className="grid grid-cols-2 border-b border-grid-dimmed pb-1">
<Header3 className="font-normal text-text-dimmed">Summary</Header3>
<Header3 className="justify-self-end font-normal text-text-dimmed">Total</Header3>
</div>
<div className="grid grid-cols-2 pt-2">
<Header3 className="pb-0 font-normal text-text-dimmed">
<span className="text-text-bright">{formatNumber(extraBranches)}</span> current
extra
</Header3>
<Header3 className="justify-self-end font-normal text-text-bright">
{formatCurrency(extraBranches * pricePerBranch, true)}
</Header3>
</div>
<div className="grid grid-cols-2 text-xs">
<span className="text-text-dimmed">
({extraBranches} {extraBranches === 1 ? "branch" : "branches"})
</span>
<span className="justify-self-end text-text-dimmed">/mth</span>
</div>
<div className="grid grid-cols-2 pt-2">
<Header3 className={cn("pb-0 font-normal", changeClassName)}>
{state === "increase" ? "+" : null}
{formatNumber(amountValue - extraBranches)}
</Header3>
<Header3 className={cn("justify-self-end font-normal", changeClassName)}>
{state === "increase" ? "+" : null}
{formatCurrency((amountValue - extraBranches) * pricePerBranch, true)}
</Header3>
</div>
<div className="grid grid-cols-2 text-xs">
<span className="text-text-dimmed">
({Math.abs(amountValue - extraBranches)}{" "}
{Math.abs(amountValue - extraBranches) === 1 ? "branch" : "branches"} @{" "}
{formatCurrency(pricePerBranch, true)}/mth)
</span>
<span className="justify-self-end text-text-dimmed">/mth</span>
</div>
<div className="grid grid-cols-2 pt-2">
<Header3 className="pb-0 font-normal text-text-dimmed">
<span className="text-text-bright">{formatNumber(amountValue)}</span> new total
</Header3>
<Header3 className="justify-self-end font-normal text-text-bright">
{formatCurrency(amountValue * pricePerBranch, true)}
</Header3>
</div>
<div className="grid grid-cols-2 text-xs">
<span className="text-text-dimmed">
({amountValue} {amountValue === 1 ? "branch" : "branches"})
</span>
<span className="justify-self-end text-text-dimmed">/mth</span>
</div>
</div>
)}
</div>
<FormButtons
confirmButton={
state === "above_quota" ? (
<>
<input type="hidden" name="action" value="quota-increase" />
<Button
LeadingIcon={isLoading ? SpinnerWhite : EnvelopeIcon}
variant="primary/medium"
type="submit"
disabled={isLoading}
>
<span className="tabular-nums text-text-bright">{`Send request for ${formatNumber(
amountValue
)}`}</span>
</Button>
</>
) : state === "decrease" || state === "need_to_archive" ? (
<>
<input type="hidden" name="action" value="purchase" />
<Button
variant="danger/medium"
type="submit"
disabled={isLoading || state === "need_to_archive"}
LeadingIcon={isLoading ? SpinnerWhite : undefined}
>
<span className="tabular-nums text-text-bright">{`Remove ${formatNumber(
extraBranches - amountValue
)} ${extraBranches - amountValue === 1 ? "branch" : "branches"}`}</span>
</Button>
</>
) : (
<>
<input type="hidden" name="action" value="purchase" />
<Button
variant="primary/medium"
type="submit"
disabled={isLoading || state === "no_change"}
LeadingIcon={isLoading ? SpinnerWhite : undefined}
>
<span className="tabular-nums text-text-bright">{`Purchase ${formatNumber(
amountValue - extraBranches
)} ${amountValue - extraBranches === 1 ? "branch" : "branches"}`}</span>
</Button>
</>
)
}
cancelButton={
<DialogClose asChild>
<Button variant="secondary/medium" disabled={isLoading}>
Cancel
</Button>
</DialogClose>
}
/>
</fetcher.Form>
</DialogContent>
</Dialog>
);
}
function updateBranchState({
value,
existingValue,
quota,
activeBranches,
planBranchLimit,
}: {
value: number;
existingValue: number;
quota: number;
activeBranches: number;
planBranchLimit: number;
}): "no_change" | "increase" | "decrease" | "above_quota" | "need_to_archive" {
if (value === existingValue) return "no_change";
if (value < existingValue) {
const newTotalLimit = planBranchLimit + value;
if (activeBranches > newTotalLimit) {
return "need_to_archive";
}
return "decrease";
}
if (value > quota) return "above_quota";
return "increase";
}