fix: resolve layout type errors in organize-imports-dialog
CI / check (push) Successful in 2m29s
CI / deploy (push) Successful in 18s
CI / publish-container (push) Successful in 1m32s

This commit is contained in:
openhands committed 2026-09-11 23:25:53 +02:00
1 parent caeb4eb0b3
commit 683fee3bd9
2 files changed
+106 -49

No files matched your search

+51 -1
View File
@@ -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>