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
@@ -400,6 +400,10 @@ function FurniGrid({ source }: FurniGridProps) {
|
||||
const [busyClassname, setBusyClassname] = useState<string | null>(null);
|
||||
const [filter, setFilter] = useState<"all" | "missing" | "present">("all");
|
||||
const batchAbortRef = useRef<AbortController | null>(null);
|
||||
// Classnames the server already has, mirroring `items`. Lets markDone flip a
|
||||
// card and adjust the counters without a refetch, and idempotently — React
|
||||
// may run a state updater twice in dev.
|
||||
const presentRef = useRef<Set<string>>(new Set());
|
||||
|
||||
const fetchItems = useCallback(
|
||||
async (
|
||||
@@ -426,6 +430,11 @@ function FurniGrid({ source }: FurniGridProps) {
|
||||
setItems(data.items || []);
|
||||
setMeta(data.meta || null);
|
||||
setStats(data.stats || null);
|
||||
presentRef.current = new Set(
|
||||
(data.items || [])
|
||||
.filter((it: FurniItem) => it.present)
|
||||
.map((it: FurniItem) => it.classname),
|
||||
);
|
||||
setSelected(new Set());
|
||||
} catch {
|
||||
setError("Network error");
|
||||
@@ -479,6 +488,19 @@ function FurniGrid({ source }: FurniGridProps) {
|
||||
}
|
||||
|
||||
function markDone(classname: string) {
|
||||
// Counters move as each clone lands, so total/present/clonable stay
|
||||
// truthful for the whole run instead of jumping at the end.
|
||||
if (presentRef.current.has(classname)) return;
|
||||
presentRef.current.add(classname);
|
||||
setStats((prev) =>
|
||||
prev
|
||||
? {
|
||||
...prev,
|
||||
present: prev.present + 1,
|
||||
clonable: Math.max(0, prev.clonable - 1),
|
||||
}
|
||||
: prev,
|
||||
);
|
||||
setItems((prev) =>
|
||||
prev.map((it) =>
|
||||
it.classname === classname ? { ...it, present: true } : it,
|
||||
|
||||
@@ -2,7 +2,6 @@ import { redirect } from "next/navigation";
|
||||
import { StudioClient } from "@/components/admin/studio/studio-client";
|
||||
import { buildFurniImportSource } from "@/lib/habbo-gamedata-hotel";
|
||||
import { canAccess, getAdminContext, PERMS } from "@/lib/permissions";
|
||||
import { getTreeFlat } from "@/lib/services/catalog-tree";
|
||||
import { getHabboGamedataHotel } from "@/lib/services/habbo-gamedata-hotel";
|
||||
import { siteSettings } from "@/lib/services/site-settings";
|
||||
|
||||
@@ -14,9 +13,11 @@ export default async function StudioFurniPage(_props: {
|
||||
redirect("/admin");
|
||||
}
|
||||
|
||||
const [hotel, catalogTree, translateEnabled] = await Promise.all([
|
||||
// The catalog tree is not read here: StudioClient pulls it from the live
|
||||
// catalog store, which an import updates in place instead of forcing a
|
||||
// route re-render.
|
||||
const [hotel, translateEnabled] = await Promise.all([
|
||||
getHabboGamedataHotel(),
|
||||
getTreeFlat(),
|
||||
siteSettings.getBool("furnidata_translate_enabled", true),
|
||||
]);
|
||||
|
||||
@@ -24,7 +25,6 @@ export default async function StudioFurniPage(_props: {
|
||||
<StudioClient
|
||||
actorId={Number(session.user.id)}
|
||||
source={buildFurniImportSource(hotel)}
|
||||
initialTree={catalogTree}
|
||||
defaultTranslate={translateEnabled}
|
||||
/>
|
||||
);
|
||||
|
||||
Reference in new issue
Block a user