Separate mapping changes from view-stale: show Reprocess prompt instead of Generate

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Paul Trowbridge 2026-04-25 10:28:32 -04:00
parent ca266f2839
commit 9ab2052f2b
2 changed files with 35 additions and 5 deletions

View File

@ -33,6 +33,7 @@ export default function App() {
// Sets of names whose dfv view is out of sync with current definitions
const [staleSources, setStaleSources] = useState(new Set())
const [staleStacks, setStaleStacks] = useState(new Set())
const [reprocessSources, setReprocessSources] = useState(new Set())
const [generating, setGenerating] = useState({}) // { 'source:name': true }
async function handleLogin(user, pass) {
@ -56,6 +57,7 @@ export default function App() {
setSources([])
setStaleSources(new Set())
setStaleStacks(new Set())
setReprocessSources(new Set())
}
// Load initial stale state from DB once on login
@ -70,6 +72,17 @@ export default function App() {
function markSourceStale(name) {
setStaleSources(prev => new Set([...prev, name]))
}
function markNeedsReprocess(name) {
setReprocessSources(prev => new Set([...prev, name]))
}
async function handleReprocessSource(name) {
setGenerating(g => ({ ...g, [`rp:${name}`]: true }))
try {
await api.reprocess(name)
setReprocessSources(prev => { const n = new Set(prev); n.delete(name); return n })
} catch (e) { alert(e.message) }
finally { setGenerating(g => { const n = { ...g }; delete n[`rp:${name}`]; return n }) }
}
function markStackStale(name) {
setStaleStacks(prev => new Set([...prev, name]))
}
@ -214,6 +227,23 @@ export default function App() {
))}
</div>
)}
{reprocessSources.size > 0 && (
<div className="bg-blue-50 border-b border-blue-200 px-4 py-1.5 text-xs text-blue-800 flex flex-wrap items-center gap-x-3 gap-y-1">
<span className="font-medium">Mappings updated:</span>
{[...reprocessSources].map(name => (
<span key={name} className="flex items-center gap-1">
{name}
<button
onClick={() => handleReprocessSource(name)}
disabled={generating[`rp:${name}`]}
className="px-1.5 py-0.5 rounded bg-blue-200 hover:bg-blue-300 disabled:opacity-50 font-medium"
>
{generating[`rp:${name}`] ? '…' : 'Reprocess'}
</button>
</span>
))}
</div>
)}
<div className="flex-1 overflow-auto">
<Routes>
@ -221,7 +251,7 @@ export default function App() {
<Route path="/sources" element={<Sources source={source} sources={sources} setSources={setSources} setSource={setSource} />} />
<Route path="/import" element={<Import source={source} />} />
<Route path="/rules" element={<Rules source={source} onStale={markSourceStale} />} />
<Route path="/mappings" element={<Mappings source={source} onStale={markSourceStale} />} />
<Route path="/mappings" element={<Mappings source={source} onNeedsReprocess={markNeedsReprocess} />} />
<Route path="/remap" element={<Remap />} />
<Route path="/records" element={<Records source={source} />} />
<Route path="/pivot" element={<Pivot source={source} />} />

View File

@ -109,7 +109,7 @@ function SortHeader({ col, label, sortBy, onSort, className = '' }) {
)
}
export default function Mappings({ source, onStale }) {
export default function Mappings({ source, onNeedsReprocess }) {
const [rules, setRules] = useState([])
const [selectedRule, setSelectedRule] = useState('')
const [allValues, setAllValues] = useState([])
@ -268,7 +268,7 @@ export default function Mappings({ source, onStale }) {
valueKey(x.extracted_value) === k ? { ...x, is_mapped: true, mapping_id: created.id, output } : x
))
}
onStale?.(source)
onNeedsReprocess?.(source)
setDrafts(d => { const n = { ...d }; delete n[k]; return n })
} catch (err) {
alert(err.message)
@ -315,7 +315,7 @@ export default function Mappings({ source, onStale }) {
setSaving(s => ({ ...s, [k]: false }))
}
}))
onStale?.(source)
onNeedsReprocess?.(source)
setSelected(new Set())
setBulkDraft({})
}
@ -324,7 +324,7 @@ export default function Mappings({ source, onStale }) {
if (!row.mapping_id) return
try {
await api.deleteMapping(row.mapping_id)
onStale?.(source)
onNeedsReprocess?.(source)
setAllValues(av => av.map(x =>
valueKey(x.extracted_value) === valueKey(row.extracted_value)
? { ...x, is_mapped: false, mapping_id: null, output: null }