import { useState } from "react"; import { t } from "@lingui/core/macro"; import { Trans } from "@lingui/react/macro"; import { API_URL } from "../../config/api.ts"; import { VALIDATION } from "../../../shared/validation.ts"; import type { Category, CreateCategoryRequest, UpdateCategoryRequest, } from "../../../shared/category.ts"; import type { Wire } from "../../../shared/api.ts"; import { useAuth } from "../../hooks/auth/useAuth.ts"; import { useCategories } from "../../hooks/category/useCategories.ts"; import { ConfirmModal } from "../shared/ConfirmModal.tsx"; import { expectOk } from "../../utils/shared/api.ts"; import { FormError } from "../form/FormError.tsx"; import { FormProvider } from "react-hook-form"; import { SubmitButton } from "../form/SubmitButton.tsx"; import { useApiForm } from "../../hooks/form/useApiForm.ts"; interface CategoryFields { name: string; slug: string; } /** Sentinel for "the create chip is open" (vs. a category id being edited). */ const NEW = "new"; /** * Admin UI to create, edit and delete categories. Rendered inside the "Manage" * profile tab (gated by the `category:manage` permission). * * Categories show as chips; clicking one expands it into an inline editor. A * trailing "+" chip opens the same editor in create mode. Only one editor is * open at a time. */ export function CategoryManager() { const { categories, isLoaded, refresh } = useCategories(); const [editing, setEditing] = useState(null); const close = () => setEditing(null); const handleChanged = () => { refresh(); close(); }; if (!isLoaded && categories.length === 0) { return (

Loading…

); } return (
{categories.map((category) => editing === category.id ? ( ) : ( ) )} {editing === NEW ? ( ) : ( )}
); } interface CategoryEditorProps { /** Omit to render the editor in "create" mode. */ category?: Category; onCancel: () => void; onChanged: () => void; } function CategoryEditor( { category, onCancel, onChanged }: CategoryEditorProps, ) { const isNew = category === undefined; const { authFetch } = useAuth(); const form = useApiForm({ defaultValues: { name: category?.name ?? "", slug: category?.slug ?? "" }, }); const name = form.watch("name"); const slug = form.watch("slug"); const { isDirty } = form.formState; const [deleting, setDeleting] = useState(false); const [confirmOpen, setConfirmOpen] = useState(false); const onSubmit = form.submit(async ({ name, slug }) => { const body = { name: name.trim(), slug: slug.trim() }; await expectOk>( isNew ? await authFetch(`${API_URL}/api/categories`, { method: "POST", body: JSON.stringify(body satisfies CreateCategoryRequest), }) : await authFetch(`${API_URL}/api/categories/${category.id}`, { method: "PUT", body: JSON.stringify(body satisfies UpdateCategoryRequest), }), ); onChanged(); }); async function remove() { if (isNew) return; setConfirmOpen(false); form.clearErrors("root"); setDeleting(true); try { await expectOk( await authFetch(`${API_URL}/api/categories/${category.id}`, { method: "DELETE", }), ); onChanged(); } catch (err) { form.setError("root", { message: err instanceof Error ? err.message : t`Failed to delete`, }); setDeleting(false); } } // Create needs both fields; edit needs an actual change too. const canSave = !deleting && name.trim() !== "" && slug.trim() !== "" && (isNew || isDirty); return (
{ if (e.key === "Escape") onCancel(); }} > /
✓ {!isNew && ( )}
{confirmOpen && category && ( setConfirmOpen(false)} /> )}
); }