mirror of
https://github.com/pacnpal/thrilltrack-explorer.git
synced 2025-12-20 14:11:13 -05:00
feat: Implement dynamic join solution
This commit is contained in:
@@ -41,34 +41,87 @@ export const SubmissionItemsList = memo(function SubmissionItemsList({
|
|||||||
}
|
}
|
||||||
setError(null);
|
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
|
const { data: itemsData, error: itemsError } = await supabase
|
||||||
.from('submission_items')
|
.from('submission_items')
|
||||||
.select(`
|
.select('*')
|
||||||
*,
|
|
||||||
park_submission:park_submissions!item_data_id(*),
|
|
||||||
ride_submission:ride_submissions!item_data_id(*)
|
|
||||||
`)
|
|
||||||
.eq('submission_id', submissionId)
|
.eq('submission_id', submissionId)
|
||||||
.order('order_index');
|
.order('order_index');
|
||||||
|
|
||||||
if (itemsError) throw itemsError;
|
if (itemsError) throw itemsError;
|
||||||
|
|
||||||
// Transform to include item_data
|
// Fetch entity data for each item based on item_type
|
||||||
const transformedItems = itemsData?.map(item => {
|
const transformedItems = await Promise.all(
|
||||||
let itemData = {};
|
(itemsData || []).map(async (item) => {
|
||||||
switch (item.item_type) {
|
if (!item.item_data_id) {
|
||||||
case 'park':
|
return { ...item, item_data: {}, entity_data: null };
|
||||||
itemData = item.park_submission || {};
|
}
|
||||||
break;
|
|
||||||
case 'ride':
|
try {
|
||||||
itemData = item.ride_submission || {};
|
let entityData = null;
|
||||||
break;
|
|
||||||
default:
|
// Fetch from appropriate table based on item_type
|
||||||
itemData = {};
|
switch (item.item_type) {
|
||||||
}
|
case 'park': {
|
||||||
return { ...item, item_data: itemData };
|
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)
|
// Check for photo submissions (using array query to avoid 406)
|
||||||
const { data: photoData, error: photoError } = await supabase
|
const { data: photoData, error: photoError } = await supabase
|
||||||
|
|||||||
@@ -55,42 +55,10 @@ export function buildSubmissionQuery(
|
|||||||
) {
|
) {
|
||||||
const { entityFilter, statusFilter, tab, userId, isAdmin, isSuperuser } = config;
|
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
|
let query = supabase
|
||||||
.from('content_submissions')
|
.from('moderation_queue_with_entities')
|
||||||
.select(`
|
.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
|
|
||||||
)
|
|
||||||
`);
|
|
||||||
|
|
||||||
// CRITICAL: Multi-level ordering
|
// CRITICAL: Multi-level ordering
|
||||||
// Level 1: Always sort by escalated first (descending) - escalated items always appear at top
|
// Level 1: Always sort by escalated first (descending) - escalated items always appear at top
|
||||||
|
|||||||
@@ -118,6 +118,35 @@ export type SubmissionItemData =
|
|||||||
| RideModelItemData
|
| RideModelItemData
|
||||||
| PhotoItemData;
|
| 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.
|
* Represents a single item in the moderation queue.
|
||||||
* Can be either a review or a content submission.
|
* Can be either a review or a content submission.
|
||||||
@@ -135,6 +164,9 @@ export interface ModerationItem {
|
|||||||
/** Timestamp when the item was created */
|
/** Timestamp when the item was created */
|
||||||
created_at: string;
|
created_at: string;
|
||||||
|
|
||||||
|
/** Timestamp when the item was submitted */
|
||||||
|
submitted_at?: string;
|
||||||
|
|
||||||
/** Timestamp when the item was last updated */
|
/** Timestamp when the item was last updated */
|
||||||
updated_at?: string;
|
updated_at?: string;
|
||||||
|
|
||||||
@@ -147,7 +179,23 @@ export interface ModerationItem {
|
|||||||
/** Type of submission (e.g., 'park', 'ride', 'review') */
|
/** Type of submission (e.g., 'park', 'ride', 'review') */
|
||||||
submission_type?: string;
|
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?: {
|
user_profile?: {
|
||||||
username: string;
|
username: string;
|
||||||
display_name?: string;
|
display_name?: string;
|
||||||
@@ -172,7 +220,7 @@ export interface ModerationItem {
|
|||||||
/** Notes left by the reviewing moderator */
|
/** Notes left by the reviewing moderator */
|
||||||
reviewer_notes?: string;
|
reviewer_notes?: string;
|
||||||
|
|
||||||
/** Profile information of the reviewing moderator */
|
/** Legacy: Profile information of the reviewing moderator */
|
||||||
reviewer_profile?: {
|
reviewer_profile?: {
|
||||||
username: string;
|
username: string;
|
||||||
display_name?: string;
|
display_name?: string;
|
||||||
@@ -191,14 +239,8 @@ export interface ModerationItem {
|
|||||||
/** Internal flag indicating item is being removed (optimistic update) */
|
/** Internal flag indicating item is being removed (optimistic update) */
|
||||||
_removing?: boolean;
|
_removing?: boolean;
|
||||||
|
|
||||||
/** Sub-items for composite submissions */
|
/** Pre-loaded submission items with entity data from view */
|
||||||
submission_items?: Array<{
|
submission_items?: SubmissionItem[];
|
||||||
id: string;
|
|
||||||
item_type: string;
|
|
||||||
item_data: SubmissionItemData;
|
|
||||||
original_data?: SubmissionItemData;
|
|
||||||
status: string;
|
|
||||||
}>;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -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<string, any> | 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)) {
|
if (hasName(data)) {
|
||||||
return data.name;
|
return data.name;
|
||||||
}
|
}
|
||||||
|
|
||||||
return 'Unnamed';
|
return 'Unnamed';
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Safely get photos from item_data
|
* Safely get photos from item_data or entity_data
|
||||||
*/
|
*/
|
||||||
export function getItemPhotos(data: Json): Array<Record<string, Json>> {
|
export function getItemPhotos(data: Json | Record<string, any> | null | undefined): Array<Record<string, Json>> {
|
||||||
|
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<Record<string, Json>>;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle Json (legacy)
|
||||||
if (hasPhotos(data)) {
|
if (hasPhotos(data)) {
|
||||||
return data.photos;
|
return data.photos;
|
||||||
}
|
}
|
||||||
|
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user