Assessor_2.0/src/features/actionlist/ActionlistControls.tsx

76 lines
6.1 KiB
TypeScript

import { beginActivity, showActivityFrame } from '../../platform/activity-store';
import { useState } from 'react';
import { useApp } from '../../app/AppProvider';
import { Dialog } from '../../components/Dialog/Dialog';
import { buildWorkViewExport, renderWorkViewCsv, renderWorkViewHtml, renderWorkViewMarkdown } from '../../exports/work-view-export';
import { downloadText } from '../../exports/text-download';
import { browserDownloadAdapter } from '../../platform/browser/browser-download-adapter';
import { useFilteredRecords } from '../search-filter/use-filtered-records';
import { selectWorkFacets } from '../search-filter/work-facets';
import { useContextClusterIndex } from '../context-layer/use-context-cluster-index';
import { useRelatedFindings } from '../related-findings/RelatedFindings';
import { useWorkPackages } from '../../remediation/use-work-packages';
import { WorkPackagesWindow } from '../../remediation/WorkPackagesWindow';
import { UNASSIGNED_WORK_PACKAGE, WORK_PACKAGE_FACET_ID } from '../../remediation/remediation-engine';
import { numberedClusterLabel, publishedClusterColour } from '../sidebar/context-cluster-display';
import { useComplianceWorkFacets } from '../../compliance/ui/compliance-work-facets';
import { useComparisonWorkFacets } from '../comparison/comparison-work-facets';
export function ActionlistControls() {
const app = useApp();
const records = useFilteredRecords();
const contextClusters = useContextClusterIndex();
const remediation = useWorkPackages();
const related = useRelatedFindings();
const workPackages = remediation.status === 'ready' ? remediation.index : undefined;
const packageOverview = remediation.status === 'ready' ? remediation.overview : undefined;
const complianceFacets = useComplianceWorkFacets();
const comparisonFacets = useComparisonWorkFacets();
const [failed, setFailed] = useState(false);
const [packagesOpen, setPackagesOpen] = useState(false);
const exportView = async (format: 'csv' | 'md' | 'html') => {
if (!app.dataset || !app.activeSheet || !records.length) return;
const activity = beginActivity('Core Engine', `Preparing Work ${format.toUpperCase()} export`);
try {
await showActivityFrame();
const facets = selectWorkFacets(app.activeSheet, { ...app, contextClusters, workPackages, complianceFacets, comparisonFacets });
const statusLabel = app.activeProfile.presentation.statusFilters.find(filter => filter.value === app.statusFilter)?.label ?? app.statusFilter;
const filters = Object.entries(app.facetFilters).map(([id, filter]) => {
const facet = facets.find(item => item.id === id);
return `${facet?.label ?? id}: ${filter.values?.map(value => facet?.values.find(item => item.value === value)?.label ?? value).join(', ') ?? ''}${filter.minimum ? ` ≥ ${filter.minimum}` : ''}${filter.maximum ? ` ≤ ${filter.maximum}` : ''}`;
});
const report = buildWorkViewExport({ dataset: app.dataset, sheet: app.activeSheet, records,
generatedAt: new Date().toISOString(), reviews: app.reviews, titleColumn: app.titleColumn,
context: [
{ label: 'Scope', value: [`Project: ${app.projectId}`, `Assessments: ${app.activeAssessmentIds?.join(', ') ?? 'geladener Workspace'}`, `Scan runs: ${app.activeScanRunIds?.join(', ') ?? 'geladener Workspace'}`].join('\n') },
{ label: 'Filter', value: [`Result Status: ${statusLabel}`, ...filters, `Suche: ${app.search || '—'}`, `My Workspace: ${app.actionOnly ? 'Ja' : 'Nein'}`, `Drilldown: ${app.pathFindingIds.length ? `${app.pathFindingIds.length} Record IDs` : 'Nein'}`].join('\n') },
{ label: 'Sortierung', value: `${app.sortColumn} · ${app.sortDirection}` }
] });
const text = format === 'html' ? renderWorkViewHtml(report) : format === 'md' ? renderWorkViewMarkdown(report)
: renderWorkViewCsv(report, app.delimiter === 'auto' ? ';' : app.delimiter);
const stem = app.dataset.fileName.replace(/\.[^.]+$/, '').replace(/[<>:"/\\|?*\u0000-\u001f]/g, '_');
downloadText(browserDownloadAdapter, `${stem}-work-view.${format}`, text,
format === 'html' ? 'text/html;charset=utf-8' : format === 'md' ? 'text/markdown;charset=utf-8' : 'text/csv;charset=utf-8');
setFailed(false);
} catch { activity.fail(); setFailed(true); } finally { activity.complete(); }
};
return <div className="header-export-row" aria-label="Export current view including Results">
{/* Remediation Engine extension point: packages appear only while the engine is on and its result is ready. */}
{workPackages && <button className="btn export-btn work-packages-button" type="button" data-work-packages-open
aria-pressed={Boolean(packageOverview && packagesOpen)}
title={`Show all work packages and Unassigned for this assessment`} onClick={() => setPackagesOpen(open => !open)}>Work Packages</button>}
{(['csv', 'md', 'html'] as const).map(format => <button key={format} className="btn export-btn" type="button" disabled={!records.length}
title={`Export current view including Results (${records.length} records)`} onClick={() => exportView(format)}>{format.toUpperCase()}</button>)}
{packageOverview && packagesOpen && <WorkPackagesWindow overview={packageOverview}
clusterOrder={contextClusters?.publication.clusters.map(cluster => cluster.id)}
clusterLabels={Object.fromEntries(workPackages!.packages.map(p => [p.clusterId, numberedClusterLabel(p.clusterId, p.clusterLabel, contextClusters?.publication.clusters)]))}
clusterSeverities={Object.fromEntries(workPackages!.packages.map(p => [p.clusterId,
publishedClusterColour(contextClusters?.publication.clusters.find(cluster => cluster.id === p.clusterId))]))}
onSelectRelated={() => related?.showAllInWork()}
onClose={() => setPackagesOpen(false)}
onSelectUnassigned={() => app.setFacetFilter(WORK_PACKAGE_FACET_ID, { values: [UNASSIGNED_WORK_PACKAGE] })}
onSelect={workPackage => app.setFacetFilter(WORK_PACKAGE_FACET_ID, { values: [workPackage.id] })}/>}
{failed && <Dialog title="Export unavailable" onClose={() => setFailed(false)}><p>The current view could not be exported completely. No partial file was downloaded.</p></Dialog>}
</div>;
}