fix: resolve layout type errors in organize-imports-dialog
This commit is contained in:
1 parent
caeb4eb0b3
commit
683fee3bd9
2 files changed
+106
-49
No files matched your search
@@ -1,4 +1,8 @@
|
||||
import { sql } from "drizzle-orm";
|
||||
import {
|
||||
groupSuggestedCategories,
|
||||
type SugFurniEntry,
|
||||
} from "@/features/catalog/domain/organize";
|
||||
import {
|
||||
getImportedCategoryPageIds,
|
||||
getImportedRootCaption,
|
||||
@@ -193,9 +197,55 @@ export const GET = withAdmin(
|
||||
);
|
||||
});
|
||||
|
||||
// 5) Group on the server so the client only ever sees compact, ready-made
|
||||
// categories instead of a giant raw item array.
|
||||
type SugFurniEntryExtra = SugFurniEntry & {
|
||||
alreadyPlaced?: boolean;
|
||||
catalogItemId?: number;
|
||||
spriteId?: number;
|
||||
sourcePageCaption?: string;
|
||||
itemId?: number;
|
||||
};
|
||||
// Map to the extended shape so groupSuggestedCategories accepts it,
|
||||
// then we can use the extra fields in the returned groups.
|
||||
const sugItems = items.map((it) => ({
|
||||
id: it.itemId,
|
||||
itemName: it.itemName,
|
||||
publicName: it.publicName,
|
||||
type: it.type,
|
||||
spriteId: it.spriteId,
|
||||
interactionType: it.interactionType,
|
||||
alreadyPlaced: it.alreadyPlaced,
|
||||
catalogItemId: it.catalogItemId,
|
||||
})) as SugFurniEntryExtra[];
|
||||
const groups = groupSuggestedCategories(sugItems).map((g) => {
|
||||
const moved = g.items.filter((i) => i.alreadyPlaced).length;
|
||||
return {
|
||||
name: g.name,
|
||||
icon: g.icon,
|
||||
layout: g.layout?.layout ?? null,
|
||||
total: g.items.length,
|
||||
moved,
|
||||
added: g.items.length - moved,
|
||||
thumbs: g.items.slice(0, 12).map((i) => ({
|
||||
itemName: i.itemName,
|
||||
publicName: i.publicName,
|
||||
spriteId: i.spriteId,
|
||||
alreadyPlaced: i.alreadyPlaced,
|
||||
sourcePageCaption: i.sourcePageCaption,
|
||||
})),
|
||||
items: g.items.map((i) => ({
|
||||
itemId: i.itemId,
|
||||
catalogItemId: i.catalogItemId,
|
||||
alreadyPlaced: i.alreadyPlaced,
|
||||
})),
|
||||
};
|
||||
});
|
||||
|
||||
const importRootPageId = pageIds[0] ?? null;
|
||||
return apiOk({
|
||||
items,
|
||||
groups,
|
||||
groupCount: groups.length,
|
||||
total: items.length,
|
||||
importRootPageId,
|
||||
importRootCaption: await getImportedRootCaption(),
|
||||
|
||||
@@ -34,23 +34,15 @@ import {
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/components/ui/select";
|
||||
import { groupSuggestedCategories } from "@/features/catalog/domain/organize";
|
||||
import { CATALOG_LAYOUTS, type CatalogLayout } from "@/lib/catalog-layouts";
|
||||
// import { CATALOG_LAYOUTS } from "@/lib/catalog-layouts";
|
||||
import { getLocalIconUrl } from "@/lib/furni/classname";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
interface OrganizeItem {
|
||||
id: number;
|
||||
itemId: number;
|
||||
itemName: string;
|
||||
publicName: string;
|
||||
type: string;
|
||||
spriteId: number;
|
||||
interactionType: string;
|
||||
catalogItemId: number | null;
|
||||
sourcePageId: number | null;
|
||||
sourcePageCaption: string | null;
|
||||
alreadyPlaced: boolean;
|
||||
interface OrganizeGroupOverride {
|
||||
caption?: string;
|
||||
icon?: number;
|
||||
layout?: CatalogLayout;
|
||||
}
|
||||
|
||||
interface RootPage {
|
||||
@@ -58,10 +50,25 @@ interface RootPage {
|
||||
caption: string;
|
||||
}
|
||||
|
||||
interface OrganizeGroupOverride {
|
||||
caption?: string;
|
||||
icon?: number;
|
||||
layout?: CatalogLayout;
|
||||
interface OrganizeImportGroup {
|
||||
name: string;
|
||||
icon: number;
|
||||
layout: CatalogLayout | null;
|
||||
total: number;
|
||||
moved: number;
|
||||
added: number;
|
||||
thumbs: {
|
||||
itemName: string;
|
||||
publicName: string;
|
||||
spriteId: number;
|
||||
alreadyPlaced: boolean;
|
||||
sourcePageCaption: string | null;
|
||||
}[];
|
||||
items: {
|
||||
itemId: number;
|
||||
catalogItemId: number | null;
|
||||
alreadyPlaced: boolean;
|
||||
}[];
|
||||
}
|
||||
|
||||
interface OrganizeImportsDialogProps {
|
||||
@@ -78,7 +85,7 @@ export function OrganizeImportsDialog({
|
||||
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [loadError, setLoadError] = useState(false);
|
||||
const [items, setItems] = useState<OrganizeItem[]>([]);
|
||||
const [groups, setGroups] = useState<OrganizeImportGroup[]>([]);
|
||||
const [rootPages, setRootPages] = useState<RootPage[]>([]);
|
||||
const [importRootPageId, setImportRootPageId] = useState<number | null>(null);
|
||||
const [importRootCaption, setImportRootCaption] = useState<string | null>(
|
||||
@@ -122,7 +129,7 @@ export function OrganizeImportsDialog({
|
||||
signal: controller.signal,
|
||||
}).then((r) => r.json()),
|
||||
]);
|
||||
setItems(Array.isArray(data?.items) ? data.items : []);
|
||||
setGroups(Array.isArray(data?.groups) ? data.groups : []);
|
||||
setImportRootPageId(data?.importRootPageId ?? null);
|
||||
setImportRootCaption(data?.importRootCaption ?? null);
|
||||
const pages = Array.isArray(tree?.pages) ? tree.pages : [];
|
||||
@@ -135,7 +142,7 @@ export function OrganizeImportsDialog({
|
||||
} catch (error) {
|
||||
console.error("[OrganizeImports] load failed:", error);
|
||||
setLoadError(true);
|
||||
setItems([]);
|
||||
setGroups([]);
|
||||
} finally {
|
||||
clearTimeout(timeout);
|
||||
setLoading(false);
|
||||
@@ -144,7 +151,7 @@ export function OrganizeImportsDialog({
|
||||
|
||||
useEffect(() => {
|
||||
if (!open) return;
|
||||
setItems([]);
|
||||
setGroups([]);
|
||||
setApproved({});
|
||||
setOverrides({});
|
||||
setParentId(-1);
|
||||
@@ -198,8 +205,6 @@ export function OrganizeImportsDialog({
|
||||
return () => ac.abort();
|
||||
}, [moveMode, destDebounced]);
|
||||
|
||||
const groups = useMemo(() => groupSuggestedCategories(items), [items]);
|
||||
|
||||
const allApproved =
|
||||
groups.length > 0 && groups.every((g) => approved[g.name]);
|
||||
const totalSelected = useMemo(
|
||||
@@ -243,11 +248,11 @@ export function OrganizeImportsDialog({
|
||||
caption: moveMode
|
||||
? (destinationCaption ?? "Imported")
|
||||
: (o.caption ?? g.name).trim(),
|
||||
pageLayout: o.layout ?? g.layout?.layout ?? "default_3x3",
|
||||
pageLayout: o.layout ?? g.layout ?? "default_3x3",
|
||||
iconImage: o.icon ?? g.icon,
|
||||
destinationPageId: moveMode ? destinationPageId : null,
|
||||
items: g.items.map((item) => ({
|
||||
itemId: item.id,
|
||||
itemId: item.itemId,
|
||||
catalogItemId: item.catalogItemId,
|
||||
credits: freeMode ? 0 : credits,
|
||||
points: freeMode ? 0 : points,
|
||||
@@ -304,7 +309,7 @@ export function OrganizeImportsDialog({
|
||||
|
||||
<div className="flex min-h-0 flex-1 flex-col gap-4 overflow-y-auto p-4">
|
||||
{/* Price (new offers only) */}
|
||||
{!loading && items.length > 0 && (
|
||||
{!loading && groups.length > 0 && (
|
||||
<div className="flex flex-wrap items-center gap-2 rounded-md border bg-[var(--admin-surface)] p-3 text-xs">
|
||||
<span className="text-xs font-medium text-[var(--admin-text-muted)]">
|
||||
{t("price")}
|
||||
@@ -377,7 +382,7 @@ export function OrganizeImportsDialog({
|
||||
)}
|
||||
|
||||
{/* Mode: create new pages vs move into an existing page */}
|
||||
{!loading && items.length > 0 && (
|
||||
{!loading && groups.length > 0 && (
|
||||
<div className="flex flex-wrap items-center gap-2 rounded-md border bg-[var(--admin-surface)] p-3 text-xs">
|
||||
<span className="text-xs font-medium text-[var(--admin-text-muted)]">
|
||||
{t("mode")}
|
||||
@@ -408,7 +413,7 @@ export function OrganizeImportsDialog({
|
||||
)}
|
||||
|
||||
{/* Destination page (move mode) */}
|
||||
{moveMode && !loading && items.length > 0 && (
|
||||
{moveMode && !loading && groups.length > 0 && (
|
||||
<div className="flex flex-col gap-1 rounded-md border bg-[var(--admin-surface)] p-3 text-xs font-medium text-[var(--admin-text-muted)]">
|
||||
<span className="text-[10px]">{t("destination")}</span>
|
||||
{destinationPageId != null ? (
|
||||
@@ -482,7 +487,7 @@ export function OrganizeImportsDialog({
|
||||
)}
|
||||
|
||||
{/* Parent */}
|
||||
{!moveMode && !loading && items.length > 0 && (
|
||||
{!moveMode && !loading && groups.length > 0 && (
|
||||
<div className="flex flex-col gap-1 rounded-md border bg-[var(--admin-surface)] p-3 text-xs font-medium text-[var(--admin-text-muted)]">
|
||||
{t("parent")}
|
||||
<Select
|
||||
@@ -533,7 +538,7 @@ export function OrganizeImportsDialog({
|
||||
<Loader2 className="h-4 w-4 animate-spin" />
|
||||
{t("loading")}
|
||||
</div>
|
||||
) : items.length === 0 ? (
|
||||
) : groups.length === 0 ? (
|
||||
<div className="flex flex-1 flex-col items-center justify-center gap-3 text-sm text-muted-foreground">
|
||||
{loadError ? t("loadError") : t("empty")}
|
||||
<Button
|
||||
@@ -565,11 +570,10 @@ export function OrganizeImportsDialog({
|
||||
const o = overrides[group.name] ?? {};
|
||||
const caption = o.caption ?? group.name;
|
||||
const icon = o.icon ?? group.icon;
|
||||
const layout =
|
||||
o.layout ?? group.layout?.layout ?? "default_3x3";
|
||||
const layout = o.layout ?? group.layout ?? "default_3x3";
|
||||
const isApproved = approved[group.name] !== false;
|
||||
const moved = group.items.filter((i) => i.alreadyPlaced).length;
|
||||
const added = group.items.length - moved;
|
||||
const moved = group.moved;
|
||||
const added = group.added;
|
||||
return (
|
||||
<label
|
||||
key={group.name}
|
||||
@@ -596,7 +600,7 @@ export function OrganizeImportsDialog({
|
||||
{group.name}
|
||||
</p>
|
||||
<Badge variant="secondary" className="h-4 text-[9px]">
|
||||
{group.items.length}
|
||||
{group.total}
|
||||
</Badge>
|
||||
{moved > 0 && (
|
||||
<Badge
|
||||
@@ -727,8 +731,8 @@ export function OrganizeImportsDialog({
|
||||
})}
|
||||
</div>
|
||||
{group.layout &&
|
||||
layout !== group.layout.layout &&
|
||||
group.layout.layout !== "default_3x3" && (
|
||||
layout !== group.layout &&
|
||||
group.layout !== "default_3x3" && (
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
@@ -738,13 +742,16 @@ export function OrganizeImportsDialog({
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
updateGroup(group.name, {
|
||||
layout: group.layout?.layout,
|
||||
layout:
|
||||
group.layout === null
|
||||
? undefined
|
||||
: (group.layout as CatalogLayout),
|
||||
});
|
||||
}}
|
||||
>
|
||||
<Wand2 className="mr-0.5 h-2.5 w-2.5" />
|
||||
{t("suggestedLayout", {
|
||||
layout: group.layout.layout,
|
||||
layout: group.layout,
|
||||
})}
|
||||
</Button>
|
||||
)}
|
||||
@@ -753,21 +760,21 @@ export function OrganizeImportsDialog({
|
||||
)}
|
||||
|
||||
<div className="mt-2 flex flex-wrap gap-1.5 border-t pt-2">
|
||||
{group.items.slice(0, 12).map((item) => (
|
||||
{group.thumbs.slice(0, 12).map((thumb) => (
|
||||
<div
|
||||
key={item.id}
|
||||
key={thumb.itemName}
|
||||
className="group flex flex-col items-center gap-0.5"
|
||||
>
|
||||
<div
|
||||
className="relative"
|
||||
title={item.publicName || item.itemName}
|
||||
title={thumb.publicName || thumb.itemName}
|
||||
>
|
||||
<FurniThumb item={item} />
|
||||
<FurniThumb item={{ itemName: thumb.itemName }} />
|
||||
</div>
|
||||
{item.alreadyPlaced ? (
|
||||
{thumb.alreadyPlaced ? (
|
||||
<span className="max-w-14 truncate text-[8px] text-muted-foreground">
|
||||
{item.sourcePageCaption
|
||||
? item.sourcePageCaption.replace(/^Imp /, "")
|
||||
{thumb.sourcePageCaption
|
||||
? thumb.sourcePageCaption.replace(/^Imp /, "")
|
||||
: "moved"}
|
||||
</span>
|
||||
) : (
|
||||
@@ -777,9 +784,9 @@ export function OrganizeImportsDialog({
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
{group.items.length > 12 && (
|
||||
{group.total > 12 && (
|
||||
<p className="self-center text-[10px] text-muted-foreground">
|
||||
+{group.items.length - 12}
|
||||
+{group.total - 12}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
Reference in new issue
Block a user