mirror of
https://github.com/pacnpal/thrilltrack-explorer.git
synced 2025-12-23 14:11:13 -05:00
feat: Implement Array Field Comparison
This commit is contained in:
218
src/components/moderation/ArrayFieldDiff.tsx
Normal file
218
src/components/moderation/ArrayFieldDiff.tsx
Normal file
@@ -0,0 +1,218 @@
|
|||||||
|
import { Badge } from '@/components/ui/badge';
|
||||||
|
import { Plus, Minus, Edit, Check } from 'lucide-react';
|
||||||
|
import { formatFieldValue } from '@/lib/submissionChangeDetection';
|
||||||
|
import { useState } from 'react';
|
||||||
|
import { Button } from '@/components/ui/button';
|
||||||
|
|
||||||
|
interface ArrayFieldDiffProps {
|
||||||
|
fieldName: string;
|
||||||
|
oldArray: any[];
|
||||||
|
newArray: any[];
|
||||||
|
compact?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ArrayDiffItem {
|
||||||
|
type: 'added' | 'removed' | 'modified' | 'unchanged';
|
||||||
|
oldValue?: any;
|
||||||
|
newValue?: any;
|
||||||
|
index: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ArrayFieldDiff({ fieldName, oldArray, newArray, compact = false }: ArrayFieldDiffProps) {
|
||||||
|
const [showUnchanged, setShowUnchanged] = useState(false);
|
||||||
|
|
||||||
|
// Compute array differences
|
||||||
|
const differences = computeArrayDiff(oldArray || [], newArray || []);
|
||||||
|
const changedItems = differences.filter(d => d.type !== 'unchanged');
|
||||||
|
const unchangedCount = differences.filter(d => d.type === 'unchanged').length;
|
||||||
|
const totalChanges = changedItems.length;
|
||||||
|
|
||||||
|
if (compact) {
|
||||||
|
return (
|
||||||
|
<Badge variant="outline" className="text-blue-600 dark:text-blue-400">
|
||||||
|
<Edit className="h-3 w-3 mr-1" />
|
||||||
|
{fieldName} ({totalChanges} changes)
|
||||||
|
</Badge>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col gap-2 p-3 rounded-md bg-muted/30 border">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div className="text-sm font-medium">
|
||||||
|
{fieldName} ({differences.length} items, {totalChanges} changed)
|
||||||
|
</div>
|
||||||
|
{unchangedCount > 0 && (
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
onClick={() => setShowUnchanged(!showUnchanged)}
|
||||||
|
className="h-6 text-xs"
|
||||||
|
>
|
||||||
|
{showUnchanged ? 'Hide' : 'Show'} {unchangedCount} unchanged
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex flex-col gap-1">
|
||||||
|
{differences.map((diff, idx) => {
|
||||||
|
if (diff.type === 'unchanged' && !showUnchanged) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ArrayDiffItemDisplay key={idx} diff={diff} />
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function ArrayDiffItemDisplay({ diff }: { diff: ArrayDiffItem }) {
|
||||||
|
const isObject = typeof diff.newValue === 'object' || typeof diff.oldValue === 'object';
|
||||||
|
|
||||||
|
switch (diff.type) {
|
||||||
|
case 'added':
|
||||||
|
return (
|
||||||
|
<div className="flex items-start gap-2 p-2 rounded bg-green-500/10 border border-green-500/20">
|
||||||
|
<Plus className="h-4 w-4 text-green-600 dark:text-green-400 mt-0.5 flex-shrink-0" />
|
||||||
|
<div className="flex-1 text-sm">
|
||||||
|
{isObject ? (
|
||||||
|
<ObjectDisplay value={diff.newValue} />
|
||||||
|
) : (
|
||||||
|
<span className="text-green-600 dark:text-green-400">
|
||||||
|
{formatFieldValue(diff.newValue)}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
case 'removed':
|
||||||
|
return (
|
||||||
|
<div className="flex items-start gap-2 p-2 rounded bg-red-500/10 border border-red-500/20">
|
||||||
|
<Minus className="h-4 w-4 text-red-600 dark:text-red-400 mt-0.5 flex-shrink-0" />
|
||||||
|
<div className="flex-1 text-sm">
|
||||||
|
{isObject ? (
|
||||||
|
<ObjectDisplay value={diff.oldValue} className="line-through opacity-75" />
|
||||||
|
) : (
|
||||||
|
<span className="text-red-600 dark:text-red-400 line-through">
|
||||||
|
{formatFieldValue(diff.oldValue)}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
case 'modified':
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col gap-1 p-2 rounded bg-amber-500/10 border border-amber-500/20">
|
||||||
|
<div className="flex items-start gap-2">
|
||||||
|
<Edit className="h-4 w-4 text-amber-600 dark:text-amber-400 mt-0.5 flex-shrink-0" />
|
||||||
|
<div className="flex-1 text-sm">
|
||||||
|
<div className="text-red-600 dark:text-red-400 line-through mb-1">
|
||||||
|
{isObject ? (
|
||||||
|
<ObjectDisplay value={diff.oldValue} />
|
||||||
|
) : (
|
||||||
|
formatFieldValue(diff.oldValue)
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className="text-green-600 dark:text-green-400">
|
||||||
|
{isObject ? (
|
||||||
|
<ObjectDisplay value={diff.newValue} />
|
||||||
|
) : (
|
||||||
|
formatFieldValue(diff.newValue)
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
case 'unchanged':
|
||||||
|
return (
|
||||||
|
<div className="flex items-start gap-2 p-2 rounded bg-muted/20">
|
||||||
|
<Check className="h-4 w-4 text-muted-foreground mt-0.5 flex-shrink-0" />
|
||||||
|
<div className="flex-1 text-sm text-muted-foreground">
|
||||||
|
{isObject ? (
|
||||||
|
<ObjectDisplay value={diff.newValue} />
|
||||||
|
) : (
|
||||||
|
formatFieldValue(diff.newValue)
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function ObjectDisplay({ value, className = '' }: { value: any; className?: string }) {
|
||||||
|
if (!value || typeof value !== 'object') {
|
||||||
|
return <span className={className}>{formatFieldValue(value)}</span>;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={`space-y-0.5 ${className}`}>
|
||||||
|
{Object.entries(value).map(([key, val]) => (
|
||||||
|
<div key={key} className="flex gap-2">
|
||||||
|
<span className="font-medium capitalize">{key.replace(/_/g, ' ')}:</span>
|
||||||
|
<span>{formatFieldValue(val)}</span>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Compute differences between two arrays
|
||||||
|
*/
|
||||||
|
function computeArrayDiff(oldArray: any[], newArray: any[]): ArrayDiffItem[] {
|
||||||
|
const results: ArrayDiffItem[] = [];
|
||||||
|
const maxLength = Math.max(oldArray.length, newArray.length);
|
||||||
|
|
||||||
|
// Simple position-based comparison
|
||||||
|
for (let i = 0; i < maxLength; i++) {
|
||||||
|
const oldValue = i < oldArray.length ? oldArray[i] : undefined;
|
||||||
|
const newValue = i < newArray.length ? newArray[i] : undefined;
|
||||||
|
|
||||||
|
if (oldValue === undefined && newValue !== undefined) {
|
||||||
|
// Added
|
||||||
|
results.push({ type: 'added', newValue, index: i });
|
||||||
|
} else if (oldValue !== undefined && newValue === undefined) {
|
||||||
|
// Removed
|
||||||
|
results.push({ type: 'removed', oldValue, index: i });
|
||||||
|
} else if (!isEqual(oldValue, newValue)) {
|
||||||
|
// Modified
|
||||||
|
results.push({ type: 'modified', oldValue, newValue, index: i });
|
||||||
|
} else {
|
||||||
|
// Unchanged
|
||||||
|
results.push({ type: 'unchanged', oldValue, newValue, index: i });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return results;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Deep equality check
|
||||||
|
*/
|
||||||
|
function isEqual(a: any, b: any): boolean {
|
||||||
|
if (a === b) return true;
|
||||||
|
if (a == null || b == null) return a === b;
|
||||||
|
if (typeof a !== typeof b) return false;
|
||||||
|
|
||||||
|
if (typeof a === 'object') {
|
||||||
|
if (Array.isArray(a) && Array.isArray(b)) {
|
||||||
|
if (a.length !== b.length) return false;
|
||||||
|
return a.every((item, i) => isEqual(item, b[i]));
|
||||||
|
}
|
||||||
|
|
||||||
|
const keysA = Object.keys(a);
|
||||||
|
const keysB = Object.keys(b);
|
||||||
|
if (keysA.length !== keysB.length) return false;
|
||||||
|
|
||||||
|
return keysA.every(key => isEqual(a[key], b[key]));
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
@@ -1,7 +1,8 @@
|
|||||||
|
import { Badge } from '@/components/ui/badge';
|
||||||
import { formatFieldName, formatFieldValue } from '@/lib/submissionChangeDetection';
|
import { formatFieldName, formatFieldValue } from '@/lib/submissionChangeDetection';
|
||||||
import type { FieldChange, ImageChange } from '@/lib/submissionChangeDetection';
|
import type { FieldChange, ImageChange } from '@/lib/submissionChangeDetection';
|
||||||
import { Badge } from '@/components/ui/badge';
|
|
||||||
import { ArrowRight } from 'lucide-react';
|
import { ArrowRight } from 'lucide-react';
|
||||||
|
import { ArrayFieldDiff } from './ArrayFieldDiff';
|
||||||
|
|
||||||
interface FieldDiffProps {
|
interface FieldDiffProps {
|
||||||
change: FieldChange;
|
change: FieldChange;
|
||||||
@@ -11,6 +12,18 @@ interface FieldDiffProps {
|
|||||||
export function FieldDiff({ change, compact = false }: FieldDiffProps) {
|
export function FieldDiff({ change, compact = false }: FieldDiffProps) {
|
||||||
const { field, oldValue, newValue, changeType } = change;
|
const { field, oldValue, newValue, changeType } = change;
|
||||||
|
|
||||||
|
// Check if this is an array field that needs special handling
|
||||||
|
if (Array.isArray(oldValue) && Array.isArray(newValue)) {
|
||||||
|
return (
|
||||||
|
<ArrayFieldDiff
|
||||||
|
fieldName={formatFieldName(field)}
|
||||||
|
oldArray={oldValue}
|
||||||
|
newArray={newValue}
|
||||||
|
compact={compact}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
const getChangeColor = () => {
|
const getChangeColor = () => {
|
||||||
switch (changeType) {
|
switch (changeType) {
|
||||||
case 'added': return 'text-green-600 dark:text-green-400';
|
case 'added': return 'text-green-600 dark:text-green-400';
|
||||||
|
|||||||
Reference in New Issue
Block a user