feat(studio): add organize-imports tool with suggestion review and approval
Adds a Studio 'Organize imports' dialog that groups recently imported furniture and furniture already sitting in the auto-created import pages into suggested catalog categories. Each group is presented with its suggested name, icon, and layout which can be overridden before approval; approved groups are turned into real catalog pages in a single atomic export run. Offers already inside the import subtree are moved to the new pages; brand-new furniture gets a fresh offer. - groupSuggestedCategories: generic pure helper reusing the same per-item label heuristic that drives suggestCategoryName; items with no label land in a 'Other Furni' remainder bucket - GET /api/admin/import/organize: returns items from the imported furniture tree (catalog_items JOIN items_base via page id set from the imported-furniture root) union audit-logged but not-yet-placed recent imports; marks alreadyPlaced vs new - organizeImportFurni server action: validates import-page membership before moving any offer, creates pages + inserts/moves items in one withCatalogExport snapshot, logs activity - OrganizeImportsDialog: full-featured Studio dialog with price panel (applies to new offers only), parent select, per-group approval, editable name/icon/layout with suggestion reset chips, source tags - import-pages.ts server helper: locates the imported-furniture tree - Studio nav: 'Organize imports' button with FolderTree icon, gated on CATALOG_EDIT, wired next to the catalog manager - en + nl translations for organizeImports.* keys with ICU plurals - groupSuggestedCategories unit tests (deterministic grouping, remainder handling, size+alpha ordering, label consistency)
This commit is contained in:
@@ -2,8 +2,10 @@ import { describe, expect, it } from "vitest";
|
||||
import type { CatalogItemData } from "@/app/admin/catalog/[id]/catalog-items-table/types";
|
||||
import {
|
||||
buildSortOrders,
|
||||
groupSuggestedCategories,
|
||||
type SortStrategy,
|
||||
type SugFurni,
|
||||
type SugFurniEntry,
|
||||
sortCatalogItems,
|
||||
suggestCatalogIcon,
|
||||
suggestCatalogLayout,
|
||||
@@ -227,3 +229,64 @@ describe("suggestCatalogIcon", () => {
|
||||
expect(a).toBe(b);
|
||||
});
|
||||
});
|
||||
|
||||
describe("groupSuggestedCategories", () => {
|
||||
function entry(
|
||||
id: number,
|
||||
itemName?: string,
|
||||
interactionType?: string,
|
||||
): SugFurniEntry {
|
||||
return { id, itemName, interactionType };
|
||||
}
|
||||
|
||||
it("returns an empty list for no input", () => {
|
||||
expect(groupSuggestedCategories([])).toEqual([]);
|
||||
});
|
||||
|
||||
it("groups furniture by shared label, largest group first", () => {
|
||||
const groups = groupSuggestedCategories([
|
||||
entry(1, "chair_wooden"),
|
||||
entry(2, "chair_oak"),
|
||||
entry(3, "sofa_polyfon"),
|
||||
entry(4, "chair_polyfon"),
|
||||
]);
|
||||
expect(groups.map((g) => g.name)).toEqual(["Chairs", "Sofas"]);
|
||||
expect(groups[0]?.items.map((i) => i.id)).toEqual([1, 2, 4]);
|
||||
expect(groups[1]?.items.map((i) => i.id)).toEqual([3]);
|
||||
});
|
||||
|
||||
it("sorts by size then alphabetically on ties", () => {
|
||||
const groups = groupSuggestedCategories([
|
||||
entry(1, "sofa_polyfon"),
|
||||
entry(2, "chair_oak"),
|
||||
]);
|
||||
expect(groups.map((g) => g.name)).toEqual(["Chairs", "Sofas"]);
|
||||
});
|
||||
|
||||
it("falls back deterministically and drops nothing", () => {
|
||||
const groups = groupSuggestedCategories([
|
||||
entry(1, "sofa_polyfon"),
|
||||
entry(2, ""),
|
||||
entry(3, ""),
|
||||
]);
|
||||
expect(groups.map((g) => g.name)).toEqual(["Other Furni", "Sofas"]);
|
||||
const leftover = groups.find((g) => g.name === "Other Furni");
|
||||
expect(leftover?.items.map((i) => i.id)).toEqual([2, 3]);
|
||||
expect(leftover).not.toBeNull();
|
||||
});
|
||||
|
||||
it("suggests a plausible icon and layout per group", () => {
|
||||
const groups = groupSuggestedCategories([entry(1, "chair_oak")]);
|
||||
expect(groups[0]).toBeDefined();
|
||||
expect(groups[0]?.icon).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it("uses the same label as the batch name suggestion", () => {
|
||||
const group = groupSuggestedCategories([
|
||||
entry(1, "trophy_cup", "trophy"),
|
||||
entry(2, "trophy_crown", "trophy"),
|
||||
entry(3, "chair_oak"),
|
||||
])[0];
|
||||
expect(group?.name).toBe("Trophies");
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user