Compare commits

..

17 Commits

Author SHA1 Message Date
gpt-engineer-app[bot]
6087b54213 Changes 2025-11-12 15:23:55 +00:00
gpt-engineer-app[bot]
68384156ab Fix selective-approval RPC params
Update edge function to call process_approval_transaction with correct parameters:
- remove p_trace_id and p_parent_span_id
- add p_approval_mode: 'selective' and p_idempotency_key: idempotencyKey
This aligns with database function signature and resolves 500 error.

X-Lovable-Edit-ID: edt-6e45b77e-1d54-4173-af1a-dcbcd886645d
2025-11-12 15:12:31 +00:00
gpt-engineer-app[bot]
5cc5d3eab6 testing changes with virtual file cleanup 2025-11-12 15:12:30 +00:00
gpt-engineer-app[bot]
706e36c847 Add staggered expand animation
Implement sequential delays for detailed view expansions:
- Add staggerIndex prop support to DetailedViewCollapsible and apply per-item animation delays.
- Pass item index in SubmissionItemsList when rendering detailed sections.
- Ensure each detailed view expands with a 50ms incremental delay (up to a max) for a staggered effect.

X-Lovable-Edit-ID: edt-6eb47d5c-853d-43ab-96a7-16a5cc006c30
2025-11-12 14:56:11 +00:00
gpt-engineer-app[bot]
a1beba6996 testing changes with virtual file cleanup 2025-11-12 14:56:11 +00:00
gpt-engineer-app[bot]
d7158756ef Animate detailed view transitions
Improve user experience by adding smooth animation transitions to expand/collapse of All Fields (Detailed View) sections, enhance collapsible base to support animation, and apply transitions to detailed view wrapper and chevron indicators.

X-Lovable-Edit-ID: edt-9a567ba5-b52f-46b3-bdef-b847b9ba7963
2025-11-12 14:53:19 +00:00
gpt-engineer-app[bot]
3330a8fac9 testing changes with virtual file cleanup 2025-11-12 14:53:18 +00:00
gpt-engineer-app[bot]
c09a343d08 Add moderation_preferences column
Adds a JSONB moderation_preferences column to user_preferences (with default '{}'), plus comment and GIN index, enabling per-user persistence of detailed view state and resolving TS errors.

X-Lovable-Edit-ID: edt-b953d926-c053-45f2-b434-2b776f3d9569
2025-11-12 14:50:57 +00:00
gpt-engineer-app[bot]
9893567a30 testing changes with virtual file cleanup 2025-11-12 14:50:56 +00:00
gpt-engineer-app[bot]
771405961f Add tooltip for expanded count
Enhance persistence for moderator preferences

- Add tooltip to moderation queue toggle showing number of items with detailed views expanded (based on global state, tooltip adapts to expanded/collapsed).
- Persist expanded/collapsed state per moderator in the database instead of localStorage, integrating with user preferences and Supabase backend.

X-Lovable-Edit-ID: edt-61e75a20-f83d-40b2-8bc4-b6ff40b23450
2025-11-12 14:45:07 +00:00
gpt-engineer-app[bot]
437e2b353c testing changes with virtual file cleanup 2025-11-12 14:45:06 +00:00
gpt-engineer-app[bot]
44a713af62 Add global toggle for detailed views
Implement a new global control in the moderation queue header to expand/collapse all "All Fields (Detailed View)" sections at once. This includes:
- Integrating useDetailedViewState with a new header-level button in QueueFilters
- Adding a button that toggles all detailed views and shows Expand/Collapse state
- Ensuring the toggle updates all DetailedViewCollapsible instances via shared state
- Keeping UI consistent with existing icons and styling

X-Lovable-Edit-ID: edt-22d9eca7-0c70-44d8-865d-791ef884dfbd
2025-11-12 14:42:34 +00:00
gpt-engineer-app[bot]
46275e0f1e testing changes with virtual file cleanup 2025-11-12 14:42:33 +00:00
gpt-engineer-app[bot]
6bd7d24a1b Add item-level history badge and animations
- Show a dynamic field-count badge next to All Fields (Detailed View) in the moderation queue
- Animate collapsible sections with smooth transitions for expand/collapse
- Pass fieldCount to DetailedViewCollapsible and render count alongside header; add animation utility in DetailedViewCollapsible.tsx
- Ensure SubmissionItemsList passes item data to calculate field counts and display badges accordingly

X-Lovable-Edit-ID: edt-ffd226b0-af99-491b-b6b8-3fe0063e0082
2025-11-12 14:40:52 +00:00
gpt-engineer-app[bot]
72e76e86af testing changes with virtual file cleanup 2025-11-12 14:40:51 +00:00
gpt-engineer-app[bot]
a35486fb11 Add collapsible detailed view
Implements expand/collapse for the All Fields (Detailed View) sections in the moderation queue:
- Adds useDetailedViewState hook to persist collapse state in localStorage (default collapsed)
- Adds DetailedViewCollapsible wrapper component using Radix Collapsible
- Updates SubmissionItemsList to wrap each detailed view block with the new collapsible, and imports the new hook and component

X-Lovable-Edit-ID: edt-a95a840d-e7e7-4f9e-aa25-03bb68194aee
2025-11-12 14:36:50 +00:00
gpt-engineer-app[bot]
3d3ae57ee3 testing changes with virtual file cleanup 2025-11-12 14:36:49 +00:00
8 changed files with 262 additions and 52 deletions

View File

@@ -1,13 +1,16 @@
import { ChevronDown, ChevronUp } from 'lucide-react'; import { ChevronDown } from 'lucide-react';
import { Collapsible, CollapsibleTrigger, CollapsibleContent } from '@/components/ui/collapsible'; import { Collapsible, CollapsibleTrigger, CollapsibleContent } from '@/components/ui/collapsible';
import { Button } from '@/components/ui/button'; import { Button } from '@/components/ui/button';
import { Badge } from '@/components/ui/badge';
import { cn } from '@/lib/utils'; import { cn } from '@/lib/utils';
interface DetailedViewCollapsibleProps { interface DetailedViewCollapsibleProps {
isCollapsed: boolean; isCollapsed: boolean;
onToggle: () => void; onToggle: () => void;
children: React.ReactNode; children: React.ReactNode;
fieldCount?: number;
className?: string; className?: string;
staggerIndex?: number;
} }
/** /**
@@ -18,8 +21,12 @@ export function DetailedViewCollapsible({
isCollapsed, isCollapsed,
onToggle, onToggle,
children, children,
className fieldCount,
className,
staggerIndex = 0
}: DetailedViewCollapsibleProps) { }: DetailedViewCollapsibleProps) {
// Calculate stagger delay: 50ms per item, max 300ms
const staggerDelay = Math.min(staggerIndex * 50, 300);
return ( return (
<Collapsible open={!isCollapsed} onOpenChange={() => onToggle()}> <Collapsible open={!isCollapsed} onOpenChange={() => onToggle()}>
<div className={cn("mt-6 pt-6 border-t", className)}> <div className={cn("mt-6 pt-6 border-t", className)}>
@@ -27,25 +34,42 @@ export function DetailedViewCollapsible({
<Button <Button
variant="ghost" variant="ghost"
size="sm" size="sm"
className="w-full flex items-center justify-between hover:bg-muted/50 p-2 h-auto" className="w-full flex items-center justify-between hover:bg-muted/50 p-2 h-auto transition-colors"
> >
<div className="text-xs font-semibold text-muted-foreground uppercase tracking-wide"> <div className="flex items-center gap-2">
All Fields (Detailed View) <span className="text-xs font-semibold text-muted-foreground uppercase tracking-wide">
All Fields (Detailed View)
</span>
{fieldCount !== undefined && fieldCount > 0 && (
<Badge
variant="secondary"
className="h-5 px-1.5 text-xs font-normal transition-transform duration-200 hover:scale-105"
>
{fieldCount}
</Badge>
)}
</div> </div>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<span className="text-xs text-muted-foreground normal-case font-normal"> <span className="text-xs text-muted-foreground normal-case font-normal">
{isCollapsed ? 'Show' : 'Hide'} {isCollapsed ? 'Show' : 'Hide'}
</span> </span>
{isCollapsed ? ( <ChevronDown
<ChevronDown className="h-4 w-4 text-muted-foreground" /> className={cn(
) : ( "h-4 w-4 text-muted-foreground transition-all duration-300 ease-out",
<ChevronUp className="h-4 w-4 text-muted-foreground" /> !isCollapsed && "rotate-180"
)} )}
/>
</div> </div>
</Button> </Button>
</CollapsibleTrigger> </CollapsibleTrigger>
<CollapsibleContent className="mt-3"> <CollapsibleContent
className="mt-3"
style={{
animationDelay: `${staggerDelay}ms`,
transitionDelay: `${staggerDelay}ms`
}}
>
{children} {children}
</CollapsibleContent> </CollapsibleContent>
</div> </div>

View File

@@ -1,12 +1,14 @@
import { Filter, MessageSquare, FileText, Image, X, ChevronDown, Calendar } from 'lucide-react'; import { Filter, MessageSquare, FileText, Image, X, ChevronDown, Calendar, Maximize2, Minimize2 } from 'lucide-react';
import { Label } from '@/components/ui/label'; import { Label } from '@/components/ui/label';
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
import { Button } from '@/components/ui/button'; import { Button } from '@/components/ui/button';
import { Badge } from '@/components/ui/badge'; import { Badge } from '@/components/ui/badge';
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from '@/components/ui/collapsible'; import { Collapsible, CollapsibleContent, CollapsibleTrigger } from '@/components/ui/collapsible';
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip';
import { RefreshButton } from '@/components/ui/refresh-button'; import { RefreshButton } from '@/components/ui/refresh-button';
import { QueueSortControls } from './QueueSortControls'; import { QueueSortControls } from './QueueSortControls';
import { useFilterPanelState } from '@/hooks/useFilterPanelState'; import { useFilterPanelState } from '@/hooks/useFilterPanelState';
import { useDetailedViewState } from '@/hooks/useDetailedViewState';
import { FilterDateRangePicker } from '@/components/filters/FilterDateRangePicker'; import { FilterDateRangePicker } from '@/components/filters/FilterDateRangePicker';
import type { EntityFilter, StatusFilter, SortConfig, QueueTab, ApprovalDateRangeFilter } from '@/types/moderation'; import type { EntityFilter, StatusFilter, SortConfig, QueueTab, ApprovalDateRangeFilter } from '@/types/moderation';
@@ -55,6 +57,7 @@ export const QueueFilters = ({
isRefreshing = false isRefreshing = false
}: QueueFiltersProps) => { }: QueueFiltersProps) => {
const { isCollapsed, toggle } = useFilterPanelState(); const { isCollapsed, toggle } = useFilterPanelState();
const { isCollapsed: detailsCollapsed, toggle: toggleDetails } = useDetailedViewState();
// Count active filters // Count active filters
const activeFilterCount = [ const activeFilterCount = [
@@ -76,14 +79,51 @@ export const QueueFilters = ({
</Badge> </Badge>
)} )}
</div> </div>
{isMobile && ( <div className="flex items-center gap-2">
<CollapsibleTrigger asChild> {/* Global toggle for detailed views */}
<Button variant="ghost" size="sm" className="h-8 w-8 p-0"> <TooltipProvider>
<ChevronDown className={`h-4 w-4 transition-transform duration-250 ${isCollapsed ? '' : 'rotate-180'}`} /> <Tooltip>
<span className="sr-only">{isCollapsed ? 'Expand filters' : 'Collapse filters'}</span> <TooltipTrigger asChild>
</Button> <Button
</CollapsibleTrigger> variant="ghost"
)} size="sm"
onClick={toggleDetails}
className="h-8 gap-2 text-xs text-muted-foreground hover:text-foreground transition-colors"
>
{detailsCollapsed ? (
<>
<Maximize2 className="h-3.5 w-3.5" />
{!isMobile && <span>Expand All</span>}
</>
) : (
<>
<Minimize2 className="h-3.5 w-3.5" />
{!isMobile && <span>Collapse All</span>}
</>
)}
</Button>
</TooltipTrigger>
<TooltipContent side="bottom" className="max-w-xs">
<p className="text-xs">
{detailsCollapsed
? "Show detailed field-by-field view for all items in the queue"
: "Hide detailed field-by-field view for all items in the queue"}
</p>
<p className="text-xs text-muted-foreground mt-1">
This preference is saved to your account
</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
{isMobile && (
<CollapsibleTrigger asChild>
<Button variant="ghost" size="sm" className="h-8 w-8 p-0">
<ChevronDown className={`h-4 w-4 transition-transform duration-250 ${isCollapsed ? '' : 'rotate-180'}`} />
<span className="sr-only">{isCollapsed ? 'Expand filters' : 'Collapse filters'}</span>
</Button>
</CollapsibleTrigger>
)}
</div>
</div> </div>
<CollapsibleContent className="space-y-4"> <CollapsibleContent className="space-y-4">

View File

@@ -42,6 +42,12 @@ export const SubmissionItemsList = memo(function SubmissionItemsList({
fetchSubmissionItems(); fetchSubmissionItems();
}, [submissionId]); }, [submissionId]);
// Helper function to count non-null fields in entity data
const countFields = (data: any): number => {
if (!data || typeof data !== 'object') return 0;
return Object.values(data).filter(value => value !== null && value !== undefined).length;
};
const fetchSubmissionItems = async () => { const fetchSubmissionItems = async () => {
try { try {
// Only show skeleton on initial load, show refreshing indicator on refresh // Only show skeleton on initial load, show refreshing indicator on refresh
@@ -129,7 +135,7 @@ export const SubmissionItemsList = memo(function SubmissionItemsList({
} }
// Render item with appropriate display component // Render item with appropriate display component
const renderItem = (item: SubmissionItemData) => { const renderItem = (item: SubmissionItemData, index: number = 0) => {
// SubmissionItemData from submissions.ts has item_data property // SubmissionItemData from submissions.ts has item_data property
const entityData = item.item_data; const entityData = item.item_data;
const actionType = item.action_type || 'create'; const actionType = item.action_type || 'create';
@@ -191,7 +197,12 @@ export const SubmissionItemsList = memo(function SubmissionItemsList({
data={entityData as unknown as ParkSubmissionData} data={entityData as unknown as ParkSubmissionData}
actionType={actionType} actionType={actionType}
/> />
<DetailedViewCollapsible isCollapsed={isCollapsed} onToggle={toggle}> <DetailedViewCollapsible
isCollapsed={isCollapsed}
onToggle={toggle}
fieldCount={countFields(entityData)}
staggerIndex={index}
>
<SubmissionChangesDisplay <SubmissionChangesDisplay
item={item} item={item}
view="detailed" view="detailed"
@@ -211,7 +222,12 @@ export const SubmissionItemsList = memo(function SubmissionItemsList({
data={entityData as unknown as RideSubmissionData} data={entityData as unknown as RideSubmissionData}
actionType={actionType} actionType={actionType}
/> />
<DetailedViewCollapsible isCollapsed={isCollapsed} onToggle={toggle}> <DetailedViewCollapsible
isCollapsed={isCollapsed}
onToggle={toggle}
fieldCount={countFields(entityData)}
staggerIndex={index}
>
<SubmissionChangesDisplay <SubmissionChangesDisplay
item={item} item={item}
view="detailed" view="detailed"
@@ -231,7 +247,12 @@ export const SubmissionItemsList = memo(function SubmissionItemsList({
data={entityData as unknown as CompanySubmissionData} data={entityData as unknown as CompanySubmissionData}
actionType={actionType} actionType={actionType}
/> />
<DetailedViewCollapsible isCollapsed={isCollapsed} onToggle={toggle}> <DetailedViewCollapsible
isCollapsed={isCollapsed}
onToggle={toggle}
fieldCount={countFields(entityData)}
staggerIndex={index}
>
<SubmissionChangesDisplay <SubmissionChangesDisplay
item={item} item={item}
view="detailed" view="detailed"
@@ -251,7 +272,12 @@ export const SubmissionItemsList = memo(function SubmissionItemsList({
data={entityData as unknown as RideModelSubmissionData} data={entityData as unknown as RideModelSubmissionData}
actionType={actionType} actionType={actionType}
/> />
<DetailedViewCollapsible isCollapsed={isCollapsed} onToggle={toggle}> <DetailedViewCollapsible
isCollapsed={isCollapsed}
onToggle={toggle}
fieldCount={countFields(entityData)}
staggerIndex={index}
>
<SubmissionChangesDisplay <SubmissionChangesDisplay
item={item} item={item}
view="detailed" view="detailed"
@@ -271,7 +297,12 @@ export const SubmissionItemsList = memo(function SubmissionItemsList({
data={entityData as unknown as TimelineSubmissionData} data={entityData as unknown as TimelineSubmissionData}
actionType={actionType} actionType={actionType}
/> />
<DetailedViewCollapsible isCollapsed={isCollapsed} onToggle={toggle}> <DetailedViewCollapsible
isCollapsed={isCollapsed}
onToggle={toggle}
fieldCount={countFields(entityData)}
staggerIndex={index}
>
<SubmissionChangesDisplay <SubmissionChangesDisplay
item={item} item={item}
view="detailed" view="detailed"
@@ -308,9 +339,9 @@ export const SubmissionItemsList = memo(function SubmissionItemsList({
)} )}
{/* Show regular submission items */} {/* Show regular submission items */}
{items.map((item) => ( {items.map((item, index) => (
<div key={item.id} className={view === 'summary' ? 'border-l-2 border-primary/20 pl-3' : ''}> <div key={item.id} className={view === 'summary' ? 'border-l-2 border-primary/20 pl-3' : ''}>
{renderItem(item)} {renderItem(item, index)}
</div> </div>
))} ))}

View File

@@ -1,9 +1,30 @@
import * as React from "react";
import * as CollapsiblePrimitive from "@radix-ui/react-collapsible"; import * as CollapsiblePrimitive from "@radix-ui/react-collapsible";
import { cn } from "@/lib/utils";
const Collapsible = CollapsiblePrimitive.Root; const Collapsible = CollapsiblePrimitive.Root;
const CollapsibleTrigger = CollapsiblePrimitive.CollapsibleTrigger; const CollapsibleTrigger = CollapsiblePrimitive.CollapsibleTrigger;
const CollapsibleContent = CollapsiblePrimitive.CollapsibleContent; const CollapsibleContent = React.forwardRef<
React.ElementRef<typeof CollapsiblePrimitive.Content>,
React.ComponentPropsWithoutRef<typeof CollapsiblePrimitive.Content>
>(({ className, children, ...props }, ref) => (
<CollapsiblePrimitive.Content
ref={ref}
className={cn(
"overflow-hidden transition-all duration-300 ease-out",
"data-[state=closed]:animate-accordion-up",
"data-[state=open]:animate-accordion-down",
className
)}
{...props}
>
<div className="animate-fade-in">
{children}
</div>
</CollapsiblePrimitive.Content>
));
CollapsibleContent.displayName = "CollapsibleContent";
export { Collapsible, CollapsibleTrigger, CollapsibleContent }; export { Collapsible, CollapsibleTrigger, CollapsibleContent };

View File

@@ -1,48 +1,129 @@
import { useState, useEffect } from 'react'; import { useState, useEffect } from 'react';
import { logger } from '@/lib/logger'; import { logger } from '@/lib/logger';
import { useAuth } from '@/hooks/useAuth';
import { supabase } from '@/lib/supabaseClient';
import { handleNonCriticalError } from '@/lib/errorHandler';
import type { Json } from '@/integrations/supabase/types';
const STORAGE_KEY = 'detailed-view-collapsed'; const STORAGE_KEY = 'detailed-view-collapsed';
interface ModerationPreferences {
detailed_view_collapsed: boolean;
}
interface UseDetailedViewStateReturn { interface UseDetailedViewStateReturn {
isCollapsed: boolean; isCollapsed: boolean;
toggle: () => void; toggle: () => void;
setCollapsed: (value: boolean) => void; setCollapsed: (value: boolean) => void;
loading: boolean;
} }
/** /**
* Hook to manage detailed view collapsed/expanded state * Hook to manage detailed view collapsed/expanded state
* Syncs with localStorage for persistence across sessions * Persists to database for authenticated users, localStorage for guests
* Defaults to collapsed to reduce visual clutter * Defaults to collapsed to reduce visual clutter
*/ */
export function useDetailedViewState(): UseDetailedViewStateReturn { export function useDetailedViewState(): UseDetailedViewStateReturn {
const [isCollapsed, setIsCollapsed] = useState<boolean>(() => { const { user } = useAuth();
// Initialize from localStorage on mount const [isCollapsed, setIsCollapsed] = useState<boolean>(true);
try { const [loading, setLoading] = useState(true);
const stored = localStorage.getItem(STORAGE_KEY);
// Default to collapsed (true) to reduce visual clutter
return stored ? JSON.parse(stored) : true;
} catch (error) {
logger.warn('Error reading detailed view state from localStorage', { error });
return true;
}
});
// Sync to localStorage when state changes // Load preferences on mount
useEffect(() => { useEffect(() => {
try { loadPreferences();
localStorage.setItem(STORAGE_KEY, JSON.stringify(isCollapsed)); }, [user]);
} catch (error) {
logger.warn('Error saving detailed view state to localStorage', { error });
}
}, [isCollapsed]);
const toggle = () => setIsCollapsed(prev => !prev); const loadPreferences = async () => {
try {
if (user) {
// Load from database for authenticated users
const { data, error } = await supabase
.from('user_preferences')
.select('moderation_preferences')
.eq('user_id', user.id)
.maybeSingle();
if (error && error.code !== 'PGRST116') {
handleNonCriticalError(error, {
action: 'Load moderation preferences',
userId: user.id,
});
}
// Type assertion needed until Supabase regenerates types after migration
const preferences = (data as any)?.moderation_preferences;
if (preferences) {
const prefs = preferences as ModerationPreferences;
setIsCollapsed(prefs.detailed_view_collapsed ?? true);
}
} else {
// Load from localStorage for guests
try {
const stored = localStorage.getItem(STORAGE_KEY);
setIsCollapsed(stored ? JSON.parse(stored) : true);
} catch (error) {
logger.warn('Error reading detailed view state from localStorage', { error });
}
}
} catch (error) {
logger.warn('Error loading detailed view preferences', { error });
} finally {
setLoading(false);
}
};
const savePreferences = async (collapsed: boolean) => {
try {
if (user) {
// Save to database for authenticated users
const moderationPrefs: ModerationPreferences = {
detailed_view_collapsed: collapsed,
};
const { error } = await supabase
.from('user_preferences')
.upsert({
user_id: user.id,
moderation_preferences: moderationPrefs as unknown as Json,
updated_at: new Date().toISOString(),
}, {
onConflict: 'user_id',
});
if (error) {
handleNonCriticalError(error, {
action: 'Save moderation preferences',
userId: user.id,
});
}
} else {
// Save to localStorage for guests
try {
localStorage.setItem(STORAGE_KEY, JSON.stringify(collapsed));
} catch (error) {
logger.warn('Error saving detailed view state to localStorage', { error });
}
}
} catch (error) {
logger.warn('Error saving detailed view preferences', { error });
}
};
const toggle = () => {
const newValue = !isCollapsed;
setIsCollapsed(newValue);
savePreferences(newValue);
};
const setCollapsed = (value: boolean) => setIsCollapsed(value); const setCollapsed = (value: boolean) => {
setIsCollapsed(value);
savePreferences(value);
};
return { return {
isCollapsed, isCollapsed,
toggle, toggle,
setCollapsed, setCollapsed,
loading,
}; };
} }

View File

@@ -7,6 +7,7 @@ import { twMerge } from "tailwind-merge";
* *
* @param inputs - Class values to combine (strings, objects, arrays) * @param inputs - Class values to combine (strings, objects, arrays)
* @returns Merged class string with Tailwind conflicts resolved * @returns Merged class string with Tailwind conflicts resolved
* @example cn('px-2 py-1', 'px-4') // Returns 'py-1 px-4'
*/ */
export function cn(...inputs: ClassValue[]) { export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs)); return twMerge(clsx(inputs));

View File

@@ -207,8 +207,8 @@ const handler = async (req: Request, context: { supabase: any; user: any; span:
p_moderator_id: user.id, p_moderator_id: user.id,
p_submitter_id: submission.user_id, p_submitter_id: submission.user_id,
p_request_id: requestId, p_request_id: requestId,
p_trace_id: rootSpan.traceId, p_approval_mode: 'selective',
p_parent_span_id: rpcSpan.spanId p_idempotency_key: idempotencyKey
} }
); );

View File

@@ -0,0 +1,12 @@
-- Add moderation_preferences column to user_preferences table
-- This stores moderator UI preferences like detailed view collapsed state
ALTER TABLE public.user_preferences
ADD COLUMN IF NOT EXISTS moderation_preferences JSONB NOT NULL DEFAULT '{}'::jsonb;
COMMENT ON COLUMN public.user_preferences.moderation_preferences IS
'Stores moderator UI preferences like detailed view collapsed state';
-- Add GIN index for efficient JSONB queries
CREATE INDEX IF NOT EXISTS idx_user_preferences_moderation_prefs
ON public.user_preferences USING gin(moderation_preferences);