Merge pull request #49 from johndoe6345789/codex/refactor-conflictresolutionpage-into-subcomponents

Refactor ConflictResolutionPage into smaller components and externalize UI copy
This commit is contained in:
2026-01-18 00:38:25 +00:00
committed by GitHub
10 changed files with 604 additions and 271 deletions

View File

@@ -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}

View File

@@ -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>
)
}

View File

@@ -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}
/>
)
}

View File

@@ -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>
)
}

View File

@@ -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>
)
}

View File

@@ -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>
)
}

View File

@@ -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>
)
}

View File

@@ -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>
)
}

View 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>

View 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"
}
}