mirror of
https://github.com/johndoe6345789/low-code-react-app-b.git
synced 2026-04-24 21:54:56 +00:00
Refactor conflict resolution page
This commit is contained in:
@@ -1,32 +1,18 @@
|
||||
import { useState, useEffect } from 'react'
|
||||
import { useConflictResolution } from '@/hooks/use-conflict-resolution'
|
||||
import { ConflictItem, ConflictResolutionStrategy } 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 {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from '@/components/ui/select'
|
||||
import { ConflictCard } from '@/components/ConflictCard'
|
||||
import { ConflictDetailsDialog } from '@/components/ConflictDetailsDialog'
|
||||
import {
|
||||
Warning,
|
||||
ArrowsClockwise,
|
||||
CheckCircle,
|
||||
XCircle,
|
||||
Database,
|
||||
Cloud,
|
||||
ArrowsLeftRight,
|
||||
Trash,
|
||||
MagnifyingGlass,
|
||||
} from '@phosphor-icons/react'
|
||||
import { motion, AnimatePresence } from 'framer-motion'
|
||||
import conflictResolutionCopy from '@/data/conflict-resolution.json'
|
||||
import { ConflictResolutionHeader } from '@/components/conflict-resolution/ConflictResolutionHeader'
|
||||
import { ConflictResolutionStatsSection } from '@/components/conflict-resolution/ConflictResolutionStats'
|
||||
import { ConflictResolutionBulkActions } from '@/components/conflict-resolution/ConflictResolutionBulkActions'
|
||||
import { ConflictResolutionFilters } from '@/components/conflict-resolution/ConflictResolutionFilters'
|
||||
import { ConflictResolutionList } from '@/components/conflict-resolution/ConflictResolutionList'
|
||||
import { ConflictResolutionError } from '@/components/conflict-resolution/ConflictResolutionError'
|
||||
import { ConflictResolutionDetails } from '@/components/conflict-resolution/ConflictResolutionDetails'
|
||||
import type {
|
||||
ConflictResolutionCopy,
|
||||
ConflictResolutionFilters as ConflictResolutionFilterType,
|
||||
} from '@/components/conflict-resolution/types'
|
||||
import { toast } from 'sonner'
|
||||
|
||||
export function ConflictResolutionPage() {
|
||||
@@ -47,7 +33,8 @@ export function ConflictResolutionPage() {
|
||||
|
||||
const [selectedConflict, setSelectedConflict] = useState<ConflictItem | null>(null)
|
||||
const [detailsDialogOpen, setDetailsDialogOpen] = useState(false)
|
||||
const [filterType, setFilterType] = useState<string>('all')
|
||||
const [filterType, setFilterType] = useState<ConflictResolutionFilterType>('all')
|
||||
const copy = conflictResolutionCopy as ConflictResolutionCopy
|
||||
|
||||
useEffect(() => {
|
||||
detect().catch(() => {})
|
||||
@@ -57,30 +44,34 @@ export function ConflictResolutionPage() {
|
||||
try {
|
||||
const detected = await detect()
|
||||
if (detected.length === 0) {
|
||||
toast.success('No conflicts detected')
|
||||
toast.success(copy.toasts.noConflictsDetected)
|
||||
} else {
|
||||
toast.info(`Found ${detected.length} conflict${detected.length === 1 ? '' : 's'}`)
|
||||
const label =
|
||||
detected.length === 1 ? copy.labels.conflictSingular : copy.labels.conflictPlural
|
||||
toast.info(copy.toasts.foundConflicts
|
||||
.replace('{count}', String(detected.length))
|
||||
.replace('{label}', label))
|
||||
}
|
||||
} catch (err: any) {
|
||||
toast.error(err.message || 'Failed to detect conflicts')
|
||||
toast.error(err.message || copy.toasts.detectFailed)
|
||||
}
|
||||
}
|
||||
|
||||
const handleResolve = async (conflictId: string, strategy: ConflictResolutionStrategy) => {
|
||||
try {
|
||||
await resolve(conflictId, strategy)
|
||||
toast.success(`Conflict resolved using ${strategy} version`)
|
||||
toast.success(copy.toasts.resolved.replace('{strategy}', strategy))
|
||||
} catch (err: any) {
|
||||
toast.error(err.message || 'Failed to resolve conflict')
|
||||
toast.error(err.message || copy.toasts.resolveFailed)
|
||||
}
|
||||
}
|
||||
|
||||
const handleResolveAll = async (strategy: ConflictResolutionStrategy) => {
|
||||
try {
|
||||
await resolveAll(strategy)
|
||||
toast.success(`All conflicts resolved using ${strategy} version`)
|
||||
toast.success(copy.toasts.resolvedAll.replace('{strategy}', strategy))
|
||||
} catch (err: any) {
|
||||
toast.error(err.message || 'Failed to resolve all conflicts')
|
||||
toast.error(err.message || copy.toasts.resolveAllFailed)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -97,253 +88,51 @@ export function ConflictResolutionPage() {
|
||||
<div className="h-full flex flex-col bg-background">
|
||||
<div className="flex-none border-b bg-card/50">
|
||||
<div className="p-6 space-y-4">
|
||||
<div className="flex items-start justify-between gap-4">
|
||||
<div>
|
||||
<h1 className="text-3xl font-bold font-mono tracking-tight">
|
||||
Conflict Resolution
|
||||
</h1>
|
||||
<p className="text-muted-foreground mt-1">
|
||||
Manage sync conflicts between local and remote data
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-2">
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
onClick={handleDetect}
|
||||
disabled={detectingConflicts}
|
||||
>
|
||||
<ArrowsClockwise size={16} className={detectingConflicts ? 'animate-spin' : ''} />
|
||||
Detect Conflicts
|
||||
</Button>
|
||||
|
||||
{hasConflicts && (
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
onClick={() => clear()}
|
||||
>
|
||||
<Trash size={16} />
|
||||
Clear All
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-4 gap-4">
|
||||
<Card>
|
||||
<CardContent className="pt-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<div className="text-2xl font-bold">{stats.totalConflicts}</div>
|
||||
<div className="text-xs text-muted-foreground">Total Conflicts</div>
|
||||
</div>
|
||||
<Warning size={24} className="text-destructive" weight="duotone" />
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardContent className="pt-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<div className="text-2xl font-bold">{stats.conflictsByType.files || 0}</div>
|
||||
<div className="text-xs text-muted-foreground">Files</div>
|
||||
</div>
|
||||
<Database size={24} className="text-primary" weight="duotone" />
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardContent className="pt-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<div className="text-2xl font-bold">{stats.conflictsByType.models || 0}</div>
|
||||
<div className="text-xs text-muted-foreground">Models</div>
|
||||
</div>
|
||||
<Database size={24} className="text-accent" weight="duotone" />
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardContent className="pt-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<div className="text-2xl font-bold">
|
||||
{(stats.conflictsByType.components || 0) +
|
||||
(stats.conflictsByType.workflows || 0)}
|
||||
</div>
|
||||
<div className="text-xs text-muted-foreground">Other</div>
|
||||
</div>
|
||||
<Cloud size={24} className="text-muted-foreground" weight="duotone" />
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
<ConflictResolutionHeader
|
||||
copy={copy}
|
||||
hasConflicts={hasConflicts}
|
||||
detectingConflicts={detectingConflicts}
|
||||
onDetect={handleDetect}
|
||||
onClear={clear}
|
||||
/>
|
||||
<ConflictResolutionStatsSection copy={copy} stats={stats} />
|
||||
{hasConflicts && (
|
||||
<Card className="border-destructive/30">
|
||||
<CardHeader className="pb-3">
|
||||
<CardTitle className="text-base flex items-center gap-2">
|
||||
<ArrowsLeftRight size={20} />
|
||||
Bulk Resolution
|
||||
</CardTitle>
|
||||
<CardDescription>
|
||||
Apply a resolution strategy to all conflicts at once
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="flex flex-wrap gap-2">
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
onClick={() => handleResolveAll('local')}
|
||||
disabled={detectingConflicts || !!resolvingConflict}
|
||||
>
|
||||
<Database size={16} />
|
||||
Keep All Local
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
onClick={() => handleResolveAll('remote')}
|
||||
disabled={detectingConflicts || !!resolvingConflict}
|
||||
>
|
||||
<Cloud size={16} />
|
||||
Keep All Remote
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
onClick={() => handleResolveAll('merge')}
|
||||
disabled={detectingConflicts || !!resolvingConflict}
|
||||
>
|
||||
<ArrowsLeftRight size={16} />
|
||||
Merge All
|
||||
</Button>
|
||||
|
||||
<Separator orientation="vertical" className="h-8 mx-2" />
|
||||
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-sm text-muted-foreground">Auto-resolve:</span>
|
||||
<Select
|
||||
value={autoResolveStrategy || 'none'}
|
||||
onValueChange={(value) =>
|
||||
setAutoResolve(value === 'none' ? null : value as ConflictResolutionStrategy)
|
||||
}
|
||||
>
|
||||
<SelectTrigger className="w-[140px] h-8">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="none">Disabled</SelectItem>
|
||||
<SelectItem value="local">Always Local</SelectItem>
|
||||
<SelectItem value="remote">Always Remote</SelectItem>
|
||||
<SelectItem value="merge">Always Merge</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<ConflictResolutionBulkActions
|
||||
copy={copy}
|
||||
detectingConflicts={detectingConflicts}
|
||||
resolvingConflict={resolvingConflict}
|
||||
autoResolveStrategy={autoResolveStrategy}
|
||||
onResolveAll={handleResolveAll}
|
||||
onAutoResolveChange={(value) => setAutoResolve(value)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex-1 overflow-hidden p-6">
|
||||
{hasConflicts && (
|
||||
<div className="mb-4 flex items-center justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
<MagnifyingGlass size={20} className="text-muted-foreground" />
|
||||
<span className="text-sm text-muted-foreground">Filter by type:</span>
|
||||
<Select value={filterType} onValueChange={setFilterType}>
|
||||
<SelectTrigger className="w-[160px] h-9">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="all">All Types</SelectItem>
|
||||
<SelectItem value="files">Files</SelectItem>
|
||||
<SelectItem value="models">Models</SelectItem>
|
||||
<SelectItem value="components">Components</SelectItem>
|
||||
<SelectItem value="workflows">Workflows</SelectItem>
|
||||
<SelectItem value="lambdas">Lambdas</SelectItem>
|
||||
<SelectItem value="componentTrees">Component Trees</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<ConflictResolutionFilters
|
||||
copy={copy}
|
||||
hasConflicts={hasConflicts}
|
||||
filterType={filterType}
|
||||
onFilterChange={setFilterType}
|
||||
conflictCount={filteredConflicts.length}
|
||||
/>
|
||||
|
||||
<Badge variant="secondary">
|
||||
{filteredConflicts.length} conflict{filteredConflicts.length === 1 ? '' : 's'}
|
||||
</Badge>
|
||||
</div>
|
||||
)}
|
||||
<ConflictResolutionList
|
||||
copy={copy}
|
||||
conflicts={filteredConflicts}
|
||||
hasConflicts={hasConflicts}
|
||||
isDetecting={detectingConflicts}
|
||||
resolvingConflict={resolvingConflict}
|
||||
onResolve={handleResolve}
|
||||
onViewDetails={handleViewDetails}
|
||||
onDetect={handleDetect}
|
||||
/>
|
||||
|
||||
<ScrollArea className="h-[calc(100vh-500px)]">
|
||||
<div className="space-y-4 pr-4">
|
||||
<AnimatePresence mode="popLayout">
|
||||
{filteredConflicts.length > 0 ? (
|
||||
filteredConflicts.map((conflict) => (
|
||||
<ConflictCard
|
||||
key={conflict.id}
|
||||
conflict={conflict}
|
||||
onResolve={handleResolve}
|
||||
onViewDetails={handleViewDetails}
|
||||
isResolving={resolvingConflict === conflict.id}
|
||||
/>
|
||||
))
|
||||
) : hasConflicts ? (
|
||||
<motion.div
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
className="text-center py-12"
|
||||
>
|
||||
<XCircle size={48} className="mx-auto text-muted-foreground mb-4" weight="duotone" />
|
||||
<p className="text-muted-foreground">
|
||||
No conflicts found for this filter
|
||||
</p>
|
||||
</motion.div>
|
||||
) : (
|
||||
<motion.div
|
||||
initial={{ opacity: 0, scale: 0.95 }}
|
||||
animate={{ opacity: 1, scale: 1 }}
|
||||
className="text-center py-12"
|
||||
>
|
||||
<CheckCircle size={64} className="mx-auto text-accent mb-4" weight="duotone" />
|
||||
<h3 className="text-xl font-semibold mb-2">No Conflicts Detected</h3>
|
||||
<p className="text-muted-foreground mb-6">
|
||||
Your local and remote data are in sync
|
||||
</p>
|
||||
<Button onClick={handleDetect} disabled={detectingConflicts}>
|
||||
<ArrowsClockwise size={16} />
|
||||
Check Again
|
||||
</Button>
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
</div>
|
||||
</ScrollArea>
|
||||
|
||||
{error && (
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
className="mt-4"
|
||||
>
|
||||
<Card className="border-destructive">
|
||||
<CardContent className="pt-6 flex items-center gap-3">
|
||||
<XCircle size={24} className="text-destructive" weight="duotone" />
|
||||
<div className="flex-1">
|
||||
<div className="font-medium">Error</div>
|
||||
<div className="text-sm text-muted-foreground">{error}</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</motion.div>
|
||||
)}
|
||||
<ConflictResolutionError copy={copy} error={error} />
|
||||
</div>
|
||||
|
||||
<ConflictDetailsDialog
|
||||
<ConflictResolutionDetails
|
||||
conflict={selectedConflict}
|
||||
open={detailsDialogOpen}
|
||||
onOpenChange={setDetailsDialogOpen}
|
||||
|
||||
@@ -0,0 +1,97 @@
|
||||
import type { ConflictResolutionStrategy } from '@/types/conflicts'
|
||||
import type { ConflictResolutionCopy } from '@/components/conflict-resolution/types'
|
||||
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Separator } from '@/components/ui/separator'
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from '@/components/ui/select'
|
||||
import { ArrowsLeftRight, Cloud, Database } from '@phosphor-icons/react'
|
||||
|
||||
interface ConflictResolutionBulkActionsProps {
|
||||
copy: ConflictResolutionCopy
|
||||
detectingConflicts: boolean
|
||||
resolvingConflict: string | null
|
||||
autoResolveStrategy: ConflictResolutionStrategy | null
|
||||
onResolveAll: (strategy: ConflictResolutionStrategy) => void
|
||||
onAutoResolveChange: (strategy: ConflictResolutionStrategy | null) => void
|
||||
}
|
||||
|
||||
export function ConflictResolutionBulkActions({
|
||||
copy,
|
||||
detectingConflicts,
|
||||
resolvingConflict,
|
||||
autoResolveStrategy,
|
||||
onResolveAll,
|
||||
onAutoResolveChange,
|
||||
}: ConflictResolutionBulkActionsProps) {
|
||||
return (
|
||||
<Card className="border-destructive/30">
|
||||
<CardHeader className="pb-3">
|
||||
<CardTitle className="text-base flex items-center gap-2">
|
||||
<ArrowsLeftRight size={20} />
|
||||
{copy.bulk.title}
|
||||
</CardTitle>
|
||||
<CardDescription>{copy.bulk.description}</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="flex flex-wrap gap-2">
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
onClick={() => onResolveAll('local')}
|
||||
disabled={detectingConflicts || !!resolvingConflict}
|
||||
>
|
||||
<Database size={16} />
|
||||
{copy.buttons.keepAllLocal}
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
onClick={() => onResolveAll('remote')}
|
||||
disabled={detectingConflicts || !!resolvingConflict}
|
||||
>
|
||||
<Cloud size={16} />
|
||||
{copy.buttons.keepAllRemote}
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
onClick={() => onResolveAll('merge')}
|
||||
disabled={detectingConflicts || !!resolvingConflict}
|
||||
>
|
||||
<ArrowsLeftRight size={16} />
|
||||
{copy.buttons.mergeAll}
|
||||
</Button>
|
||||
|
||||
<Separator orientation="vertical" className="h-8 mx-2" />
|
||||
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-sm text-muted-foreground">{copy.bulk.autoResolveLabel}</span>
|
||||
<Select
|
||||
value={autoResolveStrategy || 'none'}
|
||||
onValueChange={(value) =>
|
||||
onAutoResolveChange(
|
||||
value === 'none' ? null : (value as ConflictResolutionStrategy),
|
||||
)
|
||||
}
|
||||
>
|
||||
<SelectTrigger className="w-[140px] h-8">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="none">{copy.bulk.autoResolveOptions.none}</SelectItem>
|
||||
<SelectItem value="local">{copy.bulk.autoResolveOptions.local}</SelectItem>
|
||||
<SelectItem value="remote">{copy.bulk.autoResolveOptions.remote}</SelectItem>
|
||||
<SelectItem value="merge">{copy.bulk.autoResolveOptions.merge}</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
import type { ConflictResolutionItem, ConflictResolveHandler } from '@/components/conflict-resolution/types'
|
||||
|
||||
import { ConflictDetailsDialog } from '@/components/ConflictDetailsDialog'
|
||||
|
||||
interface ConflictResolutionDetailsProps {
|
||||
conflict: ConflictResolutionItem | null
|
||||
open: boolean
|
||||
onOpenChange: (open: boolean) => void
|
||||
onResolve: ConflictResolveHandler
|
||||
isResolving: boolean
|
||||
}
|
||||
|
||||
export function ConflictResolutionDetails({
|
||||
conflict,
|
||||
open,
|
||||
onOpenChange,
|
||||
onResolve,
|
||||
isResolving,
|
||||
}: ConflictResolutionDetailsProps) {
|
||||
return (
|
||||
<ConflictDetailsDialog
|
||||
conflict={conflict}
|
||||
open={open}
|
||||
onOpenChange={onOpenChange}
|
||||
onResolve={onResolve}
|
||||
isResolving={isResolving}
|
||||
/>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
import type { ConflictResolutionCopy } from '@/components/conflict-resolution/types'
|
||||
|
||||
import { Card, CardContent } from '@/components/ui/card'
|
||||
import { XCircle } from '@phosphor-icons/react'
|
||||
import { motion } from 'framer-motion'
|
||||
|
||||
interface ConflictResolutionErrorProps {
|
||||
copy: ConflictResolutionCopy
|
||||
error: string | null
|
||||
}
|
||||
|
||||
export function ConflictResolutionError({ copy, error }: ConflictResolutionErrorProps) {
|
||||
if (!error) {
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<motion.div initial={{ opacity: 0, y: 20 }} animate={{ opacity: 1, y: 0 }} className="mt-4">
|
||||
<Card className="border-destructive">
|
||||
<CardContent className="pt-6 flex items-center gap-3">
|
||||
<XCircle size={24} className="text-destructive" weight="duotone" />
|
||||
<div className="flex-1">
|
||||
<div className="font-medium">{copy.error.title}</div>
|
||||
<div className="text-sm text-muted-foreground">{error}</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</motion.div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
import type {
|
||||
ConflictResolutionCopy,
|
||||
ConflictResolutionFilters,
|
||||
} from '@/components/conflict-resolution/types'
|
||||
|
||||
import { Badge } from '@/components/ui/badge'
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from '@/components/ui/select'
|
||||
import { MagnifyingGlass } from '@phosphor-icons/react'
|
||||
|
||||
interface ConflictResolutionFiltersProps {
|
||||
copy: ConflictResolutionCopy
|
||||
hasConflicts: boolean
|
||||
filterType: ConflictResolutionFilters
|
||||
onFilterChange: (value: ConflictResolutionFilters) => void
|
||||
conflictCount: number
|
||||
}
|
||||
|
||||
export function ConflictResolutionFilters({
|
||||
copy,
|
||||
hasConflicts,
|
||||
filterType,
|
||||
onFilterChange,
|
||||
conflictCount,
|
||||
}: ConflictResolutionFiltersProps) {
|
||||
if (!hasConflicts) {
|
||||
return null
|
||||
}
|
||||
|
||||
const label = conflictCount === 1 ? copy.labels.conflictSingular : copy.labels.conflictPlural
|
||||
|
||||
return (
|
||||
<div className="mb-4 flex items-center justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
<MagnifyingGlass size={20} className="text-muted-foreground" />
|
||||
<span className="text-sm text-muted-foreground">{copy.filters.label}</span>
|
||||
<Select value={filterType} onValueChange={onFilterChange}>
|
||||
<SelectTrigger className="w-[160px] h-9">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="all">{copy.filters.allTypes}</SelectItem>
|
||||
<SelectItem value="files">{copy.filters.files}</SelectItem>
|
||||
<SelectItem value="models">{copy.filters.models}</SelectItem>
|
||||
<SelectItem value="components">{copy.filters.components}</SelectItem>
|
||||
<SelectItem value="workflows">{copy.filters.workflows}</SelectItem>
|
||||
<SelectItem value="lambdas">{copy.filters.lambdas}</SelectItem>
|
||||
<SelectItem value="componentTrees">{copy.filters.componentTrees}</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
<Badge variant="secondary">
|
||||
{copy.badges.conflictCount
|
||||
.replace('{count}', String(conflictCount))
|
||||
.replace('{label}', label)}
|
||||
</Badge>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
import type { ConflictResolutionCopy } from '@/components/conflict-resolution/types'
|
||||
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { ArrowsClockwise, Trash } from '@phosphor-icons/react'
|
||||
|
||||
interface ConflictResolutionHeaderProps {
|
||||
copy: ConflictResolutionCopy
|
||||
hasConflicts: boolean
|
||||
detectingConflicts: boolean
|
||||
onDetect: () => void
|
||||
onClear: () => void
|
||||
}
|
||||
|
||||
export function ConflictResolutionHeader({
|
||||
copy,
|
||||
hasConflicts,
|
||||
detectingConflicts,
|
||||
onDetect,
|
||||
onClear,
|
||||
}: ConflictResolutionHeaderProps) {
|
||||
return (
|
||||
<div className="flex items-start justify-between gap-4">
|
||||
<div>
|
||||
<h1 className="text-3xl font-bold font-mono tracking-tight">{copy.header.title}</h1>
|
||||
<p className="text-muted-foreground mt-1">{copy.header.description}</p>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-2">
|
||||
<Button size="sm" variant="outline" onClick={onDetect} disabled={detectingConflicts}>
|
||||
<ArrowsClockwise size={16} className={detectingConflicts ? 'animate-spin' : ''} />
|
||||
{copy.buttons.detect}
|
||||
</Button>
|
||||
|
||||
{hasConflicts && (
|
||||
<Button size="sm" variant="outline" onClick={onClear}>
|
||||
<Trash size={16} />
|
||||
{copy.buttons.clearAll}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
import type {
|
||||
ConflictResolutionCopy,
|
||||
ConflictResolutionItem,
|
||||
ConflictResolveHandler,
|
||||
} from '@/components/conflict-resolution/types'
|
||||
|
||||
import { ScrollArea } from '@/components/ui/scroll-area'
|
||||
import { ConflictCard } from '@/components/ConflictCard'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { AnimatePresence, motion } from 'framer-motion'
|
||||
import { CheckCircle, XCircle, ArrowsClockwise } from '@phosphor-icons/react'
|
||||
|
||||
interface ConflictResolutionListProps {
|
||||
copy: ConflictResolutionCopy
|
||||
conflicts: ConflictResolutionItem[]
|
||||
hasConflicts: boolean
|
||||
isDetecting: boolean
|
||||
resolvingConflict: string | null
|
||||
onResolve: ConflictResolveHandler
|
||||
onViewDetails: (conflict: ConflictResolutionItem) => void
|
||||
onDetect: () => void
|
||||
}
|
||||
|
||||
export function ConflictResolutionList({
|
||||
copy,
|
||||
conflicts,
|
||||
hasConflicts,
|
||||
isDetecting,
|
||||
resolvingConflict,
|
||||
onResolve,
|
||||
onViewDetails,
|
||||
onDetect,
|
||||
}: ConflictResolutionListProps) {
|
||||
return (
|
||||
<ScrollArea className="h-[calc(100vh-500px)]">
|
||||
<div className="space-y-4 pr-4">
|
||||
<AnimatePresence mode="popLayout">
|
||||
{conflicts.length > 0 ? (
|
||||
conflicts.map((conflict) => (
|
||||
<ConflictCard
|
||||
key={conflict.id}
|
||||
conflict={conflict}
|
||||
onResolve={onResolve}
|
||||
onViewDetails={onViewDetails}
|
||||
isResolving={resolvingConflict === conflict.id}
|
||||
/>
|
||||
))
|
||||
) : hasConflicts ? (
|
||||
<motion.div
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
className="text-center py-12"
|
||||
>
|
||||
<XCircle size={48} className="mx-auto text-muted-foreground mb-4" weight="duotone" />
|
||||
<p className="text-muted-foreground">{copy.emptyStates.filtered}</p>
|
||||
</motion.div>
|
||||
) : (
|
||||
<motion.div
|
||||
initial={{ opacity: 0, scale: 0.95 }}
|
||||
animate={{ opacity: 1, scale: 1 }}
|
||||
className="text-center py-12"
|
||||
>
|
||||
<CheckCircle size={64} className="mx-auto text-accent mb-4" weight="duotone" />
|
||||
<h3 className="text-xl font-semibold mb-2">{copy.emptyStates.noConflictsTitle}</h3>
|
||||
<p className="text-muted-foreground mb-6">{copy.emptyStates.noConflictsDescription}</p>
|
||||
<Button onClick={onDetect} disabled={isDetecting}>
|
||||
<ArrowsClockwise size={16} />
|
||||
{copy.buttons.checkAgain}
|
||||
</Button>
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
</div>
|
||||
</ScrollArea>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
import type { ReactNode } from 'react'
|
||||
import type { ConflictResolutionCopy, ConflictResolutionStats } from '@/components/conflict-resolution/types'
|
||||
|
||||
import { Card, CardContent } from '@/components/ui/card'
|
||||
import { Warning, Database, Cloud } from '@phosphor-icons/react'
|
||||
|
||||
interface ConflictResolutionStatsProps {
|
||||
copy: ConflictResolutionCopy
|
||||
stats: ConflictResolutionStats
|
||||
}
|
||||
|
||||
function StatsCard({ count, label, icon }: { count: number; label: string; icon: ReactNode }) {
|
||||
return (
|
||||
<Card>
|
||||
<CardContent className="pt-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<div className="text-2xl font-bold">{count}</div>
|
||||
<div className="text-xs text-muted-foreground">{label}</div>
|
||||
</div>
|
||||
{icon}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
|
||||
export function ConflictResolutionStatsSection({ copy, stats }: ConflictResolutionStatsProps) {
|
||||
const otherCount =
|
||||
(stats.conflictsByType.components || 0) + (stats.conflictsByType.workflows || 0)
|
||||
|
||||
return (
|
||||
<div className="grid grid-cols-1 md:grid-cols-4 gap-4">
|
||||
<StatsCard
|
||||
count={stats.totalConflicts}
|
||||
label={copy.stats.total}
|
||||
icon={<Warning size={24} className="text-destructive" weight="duotone" />}
|
||||
/>
|
||||
<StatsCard
|
||||
count={stats.conflictsByType.files || 0}
|
||||
label={copy.stats.files}
|
||||
icon={<Database size={24} className="text-primary" weight="duotone" />}
|
||||
/>
|
||||
<StatsCard
|
||||
count={stats.conflictsByType.models || 0}
|
||||
label={copy.stats.models}
|
||||
icon={<Database size={24} className="text-accent" weight="duotone" />}
|
||||
/>
|
||||
<StatsCard
|
||||
count={otherCount}
|
||||
label={copy.stats.other}
|
||||
icon={<Cloud size={24} className="text-muted-foreground" weight="duotone" />}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
83
src/components/conflict-resolution/types.ts
Normal file
83
src/components/conflict-resolution/types.ts
Normal file
@@ -0,0 +1,83 @@
|
||||
import type { ConflictItem, ConflictResolutionStrategy, ConflictStats, EntityType } from '@/types/conflicts'
|
||||
|
||||
export interface ConflictResolutionCopy {
|
||||
header: {
|
||||
title: string
|
||||
description: string
|
||||
}
|
||||
buttons: {
|
||||
detect: string
|
||||
clearAll: string
|
||||
keepAllLocal: string
|
||||
keepAllRemote: string
|
||||
mergeAll: string
|
||||
checkAgain: string
|
||||
}
|
||||
stats: {
|
||||
total: string
|
||||
files: string
|
||||
models: string
|
||||
other: string
|
||||
}
|
||||
bulk: {
|
||||
title: string
|
||||
description: string
|
||||
autoResolveLabel: string
|
||||
autoResolveOptions: {
|
||||
none: string
|
||||
local: string
|
||||
remote: string
|
||||
merge: string
|
||||
}
|
||||
}
|
||||
filters: {
|
||||
label: string
|
||||
allTypes: string
|
||||
files: string
|
||||
models: string
|
||||
components: string
|
||||
workflows: string
|
||||
lambdas: string
|
||||
componentTrees: string
|
||||
}
|
||||
badges: {
|
||||
conflictCount: string
|
||||
}
|
||||
emptyStates: {
|
||||
filtered: string
|
||||
noConflictsTitle: string
|
||||
noConflictsDescription: string
|
||||
}
|
||||
labels: {
|
||||
conflictSingular: string
|
||||
conflictPlural: string
|
||||
}
|
||||
toasts: {
|
||||
noConflictsDetected: string
|
||||
foundConflicts: string
|
||||
detectFailed: string
|
||||
resolved: string
|
||||
resolveFailed: string
|
||||
resolvedAll: string
|
||||
resolveAllFailed: string
|
||||
}
|
||||
error: {
|
||||
title: string
|
||||
}
|
||||
}
|
||||
|
||||
export type ConflictResolutionFilters = EntityType | 'all'
|
||||
|
||||
export type ConflictResolutionStats = ConflictStats
|
||||
|
||||
export type ConflictResolutionItem = ConflictItem
|
||||
|
||||
export type ConflictResolveHandler = (
|
||||
conflictId: string,
|
||||
strategy: ConflictResolutionStrategy,
|
||||
) => Promise<void>
|
||||
|
||||
export type ConflictResolveAllHandler = (
|
||||
strategy: ConflictResolutionStrategy,
|
||||
) => Promise<void>
|
||||
|
||||
65
src/data/conflict-resolution.json
Normal file
65
src/data/conflict-resolution.json
Normal file
@@ -0,0 +1,65 @@
|
||||
{
|
||||
"header": {
|
||||
"title": "Conflict Resolution",
|
||||
"description": "Manage sync conflicts between local and remote data"
|
||||
},
|
||||
"buttons": {
|
||||
"detect": "Detect Conflicts",
|
||||
"clearAll": "Clear All",
|
||||
"keepAllLocal": "Keep All Local",
|
||||
"keepAllRemote": "Keep All Remote",
|
||||
"mergeAll": "Merge All",
|
||||
"checkAgain": "Check Again"
|
||||
},
|
||||
"stats": {
|
||||
"total": "Total Conflicts",
|
||||
"files": "Files",
|
||||
"models": "Models",
|
||||
"other": "Other"
|
||||
},
|
||||
"bulk": {
|
||||
"title": "Bulk Resolution",
|
||||
"description": "Apply a resolution strategy to all conflicts at once",
|
||||
"autoResolveLabel": "Auto-resolve:",
|
||||
"autoResolveOptions": {
|
||||
"none": "Disabled",
|
||||
"local": "Always Local",
|
||||
"remote": "Always Remote",
|
||||
"merge": "Always Merge"
|
||||
}
|
||||
},
|
||||
"filters": {
|
||||
"label": "Filter by type:",
|
||||
"allTypes": "All Types",
|
||||
"files": "Files",
|
||||
"models": "Models",
|
||||
"components": "Components",
|
||||
"workflows": "Workflows",
|
||||
"lambdas": "Lambdas",
|
||||
"componentTrees": "Component Trees"
|
||||
},
|
||||
"badges": {
|
||||
"conflictCount": "{count} {label}"
|
||||
},
|
||||
"emptyStates": {
|
||||
"filtered": "No conflicts found for this filter",
|
||||
"noConflictsTitle": "No Conflicts Detected",
|
||||
"noConflictsDescription": "Your local and remote data are in sync"
|
||||
},
|
||||
"labels": {
|
||||
"conflictSingular": "conflict",
|
||||
"conflictPlural": "conflicts"
|
||||
},
|
||||
"toasts": {
|
||||
"noConflictsDetected": "No conflicts detected",
|
||||
"foundConflicts": "Found {count} {label}",
|
||||
"detectFailed": "Failed to detect conflicts",
|
||||
"resolved": "Conflict resolved using {strategy} version",
|
||||
"resolveFailed": "Failed to resolve conflict",
|
||||
"resolvedAll": "All conflicts resolved using {strategy} version",
|
||||
"resolveAllFailed": "Failed to resolve all conflicts"
|
||||
},
|
||||
"error": {
|
||||
"title": "Error"
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user