Fix N+1 query and UI flashing

This commit is contained in:
gpt-engineer-app[bot]
2025-10-06 19:19:02 +00:00
parent cc3ec7a6e4
commit 71f497a001
2 changed files with 144 additions and 55 deletions

View File

@@ -79,6 +79,7 @@ export const ModerationQueue = forwardRef<ModerationQueueRef>((props, ref) => {
const [selectedItemForAction, setSelectedItemForAction] = useState<ModerationItem | null>(null);
const [interactingWith, setInteractingWith] = useState<Set<string>>(new Set());
const [newItemsCount, setNewItemsCount] = useState(0);
const [isRefreshing, setIsRefreshing] = useState(false);
const { toast } = useToast();
const { isAdmin, isSuperuser } = useUserRole();
const { user } = useAuth();
@@ -108,12 +109,21 @@ export const ModerationQueue = forwardRef<ModerationQueueRef>((props, ref) => {
return;
}
// Prevent duplicate requests
if (isRefreshing && silent) {
console.log('⏭️ Skipping refresh - already in progress');
return;
}
try {
// Only show loading on initial load or filter change
if (!silent) {
setLoading(true);
setNewItemsCount(0);
}
setIsRefreshing(true);
let reviewStatuses: string[] = [];
let submissionStatuses: string[] = [];
@@ -183,7 +193,7 @@ export const ModerationQueue = forwardRef<ModerationQueueRef>((props, ref) => {
reviews = reviewsData || [];
}
// Fetch content submissions with entity data
// Fetch content submissions with entity data (OPTIMIZED: Single query with all entity data)
let submissions = [];
if ((entityFilter === 'all' || entityFilter === 'submissions' || entityFilter === 'photos') && submissionStatuses.length > 0) {
let query = supabase
@@ -213,13 +223,16 @@ export const ModerationQueue = forwardRef<ModerationQueueRef>((props, ref) => {
if (submissionsError) throw submissionsError;
// Get entity data for photo submissions
let submissionsWithEntities = submissionsData || [];
for (const submission of submissionsWithEntities) {
// Collect all entity IDs by type for batch fetching
const rideIds = new Set<string>();
const parkIds = new Set<string>();
const companyIds = new Set<string>();
// First pass: collect all entity IDs
for (const submission of submissionsData || []) {
if (submission.submission_type === 'photo' && submission.content && typeof submission.content === 'object') {
const contentObj = submission.content as any;
// Handle both old format (context as object) and new format (context as string)
let contextType = null;
let entityId = null;
let rideId = null;
@@ -227,12 +240,10 @@ export const ModerationQueue = forwardRef<ModerationQueueRef>((props, ref) => {
let companyId = null;
if (typeof contentObj.context === 'object' && contentObj.context !== null) {
// OLD FORMAT: context is an object like {ride_id: "...", park_id: "..."}
rideId = contentObj.context.ride_id;
parkId = contentObj.context.park_id;
contextType = rideId ? 'ride' : parkId ? 'park' : null;
} else if (typeof contentObj.context === 'string') {
// NEW FORMAT: context is a string, IDs are at top level
contextType = contentObj.context;
entityId = contentObj.entity_id;
rideId = contentObj.ride_id;
@@ -240,54 +251,115 @@ export const ModerationQueue = forwardRef<ModerationQueueRef>((props, ref) => {
companyId = contentObj.company_id;
}
// Determine entity ID based on context type
if (!entityId) {
if (contextType === 'ride') entityId = rideId;
else if (contextType === 'park') entityId = parkId;
else if (['manufacturer', 'operator', 'designer', 'property_owner'].includes(contextType)) entityId = companyId;
}
if (contextType === 'ride' && entityId) {
const { data: rideData } = await supabase
.from('rides')
.select(`
name,
parks:park_id (
name
)
`)
.eq('id', entityId)
.single();
if (rideData) {
(submission as any).entity_name = rideData.name;
(submission as any).park_name = rideData.parks?.name;
}
} else if (contextType === 'park' && entityId) {
const { data: parkData } = await supabase
.from('parks')
.select('name')
.eq('id', entityId)
.single();
if (parkData) {
(submission as any).entity_name = parkData.name;
}
} else if (['manufacturer', 'operator', 'designer', 'property_owner'].includes(contextType) && entityId) {
const { data: companyData } = await supabase
.from('companies')
.select('name, company_type')
.eq('id', entityId)
.single();
if (companyData) {
(submission as any).entity_name = companyData.name;
(submission as any).company_type = companyData.company_type;
}
// Collect IDs by type
if (entityId) {
if (contextType === 'ride') rideIds.add(entityId);
else if (contextType === 'park') parkIds.add(entityId);
else if (['manufacturer', 'operator', 'designer', 'property_owner'].includes(contextType)) companyIds.add(entityId);
}
}
}
// Batch fetch all entity data (3 queries instead of N queries)
const [ridesData, parksData, companiesData] = await Promise.all([
rideIds.size > 0 ? supabase
.from('rides')
.select(`
id,
name,
parks:park_id (
name
)
`)
.in('id', Array.from(rideIds))
.then(res => res.data || []) : Promise.resolve([]),
parkIds.size > 0 ? supabase
.from('parks')
.select('id, name')
.in('id', Array.from(parkIds))
.then(res => res.data || []) : Promise.resolve([]),
companyIds.size > 0 ? supabase
.from('companies')
.select('id, name, company_type')
.in('id', Array.from(companyIds))
.then(res => res.data || []) : Promise.resolve([])
]);
// Create lookup maps for O(1) access
const ridesMap = new Map(ridesData.map((r: any) => [r.id, r] as [string, any]));
const parksMap = new Map(parksData.map((p: any) => [p.id, p] as [string, any]));
const companiesMap = new Map(companiesData.map((c: any) => [c.id, c] as [string, any]));
// Second pass: attach entity data using maps
let submissionsWithEntities = (submissionsData || []).map(submission => {
if (submission.submission_type === 'photo' && submission.content && typeof submission.content === 'object') {
const contentObj = submission.content as any;
let contextType = null;
let entityId = null;
let rideId = null;
let parkId = null;
let companyId = null;
if (typeof contentObj.context === 'object' && contentObj.context !== null) {
rideId = contentObj.context.ride_id;
parkId = contentObj.context.park_id;
contextType = rideId ? 'ride' : parkId ? 'park' : null;
} else if (typeof contentObj.context === 'string') {
contextType = contentObj.context;
entityId = contentObj.entity_id;
rideId = contentObj.ride_id;
parkId = contentObj.park_id;
companyId = contentObj.company_id;
}
if (!entityId) {
if (contextType === 'ride') entityId = rideId;
else if (contextType === 'park') entityId = parkId;
else if (['manufacturer', 'operator', 'designer', 'property_owner'].includes(contextType)) entityId = companyId;
}
// Attach entity data from maps
if (contextType === 'ride' && entityId) {
const rideData = ridesMap.get(entityId) as any;
if (rideData) {
return {
...submission,
entity_name: rideData.name,
park_name: rideData.parks?.name
};
}
} else if (contextType === 'park' && entityId) {
const parkData = parksMap.get(entityId) as any;
if (parkData) {
return {
...submission,
entity_name: parkData.name
};
}
} else if (['manufacturer', 'operator', 'designer', 'property_owner'].includes(contextType) && entityId) {
const companyData = companiesMap.get(entityId) as any;
if (companyData) {
return {
...submission,
entity_name: companyData.name,
company_type: companyData.company_type
};
}
}
}
return submission;
});
submissions = submissionsWithEntities;
}
@@ -371,7 +443,7 @@ export const ModerationQueue = forwardRef<ModerationQueueRef>((props, ref) => {
// Use smart merging for silent refreshes if strategy is 'merge'
if (silent && refreshStrategy === 'merge') {
const mergeResult = smartMergeArray(items, formattedItems, {
compareFields: ['status', 'reviewed_at', 'reviewed_by', 'reviewer_notes', 'content', 'submission_type'],
compareFields: ['status', 'reviewed_at', 'reviewed_by', 'reviewer_notes', 'submission_type', 'entity_name', 'park_name'],
preserveOrder: true,
addToTop: true,
});
@@ -437,6 +509,7 @@ export const ModerationQueue = forwardRef<ModerationQueueRef>((props, ref) => {
if (isInitialLoad) {
setIsInitialLoad(false);
}
setIsRefreshing(false);
}
};