feat(catalog): add reviewed bulk edits and complete category duplication
CI / check (push) Successful in 1m14s
CI / deploy (push) Successful in 1m11s

This commit is contained in:
Simo committed 2026-09-06 20:25:19 +02:00
1 parent 0bedc04692
commit 55a47949f1
26 files changed
+2504 -85

No files matched your search

@@ -34,6 +34,7 @@ import {
X,
} from "lucide-react";
import { useRouter } from "next/navigation";
import { useTranslations } from "next-intl";
import { useCallback, useEffect, useMemo, useState } from "react";
import { toast } from "sonner";
import {
@@ -80,6 +81,7 @@ import {
TableRow,
} from "@/components/ui/table";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { BulkOfferEditor } from "@/features/catalog/components/bulk-offer-editor";
import { useServerAction } from "@/hooks/use-server-action";
import { getSuggestions, recordMove } from "@/lib/move-suggestions";
import { cn } from "@/lib/utils";
@@ -167,6 +169,10 @@ export function CatalogItemsTable({
allPages,
}: CatalogItemsTableProps) {
const router = useRouter();
const bulk = useTranslations("pages.admin.catalog.bulk");
const [bulkRefreshFrom, setBulkRefreshFrom] = useState<
CatalogItemData[] | null
>(null);
const { run, isPending } = useServerAction();
const { confirm, dialog: confirmDialog } = useConfirmDialog();
const [addOpen, setAddOpen] = useState(false);
@@ -294,7 +300,26 @@ export function CatalogItemsTable({
return itemOrder.some((id, idx) => id !== originalOrder[idx]);
}, [items, itemOrder]);
const hasUnsavedChanges = modifiedItems.length > 0 || hasOrderChanges;
const hasUnsavedChanges =
bulkRefreshFrom === null && (modifiedItems.length > 0 || hasOrderChanges);
useEffect(() => {
if (bulkRefreshFrom === null || items === bulkRefreshFrom) return;
setInlineEdits(
Object.fromEntries(
items.map((item) => [
item.id,
{
costCredits: item.costCredits,
costPoints: item.costPoints,
pointsType: item.pointsType,
amount: item.amount,
},
]),
),
);
setItemOrder(items.map((item) => item.id));
setBulkRefreshFrom(null);
}, [items, bulkRefreshFrom]);
useEffect(() => {
onDirtyChange?.(hasUnsavedChanges);
}, [hasUnsavedChanges, onDirtyChange]);
@@ -904,7 +929,19 @@ export function CatalogItemsTable({
return (
<>
{confirmDialog}
<Card>
{bulkRefreshFrom !== null && (
<p
role="status"
className="flex items-center gap-2 text-sm text-muted-foreground"
>
<Loader2 className="size-4 animate-spin" />
{bulk("refreshing")}
</p>
)}
<Card
inert={bulkRefreshFrom !== null}
aria-busy={bulkRefreshFrom !== null}
>
<CardHeader className="space-y-3">
<div className="flex flex-row items-center justify-between gap-4">
<div className="min-w-0">
@@ -958,6 +995,21 @@ export function CatalogItemsTable({
<>
{selected.size > 0 && (
<>
<BulkOfferEditor
ids={[...selected]}
allPages={allPages}
disabled={
hasUnsavedChanges ||
isPending ||
savingItemId !== null
}
onApplied={() => {
setBulkRefreshFrom(items);
setSelected(new Set());
if (onRefresh) onRefresh();
else router.refresh();
}}
/>
<Button
variant="outline"
size="sm"
@@ -151,6 +151,8 @@ export function MovePageCombobox({
pages,
currentPageId,
suggestedPageIds,
disabled = false,
labels,
}: {
value: number | null;
onChange: (id: number | null) => void;
@@ -158,6 +160,14 @@ export function MovePageCombobox({
currentPageId: number;
/** Page IDs to show first as suggestions (from move-suggestions.ts) */
suggestedPageIds?: number[];
disabled?: boolean;
labels?: {
placeholder: string;
search: string;
empty: string;
suggested: string;
all: string;
};
}) {
const [open, setOpen] = useState(false);
@@ -194,6 +204,7 @@ export function MovePageCombobox({
<Button
variant="outline"
role="combobox"
disabled={disabled}
aria-expanded={open}
className="w-full justify-between font-normal"
>
@@ -205,7 +216,9 @@ export function MovePageCombobox({
{selectedPage.caption}
</span>
) : (
<span className="text-muted-foreground">Select target page...</span>
<span className="text-muted-foreground">
{labels?.placeholder ?? "Select target page..."}
</span>
)}
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
</Button>
@@ -215,13 +228,13 @@ export function MovePageCombobox({
align="start"
>
<Command>
<CommandInput placeholder="Search pages..." />
<CommandInput placeholder={labels?.search ?? "Search pages..."} />
<CommandList>
<CommandEmpty>No page found.</CommandEmpty>
<CommandEmpty>{labels?.empty ?? "No page found."}</CommandEmpty>
{/* Suggested pages */}
{suggested.length > 0 && (
<CommandGroup heading="Suggested">
<CommandGroup heading={labels?.suggested ?? "Suggested"}>
{suggested.map((page) => (
<CommandItem
key={`s-${page.id}`}
@@ -249,7 +262,9 @@ export function MovePageCombobox({
{/* All pages */}
<CommandGroup
heading={suggested.length > 0 ? "All pages" : undefined}
heading={
suggested.length > 0 ? (labels?.all ?? "All pages") : undefined
}
>
{remaining.map((page) => (
<CommandItem
+2 -2
View File
@@ -263,7 +263,7 @@ export default async function CatalogPagesPage({
<AdminPageShell icon={Store} title={t("title")} subtitle={t("subtitle")}>
<div className="mb-4 space-y-3">
<CatalogSyncStatus canEdit={canEdit} />
<CatalogSearch catalogType={catalogType} />
<CatalogSearch catalogType={catalogType} canEdit={canEdit} />
</div>
<div className="space-y-6">
<div className="flex items-center justify-between">
@@ -496,7 +496,7 @@ export default async function CatalogPagesPage({
<AdminPageShell icon={Store} title={t("title")} subtitle={t("subtitle")}>
<div className="mb-4 space-y-3">
<CatalogSyncStatus canEdit={canEdit} />
<CatalogSearch catalogType={catalogType} />
<CatalogSearch catalogType={catalogType} canEdit={canEdit} />
</div>
<div className="space-y-6">
<div className="flex items-center justify-between">