Add export history controls
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
import { useState } from 'react'
|
||||
import { useMemo, useState } from 'react'
|
||||
import type { DatasetCreateResponse, ExportCreateResponse, ExportRead } from '../../types'
|
||||
|
||||
interface ExportCenterProps {
|
||||
@@ -43,6 +43,16 @@ function compactPath(value: string): string {
|
||||
return parts.slice(-2).join('/')
|
||||
}
|
||||
|
||||
function exportMatchesSearch(item: ExportRead, query: string): boolean {
|
||||
const normalizedQuery = query.trim().toLowerCase()
|
||||
if (!normalizedQuery) {
|
||||
return true
|
||||
}
|
||||
return [item.export_type, item.status, item.id, item.storage_path]
|
||||
.filter(Boolean)
|
||||
.some((value) => String(value).toLowerCase().includes(normalizedQuery))
|
||||
}
|
||||
|
||||
export function ExportCenter({
|
||||
selectedProjectId,
|
||||
selectedDataset,
|
||||
@@ -63,11 +73,26 @@ export function ExportCenter({
|
||||
onDownload,
|
||||
}: ExportCenterProps): JSX.Element {
|
||||
const [showAllExports, setShowAllExports] = useState(false)
|
||||
const [exportTypeFilter, setExportTypeFilter] = useState('all')
|
||||
const [exportStatusFilter, setExportStatusFilter] = useState('all')
|
||||
const [exportSearchQuery, setExportSearchQuery] = useState('')
|
||||
const canExportDataset = Boolean(selectedDataset && isVectorDatasetType(selectedDataset.dataset_type))
|
||||
const geojsonExports = exports.filter((item) => item.export_type.includes('geojson')).length
|
||||
const reportExports = exports.filter((item) => item.export_type === 'project_report_html').length
|
||||
const visibleExports = showAllExports ? exports : exports.slice(0, 10)
|
||||
const hiddenExportCount = Math.max(exports.length - visibleExports.length, 0)
|
||||
const exportTypes = useMemo(() => Array.from(new Set(exports.map((item) => item.export_type))).sort(), [exports])
|
||||
const exportStatuses = useMemo(() => Array.from(new Set(exports.map((item) => item.status))).sort(), [exports])
|
||||
const filteredExports = useMemo(
|
||||
() =>
|
||||
exports.filter((item) => {
|
||||
const typeMatches = exportTypeFilter === 'all' || item.export_type === exportTypeFilter
|
||||
const statusMatches = exportStatusFilter === 'all' || item.status === exportStatusFilter
|
||||
return typeMatches && statusMatches && exportMatchesSearch(item, exportSearchQuery)
|
||||
}),
|
||||
[exports, exportTypeFilter, exportStatusFilter, exportSearchQuery],
|
||||
)
|
||||
const visibleExports = showAllExports ? filteredExports : filteredExports.slice(0, 10)
|
||||
const hiddenExportCount = Math.max(filteredExports.length - visibleExports.length, 0)
|
||||
const hasActiveExportFilters = exportTypeFilter !== 'all' || exportStatusFilter !== 'all' || exportSearchQuery.trim().length > 0
|
||||
|
||||
return (
|
||||
<section className="export-center" data-testid="export-center">
|
||||
@@ -138,16 +163,71 @@ export function ExportCenter({
|
||||
<p>Create metadata, GeoJSON or HTML report artifacts once the active project has data to hand off.</p>
|
||||
</div>
|
||||
) : null}
|
||||
{exports.length > 10 ? (
|
||||
{exports.length > 0 ? (
|
||||
<div className="export-history-controls" aria-label="Export history filters">
|
||||
<label>
|
||||
Search exports
|
||||
<input
|
||||
type="search"
|
||||
placeholder="Type, id or path"
|
||||
value={exportSearchQuery}
|
||||
onChange={(event) => setExportSearchQuery(event.target.value)}
|
||||
/>
|
||||
</label>
|
||||
<label>
|
||||
Type
|
||||
<select value={exportTypeFilter} onChange={(event) => setExportTypeFilter(event.target.value)}>
|
||||
<option value="all">All types</option>
|
||||
{exportTypes.map((type) => (
|
||||
<option key={type} value={type}>
|
||||
{type}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</label>
|
||||
<label>
|
||||
Status
|
||||
<select value={exportStatusFilter} onChange={(event) => setExportStatusFilter(event.target.value)}>
|
||||
<option value="all">All statuses</option>
|
||||
{exportStatuses.map((status) => (
|
||||
<option key={status} value={status}>
|
||||
{status}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</label>
|
||||
<button
|
||||
type="button"
|
||||
className="secondary-action"
|
||||
onClick={() => {
|
||||
setExportSearchQuery('')
|
||||
setExportTypeFilter('all')
|
||||
setExportStatusFilter('all')
|
||||
setShowAllExports(false)
|
||||
}}
|
||||
disabled={!hasActiveExportFilters && !showAllExports}
|
||||
>
|
||||
Reset view
|
||||
</button>
|
||||
</div>
|
||||
) : null}
|
||||
{filteredExports.length > 10 ? (
|
||||
<div className="list-limit-banner">
|
||||
<span>
|
||||
Showing {visibleExports.length} latest exports. {hiddenExportCount > 0 ? `${hiddenExportCount} older artifacts hidden.` : 'All artifacts shown.'}
|
||||
Showing {visibleExports.length} of {filteredExports.length} matching exports.
|
||||
{hiddenExportCount > 0 ? ` ${hiddenExportCount} older artifacts hidden.` : ' All matching artifacts shown.'}
|
||||
</span>
|
||||
<button type="button" className="secondary-action" onClick={() => setShowAllExports((value) => !value)}>
|
||||
{showAllExports ? 'Show latest 10' : 'Show all exports'}
|
||||
</button>
|
||||
</div>
|
||||
) : null}
|
||||
{exports.length > 0 && filteredExports.length === 0 ? (
|
||||
<div className="empty-state">
|
||||
<strong>No exports match the current filters.</strong>
|
||||
<p>Clear the search, type or status filter to return to the latest artifacts.</p>
|
||||
</div>
|
||||
) : null}
|
||||
<ul className="export-list">
|
||||
{visibleExports.map((item) => (
|
||||
<li className="export-card" key={item.id}>
|
||||
|
||||
@@ -1075,7 +1075,7 @@ button.entity-card {
|
||||
}
|
||||
|
||||
.export-action-grid button {
|
||||
min-height: 3.2rem;
|
||||
min-height: 2.8rem;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
@@ -1087,6 +1087,27 @@ button.entity-card {
|
||||
background: var(--accent-soft);
|
||||
}
|
||||
|
||||
.export-history-controls {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(14rem, 1.2fr) minmax(10rem, 0.9fr) minmax(9rem, 0.8fr) auto;
|
||||
gap: 0.65rem;
|
||||
align-items: end;
|
||||
margin-top: 0.8rem;
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 8px;
|
||||
padding: 0.72rem;
|
||||
background: #ffffff;
|
||||
}
|
||||
|
||||
.export-history-controls label {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.export-history-controls button {
|
||||
min-height: 2.35rem;
|
||||
width: auto;
|
||||
}
|
||||
|
||||
.list-limit-banner {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr) auto;
|
||||
@@ -1378,6 +1399,7 @@ button.entity-card {
|
||||
.quality-summary-grid,
|
||||
.quality-score-row,
|
||||
.export-action-grid,
|
||||
.export-history-controls,
|
||||
.inspector-action-bar,
|
||||
.list-limit-banner {
|
||||
grid-template-columns: 1fr;
|
||||
|
||||
Reference in New Issue
Block a user