From 6d664c26f19f24d58383f3c5f30545ac2274ce75 Mon Sep 17 00:00:00 2001 From: Christian Vidal Wolf Date: Fri, 15 May 2026 13:45:44 +0200 Subject: [PATCH] feat(history): add Discard selected button to cancel pending BC syncs --- src/components/HistoryView.tsx | 49 ++++++++++++++++++++++++++++------ 1 file changed, 41 insertions(+), 8 deletions(-) diff --git a/src/components/HistoryView.tsx b/src/components/HistoryView.tsx index 203a79c..22aafee 100644 --- a/src/components/HistoryView.tsx +++ b/src/components/HistoryView.tsx @@ -15,13 +15,14 @@ interface HistoryViewProps { onEdit?: (rowIndex: number) => void; } -type HistorySyncStatus = 'bc_pending' | 'previewed' | 'syncing' | 'synced' | 'failed'; +type HistorySyncStatus = 'bc_pending' | 'previewed' | 'preview_only' | 'syncing' | 'synced' | 'failed'; interface HistorySyncRecord { selected: boolean; status: HistorySyncStatus; previewToken?: string; error?: string; + warning?: string; } type HistorySyncMap = Record; @@ -45,7 +46,7 @@ export function HistoryView({ headers, data, onRevert, onEdit }: HistoryViewProp const [expandedId, setExpandedId] = useState(null); const [search, setSearch] = usePersistentState('history-search', ''); const [userFilter, setUserFilter] = usePersistentState('history-userFilter', ''); - const [statusFilter, setStatusFilter] = usePersistentState<'all' | 'bc_pending' | 'previewed' | 'synced' | 'failed'>('history-statusFilter', 'all'); + const [statusFilter, setStatusFilter] = usePersistentState<'all' | 'bc_pending' | 'previewed' | 'preview_only' | 'synced' | 'failed'>('history-statusFilter', 'all'); const [bcHistorySync, setBcHistorySync] = useState(() => readStoredHistorySyncMap()); const [bcSyncBusy, setBcSyncBusy] = useState(false); const [isFullscreen, setIsFullscreen] = useState(false); @@ -163,7 +164,7 @@ export function HistoryView({ headers, data, onRevert, onEdit }: HistoryViewProp ); const statusCounts = useMemo(() => { - const counts = { bc_pending: 0, previewed: 0, synced: 0, failed: 0 }; + const counts = { bc_pending: 0, previewed: 0, preview_only: 0, synced: 0, failed: 0 }; history.forEach(entry => { const status = getEntryStatus(entry); if (status in counts) counts[status as keyof typeof counts] += 1; @@ -196,12 +197,26 @@ export function HistoryView({ headers, data, onRevert, onEdit }: HistoryViewProp status: next[key]?.status ?? 'bc_pending', previewToken: next[key]?.previewToken, error: next[key]?.error, + warning: next[key]?.warning, }; }); return next; }); }; + const discardSelected = () => { + if (selectedEntries.length === 0) return; + if (!window.confirm(`Discard ${selectedEntries.length} pending sync(s)? They will be reset to "Pending BC" and will not be synced to Business Central.`)) return; + setBcHistorySync(prev => { + const next = { ...prev }; + selectedEntries.forEach(entry => { + const key = getEntryKey(entry); + next[key] = { selected: false, status: 'bc_pending' }; + }); + return next; + }); + }; + const previewSelected = async () => { if (selectedEntries.length === 0) return; setBcSyncBusy(true); @@ -213,13 +228,14 @@ export function HistoryView({ headers, data, onRevert, onEdit }: HistoryViewProp })) { const preview = await previewBusinessCentralSync(headers, entry.new_data); if (!preview.success) { - updateEntry(entry, { status: 'failed', error: preview.error || 'Preview failed', selected: true }); + updateEntry(entry, { status: 'failed', error: preview.error || 'Preview failed', warning: undefined, selected: true }); continue; } updateEntry(entry, { status: 'previewed', previewToken: preview.previewToken, error: undefined, + warning: undefined, selected: true, }); } @@ -247,11 +263,11 @@ export function HistoryView({ headers, data, onRevert, onEdit }: HistoryViewProp if (!previewToken) { const preview = await previewBusinessCentralSync(headers, entry.new_data); if (!preview.success) { - updateEntry(entry, { status: 'failed', error: preview.error || 'Preview failed', selected: true }); + updateEntry(entry, { status: 'failed', error: preview.error || 'Preview failed', warning: undefined, selected: true }); continue; } previewToken = preview.previewToken; - updateEntry(entry, { status: 'previewed', previewToken, error: undefined, selected: true }); + updateEntry(entry, { status: 'previewed', previewToken, error: undefined, warning: undefined, selected: true }); } const apply = await applyBusinessCentralSync(headers, entry.new_data, previewToken); @@ -263,6 +279,7 @@ export function HistoryView({ headers, data, onRevert, onEdit }: HistoryViewProp status: 'previewed', previewToken: refreshedPreview.previewToken, error: undefined, + warning: undefined, selected: true, }); const retryApply = await applyBusinessCentralSync(headers, entry.new_data, refreshedPreview.previewToken); @@ -270,22 +287,24 @@ export function HistoryView({ headers, data, onRevert, onEdit }: HistoryViewProp updateEntry(entry, { status: 'synced', error: undefined, + warning: retryApply.warning, selected: false, previewToken: retryApply.previewToken || refreshedPreview.previewToken, }); continue; } - updateEntry(entry, { status: 'failed', error: retryApply.error || 'BC sync failed', selected: true }); + updateEntry(entry, { status: 'failed', error: retryApply.error || 'BC sync failed', warning: retryApply.warning, selected: true }); continue; } } - updateEntry(entry, { status: 'failed', error: apply.error || 'BC sync failed', selected: true }); + updateEntry(entry, { status: 'failed', error: apply.error || 'BC sync failed', warning: apply.warning, selected: true }); continue; } updateEntry(entry, { status: 'synced', error: undefined, + warning: apply.warning, selected: false, previewToken: apply.previewToken || previewToken, }); @@ -392,6 +411,7 @@ export function HistoryView({ headers, data, onRevert, onEdit }: HistoryViewProp ยท {statusCounts.bc_pending} pending {statusCounts.previewed} previewed + {statusCounts.preview_only} preview only {statusCounts.synced} synced {statusCounts.failed} failed @@ -404,6 +424,7 @@ export function HistoryView({ headers, data, onRevert, onEdit }: HistoryViewProp + @@ -419,6 +440,18 @@ export function HistoryView({ headers, data, onRevert, onEdit }: HistoryViewProp > Clear selection +