fix
This commit is contained in:
@@ -14,8 +14,9 @@ import {
|
||||
Textarea,
|
||||
Modal,
|
||||
PageHeader,
|
||||
Switch,
|
||||
} from "@/components/ui";
|
||||
import { EditIcon, PlusIcon, TrashIcon } from "@/components/icons";
|
||||
import { EditIcon, EyeIcon, PlusIcon, TrashIcon } from "@/components/icons";
|
||||
import { MediaPreview } from "@/components/MediaPreview";
|
||||
import { pickUrl } from "@/lib/media";
|
||||
import { toFa } from "@/lib/utils";
|
||||
@@ -29,6 +30,8 @@ interface Category {
|
||||
order?: number;
|
||||
description?: string | null;
|
||||
icon?: string | null;
|
||||
is_deeplink_only?: boolean;
|
||||
deep_link?: string | null;
|
||||
subcategories_count?: number;
|
||||
}
|
||||
|
||||
@@ -48,9 +51,16 @@ const ICON_KEYS = ["icon", "icon_url"];
|
||||
|
||||
export default function CategoriesPage() {
|
||||
const [typeFilter, setTypeFilter] = useState<CategoryType>("media");
|
||||
const { data, loading, error, reload } = useList<Category>("/categories", {
|
||||
type: typeFilter,
|
||||
});
|
||||
// Deep-link-only categories are hidden from the normal listing, so they need
|
||||
// their own view here — otherwise a category sent to deep links disappears
|
||||
// from this page and can only be brought back from the deep-links screen.
|
||||
const [showDeepLinks, setShowDeepLinks] = useState(false);
|
||||
const { data, loading, error, reload } = useList<Category>(
|
||||
"/categories",
|
||||
// `deeplink_only` returns every hidden category regardless of type, so the
|
||||
// type filter is not sent alongside it.
|
||||
showDeepLinks ? { deeplink_only: 1 } : { type: typeFilter },
|
||||
);
|
||||
const toast = useToast();
|
||||
|
||||
const [editing, setEditing] = useState<Category | null>(null);
|
||||
@@ -128,6 +138,25 @@ export default function CategoriesPage() {
|
||||
}
|
||||
}
|
||||
|
||||
// Move a category into or out of deep-link-only mode, both directions.
|
||||
// Playlist categories live in music_categories, which has no such column.
|
||||
async function toggleDeepLink(row: Category) {
|
||||
try {
|
||||
await apiFetch(`/categories/${row.id}`, {
|
||||
method: "POST",
|
||||
body: toFormData({ is_deeplink_only: row.is_deeplink_only ? 0 : 1 }),
|
||||
});
|
||||
toast.success(
|
||||
row.is_deeplink_only
|
||||
? "به فهرست عادی بازگشت."
|
||||
: "به لینک مستقیم تبدیل شد.",
|
||||
);
|
||||
reload();
|
||||
} catch (err) {
|
||||
toast.error(err instanceof ApiError ? err.message : "خطا در ذخیرهسازی");
|
||||
}
|
||||
}
|
||||
|
||||
async function confirmDelete() {
|
||||
if (!deleting) return;
|
||||
setRemoving(true);
|
||||
@@ -174,6 +203,25 @@ export default function CategoriesPage() {
|
||||
header: "توضیحات",
|
||||
render: (r) => r.description || "—",
|
||||
},
|
||||
{
|
||||
key: "deep_link",
|
||||
header: "لینک مستقیم",
|
||||
className: "w-56",
|
||||
render: (r) =>
|
||||
r.is_deeplink_only ? (
|
||||
<a
|
||||
href={r.deep_link ?? undefined}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
dir="ltr"
|
||||
className="inline-block max-w-[200px] truncate text-xs text-primary underline"
|
||||
>
|
||||
{r.deep_link ?? "—"}
|
||||
</a>
|
||||
) : (
|
||||
<span className="text-xs text-muted">—</span>
|
||||
),
|
||||
},
|
||||
{
|
||||
key: "subcategories_count",
|
||||
header: "زیردستهها",
|
||||
@@ -189,11 +237,17 @@ export default function CategoriesPage() {
|
||||
subtitle="مدیریت دستهبندیهای عمومی رسانه، پلیلیست و قالب تنفس"
|
||||
action={
|
||||
<div className="flex items-center gap-3">
|
||||
<Switch
|
||||
checked={showDeepLinks}
|
||||
onChange={setShowDeepLinks}
|
||||
label="فقط لینکهای مستقیم"
|
||||
/>
|
||||
<Select
|
||||
value={typeFilter}
|
||||
onChange={(e) => setTypeFilter(e.target.value as CategoryType)}
|
||||
aria-label="نوع دستهبندی"
|
||||
className="w-40"
|
||||
disabled={showDeepLinks}
|
||||
>
|
||||
{TYPE_OPTIONS.map((o) => (
|
||||
<option key={o.value} value={o.value}>
|
||||
@@ -214,7 +268,11 @@ export default function CategoriesPage() {
|
||||
loading={loading}
|
||||
error={error}
|
||||
onRetry={reload}
|
||||
emptyMessage="هنوز دستهبندیای ثبت نشده است. اولین دستهبندی را بسازید."
|
||||
emptyMessage={
|
||||
showDeepLinks
|
||||
? "هیچ دستهبندی لینک مستقیمی وجود ندارد."
|
||||
: "هنوز دستهبندیای ثبت نشده است. اولین دستهبندی را بسازید."
|
||||
}
|
||||
emptyAction={
|
||||
<Button icon={<PlusIcon className="h-4 w-4" />} onClick={openCreate}>
|
||||
دستهبندی جدید
|
||||
@@ -225,6 +283,25 @@ export default function CategoriesPage() {
|
||||
<Button variant="ghost" onClick={() => openEdit(row)} aria-label="ویرایش">
|
||||
<EditIcon className="h-4 w-4" />
|
||||
</Button>
|
||||
{row.type !== "playlist" && (
|
||||
<Button
|
||||
variant="ghost"
|
||||
onClick={() => toggleDeepLink(row)}
|
||||
aria-label={
|
||||
row.is_deeplink_only
|
||||
? "بازگرداندن به فهرست"
|
||||
: "تبدیل به لینک مستقیم"
|
||||
}
|
||||
title={
|
||||
row.is_deeplink_only
|
||||
? "بازگرداندن به فهرست"
|
||||
: "تبدیل به لینک مستقیم"
|
||||
}
|
||||
className={row.is_deeplink_only ? "text-primary" : undefined}
|
||||
>
|
||||
<EyeIcon className="h-4 w-4" />
|
||||
</Button>
|
||||
)}
|
||||
<Button
|
||||
variant="ghost"
|
||||
onClick={() => setDeleting(row)}
|
||||
|
||||
Reference in New Issue
Block a user