mirror of
https://github.com/johndoe6345789/low-code-react-app-b.git
synced 2026-04-24 13:44:54 +00:00
Refactor conflict resolution components
This commit is contained in:
@@ -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<string, unknown>
|
||||
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 <Code size={20} weight="duotone" className="text-primary" />
|
||||
case 'models':
|
||||
return <Database size={20} weight="duotone" className="text-accent" />
|
||||
default:
|
||||
return <Database size={20} weight="duotone" className="text-muted-foreground" />
|
||||
}
|
||||
}
|
||||
|
||||
function ConflictCardHeader({ conflict, expanded, onToggle, timeDiffMinutes }: ConflictCardHeaderProps) {
|
||||
return (
|
||||
<CardHeader className="pb-3">
|
||||
<div className="flex items-start justify-between gap-4">
|
||||
<div className="flex items-start gap-3 flex-1 min-w-0">
|
||||
<div className="mt-0.5">{getEntityIcon(conflict.entityType)}</div>
|
||||
<div className="flex-1 min-w-0">
|
||||
<CardTitle className="text-base font-mono truncate">
|
||||
{conflict.id}
|
||||
</CardTitle>
|
||||
<CardDescription className="flex items-center gap-2 mt-1">
|
||||
<Badge variant="outline" className="text-xs">
|
||||
{conflict.entityType}
|
||||
</Badge>
|
||||
<span className="text-xs text-muted-foreground">
|
||||
{timeDiffMinutes}
|
||||
{cardCopy.timeDifferenceSuffix}
|
||||
</span>
|
||||
</CardDescription>
|
||||
</div>
|
||||
</div>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
onClick={onToggle}
|
||||
>
|
||||
{expanded ? <CaretDown size={16} /> : <CaretRight size={16} />}
|
||||
</Button>
|
||||
</div>
|
||||
</CardHeader>
|
||||
)
|
||||
}
|
||||
|
||||
function ConflictVersionPanel({
|
||||
label,
|
||||
timestamp,
|
||||
version,
|
||||
icon,
|
||||
highlightBadge,
|
||||
accentClassName,
|
||||
}: ConflictVersionPanelProps) {
|
||||
return (
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center gap-2">
|
||||
{icon}
|
||||
<h4 className="text-sm font-medium">{label}</h4>
|
||||
{highlightBadge && (
|
||||
<Badge variant="secondary" className="text-xs">
|
||||
{cardCopy.newerLabel}
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
<div className="bg-muted/50 rounded-md p-3 space-y-1">
|
||||
<div className="flex items-center gap-1.5 text-xs text-muted-foreground">
|
||||
<Clock size={12} />
|
||||
{format(new Date(timestamp), 'MMM d, h:mm a')}
|
||||
</div>
|
||||
<pre className={`text-xs overflow-hidden text-ellipsis ${accentClassName ?? ''}`}>
|
||||
{JSON.stringify(version, null, 2).slice(0, 200)}...
|
||||
</pre>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function ConflictCardActions({ conflict, onResolve, onViewDetails, isResolving }: ConflictCardActionsProps) {
|
||||
return (
|
||||
<div className="flex flex-wrap gap-2">
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
onClick={() => onResolve(conflict.id, 'local')}
|
||||
disabled={isResolving}
|
||||
className="flex-1 min-w-[120px]"
|
||||
>
|
||||
<Database size={16} />
|
||||
{cardCopy.keepLocal}
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
onClick={() => onResolve(conflict.id, 'remote')}
|
||||
disabled={isResolving}
|
||||
className="flex-1 min-w-[120px]"
|
||||
>
|
||||
<Cloud size={16} />
|
||||
{cardCopy.keepRemote}
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
onClick={() => onResolve(conflict.id, 'merge')}
|
||||
disabled={isResolving}
|
||||
className="flex-1 min-w-[120px]"
|
||||
>
|
||||
<ArrowsLeftRight size={16} />
|
||||
{cardCopy.mergeBoth}
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="secondary"
|
||||
onClick={() => onViewDetails(conflict)}
|
||||
disabled={isResolving}
|
||||
>
|
||||
<MagnifyingGlass size={16} />
|
||||
{cardCopy.details}
|
||||
</Button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
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 <Code size={20} weight="duotone" className="text-primary" />
|
||||
case 'models':
|
||||
return <Database size={20} weight="duotone" className="text-accent" />
|
||||
default:
|
||||
return <Database size={20} weight="duotone" className="text-muted-foreground" />
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<motion.div
|
||||
layout
|
||||
@@ -53,33 +188,12 @@ export function ConflictCard({ conflict, onResolve, onViewDetails, isResolving }
|
||||
transition={{ duration: 0.2 }}
|
||||
>
|
||||
<Card className="border-destructive/30 hover:border-destructive/50 transition-colors">
|
||||
<CardHeader className="pb-3">
|
||||
<div className="flex items-start justify-between gap-4">
|
||||
<div className="flex items-start gap-3 flex-1 min-w-0">
|
||||
<div className="mt-0.5">{getEntityIcon()}</div>
|
||||
<div className="flex-1 min-w-0">
|
||||
<CardTitle className="text-base font-mono truncate">
|
||||
{conflict.id}
|
||||
</CardTitle>
|
||||
<CardDescription className="flex items-center gap-2 mt-1">
|
||||
<Badge variant="outline" className="text-xs">
|
||||
{conflict.entityType}
|
||||
</Badge>
|
||||
<span className="text-xs text-muted-foreground">
|
||||
{timeDiffMinutes}m difference
|
||||
</span>
|
||||
</CardDescription>
|
||||
</div>
|
||||
</div>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
onClick={() => setExpanded(!expanded)}
|
||||
>
|
||||
{expanded ? <CaretDown size={16} /> : <CaretRight size={16} />}
|
||||
</Button>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<ConflictCardHeader
|
||||
conflict={conflict}
|
||||
expanded={expanded}
|
||||
onToggle={() => setExpanded(!expanded)}
|
||||
timeDiffMinutes={timeDiffMinutes}
|
||||
/>
|
||||
|
||||
<AnimatePresence>
|
||||
{expanded && (
|
||||
@@ -93,92 +207,31 @@ export function ConflictCard({ conflict, onResolve, onViewDetails, isResolving }
|
||||
<Separator />
|
||||
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<Database size={16} className="text-primary" />
|
||||
<h4 className="text-sm font-medium">Local Version</h4>
|
||||
{isLocalNewer && (
|
||||
<Badge variant="secondary" className="text-xs">
|
||||
Newer
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
<div className="bg-muted/50 rounded-md p-3 space-y-1">
|
||||
<div className="flex items-center gap-1.5 text-xs text-muted-foreground">
|
||||
<Clock size={12} />
|
||||
{format(new Date(conflict.localTimestamp), 'MMM d, h:mm a')}
|
||||
</div>
|
||||
<pre className="text-xs overflow-hidden text-ellipsis">
|
||||
{JSON.stringify(conflict.localVersion, null, 2).slice(0, 200)}...
|
||||
</pre>
|
||||
</div>
|
||||
</div>
|
||||
<ConflictVersionPanel
|
||||
label={cardCopy.localVersionLabel}
|
||||
timestamp={conflict.localTimestamp}
|
||||
version={conflict.localVersion}
|
||||
icon={<Database size={16} className="text-primary" />}
|
||||
highlightBadge={isLocalNewer}
|
||||
/>
|
||||
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<Cloud size={16} className="text-accent" />
|
||||
<h4 className="text-sm font-medium">Remote Version</h4>
|
||||
{!isLocalNewer && (
|
||||
<Badge variant="secondary" className="text-xs">
|
||||
Newer
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
<div className="bg-muted/50 rounded-md p-3 space-y-1">
|
||||
<div className="flex items-center gap-1.5 text-xs text-muted-foreground">
|
||||
<Clock size={12} />
|
||||
{format(new Date(conflict.remoteTimestamp), 'MMM d, h:mm a')}
|
||||
</div>
|
||||
<pre className="text-xs overflow-hidden text-ellipsis">
|
||||
{JSON.stringify(conflict.remoteVersion, null, 2).slice(0, 200)}...
|
||||
</pre>
|
||||
</div>
|
||||
</div>
|
||||
<ConflictVersionPanel
|
||||
label={cardCopy.remoteVersionLabel}
|
||||
timestamp={conflict.remoteTimestamp}
|
||||
version={conflict.remoteVersion}
|
||||
icon={<Cloud size={16} className="text-accent" />}
|
||||
highlightBadge={!isLocalNewer}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<Separator />
|
||||
|
||||
<div className="flex flex-wrap gap-2">
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
onClick={() => onResolve(conflict.id, 'local')}
|
||||
disabled={isResolving}
|
||||
className="flex-1 min-w-[120px]"
|
||||
>
|
||||
<Database size={16} />
|
||||
Keep Local
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
onClick={() => onResolve(conflict.id, 'remote')}
|
||||
disabled={isResolving}
|
||||
className="flex-1 min-w-[120px]"
|
||||
>
|
||||
<Cloud size={16} />
|
||||
Keep Remote
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
onClick={() => onResolve(conflict.id, 'merge')}
|
||||
disabled={isResolving}
|
||||
className="flex-1 min-w-[120px]"
|
||||
>
|
||||
<ArrowsLeftRight size={16} />
|
||||
Merge Both
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="secondary"
|
||||
onClick={() => onViewDetails(conflict)}
|
||||
disabled={isResolving}
|
||||
>
|
||||
<MagnifyingGlass size={16} />
|
||||
Details
|
||||
</Button>
|
||||
</div>
|
||||
<ConflictCardActions
|
||||
conflict={conflict}
|
||||
onResolve={onResolve}
|
||||
onViewDetails={onViewDetails}
|
||||
isResolving={isResolving}
|
||||
/>
|
||||
</CardContent>
|
||||
</motion.div>
|
||||
)}
|
||||
|
||||
@@ -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 (
|
||||
<DialogHeader>
|
||||
<DialogTitle className="font-mono text-lg">{dialogCopy.title}</DialogTitle>
|
||||
<DialogDescription className="flex items-center gap-2">
|
||||
<Badge variant="outline">{conflict.entityType}</Badge>
|
||||
<span>{conflict.id}</span>
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
)
|
||||
}
|
||||
|
||||
function ConflictVersionSummary({ conflict, isLocalNewer }: { conflict: ConflictItem; isLocalNewer: boolean }) {
|
||||
return (
|
||||
<div className="grid grid-cols-2 gap-4 py-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<Database size={20} className="text-primary" />
|
||||
<div className="flex-1">
|
||||
<div className="text-sm font-medium">{dialogCopy.localVersionLabel}</div>
|
||||
<div className="flex items-center gap-1.5 text-xs text-muted-foreground">
|
||||
<Clock size={12} />
|
||||
{format(new Date(conflict.localTimestamp), 'PPp')}
|
||||
</div>
|
||||
</div>
|
||||
{isLocalNewer && (
|
||||
<Badge variant="secondary" className="text-xs">
|
||||
{dialogCopy.newerLabel}
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-2">
|
||||
<Cloud size={20} className="text-accent" />
|
||||
<div className="flex-1">
|
||||
<div className="text-sm font-medium">{dialogCopy.remoteVersionLabel}</div>
|
||||
<div className="flex items-center gap-1.5 text-xs text-muted-foreground">
|
||||
<Clock size={12} />
|
||||
{format(new Date(conflict.remoteTimestamp), 'PPp')}
|
||||
</div>
|
||||
</div>
|
||||
{!isLocalNewer && (
|
||||
<Badge variant="secondary" className="text-xs">
|
||||
{dialogCopy.newerLabel}
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function DiffItemCard({ item }: { item: ConflictDiffItem }) {
|
||||
return (
|
||||
<div
|
||||
key={item.key}
|
||||
className={`p-3 rounded-md border ${
|
||||
item.isDifferent
|
||||
? 'border-destructive/30 bg-destructive/5'
|
||||
: 'border-border bg-muted/30'
|
||||
}`}
|
||||
>
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<span className="font-mono text-sm font-medium">{item.key}</span>
|
||||
{item.isDifferent && (
|
||||
<Badge variant="destructive" className="text-xs">
|
||||
{dialogCopy.conflictBadge}
|
||||
</Badge>
|
||||
)}
|
||||
{!item.isDifferent && (
|
||||
<Badge variant="secondary" className="text-xs">
|
||||
<CheckCircle size={12} />
|
||||
{dialogCopy.matchBadge}
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 gap-3 text-xs font-mono">
|
||||
<div>
|
||||
<div className="text-muted-foreground mb-1">{dialogCopy.localFieldLabel}</div>
|
||||
<div className={item.onlyInLocal ? 'text-primary font-medium' : ''}>
|
||||
{item.onlyInLocal ? (
|
||||
<Badge variant="outline" className="text-xs">{dialogCopy.onlyInLocal}</Badge>
|
||||
) : (
|
||||
<pre className="whitespace-pre-wrap break-words">
|
||||
{JSON.stringify(item.localValue, null, 2)}
|
||||
</pre>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div className="text-muted-foreground mb-1">{dialogCopy.remoteFieldLabel}</div>
|
||||
<div className={item.onlyInRemote ? 'text-accent font-medium' : ''}>
|
||||
{item.onlyInRemote ? (
|
||||
<Badge variant="outline" className="text-xs">{dialogCopy.onlyInRemote}</Badge>
|
||||
) : (
|
||||
<pre className="whitespace-pre-wrap break-words">
|
||||
{JSON.stringify(item.remoteValue, null, 2)}
|
||||
</pre>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function DiffTabContent({ diff }: { diff: ConflictDiffItem[] }) {
|
||||
return (
|
||||
<TabsContent value="diff" className="flex-1 min-h-0">
|
||||
<ScrollArea className="h-[400px] rounded-md border">
|
||||
<div className="p-4 space-y-2">
|
||||
{diff.map((item) => (
|
||||
<DiffItemCard key={item.key} item={item} />
|
||||
))}
|
||||
</div>
|
||||
</ScrollArea>
|
||||
</TabsContent>
|
||||
)
|
||||
}
|
||||
|
||||
function JsonTabContent({ value, json }: { value: ConflictTab; json: string }) {
|
||||
return (
|
||||
<TabsContent value={value} className="flex-1 min-h-0">
|
||||
<ScrollArea className="h-[400px] rounded-md border">
|
||||
<pre className="p-4 text-xs font-mono">{json}</pre>
|
||||
</ScrollArea>
|
||||
</TabsContent>
|
||||
)
|
||||
}
|
||||
|
||||
function ResolutionFooter({
|
||||
conflict,
|
||||
isResolving,
|
||||
onOpenChange,
|
||||
onResolve,
|
||||
}: {
|
||||
conflict: ConflictItem
|
||||
isResolving: boolean
|
||||
onOpenChange: (open: boolean) => void
|
||||
onResolve: (conflictId: string, strategy: 'local' | 'remote' | 'merge') => void
|
||||
}) {
|
||||
return (
|
||||
<div className="flex justify-between gap-2">
|
||||
<Button variant="outline" onClick={() => onOpenChange(false)}>
|
||||
{dialogCopy.cancel}
|
||||
</Button>
|
||||
<div className="flex gap-2">
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => {
|
||||
onResolve(conflict.id, 'local')
|
||||
onOpenChange(false)
|
||||
}}
|
||||
disabled={isResolving}
|
||||
>
|
||||
<Database size={16} />
|
||||
{dialogCopy.keepLocal}
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => {
|
||||
onResolve(conflict.id, 'remote')
|
||||
onOpenChange(false)
|
||||
}}
|
||||
disabled={isResolving}
|
||||
>
|
||||
<Cloud size={16} />
|
||||
{dialogCopy.keepRemote}
|
||||
</Button>
|
||||
<Button
|
||||
onClick={() => {
|
||||
onResolve(conflict.id, 'merge')
|
||||
onOpenChange(false)
|
||||
}}
|
||||
disabled={isResolving}
|
||||
>
|
||||
<ArrowsLeftRight size={16} />
|
||||
{dialogCopy.mergeBoth}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
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<ConflictTab>('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 (
|
||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||
<DialogContent className="max-w-4xl max-h-[90vh] flex flex-col">
|
||||
<DialogHeader>
|
||||
<DialogTitle className="font-mono text-lg">Conflict Details</DialogTitle>
|
||||
<DialogDescription className="flex items-center gap-2">
|
||||
<Badge variant="outline">{conflict.entityType}</Badge>
|
||||
<span>{conflict.id}</span>
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<ConflictDetailsHeader conflict={conflict} />
|
||||
|
||||
<div className="grid grid-cols-2 gap-4 py-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<Database size={20} className="text-primary" />
|
||||
<div className="flex-1">
|
||||
<div className="text-sm font-medium">Local Version</div>
|
||||
<div className="flex items-center gap-1.5 text-xs text-muted-foreground">
|
||||
<Clock size={12} />
|
||||
{format(new Date(conflict.localTimestamp), 'PPp')}
|
||||
</div>
|
||||
</div>
|
||||
{isLocalNewer && (
|
||||
<Badge variant="secondary" className="text-xs">
|
||||
Newer
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-2">
|
||||
<Cloud size={20} className="text-accent" />
|
||||
<div className="flex-1">
|
||||
<div className="text-sm font-medium">Remote Version</div>
|
||||
<div className="flex items-center gap-1.5 text-xs text-muted-foreground">
|
||||
<Clock size={12} />
|
||||
{format(new Date(conflict.remoteTimestamp), 'PPp')}
|
||||
</div>
|
||||
</div>
|
||||
{!isLocalNewer && (
|
||||
<Badge variant="secondary" className="text-xs">
|
||||
Newer
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<ConflictVersionSummary conflict={conflict} isLocalNewer={isLocalNewer} />
|
||||
|
||||
<Separator />
|
||||
|
||||
<Tabs value={activeTab} onValueChange={(v) => setActiveTab(v as any)} className="flex-1 flex flex-col min-h-0">
|
||||
<Tabs value={activeTab} onValueChange={(value) => setActiveTab(value as ConflictTab)} className="flex-1 flex flex-col min-h-0">
|
||||
<TabsList className="grid grid-cols-3 w-full">
|
||||
<TabsTrigger value="diff" className="gap-2">
|
||||
<ArrowsLeftRight size={16} />
|
||||
Differences ({conflictingKeys.length})
|
||||
{dialogCopy.differencesLabel} ({conflictingKeys.length})
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="local" className="gap-2">
|
||||
<Database size={16} />
|
||||
Local
|
||||
{dialogCopy.localTabLabel}
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="remote" className="gap-2">
|
||||
<Cloud size={16} />
|
||||
Remote
|
||||
{dialogCopy.remoteTabLabel}
|
||||
</TabsTrigger>
|
||||
</TabsList>
|
||||
|
||||
<TabsContent value="diff" className="flex-1 min-h-0">
|
||||
<ScrollArea className="h-[400px] rounded-md border">
|
||||
<div className="p-4 space-y-2">
|
||||
{diff.map((item) => (
|
||||
<div
|
||||
key={item.key}
|
||||
className={`p-3 rounded-md border ${
|
||||
item.isDifferent
|
||||
? 'border-destructive/30 bg-destructive/5'
|
||||
: 'border-border bg-muted/30'
|
||||
}`}
|
||||
>
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<span className="font-mono text-sm font-medium">{item.key}</span>
|
||||
{item.isDifferent && (
|
||||
<Badge variant="destructive" className="text-xs">
|
||||
Conflict
|
||||
</Badge>
|
||||
)}
|
||||
{!item.isDifferent && (
|
||||
<Badge variant="secondary" className="text-xs">
|
||||
<CheckCircle size={12} />
|
||||
Match
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
<DiffTabContent diff={diff} />
|
||||
|
||||
<div className="grid grid-cols-2 gap-3 text-xs font-mono">
|
||||
<div>
|
||||
<div className="text-muted-foreground mb-1">Local:</div>
|
||||
<div className={item.onlyInLocal ? 'text-primary font-medium' : ''}>
|
||||
{item.onlyInLocal ? (
|
||||
<Badge variant="outline" className="text-xs">Only in local</Badge>
|
||||
) : (
|
||||
<pre className="whitespace-pre-wrap break-words">
|
||||
{JSON.stringify(item.localValue, null, 2)}
|
||||
</pre>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<JsonTabContent value="local" json={localJson} />
|
||||
|
||||
<div>
|
||||
<div className="text-muted-foreground mb-1">Remote:</div>
|
||||
<div className={item.onlyInRemote ? 'text-accent font-medium' : ''}>
|
||||
{item.onlyInRemote ? (
|
||||
<Badge variant="outline" className="text-xs">Only in remote</Badge>
|
||||
) : (
|
||||
<pre className="whitespace-pre-wrap break-words">
|
||||
{JSON.stringify(item.remoteValue, null, 2)}
|
||||
</pre>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</ScrollArea>
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="local" className="flex-1 min-h-0">
|
||||
<ScrollArea className="h-[400px] rounded-md border">
|
||||
<pre className="p-4 text-xs font-mono">{localJson}</pre>
|
||||
</ScrollArea>
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="remote" className="flex-1 min-h-0">
|
||||
<ScrollArea className="h-[400px] rounded-md border">
|
||||
<pre className="p-4 text-xs font-mono">{remoteJson}</pre>
|
||||
</ScrollArea>
|
||||
</TabsContent>
|
||||
<JsonTabContent value="remote" json={remoteJson} />
|
||||
</Tabs>
|
||||
|
||||
<Separator />
|
||||
|
||||
<div className="flex justify-between gap-2">
|
||||
<Button variant="outline" onClick={() => onOpenChange(false)}>
|
||||
Cancel
|
||||
</Button>
|
||||
<div className="flex gap-2">
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => {
|
||||
onResolve(conflict.id, 'local')
|
||||
onOpenChange(false)
|
||||
}}
|
||||
disabled={isResolving}
|
||||
>
|
||||
<Database size={16} />
|
||||
Keep Local
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => {
|
||||
onResolve(conflict.id, 'remote')
|
||||
onOpenChange(false)
|
||||
}}
|
||||
disabled={isResolving}
|
||||
>
|
||||
<Cloud size={16} />
|
||||
Keep Remote
|
||||
</Button>
|
||||
<Button
|
||||
onClick={() => {
|
||||
onResolve(conflict.id, 'merge')
|
||||
onOpenChange(false)
|
||||
}}
|
||||
disabled={isResolving}
|
||||
>
|
||||
<ArrowsLeftRight size={16} />
|
||||
Merge Both
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<ResolutionFooter
|
||||
conflict={conflict}
|
||||
isResolving={isResolving}
|
||||
onOpenChange={onOpenChange}
|
||||
onResolve={onResolve}
|
||||
/>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
)
|
||||
|
||||
@@ -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 (
|
||||
<div>
|
||||
<h2 className="text-2xl font-bold font-mono mb-2">{demoCopy.title}</h2>
|
||||
<p className="text-muted-foreground">{demoCopy.subtitle}</p>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function StatusCard({ hasConflicts, stats }: { hasConflicts: boolean; stats: { totalConflicts: number; conflictsByType: Record<string, number> } }) {
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader className="pb-3">
|
||||
<CardTitle className="text-base flex items-center gap-2">
|
||||
<Database size={20} className="text-primary" weight="duotone" />
|
||||
{demoCopy.statusTitle}
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-2">
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-sm text-muted-foreground">{demoCopy.conflictsLabel}</span>
|
||||
{hasConflicts ? (
|
||||
<Badge variant="destructive">{stats.totalConflicts}</Badge>
|
||||
) : (
|
||||
<Badge variant="secondary" className="gap-1">
|
||||
<CheckCircle size={12} />
|
||||
{demoCopy.noneLabel}
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-sm text-muted-foreground">{demoCopy.filesLabel}</span>
|
||||
<span className="text-sm font-medium">{stats.conflictsByType.files || 0}</span>
|
||||
</div>
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-sm text-muted-foreground">{demoCopy.modelsLabel}</span>
|
||||
<span className="text-sm font-medium">{stats.conflictsByType.models || 0}</span>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
|
||||
function DemoActionsCard({
|
||||
hasConflicts,
|
||||
simulatingConflict,
|
||||
onSimulate,
|
||||
onDetect,
|
||||
onResolveAll,
|
||||
conflictSummary,
|
||||
}: {
|
||||
hasConflicts: boolean
|
||||
simulatingConflict: boolean
|
||||
onSimulate: () => void
|
||||
onDetect: () => void
|
||||
onResolveAll: () => void
|
||||
conflictSummary: string
|
||||
}) {
|
||||
return (
|
||||
<Card className="md:col-span-2">
|
||||
<CardHeader className="pb-3">
|
||||
<CardTitle className="text-base flex items-center gap-2">
|
||||
<Flask size={20} className="text-accent" weight="duotone" />
|
||||
{demoCopy.demoActionsTitle}
|
||||
</CardTitle>
|
||||
<CardDescription>{demoCopy.demoActionsDescription}</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-3">
|
||||
<div className="flex flex-wrap gap-2">
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
onClick={onSimulate}
|
||||
disabled={simulatingConflict}
|
||||
>
|
||||
<Warning size={16} />
|
||||
{demoCopy.simulateConflict}
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
onClick={onDetect}
|
||||
>
|
||||
<ArrowsClockwise size={16} />
|
||||
{demoCopy.detectConflicts}
|
||||
</Button>
|
||||
|
||||
{hasConflicts && (
|
||||
<>
|
||||
<Separator orientation="vertical" className="h-8" />
|
||||
<Button
|
||||
size="sm"
|
||||
variant="destructive"
|
||||
onClick={onResolveAll}
|
||||
>
|
||||
<CheckCircle size={16} />
|
||||
{demoCopy.resolveAllLocal}
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{hasConflicts && (
|
||||
<div className="mt-4 p-3 bg-destructive/10 rounded-md border border-destructive/30">
|
||||
<div className="flex items-start gap-2">
|
||||
<Warning size={20} className="text-destructive mt-0.5" weight="fill" />
|
||||
<div className="flex-1">
|
||||
<p className="text-sm font-medium text-destructive">
|
||||
{conflictSummary}
|
||||
</p>
|
||||
<p className="text-xs text-muted-foreground mt-1">
|
||||
{demoCopy.navigateMessage}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
|
||||
function ResolutionStrategiesCard() {
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="text-base">{demoCopy.resolutionStrategiesTitle}</CardTitle>
|
||||
<CardDescription>{demoCopy.resolutionStrategiesDescription}</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-3">
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-3">
|
||||
<div className="p-3 rounded-md border bg-card">
|
||||
<div className="flex items-center gap-2 mb-1">
|
||||
<Database size={16} className="text-primary" />
|
||||
<span className="font-medium text-sm">{demoCopy.strategyKeepLocalTitle}</span>
|
||||
</div>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{demoCopy.strategyKeepLocalDescription}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="p-3 rounded-md border bg-card">
|
||||
<div className="flex items-center gap-2 mb-1">
|
||||
<Flask size={16} className="text-accent" />
|
||||
<span className="font-medium text-sm">{demoCopy.strategyKeepRemoteTitle}</span>
|
||||
</div>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{demoCopy.strategyKeepRemoteDescription}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="p-3 rounded-md border bg-card">
|
||||
<div className="flex items-center gap-2 mb-1">
|
||||
<ArrowsClockwise size={16} className="text-primary" />
|
||||
<span className="font-medium text-sm">{demoCopy.strategyMergeBothTitle}</span>
|
||||
</div>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{demoCopy.strategyMergeBothDescription}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="p-3 rounded-md border bg-card">
|
||||
<div className="flex items-center gap-2 mb-1">
|
||||
<CheckCircle size={16} className="text-accent" />
|
||||
<span className="font-medium text-sm">{demoCopy.strategyManualEditTitle}</span>
|
||||
</div>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{demoCopy.strategyManualEditDescription}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
|
||||
function ConflictIndicatorCard() {
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="text-base flex items-center gap-2">
|
||||
<ConflictIndicator variant="compact" showLabel={false} />
|
||||
{demoCopy.indicatorTitle}
|
||||
</CardTitle>
|
||||
<CardDescription>
|
||||
{demoCopy.indicatorDescription}
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="flex flex-wrap items-center gap-4">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-sm text-muted-foreground">{demoCopy.badgeVariantLabel}</span>
|
||||
<ConflictIndicator variant="badge" showLabel={true} />
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-sm text-muted-foreground">{demoCopy.compactVariantLabel}</span>
|
||||
<ConflictIndicator variant="compact" />
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
|
||||
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 (
|
||||
<div className="space-y-6 p-6">
|
||||
<div>
|
||||
<h2 className="text-2xl font-bold font-mono mb-2">Conflict Resolution System</h2>
|
||||
<p className="text-muted-foreground">
|
||||
Demo and test the conflict detection and resolution features
|
||||
</p>
|
||||
</div>
|
||||
<DemoHeader />
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||
<Card>
|
||||
<CardHeader className="pb-3">
|
||||
<CardTitle className="text-base flex items-center gap-2">
|
||||
<Database size={20} className="text-primary" weight="duotone" />
|
||||
Status
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-2">
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-sm text-muted-foreground">Conflicts:</span>
|
||||
{hasConflicts ? (
|
||||
<Badge variant="destructive">{stats.totalConflicts}</Badge>
|
||||
) : (
|
||||
<Badge variant="secondary" className="gap-1">
|
||||
<CheckCircle size={12} />
|
||||
None
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-sm text-muted-foreground">Files:</span>
|
||||
<span className="text-sm font-medium">{stats.conflictsByType.files || 0}</span>
|
||||
</div>
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-sm text-muted-foreground">Models:</span>
|
||||
<span className="text-sm font-medium">{stats.conflictsByType.models || 0}</span>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<StatusCard hasConflicts={hasConflicts} stats={stats} />
|
||||
|
||||
<Card className="md:col-span-2">
|
||||
<CardHeader className="pb-3">
|
||||
<CardTitle className="text-base flex items-center gap-2">
|
||||
<Flask size={20} className="text-accent" weight="duotone" />
|
||||
Demo Actions
|
||||
</CardTitle>
|
||||
<CardDescription>Test the conflict resolution workflow</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-3">
|
||||
<div className="flex flex-wrap gap-2">
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
onClick={simulateConflict}
|
||||
disabled={simulatingConflict}
|
||||
>
|
||||
<Warning size={16} />
|
||||
Simulate Conflict
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
onClick={() => detect()}
|
||||
>
|
||||
<ArrowsClockwise size={16} />
|
||||
Detect Conflicts
|
||||
</Button>
|
||||
|
||||
{hasConflicts && (
|
||||
<>
|
||||
<Separator orientation="vertical" className="h-8" />
|
||||
<Button
|
||||
size="sm"
|
||||
variant="destructive"
|
||||
onClick={handleQuickResolveAll}
|
||||
>
|
||||
<CheckCircle size={16} />
|
||||
Resolve All (Local)
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{hasConflicts && (
|
||||
<div className="mt-4 p-3 bg-destructive/10 rounded-md border border-destructive/30">
|
||||
<div className="flex items-start gap-2">
|
||||
<Warning size={20} className="text-destructive mt-0.5" weight="fill" />
|
||||
<div className="flex-1">
|
||||
<p className="text-sm font-medium text-destructive">
|
||||
{stats.totalConflicts} conflict{stats.totalConflicts === 1 ? '' : 's'} detected
|
||||
</p>
|
||||
<p className="text-xs text-muted-foreground mt-1">
|
||||
Navigate to the Conflict Resolution page to review and resolve them
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
<DemoActionsCard
|
||||
hasConflicts={hasConflicts}
|
||||
simulatingConflict={simulatingConflict}
|
||||
onSimulate={simulateConflict}
|
||||
onDetect={detect}
|
||||
onResolveAll={handleQuickResolveAll}
|
||||
conflictSummary={conflictSummary}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="text-base">Resolution Strategies</CardTitle>
|
||||
<CardDescription>Available approaches for handling conflicts</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-3">
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-3">
|
||||
<div className="p-3 rounded-md border bg-card">
|
||||
<div className="flex items-center gap-2 mb-1">
|
||||
<Database size={16} className="text-primary" />
|
||||
<span className="font-medium text-sm">Keep Local</span>
|
||||
</div>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Preserve the local version and discard remote changes
|
||||
</p>
|
||||
</div>
|
||||
<ResolutionStrategiesCard />
|
||||
|
||||
<div className="p-3 rounded-md border bg-card">
|
||||
<div className="flex items-center gap-2 mb-1">
|
||||
<Flask size={16} className="text-accent" />
|
||||
<span className="font-medium text-sm">Keep Remote</span>
|
||||
</div>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Accept the remote version and overwrite local changes
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="p-3 rounded-md border bg-card">
|
||||
<div className="flex items-center gap-2 mb-1">
|
||||
<ArrowsClockwise size={16} className="text-primary" />
|
||||
<span className="font-medium text-sm">Merge Both</span>
|
||||
</div>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Combine local and remote changes into a single version
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="p-3 rounded-md border bg-card">
|
||||
<div className="flex items-center gap-2 mb-1">
|
||||
<CheckCircle size={16} className="text-accent" />
|
||||
<span className="font-medium text-sm">Manual Edit</span>
|
||||
</div>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Manually edit the conflicting data to create a custom resolution
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="text-base flex items-center gap-2">
|
||||
<ConflictIndicator variant="compact" showLabel={false} />
|
||||
Conflict Indicator Component
|
||||
</CardTitle>
|
||||
<CardDescription>
|
||||
The conflict indicator can be placed anywhere in the UI to show active conflicts
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="flex flex-wrap items-center gap-4">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-sm text-muted-foreground">Badge variant:</span>
|
||||
<ConflictIndicator variant="badge" showLabel={true} />
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-sm text-muted-foreground">Compact variant:</span>
|
||||
<ConflictIndicator variant="compact" />
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<ConflictIndicatorCard />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
68
src/data/conflict-resolution-copy.json
Normal file
68
src/data/conflict-resolution-copy.json
Normal file
@@ -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"
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user