From c68b88de86467215218d8b6d58666c594e46b10d Mon Sep 17 00:00:00 2001 From: "gpt-engineer-app[bot]" <159125892+gpt-engineer-app[bot]@users.noreply.github.com> Date: Mon, 3 Nov 2025 16:20:28 +0000 Subject: [PATCH] feat: Implement dynamic join solution --- .../moderation/SubmissionItemsList.tsx | 95 +++++++++++++++---- src/lib/moderation/queries.ts | 38 +------- src/types/moderation.ts | 62 ++++++++++-- src/types/submission-item-data.ts | 27 +++++- 4 files changed, 152 insertions(+), 70 deletions(-) diff --git a/src/components/moderation/SubmissionItemsList.tsx b/src/components/moderation/SubmissionItemsList.tsx index c44aff2f..796d267a 100644 --- a/src/components/moderation/SubmissionItemsList.tsx +++ b/src/components/moderation/SubmissionItemsList.tsx @@ -41,34 +41,87 @@ export const SubmissionItemsList = memo(function SubmissionItemsList({ } setError(null); - // Fetch submission items with relational data + // Fetch submission items with entity data + // Note: entity_data is pre-loaded from the view, but we need to fetch from raw table + // when this component is used standalone const { data: itemsData, error: itemsError } = await supabase .from('submission_items') - .select(` - *, - park_submission:park_submissions!item_data_id(*), - ride_submission:ride_submissions!item_data_id(*) - `) + .select('*') .eq('submission_id', submissionId) .order('order_index'); if (itemsError) throw itemsError; - // Transform to include item_data - const transformedItems = itemsData?.map(item => { - let itemData = {}; - switch (item.item_type) { - case 'park': - itemData = item.park_submission || {}; - break; - case 'ride': - itemData = item.ride_submission || {}; - break; - default: - itemData = {}; - } - return { ...item, item_data: itemData }; - }) || []; + // Fetch entity data for each item based on item_type + const transformedItems = await Promise.all( + (itemsData || []).map(async (item) => { + if (!item.item_data_id) { + return { ...item, item_data: {}, entity_data: null }; + } + + try { + let entityData = null; + + // Fetch from appropriate table based on item_type + switch (item.item_type) { + case 'park': { + const { data } = await supabase + .from('park_submissions') + .select('*') + .eq('id', item.item_data_id) + .maybeSingle(); + entityData = data; + break; + } + case 'ride': { + const { data } = await supabase + .from('ride_submissions') + .select('*') + .eq('id', item.item_data_id) + .maybeSingle(); + entityData = data; + break; + } + case 'manufacturer': + case 'operator': + case 'designer': + case 'property_owner': { + const { data } = await supabase + .from('company_submissions') + .select('*') + .eq('id', item.item_data_id) + .maybeSingle(); + entityData = data; + break; + } + case 'photo': { + const { data } = await supabase + .from('photo_submissions') + .select('*') + .eq('id', item.item_data_id) + .maybeSingle(); + entityData = data; + break; + } + default: + entityData = null; + } + + return { + ...item, + item_data: entityData || {}, + entity_data: entityData + }; + } catch (err) { + logger.warn('Failed to fetch entity data for item', { + itemId: item.id, + itemType: item.item_type, + error: getErrorMessage(err) + }); + return { ...item, item_data: {}, entity_data: null }; + } + }) + ); // Check for photo submissions (using array query to avoid 406) const { data: photoData, error: photoError } = await supabase diff --git a/src/lib/moderation/queries.ts b/src/lib/moderation/queries.ts index ce22d73b..5b47111d 100644 --- a/src/lib/moderation/queries.ts +++ b/src/lib/moderation/queries.ts @@ -55,42 +55,10 @@ export function buildSubmissionQuery( ) { const { entityFilter, statusFilter, tab, userId, isAdmin, isSuperuser } = config; - // Build base query with all needed data + user profiles (eliminate N+1 query) + // Use optimized view with pre-joined profiles and entity data let query = supabase - .from('content_submissions') - .select(` - id, - submission_type, - status, - content, - created_at, - submitted_at, - user_id, - reviewed_at, - reviewer_id, - reviewer_notes, - escalated, - assigned_to, - locked_until, - submitter:profiles!content_submissions_user_id_fkey ( - user_id, - username, - display_name, - avatar_url - ), - reviewer:profiles!content_submissions_reviewer_id_fkey ( - user_id, - username, - display_name, - avatar_url - ), - submission_items ( - id, - item_type, - item_data, - status - ) - `); + .from('moderation_queue_with_entities') + .select('*'); // CRITICAL: Multi-level ordering // Level 1: Always sort by escalated first (descending) - escalated items always appear at top diff --git a/src/types/moderation.ts b/src/types/moderation.ts index eca3b0d9..d27ee20f 100644 --- a/src/types/moderation.ts +++ b/src/types/moderation.ts @@ -118,6 +118,35 @@ export type SubmissionItemData = | RideModelItemData | PhotoItemData; +/** + * Submission item with resolved entity data from database view + */ +export interface SubmissionItem { + id: string; + item_type: string; + item_data_id: string | null; + action_type: 'create' | 'edit' | 'delete'; + status: string; + order_index: number; + depends_on: string | null; + approved_entity_id: string | null; + rejection_reason: string | null; + + // Entity data from dynamic join (pre-loaded from view) + entity_data?: { + id: string; + submission_id: string; + name: string; + slug: string; + // Other fields vary by entity type + [key: string]: any; + } | null; + + // Legacy support for moderation actions + item_data?: SubmissionItemData; + original_data?: SubmissionItemData; +} + /** * Represents a single item in the moderation queue. * Can be either a review or a content submission. @@ -135,6 +164,9 @@ export interface ModerationItem { /** Timestamp when the item was created */ created_at: string; + /** Timestamp when the item was submitted */ + submitted_at?: string; + /** Timestamp when the item was last updated */ updated_at?: string; @@ -147,7 +179,23 @@ export interface ModerationItem { /** Type of submission (e.g., 'park', 'ride', 'review') */ submission_type?: string; - /** Profile information of the submitting user */ + /** Pre-loaded submitter profile from view */ + submitter?: { + user_id: string; + username: string; + display_name?: string; + avatar_url?: string; + }; + + /** Pre-loaded reviewer profile from view */ + reviewer?: { + user_id: string; + username: string; + display_name?: string; + avatar_url?: string; + }; + + /** Legacy: Profile information of the submitting user */ user_profile?: { username: string; display_name?: string; @@ -172,7 +220,7 @@ export interface ModerationItem { /** Notes left by the reviewing moderator */ reviewer_notes?: string; - /** Profile information of the reviewing moderator */ + /** Legacy: Profile information of the reviewing moderator */ reviewer_profile?: { username: string; display_name?: string; @@ -191,14 +239,8 @@ export interface ModerationItem { /** Internal flag indicating item is being removed (optimistic update) */ _removing?: boolean; - /** Sub-items for composite submissions */ - submission_items?: Array<{ - id: string; - item_type: string; - item_data: SubmissionItemData; - original_data?: SubmissionItemData; - status: string; - }>; + /** Pre-loaded submission items with entity data from view */ + submission_items?: SubmissionItem[]; } /** diff --git a/src/types/submission-item-data.ts b/src/types/submission-item-data.ts index cda68d5a..18f9c8dd 100644 --- a/src/types/submission-item-data.ts +++ b/src/types/submission-item-data.ts @@ -70,22 +70,41 @@ export function hasRideModelId(data: Json): data is { ride_model_id: string } & } /** - * Safely get name from item_data + * Safely get name from item_data or entity_data + * Works with both Json (legacy) and pre-loaded entity_data from view */ -export function getItemName(data: Json): string { +export function getItemName(data: Json | Record | null | undefined): string { + if (!data) return 'Unnamed'; + + // Handle entity_data object (from view) + if (typeof data === 'object' && !Array.isArray(data) && 'name' in data) { + return String(data.name); + } + + // Handle Json (legacy) if (hasName(data)) { return data.name; } + return 'Unnamed'; } /** - * Safely get photos from item_data + * Safely get photos from item_data or entity_data */ -export function getItemPhotos(data: Json): Array> { +export function getItemPhotos(data: Json | Record | null | undefined): Array> { + if (!data) return []; + + // Handle entity_data object (from view) + if (typeof data === 'object' && !Array.isArray(data) && 'photos' in data && Array.isArray(data.photos)) { + return data.photos as Array>; + } + + // Handle Json (legacy) if (hasPhotos(data)) { return data.photos; } + return []; }