From 9dc4a0d84583fcb4af8105b9c18dbc9f68a30d92 Mon Sep 17 00:00:00 2001 From: johndoe6345789 Date: Sun, 18 Jan 2026 00:37:10 +0000 Subject: [PATCH] Refactor conflict resolution components --- src/components/ConflictCard.tsx | 305 +++++++++------- src/components/ConflictDetailsDialog.tsx | 419 ++++++++++++--------- src/components/ConflictResolutionDemo.tsx | 424 ++++++++++++---------- src/data/conflict-resolution-copy.json | 68 ++++ 4 files changed, 728 insertions(+), 488 deletions(-) create mode 100644 src/data/conflict-resolution-copy.json diff --git a/src/components/ConflictCard.tsx b/src/components/ConflictCard.tsx index a414d80..212f934 100644 --- a/src/components/ConflictCard.tsx +++ b/src/components/ConflictCard.tsx @@ -1,14 +1,13 @@ -import { useState } from 'react' +import { useState, type ReactNode } from 'react' import { ConflictItem } from '@/types/conflicts' import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card' import { Button } from '@/components/ui/button' import { Badge } from '@/components/ui/badge' import { Separator } from '@/components/ui/separator' -import { ScrollArea } from '@/components/ui/scroll-area' -import { - ArrowsLeftRight, - CheckCircle, - Clock, +import conflictCopy from '@/data/conflict-resolution-copy.json' +import { + ArrowsLeftRight, + Clock, Database, Cloud, Code, @@ -26,24 +25,160 @@ interface ConflictCardProps { isResolving: boolean } +const cardCopy = conflictCopy.card + +interface ConflictCardHeaderProps { + conflict: ConflictItem + expanded: boolean + onToggle: () => void + timeDiffMinutes: number +} + +interface ConflictVersionPanelProps { + label: string + timestamp: number + version: Record + icon: ReactNode + highlightBadge?: boolean + accentClassName?: string +} + +interface ConflictCardActionsProps { + conflict: ConflictItem + onResolve: (conflictId: string, strategy: 'local' | 'remote' | 'merge') => void + onViewDetails: (conflict: ConflictItem) => void + isResolving: boolean +} + +function getEntityIcon(entityType: string) { + switch (entityType) { + case 'files': + return + case 'models': + return + default: + return + } +} + +function ConflictCardHeader({ conflict, expanded, onToggle, timeDiffMinutes }: ConflictCardHeaderProps) { + return ( + +
+
+
{getEntityIcon(conflict.entityType)}
+
+ + {conflict.id} + + + + {conflict.entityType} + + + {timeDiffMinutes} + {cardCopy.timeDifferenceSuffix} + + +
+
+ +
+
+ ) +} + +function ConflictVersionPanel({ + label, + timestamp, + version, + icon, + highlightBadge, + accentClassName, +}: ConflictVersionPanelProps) { + return ( +
+
+ {icon} +

{label}

+ {highlightBadge && ( + + {cardCopy.newerLabel} + + )} +
+
+
+ + {format(new Date(timestamp), 'MMM d, h:mm a')} +
+
+          {JSON.stringify(version, null, 2).slice(0, 200)}...
+        
+
+
+ ) +} + +function ConflictCardActions({ conflict, onResolve, onViewDetails, isResolving }: ConflictCardActionsProps) { + return ( +
+ + + + +
+ ) +} + export function ConflictCard({ conflict, onResolve, onViewDetails, isResolving }: ConflictCardProps) { const [expanded, setExpanded] = useState(false) - + const isLocalNewer = conflict.localTimestamp > conflict.remoteTimestamp const timeDiff = Math.abs(conflict.localTimestamp - conflict.remoteTimestamp) const timeDiffMinutes = Math.round(timeDiff / 1000 / 60) - const getEntityIcon = () => { - switch (conflict.entityType) { - case 'files': - return - case 'models': - return - default: - return - } - } - return ( - -
-
-
{getEntityIcon()}
-
- - {conflict.id} - - - - {conflict.entityType} - - - {timeDiffMinutes}m difference - - -
-
- -
-
+ setExpanded(!expanded)} + timeDiffMinutes={timeDiffMinutes} + /> {expanded && ( @@ -93,92 +207,31 @@ export function ConflictCard({ conflict, onResolve, onViewDetails, isResolving }
-
-
- -

Local Version

- {isLocalNewer && ( - - Newer - - )} -
-
-
- - {format(new Date(conflict.localTimestamp), 'MMM d, h:mm a')} -
-
-                        {JSON.stringify(conflict.localVersion, null, 2).slice(0, 200)}...
-                      
-
-
+ } + highlightBadge={isLocalNewer} + /> -
-
- -

Remote Version

- {!isLocalNewer && ( - - Newer - - )} -
-
-
- - {format(new Date(conflict.remoteTimestamp), 'MMM d, h:mm a')} -
-
-                        {JSON.stringify(conflict.remoteVersion, null, 2).slice(0, 200)}...
-                      
-
-
+ } + highlightBadge={!isLocalNewer} + />
-
- - - - -
+
)} diff --git a/src/components/ConflictDetailsDialog.tsx b/src/components/ConflictDetailsDialog.tsx index 7be25b6..7dba56e 100644 --- a/src/components/ConflictDetailsDialog.tsx +++ b/src/components/ConflictDetailsDialog.tsx @@ -1,4 +1,4 @@ -import { useState } from 'react' +import { useMemo, useState } from 'react' import { ConflictItem } from '@/types/conflicts' import { Dialog, @@ -12,6 +12,7 @@ import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs' import { ScrollArea } from '@/components/ui/scroll-area' import { Badge } from '@/components/ui/badge' import { Separator } from '@/components/ui/separator' +import conflictCopy from '@/data/conflict-resolution-copy.json' import { Database, Cloud, ArrowsLeftRight, Clock, CheckCircle } from '@phosphor-icons/react' import { format } from 'date-fns' @@ -23,6 +24,226 @@ interface ConflictDetailsDialogProps { isResolving: boolean } +type ConflictTab = 'local' | 'remote' | 'diff' + +type ConflictDiffItem = { + key: string + localValue: unknown + remoteValue: unknown + isDifferent: boolean + onlyInLocal: boolean + onlyInRemote: boolean +} + +const dialogCopy = conflictCopy.detailsDialog + +function getConflictDiff(conflict: ConflictItem): ConflictDiffItem[] { + const localKeys = Object.keys(conflict.localVersion) + const remoteKeys = Object.keys(conflict.remoteVersion) + const allKeys = Array.from(new Set([...localKeys, ...remoteKeys])) + + return allKeys.map((key) => { + const localValue = conflict.localVersion[key] + const remoteValue = conflict.remoteVersion[key] + const isDifferent = JSON.stringify(localValue) !== JSON.stringify(remoteValue) + const onlyInLocal = !(key in conflict.remoteVersion) + const onlyInRemote = !(key in conflict.localVersion) + + return { + key, + localValue, + remoteValue, + isDifferent, + onlyInLocal, + onlyInRemote, + } + }) +} + +function ConflictDetailsHeader({ conflict }: { conflict: ConflictItem }) { + return ( + + {dialogCopy.title} + + {conflict.entityType} + {conflict.id} + + + ) +} + +function ConflictVersionSummary({ conflict, isLocalNewer }: { conflict: ConflictItem; isLocalNewer: boolean }) { + return ( +
+
+ +
+
{dialogCopy.localVersionLabel}
+
+ + {format(new Date(conflict.localTimestamp), 'PPp')} +
+
+ {isLocalNewer && ( + + {dialogCopy.newerLabel} + + )} +
+ +
+ +
+
{dialogCopy.remoteVersionLabel}
+
+ + {format(new Date(conflict.remoteTimestamp), 'PPp')} +
+
+ {!isLocalNewer && ( + + {dialogCopy.newerLabel} + + )} +
+
+ ) +} + +function DiffItemCard({ item }: { item: ConflictDiffItem }) { + return ( +
+
+ {item.key} + {item.isDifferent && ( + + {dialogCopy.conflictBadge} + + )} + {!item.isDifferent && ( + + + {dialogCopy.matchBadge} + + )} +
+ +
+
+
{dialogCopy.localFieldLabel}
+
+ {item.onlyInLocal ? ( + {dialogCopy.onlyInLocal} + ) : ( +
+                {JSON.stringify(item.localValue, null, 2)}
+              
+ )} +
+
+ +
+
{dialogCopy.remoteFieldLabel}
+
+ {item.onlyInRemote ? ( + {dialogCopy.onlyInRemote} + ) : ( +
+                {JSON.stringify(item.remoteValue, null, 2)}
+              
+ )} +
+
+
+
+ ) +} + +function DiffTabContent({ diff }: { diff: ConflictDiffItem[] }) { + return ( + + +
+ {diff.map((item) => ( + + ))} +
+
+
+ ) +} + +function JsonTabContent({ value, json }: { value: ConflictTab; json: string }) { + return ( + + +
{json}
+
+
+ ) +} + +function ResolutionFooter({ + conflict, + isResolving, + onOpenChange, + onResolve, +}: { + conflict: ConflictItem + isResolving: boolean + onOpenChange: (open: boolean) => void + onResolve: (conflictId: string, strategy: 'local' | 'remote' | 'merge') => void +}) { + return ( +
+ +
+ + + +
+
+ ) +} + export function ConflictDetailsDialog({ conflict, open, @@ -30,7 +251,7 @@ export function ConflictDetailsDialog({ onResolve, isResolving, }: ConflictDetailsDialogProps) { - const [activeTab, setActiveTab] = useState<'local' | 'remote' | 'diff'>('diff') + const [activeTab, setActiveTab] = useState('diff') if (!conflict) return null @@ -38,209 +259,49 @@ export function ConflictDetailsDialog({ const localJson = JSON.stringify(conflict.localVersion, null, 2) const remoteJson = JSON.stringify(conflict.remoteVersion, null, 2) - const getDiff = () => { - const localKeys = Object.keys(conflict.localVersion) - const remoteKeys = Object.keys(conflict.remoteVersion) - const allKeys = Array.from(new Set([...localKeys, ...remoteKeys])) - - return allKeys.map((key) => { - const localValue = conflict.localVersion[key] - const remoteValue = conflict.remoteVersion[key] - const isDifferent = JSON.stringify(localValue) !== JSON.stringify(remoteValue) - const onlyInLocal = !(key in conflict.remoteVersion) - const onlyInRemote = !(key in conflict.localVersion) - - return { - key, - localValue, - remoteValue, - isDifferent, - onlyInLocal, - onlyInRemote, - } - }) - } - - const diff = getDiff() - const conflictingKeys = diff.filter((d) => d.isDifferent) + const diff = useMemo(() => getConflictDiff(conflict), [conflict]) + const conflictingKeys = diff.filter((item) => item.isDifferent) return ( - - Conflict Details - - {conflict.entityType} - {conflict.id} - - + -
-
- -
-
Local Version
-
- - {format(new Date(conflict.localTimestamp), 'PPp')} -
-
- {isLocalNewer && ( - - Newer - - )} -
- -
- -
-
Remote Version
-
- - {format(new Date(conflict.remoteTimestamp), 'PPp')} -
-
- {!isLocalNewer && ( - - Newer - - )} -
-
+ - setActiveTab(v as any)} className="flex-1 flex flex-col min-h-0"> + setActiveTab(value as ConflictTab)} className="flex-1 flex flex-col min-h-0"> - Differences ({conflictingKeys.length}) + {dialogCopy.differencesLabel} ({conflictingKeys.length}) - Local + {dialogCopy.localTabLabel} - Remote + {dialogCopy.remoteTabLabel} - - -
- {diff.map((item) => ( -
-
- {item.key} - {item.isDifferent && ( - - Conflict - - )} - {!item.isDifferent && ( - - - Match - - )} -
+ -
-
-
Local:
-
- {item.onlyInLocal ? ( - Only in local - ) : ( -
-                              {JSON.stringify(item.localValue, null, 2)}
-                            
- )} -
-
+ -
-
Remote:
-
- {item.onlyInRemote ? ( - Only in remote - ) : ( -
-                              {JSON.stringify(item.remoteValue, null, 2)}
-                            
- )} -
-
-
-
- ))} -
-
-
- - - -
{localJson}
-
-
- - - -
{remoteJson}
-
-
+
-
- -
- - - -
-
+
) diff --git a/src/components/ConflictResolutionDemo.tsx b/src/components/ConflictResolutionDemo.tsx index 36d6913..43fb250 100644 --- a/src/components/ConflictResolutionDemo.tsx +++ b/src/components/ConflictResolutionDemo.tsx @@ -1,4 +1,4 @@ -import { useState } from 'react' +import { useMemo, useState } from 'react' import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card' import { Button } from '@/components/ui/button' import { Badge } from '@/components/ui/badge' @@ -6,19 +6,233 @@ import { Separator } from '@/components/ui/separator' import { useConflictResolution } from '@/hooks/use-conflict-resolution' import { ConflictIndicator } from '@/components/ConflictIndicator' import { db } from '@/lib/db' -import { - Flask, - Database, - Warning, - CheckCircle, - ArrowsClockwise +import conflictCopy from '@/data/conflict-resolution-copy.json' +import { + Flask, + Database, + Warning, + CheckCircle, + ArrowsClockwise, } from '@phosphor-icons/react' import { toast } from 'sonner' +const demoCopy = conflictCopy.demo + +function DemoHeader() { + return ( +
+

{demoCopy.title}

+

{demoCopy.subtitle}

+
+ ) +} + +function StatusCard({ hasConflicts, stats }: { hasConflicts: boolean; stats: { totalConflicts: number; conflictsByType: Record } }) { + return ( + + + + + {demoCopy.statusTitle} + + + +
+ {demoCopy.conflictsLabel} + {hasConflicts ? ( + {stats.totalConflicts} + ) : ( + + + {demoCopy.noneLabel} + + )} +
+
+ {demoCopy.filesLabel} + {stats.conflictsByType.files || 0} +
+
+ {demoCopy.modelsLabel} + {stats.conflictsByType.models || 0} +
+
+
+ ) +} + +function DemoActionsCard({ + hasConflicts, + simulatingConflict, + onSimulate, + onDetect, + onResolveAll, + conflictSummary, +}: { + hasConflicts: boolean + simulatingConflict: boolean + onSimulate: () => void + onDetect: () => void + onResolveAll: () => void + conflictSummary: string +}) { + return ( + + + + + {demoCopy.demoActionsTitle} + + {demoCopy.demoActionsDescription} + + +
+ + + + + {hasConflicts && ( + <> + + + + )} +
+ + {hasConflicts && ( +
+
+ +
+

+ {conflictSummary} +

+

+ {demoCopy.navigateMessage} +

+
+
+
+ )} +
+
+ ) +} + +function ResolutionStrategiesCard() { + return ( + + + {demoCopy.resolutionStrategiesTitle} + {demoCopy.resolutionStrategiesDescription} + + +
+
+
+ + {demoCopy.strategyKeepLocalTitle} +
+

+ {demoCopy.strategyKeepLocalDescription} +

+
+ +
+
+ + {demoCopy.strategyKeepRemoteTitle} +
+

+ {demoCopy.strategyKeepRemoteDescription} +

+
+ +
+
+ + {demoCopy.strategyMergeBothTitle} +
+

+ {demoCopy.strategyMergeBothDescription} +

+
+ +
+
+ + {demoCopy.strategyManualEditTitle} +
+

+ {demoCopy.strategyManualEditDescription} +

+
+
+
+
+ ) +} + +function ConflictIndicatorCard() { + return ( + + + + + {demoCopy.indicatorTitle} + + + {demoCopy.indicatorDescription} + + + +
+
+ {demoCopy.badgeVariantLabel} + +
+
+ {demoCopy.compactVariantLabel} + +
+
+
+
+ ) +} + export function ConflictResolutionDemo() { const { hasConflicts, stats, detect, resolveAll } = useConflictResolution() const [simulatingConflict, setSimulatingConflict] = useState(false) + const conflictSummary = useMemo(() => { + const count = stats.totalConflicts + const label = count === 1 ? demoCopy.conflictSingular : demoCopy.conflictPlural + return `${count} ${label} ${demoCopy.detectedSuffix}` + }, [stats.totalConflicts]) + const simulateConflict = async () => { setSimulatingConflict(true) try { @@ -32,14 +246,14 @@ export function ConflictResolutionDemo() { } await db.put('files', testFile) - - toast.info('Simulated local file created. Now simulating a remote conflict...') - + + toast.info(demoCopy.toastLocalCreated) + await new Promise(resolve => setTimeout(resolve, 1000)) - - toast.success('Conflict simulation complete! Click "Detect Conflicts" to see it.') + + toast.success(demoCopy.toastSimulationComplete) } catch (err: any) { - toast.error(err.message || 'Failed to simulate conflict') + toast.error(err.message || demoCopy.toastSimulationError) } finally { setSimulatingConflict(false) } @@ -48,188 +262,32 @@ export function ConflictResolutionDemo() { const handleQuickResolveAll = async () => { try { await resolveAll('local') - toast.success('All conflicts resolved using local versions') + toast.success(demoCopy.toastResolveAllSuccess) } catch (err: any) { - toast.error(err.message || 'Failed to resolve conflicts') + toast.error(err.message || demoCopy.toastResolveAllError) } } return (
-
-

Conflict Resolution System

-

- Demo and test the conflict detection and resolution features -

-
+
- - - - - Status - - - -
- Conflicts: - {hasConflicts ? ( - {stats.totalConflicts} - ) : ( - - - None - - )} -
-
- Files: - {stats.conflictsByType.files || 0} -
-
- Models: - {stats.conflictsByType.models || 0} -
-
-
+ - - - - - Demo Actions - - Test the conflict resolution workflow - - -
- - - - - {hasConflicts && ( - <> - - - - )} -
- - {hasConflicts && ( -
-
- -
-

- {stats.totalConflicts} conflict{stats.totalConflicts === 1 ? '' : 's'} detected -

-

- Navigate to the Conflict Resolution page to review and resolve them -

-
-
-
- )} -
-
+
- - - Resolution Strategies - Available approaches for handling conflicts - - -
-
-
- - Keep Local -
-

- Preserve the local version and discard remote changes -

-
+ -
-
- - Keep Remote -
-

- Accept the remote version and overwrite local changes -

-
- -
-
- - Merge Both -
-

- Combine local and remote changes into a single version -

-
- -
-
- - Manual Edit -
-

- Manually edit the conflicting data to create a custom resolution -

-
-
-
-
- - - - - - Conflict Indicator Component - - - The conflict indicator can be placed anywhere in the UI to show active conflicts - - - -
-
- Badge variant: - -
-
- Compact variant: - -
-
-
-
+
) } diff --git a/src/data/conflict-resolution-copy.json b/src/data/conflict-resolution-copy.json new file mode 100644 index 0000000..0eed87c --- /dev/null +++ b/src/data/conflict-resolution-copy.json @@ -0,0 +1,68 @@ +{ + "card": { + "timeDifferenceSuffix": "m difference", + "localVersionLabel": "Local Version", + "remoteVersionLabel": "Remote Version", + "newerLabel": "Newer", + "keepLocal": "Keep Local", + "keepRemote": "Keep Remote", + "mergeBoth": "Merge Both", + "details": "Details" + }, + "detailsDialog": { + "title": "Conflict Details", + "localVersionLabel": "Local Version", + "remoteVersionLabel": "Remote Version", + "newerLabel": "Newer", + "differencesLabel": "Differences", + "localTabLabel": "Local", + "remoteTabLabel": "Remote", + "conflictBadge": "Conflict", + "matchBadge": "Match", + "localFieldLabel": "Local:", + "remoteFieldLabel": "Remote:", + "onlyInLocal": "Only in local", + "onlyInRemote": "Only in remote", + "cancel": "Cancel", + "keepLocal": "Keep Local", + "keepRemote": "Keep Remote", + "mergeBoth": "Merge Both" + }, + "demo": { + "title": "Conflict Resolution System", + "subtitle": "Demo and test the conflict detection and resolution features", + "statusTitle": "Status", + "conflictsLabel": "Conflicts:", + "filesLabel": "Files:", + "modelsLabel": "Models:", + "noneLabel": "None", + "demoActionsTitle": "Demo Actions", + "demoActionsDescription": "Test the conflict resolution workflow", + "simulateConflict": "Simulate Conflict", + "detectConflicts": "Detect Conflicts", + "resolveAllLocal": "Resolve All (Local)", + "conflictSingular": "conflict", + "conflictPlural": "conflicts", + "detectedSuffix": "detected", + "navigateMessage": "Navigate to the Conflict Resolution page to review and resolve them", + "resolutionStrategiesTitle": "Resolution Strategies", + "resolutionStrategiesDescription": "Available approaches for handling conflicts", + "strategyKeepLocalTitle": "Keep Local", + "strategyKeepLocalDescription": "Preserve the local version and discard remote changes", + "strategyKeepRemoteTitle": "Keep Remote", + "strategyKeepRemoteDescription": "Accept the remote version and overwrite local changes", + "strategyMergeBothTitle": "Merge Both", + "strategyMergeBothDescription": "Combine local and remote changes into a single version", + "strategyManualEditTitle": "Manual Edit", + "strategyManualEditDescription": "Manually edit the conflicting data to create a custom resolution", + "indicatorTitle": "Conflict Indicator Component", + "indicatorDescription": "The conflict indicator can be placed anywhere in the UI to show active conflicts", + "badgeVariantLabel": "Badge variant:", + "compactVariantLabel": "Compact variant:", + "toastLocalCreated": "Simulated local file created. Now simulating a remote conflict...", + "toastSimulationComplete": "Conflict simulation complete! Click \"Detect Conflicts\" to see it.", + "toastSimulationError": "Failed to simulate conflict", + "toastResolveAllSuccess": "All conflicts resolved using local versions", + "toastResolveAllError": "Failed to resolve conflicts" + } +}