feat(history): add Discard selected button to cancel pending BC syncs

This commit is contained in:
Christian Vidal Wolf
2026-05-15 13:45:44 +02:00
parent 143f655f3c
commit 6d664c26f1
+41 -8
View File
@@ -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<string, HistorySyncRecord>;
@@ -45,7 +46,7 @@ export function HistoryView({ headers, data, onRevert, onEdit }: HistoryViewProp
const [expandedId, setExpandedId] = useState<string | null>(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<HistorySyncMap>(() => 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
<span className="text-slate-600">·</span>
<span>{statusCounts.bc_pending} pending</span>
<span>{statusCounts.previewed} previewed</span>
<span>{statusCounts.preview_only} preview only</span>
<span>{statusCounts.synced} synced</span>
<span>{statusCounts.failed} failed</span>
</div>
@@ -404,6 +424,7 @@ export function HistoryView({ headers, data, onRevert, onEdit }: HistoryViewProp
<option value="all">All statuses</option>
<option value="bc_pending">Pending BC</option>
<option value="previewed">Previewed</option>
<option value="preview_only">Preview only</option>
<option value="synced">Synced BC</option>
<option value="failed">Failed</option>
</select>
@@ -419,6 +440,18 @@ export function HistoryView({ headers, data, onRevert, onEdit }: HistoryViewProp
>
Clear selection
</button>
<button
onClick={discardSelected}
disabled={selectedEntries.length === 0}
className={cn(
"px-3 py-2 rounded-md text-xs border transition-colors",
selectedEntries.length === 0
? "bg-slate-700 text-slate-500 border-slate-600 cursor-not-allowed"
: "bg-slate-800 hover:bg-red-900/40 text-red-400 border-red-500/30 hover:border-red-500/60"
)}
>
Discard selected
</button>
<button
onClick={previewSelected}
disabled={bcSyncBusy || selectedEntries.length === 0}