Emdash source with visual editor image upload fix
Fixes: 1. media.ts: wrap placeholder generation in try-catch 2. toolbar.ts: check r.ok, display error message in popover
This commit is contained in:
673
packages/admin/src/components/TaxonomyManager.tsx
Normal file
673
packages/admin/src/components/TaxonomyManager.tsx
Normal file
@@ -0,0 +1,673 @@
|
||||
/**
|
||||
* Taxonomy Terms Manager
|
||||
*
|
||||
* Provides UI for managing taxonomy terms (categories, tags, custom taxonomies).
|
||||
* Shows hierarchical structure for categories, flat list for tags.
|
||||
*/
|
||||
|
||||
import { Button, Checkbox, Dialog, Input, InputArea, Select, Toast } from "@cloudflare/kumo";
|
||||
import { useLingui } from "@lingui/react/macro";
|
||||
import { Plus, Pencil, Trash, X } from "@phosphor-icons/react";
|
||||
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
|
||||
import * as React from "react";
|
||||
|
||||
import { fetchManifest } from "../lib/api/client.js";
|
||||
import type { TaxonomyTerm, TaxonomyDef, CreateTaxonomyInput } from "../lib/api/taxonomies.js";
|
||||
import {
|
||||
fetchTaxonomyDef,
|
||||
fetchTerms,
|
||||
createTaxonomy,
|
||||
createTerm,
|
||||
updateTerm,
|
||||
deleteTerm,
|
||||
} from "../lib/api/taxonomies.js";
|
||||
import { slugify } from "../lib/utils";
|
||||
import { ConfirmDialog } from "./ConfirmDialog.js";
|
||||
import { DialogError, getMutationError } from "./DialogError.js";
|
||||
|
||||
interface TaxonomyManagerProps {
|
||||
taxonomyName: string;
|
||||
}
|
||||
|
||||
// Regex patterns for taxonomy name generation and validation (module-scoped per lint rules)
|
||||
const NON_ALPHANUMERIC_PATTERN = /[^a-z0-9]+/g;
|
||||
const LEADING_TRAILING_UNDERSCORE_PATTERN = /^_|_$/g;
|
||||
const TAXONOMY_NAME_PATTERN = /^[a-z][a-z0-9_]*$/;
|
||||
|
||||
/**
|
||||
* Flatten tree to get all terms
|
||||
*/
|
||||
function flattenTerms(terms: TaxonomyTerm[]): TaxonomyTerm[] {
|
||||
return terms.flatMap((t) => [t, ...flattenTerms(t.children)]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Term row component (recursive for hierarchy)
|
||||
*/
|
||||
function TermRow({
|
||||
term,
|
||||
level = 0,
|
||||
onEdit,
|
||||
onDelete,
|
||||
}: {
|
||||
term: TaxonomyTerm;
|
||||
level?: number;
|
||||
onEdit: (term: TaxonomyTerm) => void;
|
||||
onDelete: (term: TaxonomyTerm) => void;
|
||||
}) {
|
||||
const { t } = useLingui();
|
||||
return (
|
||||
<>
|
||||
<div className="flex items-center gap-4 py-2 px-4 border-b hover:bg-kumo-tint/50">
|
||||
<div style={{ marginInlineStart: `${level * 1.5}rem` }} className="flex-1">
|
||||
<span className="font-medium">{term.label}</span>
|
||||
<span className="text-sm text-kumo-subtle ms-2">({term.slug})</span>
|
||||
</div>
|
||||
<div className="text-sm text-kumo-subtle">{term.count || 0}</div>
|
||||
<div className="flex gap-2">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
aria-label={t`Edit ${term.label}`}
|
||||
onClick={() => onEdit(term)}
|
||||
>
|
||||
<Pencil className="w-4 h-4" />
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
aria-label={t`Delete ${term.label}`}
|
||||
onClick={() => onDelete(term)}
|
||||
>
|
||||
<Trash className="w-4 h-4" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
{term.children.map((child) => (
|
||||
<TermRow
|
||||
key={child.id}
|
||||
term={child}
|
||||
level={level + 1}
|
||||
onEdit={onEdit}
|
||||
onDelete={onDelete}
|
||||
/>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Term form dialog
|
||||
*/
|
||||
function TermFormDialog({
|
||||
open,
|
||||
onClose,
|
||||
taxonomyName,
|
||||
taxonomyDef,
|
||||
term,
|
||||
allTerms,
|
||||
}: {
|
||||
open: boolean;
|
||||
onClose: () => void;
|
||||
taxonomyName: string;
|
||||
taxonomyDef: TaxonomyDef;
|
||||
term?: TaxonomyTerm;
|
||||
allTerms: TaxonomyTerm[];
|
||||
}) {
|
||||
const { t } = useLingui();
|
||||
const queryClient = useQueryClient();
|
||||
const [label, setLabel] = React.useState(term?.label || "");
|
||||
const [slug, setSlug] = React.useState(term?.slug || "");
|
||||
const [parentId, setParentId] = React.useState(term?.parentId || "");
|
||||
const [description, setDescription] = React.useState(term?.description || "");
|
||||
const [autoSlug, setAutoSlug] = React.useState(!term);
|
||||
const [error, setError] = React.useState<string | null>(null);
|
||||
|
||||
// Sync form state when term prop changes (for edit mode)
|
||||
React.useEffect(() => {
|
||||
setLabel(term?.label || "");
|
||||
setSlug(term?.slug || "");
|
||||
setParentId(term?.parentId || "");
|
||||
setDescription(term?.description || "");
|
||||
setAutoSlug(!term);
|
||||
setError(null);
|
||||
}, [term]);
|
||||
|
||||
// Auto-generate slug from label
|
||||
React.useEffect(() => {
|
||||
if (autoSlug && label) {
|
||||
setSlug(slugify(label));
|
||||
}
|
||||
}, [label, autoSlug]);
|
||||
|
||||
const createMutation = useMutation({
|
||||
mutationFn: () =>
|
||||
createTerm(taxonomyName, {
|
||||
slug,
|
||||
label,
|
||||
parentId: parentId || undefined,
|
||||
description: description || undefined,
|
||||
}),
|
||||
onSuccess: () => {
|
||||
void queryClient.invalidateQueries({
|
||||
queryKey: ["taxonomy-terms", taxonomyName],
|
||||
});
|
||||
onClose();
|
||||
},
|
||||
onError: (err: Error) => {
|
||||
setError(err.message);
|
||||
},
|
||||
});
|
||||
|
||||
const updateMutation = useMutation({
|
||||
mutationFn: () => {
|
||||
if (!term) throw new Error("No term to update");
|
||||
return updateTerm(taxonomyName, term.slug, {
|
||||
slug,
|
||||
label,
|
||||
parentId: parentId || undefined,
|
||||
description: description || undefined,
|
||||
});
|
||||
},
|
||||
onSuccess: () => {
|
||||
void queryClient.invalidateQueries({
|
||||
queryKey: ["taxonomy-terms", taxonomyName],
|
||||
});
|
||||
onClose();
|
||||
},
|
||||
onError: (err: Error) => {
|
||||
setError(err.message);
|
||||
},
|
||||
});
|
||||
|
||||
const handleSubmit = (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
setError(null);
|
||||
if (term) {
|
||||
updateMutation.mutate();
|
||||
} else {
|
||||
createMutation.mutate();
|
||||
}
|
||||
};
|
||||
|
||||
// Flatten terms for parent selector (exclude current term and its children)
|
||||
const flatTerms = flattenTerms(allTerms);
|
||||
const availableParents = term
|
||||
? flatTerms.filter((item) => item.id !== term.id && item.parentId !== term.id)
|
||||
: flatTerms;
|
||||
|
||||
return (
|
||||
<Dialog.Root
|
||||
open={open}
|
||||
onOpenChange={(isOpen: boolean) => {
|
||||
if (!isOpen) {
|
||||
setError(null);
|
||||
onClose();
|
||||
}
|
||||
}}
|
||||
>
|
||||
<Dialog className="p-6" size="lg">
|
||||
<form onSubmit={handleSubmit}>
|
||||
<div className="flex items-start justify-between gap-4 mb-4">
|
||||
<div className="flex flex-col space-y-1.5">
|
||||
<Dialog.Title className="text-lg font-semibold leading-none tracking-tight">
|
||||
{term ? t`Edit` : t`Add`} {taxonomyDef.labelSingular || t`Term`}
|
||||
</Dialog.Title>
|
||||
<Dialog.Description className="text-sm text-kumo-subtle">
|
||||
{term
|
||||
? t`Update the ${taxonomyDef.labelSingular?.toLowerCase() || "term"} details`
|
||||
: t`Create a new ${taxonomyDef.labelSingular?.toLowerCase() || "term"}`}
|
||||
</Dialog.Description>
|
||||
</div>
|
||||
<Dialog.Close
|
||||
aria-label={t`Close`}
|
||||
render={(props) => (
|
||||
<Button
|
||||
{...props}
|
||||
variant="ghost"
|
||||
shape="square"
|
||||
aria-label={t`Close`}
|
||||
className="absolute end-4 top-4"
|
||||
>
|
||||
<X className="h-4 w-4" />
|
||||
<span className="sr-only">{t`Close`}</span>
|
||||
</Button>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-4 py-4">
|
||||
<Input
|
||||
label={t`Name`}
|
||||
value={label}
|
||||
onChange={(e) => setLabel(e.target.value)}
|
||||
placeholder="News"
|
||||
required
|
||||
/>
|
||||
|
||||
<div>
|
||||
<Input
|
||||
label={t`Slug`}
|
||||
value={slug}
|
||||
onChange={(e) => {
|
||||
setSlug(e.target.value);
|
||||
setAutoSlug(false);
|
||||
}}
|
||||
placeholder="news"
|
||||
required
|
||||
/>
|
||||
<p className="text-sm text-kumo-subtle mt-1">
|
||||
{t`Auto-generated from name (you can edit)`}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{taxonomyDef.hierarchical && (
|
||||
<Select
|
||||
label={t`Parent`}
|
||||
value={parentId}
|
||||
onValueChange={(v) => setParentId(v ?? "")}
|
||||
items={{
|
||||
"": t`None (top level)`,
|
||||
...Object.fromEntries(
|
||||
availableParents.map((parentTerm) => [parentTerm.id, parentTerm.label]),
|
||||
),
|
||||
}}
|
||||
>
|
||||
<Select.Option value="">{t`None (top level)`}</Select.Option>
|
||||
{availableParents.map((parentTerm) => (
|
||||
<Select.Option key={parentTerm.id} value={parentTerm.id}>
|
||||
{parentTerm.label}
|
||||
</Select.Option>
|
||||
))}
|
||||
</Select>
|
||||
)}
|
||||
|
||||
<InputArea
|
||||
label={t`Description (optional)`}
|
||||
value={description}
|
||||
onChange={(e) => setDescription(e.target.value)}
|
||||
placeholder={t`Optional description`}
|
||||
rows={3}
|
||||
/>
|
||||
|
||||
<DialogError
|
||||
message={
|
||||
error ||
|
||||
getMutationError(createMutation.error) ||
|
||||
getMutationError(updateMutation.error)
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2">
|
||||
<Button type="button" variant="outline" onClick={onClose}>
|
||||
{t`Cancel`}
|
||||
</Button>
|
||||
<Button type="submit" disabled={createMutation.isPending || updateMutation.isPending}>
|
||||
{createMutation.isPending || updateMutation.isPending
|
||||
? t`Saving...`
|
||||
: term
|
||||
? t`Update`
|
||||
: t`Create`}
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
</Dialog>
|
||||
</Dialog.Root>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create Taxonomy dialog
|
||||
*/
|
||||
function CreateTaxonomyDialog({
|
||||
open,
|
||||
onClose,
|
||||
onCreated,
|
||||
}: {
|
||||
open: boolean;
|
||||
onClose: () => void;
|
||||
onCreated: () => void;
|
||||
}) {
|
||||
const { t } = useLingui();
|
||||
const queryClient = useQueryClient();
|
||||
const [name, setName] = React.useState("");
|
||||
const [label, setLabel] = React.useState("");
|
||||
const [hierarchical, setHierarchical] = React.useState(false);
|
||||
const [selectedCollections, setSelectedCollections] = React.useState<string[]>([]);
|
||||
const [autoName, setAutoName] = React.useState(true);
|
||||
const [error, setError] = React.useState<string | null>(null);
|
||||
|
||||
const { data: manifest } = useQuery({
|
||||
queryKey: ["manifest"],
|
||||
queryFn: fetchManifest,
|
||||
});
|
||||
|
||||
const collectionEntries = manifest
|
||||
? Object.entries(manifest.collections).map(([slug, config]) => ({
|
||||
slug,
|
||||
label: config.label,
|
||||
}))
|
||||
: [];
|
||||
|
||||
// Auto-generate name from label
|
||||
React.useEffect(() => {
|
||||
if (autoName && label) {
|
||||
setName(
|
||||
label
|
||||
.toLowerCase()
|
||||
.replace(NON_ALPHANUMERIC_PATTERN, "_")
|
||||
.replace(LEADING_TRAILING_UNDERSCORE_PATTERN, ""),
|
||||
);
|
||||
}
|
||||
}, [label, autoName]);
|
||||
|
||||
const createMutation = useMutation({
|
||||
mutationFn: (input: CreateTaxonomyInput) => createTaxonomy(input),
|
||||
onSuccess: () => {
|
||||
void queryClient.invalidateQueries({ queryKey: ["taxonomy-defs"] });
|
||||
void queryClient.invalidateQueries({ queryKey: ["taxonomy-def"] });
|
||||
onCreated();
|
||||
resetForm();
|
||||
},
|
||||
});
|
||||
|
||||
const resetForm = () => {
|
||||
setName("");
|
||||
setLabel("");
|
||||
setHierarchical(false);
|
||||
setSelectedCollections([]);
|
||||
setAutoName(true);
|
||||
setError(null);
|
||||
createMutation.reset();
|
||||
};
|
||||
|
||||
const handleSubmit = (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
setError(null);
|
||||
|
||||
if (!name || !label) {
|
||||
setError(t`Name and label are required`);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!TAXONOMY_NAME_PATTERN.test(name)) {
|
||||
setError(
|
||||
t`Name must start with a letter and contain only lowercase letters, numbers, and underscores`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
createMutation.mutate({
|
||||
name,
|
||||
label,
|
||||
hierarchical,
|
||||
collections: selectedCollections,
|
||||
});
|
||||
};
|
||||
|
||||
const toggleCollection = (slug: string) => {
|
||||
setSelectedCollections((prev) =>
|
||||
prev.includes(slug) ? prev.filter((s) => s !== slug) : [...prev, slug],
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog.Root
|
||||
open={open}
|
||||
onOpenChange={(isOpen: boolean) => {
|
||||
if (!isOpen) {
|
||||
resetForm();
|
||||
onClose();
|
||||
}
|
||||
}}
|
||||
>
|
||||
<Dialog className="p-6" size="lg">
|
||||
<form onSubmit={handleSubmit}>
|
||||
<div className="flex items-start justify-between gap-4 mb-4">
|
||||
<div className="flex flex-col space-y-1.5">
|
||||
<Dialog.Title className="text-lg font-semibold leading-none tracking-tight">
|
||||
{t`Create Taxonomy`}
|
||||
</Dialog.Title>
|
||||
<Dialog.Description className="text-sm text-kumo-subtle">
|
||||
{t`Define a new taxonomy for classifying content`}
|
||||
</Dialog.Description>
|
||||
</div>
|
||||
<Dialog.Close
|
||||
aria-label={t`Close`}
|
||||
render={(props) => (
|
||||
<Button
|
||||
{...props}
|
||||
variant="ghost"
|
||||
shape="square"
|
||||
aria-label={t`Close`}
|
||||
className="absolute end-4 top-4"
|
||||
>
|
||||
<X className="h-4 w-4" />
|
||||
<span className="sr-only">{t`Close`}</span>
|
||||
</Button>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-4 py-4">
|
||||
<Input
|
||||
label={t`Label`}
|
||||
value={label}
|
||||
onChange={(e) => setLabel(e.target.value)}
|
||||
placeholder="Genres"
|
||||
required
|
||||
/>
|
||||
|
||||
<div>
|
||||
<Input
|
||||
label={t`Name`}
|
||||
value={name}
|
||||
onChange={(e) => {
|
||||
setName(e.target.value);
|
||||
setAutoName(false);
|
||||
}}
|
||||
placeholder="genre"
|
||||
required
|
||||
pattern="[a-z][a-z0-9_]*"
|
||||
title={t`Lowercase letters, numbers, and underscores only, starting with a letter`}
|
||||
/>
|
||||
<p className="text-xs text-kumo-subtle mt-1">
|
||||
{t`Used as the identifier. Lowercase letters, numbers, and underscores only.`}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<Checkbox
|
||||
label={t`Hierarchical (like categories, with parent/child relationships)`}
|
||||
checked={hierarchical}
|
||||
onCheckedChange={(checked) => setHierarchical(checked)}
|
||||
/>
|
||||
|
||||
{collectionEntries.length > 0 && (
|
||||
<div>
|
||||
<label className="text-sm font-medium">{t`Collections`}</label>
|
||||
<p className="text-xs text-kumo-subtle mb-2">
|
||||
{t`Which content types can use this taxonomy`}
|
||||
</p>
|
||||
<div className="border rounded-md p-2 space-y-1">
|
||||
{collectionEntries.map(({ slug, label: collLabel }) => (
|
||||
<label
|
||||
key={slug}
|
||||
className="flex items-center gap-2 py-1 px-2 cursor-pointer hover:bg-kumo-tint/50 rounded"
|
||||
>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={selectedCollections.includes(slug)}
|
||||
onChange={() => toggleCollection(slug)}
|
||||
className="rounded"
|
||||
/>
|
||||
<span className="text-sm">{collLabel}</span>
|
||||
</label>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<DialogError message={error || getMutationError(createMutation.error)} />
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2">
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
onClick={() => {
|
||||
resetForm();
|
||||
onClose();
|
||||
}}
|
||||
>
|
||||
{t`Cancel`}
|
||||
</Button>
|
||||
<Button type="submit" disabled={createMutation.isPending}>
|
||||
{createMutation.isPending ? t`Creating...` : t`Create Taxonomy`}
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
</Dialog>
|
||||
</Dialog.Root>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Main TaxonomyManager component
|
||||
*/
|
||||
export function TaxonomyManager({ taxonomyName }: TaxonomyManagerProps) {
|
||||
const { t } = useLingui();
|
||||
const queryClient = useQueryClient();
|
||||
const toastManager = Toast.useToastManager();
|
||||
const [formOpen, setFormOpen] = React.useState(false);
|
||||
const [editingTerm, setEditingTerm] = React.useState<TaxonomyTerm | undefined>();
|
||||
const [deleteTarget, setDeleteTarget] = React.useState<TaxonomyTerm | null>(null);
|
||||
const [createTaxonomyOpen, setCreateTaxonomyOpen] = React.useState(false);
|
||||
|
||||
const { data: taxonomyDef, isLoading: defLoading } = useQuery({
|
||||
queryKey: ["taxonomy-def", taxonomyName],
|
||||
queryFn: () => fetchTaxonomyDef(taxonomyName),
|
||||
});
|
||||
|
||||
const { data: terms = [], isLoading: termsLoading } = useQuery({
|
||||
queryKey: ["taxonomy-terms", taxonomyName],
|
||||
queryFn: () => fetchTerms(taxonomyName),
|
||||
});
|
||||
|
||||
const deleteMutation = useMutation({
|
||||
mutationFn: (term: TaxonomyTerm) => deleteTerm(taxonomyName, term.slug),
|
||||
onSuccess: () => {
|
||||
void queryClient.invalidateQueries({
|
||||
queryKey: ["taxonomy-terms", taxonomyName],
|
||||
});
|
||||
setDeleteTarget(null);
|
||||
toastManager.add({ title: t`Term deleted` });
|
||||
},
|
||||
});
|
||||
|
||||
const handleEdit = (term: TaxonomyTerm) => {
|
||||
setEditingTerm(term);
|
||||
setFormOpen(true);
|
||||
};
|
||||
|
||||
const handleDelete = (term: TaxonomyTerm) => {
|
||||
setDeleteTarget(term);
|
||||
};
|
||||
|
||||
const handleCloseForm = () => {
|
||||
setFormOpen(false);
|
||||
setEditingTerm(undefined);
|
||||
};
|
||||
|
||||
if (defLoading) {
|
||||
return <div>{t`Loading...`}</div>;
|
||||
}
|
||||
|
||||
if (!taxonomyDef) {
|
||||
return (
|
||||
<div>
|
||||
{t`Taxonomy not found:`} {taxonomyName}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const flatTerms = flattenTerms(terms);
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h1 className="text-3xl font-bold">{taxonomyDef.label}</h1>
|
||||
<p className="text-kumo-subtle mt-1">
|
||||
{t`Manage ${taxonomyDef.label.toLowerCase()} for ${taxonomyDef.collections.join(", ")}`}
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
<Button variant="outline" icon={<Plus />} onClick={() => setCreateTaxonomyOpen(true)}>
|
||||
{t`New Taxonomy`}
|
||||
</Button>
|
||||
<Button icon={<Plus />} onClick={() => setFormOpen(true)}>
|
||||
{t`Add`} {taxonomyDef.labelSingular || t`Term`}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="border rounded-lg">
|
||||
<div className="flex items-center gap-4 py-2 px-4 border-b bg-kumo-tint/50 font-medium">
|
||||
<div className="flex-1">{t`Name`}</div>
|
||||
<div className="w-16 text-center">{t`Count`}</div>
|
||||
<div className="w-24 text-center">{t`Actions`}</div>
|
||||
</div>
|
||||
|
||||
{termsLoading ? (
|
||||
<div className="p-8 text-center text-kumo-subtle">{t`Loading terms...`}</div>
|
||||
) : terms.length === 0 ? (
|
||||
<div className="p-8 text-center text-kumo-subtle">
|
||||
{t`No ${taxonomyDef.label.toLowerCase()} yet. Create one to get started.`}
|
||||
</div>
|
||||
) : (
|
||||
<div>
|
||||
{terms.map((term) => (
|
||||
<TermRow key={term.id} term={term} onEdit={handleEdit} onDelete={handleDelete} />
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<TermFormDialog
|
||||
open={formOpen}
|
||||
onClose={handleCloseForm}
|
||||
taxonomyName={taxonomyName}
|
||||
taxonomyDef={taxonomyDef}
|
||||
term={editingTerm}
|
||||
allTerms={flatTerms}
|
||||
/>
|
||||
|
||||
<ConfirmDialog
|
||||
open={!!deleteTarget}
|
||||
onClose={() => {
|
||||
setDeleteTarget(null);
|
||||
deleteMutation.reset();
|
||||
}}
|
||||
title={t`Delete ${taxonomyDef.labelSingular || "Term"}?`}
|
||||
description={
|
||||
<>{t`This will permanently delete "${deleteTarget?.label}" and remove it from all content.`}</>
|
||||
}
|
||||
confirmLabel={t`Delete`}
|
||||
pendingLabel={t`Deleting...`}
|
||||
isPending={deleteMutation.isPending}
|
||||
error={deleteMutation.error}
|
||||
onConfirm={() => deleteTarget && deleteMutation.mutate(deleteTarget)}
|
||||
/>
|
||||
|
||||
<CreateTaxonomyDialog
|
||||
open={createTaxonomyOpen}
|
||||
onClose={() => setCreateTaxonomyOpen(false)}
|
||||
onCreated={() => {
|
||||
setCreateTaxonomyOpen(false);
|
||||
toastManager.add({ title: t`Taxonomy created` });
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user