From a4805cf0cca50156a60231e7161b4f20630d311d Mon Sep 17 00:00:00 2001 From: "gpt-engineer-app[bot]" <159125892+gpt-engineer-app[bot]@users.noreply.github.com> Date: Fri, 3 Oct 2025 15:56:35 +0000 Subject: [PATCH] feat: Implement Array Field Comparison --- src/components/moderation/ArrayFieldDiff.tsx | 218 ++++++++++++++++++ src/components/moderation/FieldComparison.tsx | 15 +- 2 files changed, 232 insertions(+), 1 deletion(-) create mode 100644 src/components/moderation/ArrayFieldDiff.tsx diff --git a/src/components/moderation/ArrayFieldDiff.tsx b/src/components/moderation/ArrayFieldDiff.tsx new file mode 100644 index 00000000..584b596d --- /dev/null +++ b/src/components/moderation/ArrayFieldDiff.tsx @@ -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 ( + + + {fieldName} ({totalChanges} changes) + + ); + } + + return ( +
+
+
+ {fieldName} ({differences.length} items, {totalChanges} changed) +
+ {unchangedCount > 0 && ( + + )} +
+ +
+ {differences.map((diff, idx) => { + if (diff.type === 'unchanged' && !showUnchanged) { + return null; + } + + return ( + + ); + })} +
+
+ ); +} + +function ArrayDiffItemDisplay({ diff }: { diff: ArrayDiffItem }) { + const isObject = typeof diff.newValue === 'object' || typeof diff.oldValue === 'object'; + + switch (diff.type) { + case 'added': + return ( +
+ +
+ {isObject ? ( + + ) : ( + + {formatFieldValue(diff.newValue)} + + )} +
+
+ ); + + case 'removed': + return ( +
+ +
+ {isObject ? ( + + ) : ( + + {formatFieldValue(diff.oldValue)} + + )} +
+
+ ); + + case 'modified': + return ( +
+
+ +
+
+ {isObject ? ( + + ) : ( + formatFieldValue(diff.oldValue) + )} +
+
+ {isObject ? ( + + ) : ( + formatFieldValue(diff.newValue) + )} +
+
+
+
+ ); + + case 'unchanged': + return ( +
+ +
+ {isObject ? ( + + ) : ( + formatFieldValue(diff.newValue) + )} +
+
+ ); + } +} + +function ObjectDisplay({ value, className = '' }: { value: any; className?: string }) { + if (!value || typeof value !== 'object') { + return {formatFieldValue(value)}; + } + + return ( +
+ {Object.entries(value).map(([key, val]) => ( +
+ {key.replace(/_/g, ' ')}: + {formatFieldValue(val)} +
+ ))} +
+ ); +} + +/** + * 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; +} diff --git a/src/components/moderation/FieldComparison.tsx b/src/components/moderation/FieldComparison.tsx index 24c154e8..96876565 100644 --- a/src/components/moderation/FieldComparison.tsx +++ b/src/components/moderation/FieldComparison.tsx @@ -1,7 +1,8 @@ +import { Badge } from '@/components/ui/badge'; import { formatFieldName, formatFieldValue } from '@/lib/submissionChangeDetection'; import type { FieldChange, ImageChange } from '@/lib/submissionChangeDetection'; -import { Badge } from '@/components/ui/badge'; import { ArrowRight } from 'lucide-react'; +import { ArrayFieldDiff } from './ArrayFieldDiff'; interface FieldDiffProps { change: FieldChange; @@ -11,6 +12,18 @@ interface FieldDiffProps { export function FieldDiff({ change, compact = false }: FieldDiffProps) { 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 ( + + ); + } + const getChangeColor = () => { switch (changeType) { case 'added': return 'text-green-600 dark:text-green-400';