feat(catalog): update the catalog live after an import, no page refresh
Organising imports, the Studio furni batch, the catalog totals and the "import from a source" stats all used to need a full page reload, or at best a router.refresh() that re-rendered the whole admin route, before anything on screen reflected what the import had just written. - live-catalog-merge.ts (new): pure tree and total arithmetic. Applies a delta of created pages, added offers and moved offers, recomputes depth for the touched subtree, bumps parent child counts and the item totals. Returns the input untouched when a delta is empty, so subscribers can bail out instead of re-rendering. Depth resolution tolerates a parent cycle in a dirty DB and still terminates, matching getTreeFlat. - use-live-catalog.ts (new): one module-level store exposed through useSyncExternalStore, so every consumer shares a single instance without threading a provider through the admin layout. Deltas only apply to the "normal" catalog, so public and public_handlers trees stay separate. seedCatalogTotals() takes the first server value per mode and never overwrites it afterwards, so a later hard render cannot make the header totals jump backwards. - actions/catalog.ts: organizeImportFurni now reports each group through the new OrganizedPageChange, carrying parentId, pageLayout, the icon, isNew and the per-source movedFrom counts, so the client can fold the result into the tree without reading the page back. - organize-imports-dialog.tsx: drops useRouter and router.refresh(); the response is applied as a delta the moment the run finishes. - studio-client.tsx: reads the tree from the store instead of freezing it with useState(initialTree), loads it on mount when empty, and refreshes it once a batch import settles. The batch is server-side and derives its import pages from furnidata, so that one path re-reads the tree via GET /api/admin/catalog/tree?mode=full rather than trusting the delta. - studio/furni/page.tsx: stops calling getTreeFlat() and no longer passes initialTree; the store is the single source of truth for the rail. - import-clone-client.tsx: tracks which items are already present, so present and clonable update per cloned row instead of only at the end. - catalog-manager-dialog.tsx: seeds the totals once and renders the live values, so the header reflects an import that just ran. - e2e/ui/fixtures/entry.tsx: drops the removed initialTree prop.
This commit is contained in:
1 parent
7507c3b55c
commit
9cc57cddfc
10 files changed
+716
-32
No files matched your search
+61
-16
@@ -351,6 +351,30 @@ export async function createAutoCategory(input: {
|
||||
const MAX_ORGANIZE_GROUPS = 50;
|
||||
const MAX_ORGANIZE_ITEMS = 500;
|
||||
|
||||
/**
|
||||
* One page the run wrote to, described well enough for the client to fold the
|
||||
* result into its live catalog tree. `isNew` is false when the group reused an
|
||||
* existing destination, in which case the page already exists and every field
|
||||
* that describes a *new* page (`parentId`, `pageLayout`, the icon) is only the
|
||||
* requested value, not a verified read-back of the stored row.
|
||||
*/
|
||||
export interface OrganizedPageChange {
|
||||
pageId: number;
|
||||
caption: string;
|
||||
parentId: number;
|
||||
pageLayout: string;
|
||||
iconImage: number;
|
||||
iconColor: number;
|
||||
orderNum: number;
|
||||
visible: string;
|
||||
enabled: string;
|
||||
isNew: boolean;
|
||||
moved: number;
|
||||
added: number;
|
||||
/** Import pages the offers were taken from, so counts can be rebalanced. */
|
||||
movedFrom: Array<{ fromPageId: number; count: number }>;
|
||||
}
|
||||
|
||||
export interface OrganizeImportGroup {
|
||||
caption: string;
|
||||
pageLayout: string;
|
||||
@@ -378,12 +402,7 @@ export async function organizeImportFurni(input: {
|
||||
groups: OrganizeImportGroup[];
|
||||
}): Promise<
|
||||
ActionResult<{
|
||||
created: Array<{
|
||||
pageId: number;
|
||||
caption: string;
|
||||
moved: number;
|
||||
added: number;
|
||||
}>;
|
||||
created: OrganizedPageChange[];
|
||||
}>
|
||||
> {
|
||||
const staff = await requirePermission(PERMS.CATALOG_EDIT);
|
||||
@@ -412,12 +431,7 @@ export async function organizeImportFurni(input: {
|
||||
);
|
||||
const importPageIds = new Set(await getImportedCategoryPageIds());
|
||||
|
||||
const created: Array<{
|
||||
pageId: number;
|
||||
caption: string;
|
||||
moved: number;
|
||||
added: number;
|
||||
}> = [];
|
||||
const created: OrganizedPageChange[] = [];
|
||||
let totalMoved = 0;
|
||||
let totalAdded = 0;
|
||||
|
||||
@@ -432,15 +446,18 @@ export async function organizeImportFurni(input: {
|
||||
error: "Invalid destination page id",
|
||||
};
|
||||
}
|
||||
const isNew = destinationPageId == null;
|
||||
const pageLayout = group.pageLayout || "default_3x3";
|
||||
const iconImage = group.iconImage ?? 0;
|
||||
const pageId =
|
||||
destinationPageId ??
|
||||
Number(
|
||||
await createPageCommand("normal", {
|
||||
caption: group.caption,
|
||||
parentId: input.parentId,
|
||||
pageLayout: group.pageLayout || "default_3x3",
|
||||
pageLayout,
|
||||
captionSave: group.caption.slice(0, 25),
|
||||
iconImage: group.iconImage ?? 0,
|
||||
iconImage,
|
||||
iconColor: 0,
|
||||
minRank: 1,
|
||||
orderNum: 0,
|
||||
@@ -492,6 +509,7 @@ export async function organizeImportFurni(input: {
|
||||
),
|
||||
];
|
||||
const validMoveIds = new Set<number>();
|
||||
const hostPageByItemId = new Map<number, number>();
|
||||
if (moverIds.length > 0) {
|
||||
const hostRows = await queryRows<{ id: number; page_id: number }>(sql`
|
||||
SELECT id, page_id FROM catalog_items WHERE id IN (${sql.join(
|
||||
@@ -500,14 +518,17 @@ export async function organizeImportFurni(input: {
|
||||
)})
|
||||
`);
|
||||
for (const row of hostRows) {
|
||||
if (importPageIds.has(Number(row.page_id))) {
|
||||
const pageId = Number(row.page_id);
|
||||
if (importPageIds.has(pageId)) {
|
||||
validMoveIds.add(Number(row.id));
|
||||
hostPageByItemId.set(Number(row.id), pageId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let moved = 0;
|
||||
let added = 0;
|
||||
const movedFromCounts = new Map<number, number>();
|
||||
|
||||
const moveCaseOrder: string[] = [];
|
||||
const moveCaseName: string[] = [];
|
||||
@@ -534,6 +555,13 @@ export async function organizeImportFurni(input: {
|
||||
)}'`,
|
||||
);
|
||||
moveIds.push(Number(row.catalogItemId));
|
||||
const hostPageId = hostPageByItemId.get(Number(row.catalogItemId));
|
||||
if (hostPageId != null) {
|
||||
movedFromCounts.set(
|
||||
hostPageId,
|
||||
(movedFromCounts.get(hostPageId) ?? 0) + 1,
|
||||
);
|
||||
}
|
||||
moved++;
|
||||
} else {
|
||||
await insertCatalogItemRow({
|
||||
@@ -586,7 +614,24 @@ export async function organizeImportFurni(input: {
|
||||
targetType: "catalog_page",
|
||||
targetId: pageId,
|
||||
});
|
||||
created.push({ pageId, caption, moved, added });
|
||||
created.push({
|
||||
pageId,
|
||||
caption,
|
||||
parentId: input.parentId,
|
||||
pageLayout,
|
||||
iconImage,
|
||||
iconColor: 0,
|
||||
orderNum: 0,
|
||||
visible: "1",
|
||||
enabled: "1",
|
||||
isNew,
|
||||
moved,
|
||||
added,
|
||||
movedFrom: [...movedFromCounts].map(([fromPageId, count]) => ({
|
||||
fromPageId,
|
||||
count,
|
||||
})),
|
||||
});
|
||||
totalMoved += moved;
|
||||
totalAdded += added;
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user