forked from vavallee/bindery
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathABSAuthorConflictsPanel.tsx
More file actions
183 lines (173 loc) · 7.97 KB
/
Copy pathABSAuthorConflictsPanel.tsx
File metadata and controls
183 lines (173 loc) · 7.97 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
import { ABSMetadataConflict } from '../api/client'
type Props = {
title: string
description: string
entityType: ABSMetadataConflict['entityType']
conflicts: ABSMetadataConflict[]
show: boolean
emptyMessage: string
resolvedHeading: string
conflictError?: string | null
resolvingConflictId: number | null
onToggle: () => void
onRefresh: () => void
onResolveConflict: (id: number, source: 'abs' | 'upstream') => void
relinkAction?: {
loadingId: number | null
onRelink: (localId: number) => void
}
}
function groupConflictsByEntity(conflicts: ABSMetadataConflict[]): ABSMetadataConflict[][] {
const groups: Record<string, ABSMetadataConflict[]> = {}
for (const conflict of conflicts) {
const key = `${conflict.entityType}:${conflict.localId}`
if (!groups[key]) {
groups[key] = []
}
groups[key].push(conflict)
}
return Object.values(groups)
}
function RelinkButton({
conflict,
relinkAction,
}: {
conflict: ABSMetadataConflict
relinkAction?: Props['relinkAction']
}) {
if (!relinkAction || !conflict.authorRelinkEligible) {
return null
}
const relinking = relinkAction.loadingId === conflict.localId
return (
<button
onClick={() => relinkAction.onRelink(conflict.localId)}
disabled={relinking}
className="px-3 py-1.5 bg-emerald-600 hover:bg-emerald-500 rounded text-xs font-medium disabled:opacity-50"
>
{relinking ? 'Relinking…' : 'Attempt upstream relink'}
</button>
)
}
export default function ABSConflictPanel({
title,
description,
entityType,
conflicts,
show,
emptyMessage,
resolvedHeading,
conflictError,
resolvingConflictId,
onToggle,
onRefresh,
onResolveConflict,
relinkAction,
}: Props) {
const entityConflicts = conflicts.filter(conflict => conflict.entityType === entityType)
const unresolvedGroups = groupConflictsByEntity(entityConflicts.filter(conflict => conflict.resolutionStatus === 'unresolved'))
const resolvedGroups = groupConflictsByEntity(entityConflicts.filter(conflict => conflict.resolutionStatus === 'resolved'))
return (
<div className="pt-3 border-t border-slate-200 dark:border-zinc-800 space-y-3">
<div className="flex items-center justify-between gap-4">
<button
type="button"
onClick={onToggle}
aria-expanded={show}
className="min-w-0 flex-1 text-left"
>
<div className="flex items-start gap-2">
<span className="text-sm text-slate-500 dark:text-zinc-500 mt-0.5" aria-hidden="true">
{show ? '▾' : '▸'}
</span>
<div>
<label className="block text-sm font-medium text-slate-800 dark:text-zinc-200 cursor-pointer">{title}</label>
<p className="text-xs text-slate-600 dark:text-zinc-500 mt-0.5">{description}</p>
</div>
</div>
</button>
<button
onClick={onRefresh}
className="px-3 py-2 bg-slate-700 hover:bg-slate-600 rounded text-sm font-medium flex-shrink-0"
>
Refresh
</button>
</div>
{show && (
<>
{unresolvedGroups.length === 0 && resolvedGroups.length === 0 && (
<p className="text-sm text-slate-500 dark:text-zinc-500">{emptyMessage}</p>
)}
{unresolvedGroups.map(group => (
<div key={`${group[0].entityType}:${group[0].localId}`} className="rounded border border-amber-300 dark:border-amber-900 bg-amber-50 dark:bg-amber-950/20 px-3 py-3 space-y-2 overflow-hidden">
<div className="flex items-center justify-between gap-3">
<div className="min-w-0">
<p className="text-sm font-medium text-slate-800 dark:text-zinc-200">{group[0].entityName}</p>
<p className="text-[11px] text-slate-500 dark:text-zinc-500 uppercase tracking-wide">{entityType}</p>
</div>
<div className="flex flex-wrap items-center justify-end gap-2">
<RelinkButton conflict={group[0]} relinkAction={relinkAction} />
<span className="text-[11px] px-2 py-1 rounded bg-amber-100 dark:bg-amber-950 text-amber-700 dark:text-amber-300">Needs review</span>
</div>
</div>
{group.map(conflict => (
<div key={conflict.id} className="rounded border border-amber-200 dark:border-amber-900/60 bg-white/70 dark:bg-zinc-950/30 px-3 py-2 space-y-2 overflow-hidden">
<div className="flex items-center justify-between gap-3">
<span className="text-xs font-medium text-slate-700 dark:text-zinc-300 min-w-0">{conflict.fieldLabel}</span>
<span className="text-[10px] uppercase tracking-wide text-slate-500 dark:text-zinc-500 text-right">Using {conflict.appliedSource || 'upstream'}</span>
</div>
<div className="grid min-w-0 gap-2 md:grid-cols-2 text-xs">
<div className="min-w-0 rounded border border-slate-200 dark:border-zinc-800 px-2 py-2 bg-slate-50 dark:bg-zinc-950 overflow-hidden">
<div className="font-medium text-slate-700 dark:text-zinc-300 mb-1">ABS</div>
<div className="text-slate-600 dark:text-zinc-400 whitespace-pre-wrap break-all min-w-0">{conflict.absValue || 'Empty'}</div>
</div>
<div className="min-w-0 rounded border border-slate-200 dark:border-zinc-800 px-2 py-2 bg-slate-50 dark:bg-zinc-950 overflow-hidden">
<div className="font-medium text-slate-700 dark:text-zinc-300 mb-1">Upstream</div>
<div className="text-slate-600 dark:text-zinc-400 whitespace-pre-wrap break-all min-w-0">{conflict.upstreamValue || 'Empty'}</div>
</div>
</div>
<div className="flex flex-wrap gap-2">
<button
onClick={() => onResolveConflict(conflict.id, 'abs')}
disabled={resolvingConflictId === conflict.id}
className="px-3 py-1.5 bg-slate-700 hover:bg-slate-600 rounded text-xs font-medium disabled:opacity-50"
>
Use ABS
</button>
<button
onClick={() => onResolveConflict(conflict.id, 'upstream')}
disabled={resolvingConflictId === conflict.id}
className="px-3 py-1.5 bg-sky-600 hover:bg-sky-500 rounded text-xs font-medium disabled:opacity-50"
>
Use upstream
</button>
</div>
</div>
))}
</div>
))}
{resolvedGroups.length > 0 && (
<div className="space-y-2">
<p className="text-xs font-medium uppercase tracking-wide text-slate-500 dark:text-zinc-500">{resolvedHeading}</p>
{resolvedGroups.map(group => (
<div key={`${group[0].entityType}:${group[0].localId}`} className="rounded border border-slate-200 dark:border-zinc-800 bg-slate-50 dark:bg-zinc-950 px-3 py-2 space-y-1">
<div className="flex items-center justify-between gap-3">
<p className="text-sm font-medium text-slate-800 dark:text-zinc-200">{group[0].entityName}</p>
<RelinkButton conflict={group[0]} relinkAction={relinkAction} />
</div>
{group.map(conflict => (
<div key={conflict.id} className="flex items-center justify-between gap-3 text-xs text-slate-600 dark:text-zinc-400">
<span>{conflict.fieldLabel}</span>
<span>Prefers {conflict.preferredSource || conflict.appliedSource}</span>
</div>
))}
</div>
))}
</div>
)}
</>
)}
{conflictError && <div className="text-sm text-red-600 dark:text-red-400">{conflictError}</div>}
</div>
)
}