feat(categories): add schema image thumbnails to grid and tree views
Enrich category children with batch-fetched schema images and leaf detection on the API side. Display thumbnails in grid cards with shimmer loading placeholders and background prefetching. Add TanStack Query caching for category children. Fix insert race condition with onConflictDoNothing. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
import { Inject, Injectable, Logger, NotFoundException } from "@nestjs/common";
|
||||
import { eq } from "drizzle-orm";
|
||||
import { eq, inArray, sql } from "drizzle-orm";
|
||||
import { DATABASE, Database } from "../database/database.provider";
|
||||
import { categories, vehicles, schemaPics, parts } from "../database/schema/core";
|
||||
import { RedisService } from "../redis/redis.service";
|
||||
@@ -194,7 +194,7 @@ export class CategoriesService {
|
||||
.from(categories)
|
||||
.where(eq(categories.parentId, categoryId));
|
||||
|
||||
if (children.length > 0) return children;
|
||||
if (children.length > 0) return this.enrichWithSchemaImages(children);
|
||||
|
||||
// Fetch from PL24
|
||||
const [category] = await this.db
|
||||
@@ -251,13 +251,25 @@ export class CategoriesService {
|
||||
source: "pl24" as const,
|
||||
}));
|
||||
|
||||
children = await this.db.insert(categories).values(insertData).returning();
|
||||
children = await this.db
|
||||
.insert(categories)
|
||||
.values(insertData)
|
||||
.onConflictDoNothing()
|
||||
.returning();
|
||||
|
||||
// If onConflict skipped some, re-fetch from DB
|
||||
if (children.length < insertData.length) {
|
||||
children = await this.db
|
||||
.select()
|
||||
.from(categories)
|
||||
.where(eq(categories.parentId, categoryId));
|
||||
}
|
||||
|
||||
// Invalidate tree cache since new subgroups were added
|
||||
await this.redis.del(`cat:tree:${category.vehicleId}`);
|
||||
}
|
||||
|
||||
return children;
|
||||
return children.length > 0 ? this.enrichWithSchemaImages(children) : children;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -288,7 +300,7 @@ export class CategoriesService {
|
||||
parts: [],
|
||||
schemaPics: [],
|
||||
hotspots: [],
|
||||
children,
|
||||
children: await this.enrichWithSchemaImages(children),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -528,6 +540,42 @@ export class CategoriesService {
|
||||
return { ...category, schemaPics: pics };
|
||||
}
|
||||
|
||||
private async enrichWithSchemaImages(cats: (typeof categories.$inferSelect)[]) {
|
||||
const catIds = cats.map((c) => c.id);
|
||||
if (catIds.length === 0) return cats;
|
||||
|
||||
// Batch fetch schema images
|
||||
const pics = await this.db
|
||||
.select({ categoryId: schemaPics.categoryId, imageUrl: schemaPics.imageUrl })
|
||||
.from(schemaPics)
|
||||
.where(inArray(schemaPics.categoryId, catIds));
|
||||
|
||||
const picMap = new Map(pics.map((p) => [p.categoryId, p.imageUrl]));
|
||||
|
||||
// Batch check which categories have sub-children in DB
|
||||
const childCounts = await this.db
|
||||
.select({
|
||||
parentId: categories.parentId,
|
||||
count: sql<number>`count(*)::int`,
|
||||
})
|
||||
.from(categories)
|
||||
.where(inArray(categories.parentId, catIds))
|
||||
.groupBy(categories.parentId);
|
||||
|
||||
const childCountMap = new Map(childCounts.map((r) => [r.parentId, r.count]));
|
||||
|
||||
return cats.map((c) => {
|
||||
const dbChildCount = childCountMap.get(c.id) || 0;
|
||||
// Leaf if: has BOM linkPath, OR has no linkPath and no DB children
|
||||
const isLeaf = c.linkPath?.includes("/bom/") || (!c.linkPath && dbChildCount === 0);
|
||||
return {
|
||||
...c,
|
||||
schemaImageUrl: picMap.get(c.id) || null,
|
||||
children: isLeaf ? [] : undefined,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
private getImageDimensions(buf: Buffer, ext: string): { width: number; height: number } {
|
||||
try {
|
||||
if (ext === 'gif' && buf.length >= 10) {
|
||||
|
||||
Reference in New Issue
Block a user