import { useState } from "react"; import { t } from "@lingui/core/macro"; import { Trans } from "@lingui/react/macro"; import { API_URL, VALIDATION } from "../config/api.ts"; import { type Category, type CreateCategoryRequest, type RawCategory, type UpdateCategoryRequest, } from "../model.ts"; import { useAuth } from "../hooks/useAuth.ts"; import { useCategories } from "../hooks/useCategories.ts"; import { ConfirmModal } from "./ConfirmModal.tsx"; import { expectOk, FormError, FormProvider, SubmitButton, useApiForm, } from "./form/index.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)} /> )}
); }