cde7be9f2a
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
140 lines
4.4 KiB
TypeScript
140 lines
4.4 KiB
TypeScript
import { useState } from "react";
|
|
import { Link } from "react-router-dom";
|
|
import { useTranslation } from "react-i18next";
|
|
|
|
import type { components } from "../api/schema";
|
|
import { useSetVisibility, VisibilityError } from "../api/queries";
|
|
import { adjacentTransitions, type Visibility } from "./transitions";
|
|
import { Button } from "@/components/ui/button";
|
|
import {
|
|
AlertDialog,
|
|
AlertDialogTrigger,
|
|
AlertDialogContent,
|
|
AlertDialogTitle,
|
|
AlertDialogDescription,
|
|
AlertDialogFooter,
|
|
AlertDialogCancel,
|
|
AlertDialogAction,
|
|
} from "@/components/ui/alert-dialog";
|
|
|
|
type AdminObjectView = components["schemas"]["AdminObjectView"];
|
|
|
|
const STEPS: Visibility[] = ["draft", "internal", "public"];
|
|
|
|
export function PublishControl({ object }: { object: AdminObjectView }) {
|
|
const { t } = useTranslation();
|
|
const current = object.visibility;
|
|
const { forward, back } = adjacentTransitions(current);
|
|
const setVisibility = useSetVisibility();
|
|
const [confirmOpen, setConfirmOpen] = useState(false);
|
|
const [errorKind, setErrorKind] = useState<"gate" | "illegal" | "other" | null>(null);
|
|
|
|
const go = (visibility: Visibility) => {
|
|
setErrorKind(null);
|
|
setVisibility.mutate(
|
|
{ id: object.id, visibility },
|
|
{
|
|
onSuccess: () => setConfirmOpen(false),
|
|
onError: (err) => {
|
|
setConfirmOpen(false);
|
|
const status = err instanceof VisibilityError ? err.status : 0;
|
|
setErrorKind(status === 422 ? "gate" : status === 409 ? "illegal" : "other");
|
|
},
|
|
},
|
|
);
|
|
};
|
|
|
|
const currentIndex = STEPS.indexOf(current);
|
|
|
|
return (
|
|
<section className="border-t p-4">
|
|
<div className="mb-2 label-caption">
|
|
{t("publish.heading")}
|
|
</div>
|
|
|
|
<div className="mb-3 flex">
|
|
{STEPS.map((step, i) => (
|
|
<div
|
|
key={step}
|
|
aria-current={i === currentIndex ? "step" : undefined}
|
|
className={`flex-1 border px-2 py-1 text-center text-xs ${
|
|
i === currentIndex
|
|
? "bg-primary font-semibold text-primary-foreground"
|
|
: i < currentIndex
|
|
? "bg-muted text-muted-foreground"
|
|
: "text-muted-foreground"
|
|
}`}
|
|
>
|
|
{t(`visibility.${step}`)}
|
|
</div>
|
|
))}
|
|
</div>
|
|
|
|
<div className="flex gap-2">
|
|
{back && (
|
|
<Button
|
|
variant="ghost"
|
|
size="sm"
|
|
disabled={setVisibility.isPending}
|
|
onClick={() => go(back)}
|
|
>
|
|
{back === "draft" ? t("publish.backToDraft") : t("publish.unpublishInternal")}
|
|
</Button>
|
|
)}
|
|
{forward === "internal" && (
|
|
<Button
|
|
size="sm"
|
|
disabled={setVisibility.isPending}
|
|
onClick={() => go("internal")}
|
|
>
|
|
{t("publish.advanceInternal")}
|
|
</Button>
|
|
)}
|
|
{forward === "public" && (
|
|
<AlertDialog open={confirmOpen} onOpenChange={setConfirmOpen}>
|
|
<AlertDialogTrigger
|
|
render={
|
|
<Button size="sm" disabled={setVisibility.isPending}>
|
|
{t("publish.publish")}
|
|
</Button>
|
|
}
|
|
/>
|
|
<AlertDialogContent>
|
|
<AlertDialogTitle>{t("publish.confirmTitle")}</AlertDialogTitle>
|
|
<AlertDialogDescription>{t("publish.confirmBody")}</AlertDialogDescription>
|
|
<AlertDialogFooter>
|
|
<AlertDialogCancel>{t("form.cancel")}</AlertDialogCancel>
|
|
<AlertDialogAction
|
|
disabled={setVisibility.isPending}
|
|
onClick={() => go("public")}
|
|
>
|
|
{t("publish.confirm")}
|
|
</AlertDialogAction>
|
|
</AlertDialogFooter>
|
|
</AlertDialogContent>
|
|
</AlertDialog>
|
|
)}
|
|
</div>
|
|
|
|
{errorKind === "gate" && (
|
|
<p role="alert" className="mt-2 text-sm text-destructive">
|
|
{t("publish.gateError")}{" "}
|
|
<Link to={`/objects/${object.id}/edit`} className="underline">
|
|
{t("publish.editLink")}
|
|
</Link>
|
|
</p>
|
|
)}
|
|
{errorKind === "illegal" && (
|
|
<p role="alert" className="mt-2 text-sm text-destructive">
|
|
{t("publish.illegalError")}
|
|
</p>
|
|
)}
|
|
{errorKind === "other" && (
|
|
<p role="alert" className="mt-2 text-sm text-destructive">
|
|
{t("form.rejected")}
|
|
</p>
|
|
)}
|
|
</section>
|
|
);
|
|
}
|