Polish operation form readability
GeoIntel CI / docs-smoke (push) Has been cancelled
GeoIntel CI / contract-smoke (push) Has been cancelled

This commit is contained in:
Codex
2026-06-20 02:39:44 +02:00
parent 4e6ba707cf
commit b15b123173
8 changed files with 315 additions and 121 deletions
+8
View File
@@ -7,6 +7,14 @@
# Changelog
## Sprint 80 Operation form readability polish (2026-06-20)
- Added structured headings, helper text, field wrappers and action rows to dense raster operation controls.
- Added the same form readability structure to vector clip, buffer and intersect controls.
- Added compact CSS contracts for dataset tool headings, helper text, field grids, action rows and inline error blocks.
- Added static regression coverage for raster/vector operation form readability contracts.
- No API contracts, migrations, backend behavior, provider fetching or AI model behavior changed.
## Sprint 79 Accessibility focus polish (2026-06-20)
- Added a shared visible focus-ring contract for primary buttons, workspace navigation, command chips, inspector tabs and dataset action buttons.
@@ -0,0 +1,49 @@
from pathlib import Path
ROOT = Path(__file__).resolve().parents[2]
def test_dataset_operation_form_css_contracts() -> None:
css = (ROOT / "frontend" / "src" / "styles" / "app.css").read_text(encoding="utf-8")
assert ".dataset-tool-heading" in css
assert ".dataset-tool-helper" in css
assert ".dataset-tool-field" in css
assert ".dataset-tool-label" in css
assert ".dataset-tool-error" in css
assert ".dataset-tool-action-row" in css
assert ".dataset-tool-grid" in css
assert "grid-template-columns: repeat(auto-fit, minmax(9rem, 1fr));" in css
assert "overflow-wrap: anywhere;" in css
def test_raster_controls_expose_readable_operation_groups() -> None:
raster_controls = (ROOT / "frontend" / "src" / "components" / "datasets" / "RasterControls.tsx").read_text(
encoding="utf-8"
)
assert 'className="dataset-tool-heading"' in raster_controls
assert 'className="dataset-tool-helper"' in raster_controls
assert 'className="dataset-tool-grid"' in raster_controls
assert 'className="dataset-tool-field"' in raster_controls
assert 'className="dataset-tool-label"' in raster_controls
assert 'className="dataset-tool-action-row"' in raster_controls
assert 'className="dataset-tool-error"' in raster_controls
assert "Target CRS for the derived raster artifact." in raster_controls
assert "Tile size must be > 0." in raster_controls
def test_vector_controls_expose_readable_operation_groups() -> None:
vector_controls = (ROOT / "frontend" / "src" / "components" / "datasets" / "VectorControls.tsx").read_text(
encoding="utf-8"
)
assert 'className="dataset-tool-heading"' in vector_controls
assert 'className="dataset-tool-helper"' in vector_controls
assert 'className="dataset-tool-grid"' in vector_controls
assert 'className="dataset-tool-field"' in vector_controls
assert 'className="dataset-tool-label"' in vector_controls
assert 'className="dataset-tool-action-row"' in vector_controls
assert "Clip features to the selected project area." in vector_controls
assert "Intersect with another persisted vector dataset." in vector_controls
+26
View File
@@ -2929,3 +2929,29 @@ Limitations:
Next recommended pass:
- Continue with form-level validation/readability polish for dense raster/vector operation panels.
## Sprint 80 Operation form readability polish (2026-06-20)
Changed:
- Added structured headings, helper text, field wrappers, responsive field grids and action rows to raster operation controls.
- Added equivalent readability structure to vector clip, buffer and intersect controls.
- Added inline dataset tool error styling for raster clip/tile validation messages.
- Added `backend/tests/test_sprint80_operation_form_readability.py`.
- Updated `frontend/README.md`, `docs/TODO.md` and `CHANGELOG.md`.
Tested:
- Red step: `python -m pytest backend/tests/test_sprint80_operation_form_readability.py -q` failed on missing form readability CSS and markup contracts.
- `python -m pytest backend/tests/test_sprint80_operation_form_readability.py -q` (`3 passed`)
- `python -m pytest backend/tests/test_sprint80_operation_form_readability.py backend/tests/test_sprint77_inspector_mobile_polish.py backend/tests/test_sprint29_dataset_components.py backend/tests/test_sprint28_dataset_workflow_hook.py backend/tests/test_vector_operations_service.py backend/tests/test_raster_operations_service.py -q` (`37 passed`)
- `cd frontend && npm run typecheck`
- `cd frontend && npm run build`
- `bash scripts/run_readiness_check.sh` (`273 passed`; frontend typecheck/build passed; Alembic single head `202606120900`)
Open:
- Run full readiness, redeploy Tower and verify live runtime smoke after deployment.
Limitations:
- Frontend readability/presentation polish only; no raster/vector operation behavior, API contract, persistence, migration, provider fetching or AI/model changes.
Next recommended pass:
- Continue with compact empty/error-state polish across QA, exports and AI lab result panels.
+1
View File
@@ -69,6 +69,7 @@ This file now starts with the current implementation status. Older preparation/b
- [x] Add inspector mobile polish for dataset metadata, raster/vector tools and action groups.
- [x] Add export preview readability polish for large JSON/GeoJSON handoff artifacts.
- [x] Add accessibility focus polish for primary workbench keyboard navigation.
- [x] Add raster/vector operation form readability polish for dense tool panels.
## Sprint 8 status
+2
View File
@@ -16,6 +16,8 @@ Export Preview shows a compact JSON summary before the payload and keeps large J
Primary workbench navigation, overview shortcuts, inspector tabs and dataset action buttons now share visible keyboard focus styling. Inspector tabs are also bound to tab panels with ARIA metadata.
Raster and vector operation panels use structured group headings, compact helper text, field grids, action rows and inline error blocks so dense GIS controls remain scannable in the inspector.
## Scope implemented
- API client layer (`src/services/api`)
- Project and area list/create flows
@@ -133,29 +133,39 @@ export function RasterControls({
</div>
) : null}
<div className="dataset-tool-group">
<label>
Reproject CRS
<input
value={rasterReprojectCrs}
onChange={(event) => onSetRasterReprojectCrs(event.target.value)}
placeholder="EPSG:31370"
/>
</label>
<label>
Resampling
<select value={rasterReprojectResampling} onChange={(event) => onSetRasterReprojectResampling(event.target.value)}>
<option value="nearest">nearest</option>
<option value="bilinear">bilinear</option>
<option value="cubic">cubic</option>
</select>
</label>
<button type="button" onClick={onRunRasterReproject}>
Reproject raster
</button>
<h4 className="dataset-tool-heading">Reproject raster</h4>
<p className="dataset-tool-helper">Create a derived raster in a target CRS with the selected resampling method.</p>
<div className="dataset-tool-grid">
<label className="dataset-tool-field">
<span className="dataset-tool-label">Reproject CRS</span>
<input
value={rasterReprojectCrs}
onChange={(event) => onSetRasterReprojectCrs(event.target.value)}
placeholder="EPSG:31370"
/>
<span className="dataset-tool-helper">Target CRS for the derived raster artifact.</span>
</label>
<label className="dataset-tool-field">
<span className="dataset-tool-label">Resampling</span>
<select value={rasterReprojectResampling} onChange={(event) => onSetRasterReprojectResampling(event.target.value)}>
<option value="nearest">nearest</option>
<option value="bilinear">bilinear</option>
<option value="cubic">cubic</option>
</select>
<span className="dataset-tool-helper">Nearest preserves classes; bilinear/cubic smooth continuous rasters.</span>
</label>
</div>
<div className="dataset-tool-action-row">
<button type="button" onClick={onRunRasterReproject}>
Reproject raster
</button>
</div>
</div>
<div className="dataset-tool-group">
<label>
Clip area
<h4 className="dataset-tool-heading">Clip raster</h4>
<p className="dataset-tool-helper">Clip the selected raster to an existing project area.</p>
<label className="dataset-tool-field">
<span className="dataset-tool-label">Clip area</span>
<select value={selectedClipAreaId} onChange={(event) => onSetSelectedClipAreaId(event.target.value)}>
{areas.map((area) => (
<option key={area.id} value={area.id}>
@@ -164,43 +174,54 @@ export function RasterControls({
))}
</select>
</label>
<button type="button" onClick={onRunRasterClip} disabled={areas.length === 0}>
Clip raster by area
</button>
{areas.length === 0 ? <p className="error">Create an area before raster clipping.</p> : null}
<div className="dataset-tool-action-row">
<button type="button" onClick={onRunRasterClip} disabled={areas.length === 0}>
Clip raster by area
</button>
</div>
{areas.length === 0 ? <p className="dataset-tool-error">Create an area before raster clipping.</p> : null}
</div>
<div className="dataset-tool-group">
<label>
Tile size
<input
type="number"
min={1}
value={rasterTileSize}
onChange={(event) => onSetRasterTileSize(Number(event.target.value))}
/>
</label>
<label>
Overlap
<input
type="number"
min={0}
value={rasterTileOverlap}
onChange={(event) => onSetRasterTileOverlap(Number(event.target.value))}
/>
</label>
<label>
Tile basename
<input
value={rasterTileOutputName}
onChange={(event) => onSetRasterTileOutputName(event.target.value)}
placeholder="optional"
/>
</label>
<button type="button" onClick={onRunRasterTile} disabled={!isRasterTileInputValid}>
Generate tiles
</button>
<h4 className="dataset-tool-heading">Generate tiles</h4>
<p className="dataset-tool-helper">Create a tile manifest for downstream detection or segmentation runs.</p>
<div className="dataset-tool-grid">
<label className="dataset-tool-field">
<span className="dataset-tool-label">Tile size</span>
<input
type="number"
min={1}
value={rasterTileSize}
onChange={(event) => onSetRasterTileSize(Number(event.target.value))}
/>
<span className="dataset-tool-helper">{'Tile size must be > 0.'}</span>
</label>
<label className="dataset-tool-field">
<span className="dataset-tool-label">Overlap</span>
<input
type="number"
min={0}
value={rasterTileOverlap}
onChange={(event) => onSetRasterTileOverlap(Number(event.target.value))}
/>
<span className="dataset-tool-helper">Overlap must be smaller than tile size.</span>
</label>
<label className="dataset-tool-field">
<span className="dataset-tool-label">Tile basename</span>
<input
value={rasterTileOutputName}
onChange={(event) => onSetRasterTileOutputName(event.target.value)}
placeholder="optional"
/>
<span className="dataset-tool-helper">Optional artifact name prefix for generated tiles.</span>
</label>
</div>
<div className="dataset-tool-action-row">
<button type="button" onClick={onRunRasterTile} disabled={!isRasterTileInputValid}>
Generate tiles
</button>
</div>
{!isRasterTileInputValid ? (
<p className="error">
<p className="dataset-tool-error">
Tile size must be {'>'} 0 and overlap must be {'>='} 0 and smaller than tile size.
</p>
) : null}
@@ -209,46 +230,61 @@ export function RasterControls({
<div>
<p>Use available band indexes from the raster file (1-based).</p>
<div className="dataset-tool-group">
<p>NDVI</p>
<label>
Nir band
<input type="number" min={1} value={ndviNirBand} onChange={(event) => onSetNdviNirBand(Number(event.target.value))} />
</label>
<label>
Red band
<input type="number" min={1} value={ndviRedBand} onChange={(event) => onSetNdviRedBand(Number(event.target.value))} />
</label>
<button type="button" onClick={onRunRasterNdvi}>
Compute NDVI
</button>
<h4 className="dataset-tool-heading">NDVI</h4>
<p className="dataset-tool-helper">Vegetation index from NIR and red bands.</p>
<div className="dataset-tool-grid">
<label className="dataset-tool-field">
<span className="dataset-tool-label">NIR band</span>
<input type="number" min={1} value={ndviNirBand} onChange={(event) => onSetNdviNirBand(Number(event.target.value))} />
</label>
<label className="dataset-tool-field">
<span className="dataset-tool-label">Red band</span>
<input type="number" min={1} value={ndviRedBand} onChange={(event) => onSetNdviRedBand(Number(event.target.value))} />
</label>
</div>
<div className="dataset-tool-action-row">
<button type="button" onClick={onRunRasterNdvi}>
Compute NDVI
</button>
</div>
</div>
<div className="dataset-tool-group">
<p>NDWI</p>
<label>
Nir band
<input type="number" min={1} value={ndwiNirBand} onChange={(event) => onSetNdwiNirBand(Number(event.target.value))} />
</label>
<label>
Green band
<input type="number" min={1} value={ndwiGreenBand} onChange={(event) => onSetNdwiGreenBand(Number(event.target.value))} />
</label>
<button type="button" onClick={onRunRasterNdwi}>
Compute NDWI
</button>
<h4 className="dataset-tool-heading">NDWI</h4>
<p className="dataset-tool-helper">Water index from NIR and green bands.</p>
<div className="dataset-tool-grid">
<label className="dataset-tool-field">
<span className="dataset-tool-label">NIR band</span>
<input type="number" min={1} value={ndwiNirBand} onChange={(event) => onSetNdwiNirBand(Number(event.target.value))} />
</label>
<label className="dataset-tool-field">
<span className="dataset-tool-label">Green band</span>
<input type="number" min={1} value={ndwiGreenBand} onChange={(event) => onSetNdwiGreenBand(Number(event.target.value))} />
</label>
</div>
<div className="dataset-tool-action-row">
<button type="button" onClick={onRunRasterNdwi}>
Compute NDWI
</button>
</div>
</div>
<div className="dataset-tool-group">
<p>NDBI</p>
<label>
Swir band
<input type="number" min={1} value={ndbiSwirBand} onChange={(event) => onSetNdbiSwirBand(Number(event.target.value))} />
</label>
<label>
Nir band
<input type="number" min={1} value={ndbiNirBand} onChange={(event) => onSetNdbiNirBand(Number(event.target.value))} />
</label>
<button type="button" onClick={onRunRasterNdbi}>
Compute NDBI
</button>
<h4 className="dataset-tool-heading">NDBI</h4>
<p className="dataset-tool-helper">Built-up index from SWIR and NIR bands.</p>
<div className="dataset-tool-grid">
<label className="dataset-tool-field">
<span className="dataset-tool-label">SWIR band</span>
<input type="number" min={1} value={ndbiSwirBand} onChange={(event) => onSetNdbiSwirBand(Number(event.target.value))} />
</label>
<label className="dataset-tool-field">
<span className="dataset-tool-label">NIR band</span>
<input type="number" min={1} value={ndbiNirBand} onChange={(event) => onSetNdbiNirBand(Number(event.target.value))} />
</label>
</div>
<div className="dataset-tool-action-row">
<button type="button" onClick={onRunRasterNdbi}>
Compute NDBI
</button>
</div>
</div>
</div>
</div>
@@ -27,38 +27,57 @@ export function VectorControls({
<div className="dataset-tool-panel vector-tool-panel">
<h3>Vector operations</h3>
<div className="dataset-tool-group">
<label>
Clip area
<select value={selectedClipAreaId} onChange={(event) => onSetSelectedClipAreaId(event.target.value)}>
{areas.map((area) => (
<option key={area.id} value={area.id}>
{area.name}
</option>
))}
</select>
</label>
<button type="button" onClick={onRunVectorClip} disabled={areas.length === 0}>
Run clip
</button>
<h4 className="dataset-tool-heading">Clip vector</h4>
<p className="dataset-tool-helper">Clip features to the selected project area.</p>
<div className="dataset-tool-grid">
<label className="dataset-tool-field">
<span className="dataset-tool-label">Clip area</span>
<select value={selectedClipAreaId} onChange={(event) => onSetSelectedClipAreaId(event.target.value)}>
{areas.map((area) => (
<option key={area.id} value={area.id}>
{area.name}
</option>
))}
</select>
</label>
</div>
<div className="dataset-tool-action-row">
<button type="button" onClick={onRunVectorClip} disabled={areas.length === 0}>
Run clip
</button>
</div>
</div>
<button type="button" onClick={onRunVectorBuffer}>
Run buffer (25m)
</button>
<div className="dataset-tool-group">
<label>
Intersect target
<select value={selectedIntersectTargetId} onChange={(event) => onSetSelectedIntersectTargetId(event.target.value)}>
<option value="">auto first vector</option>
{availableVectorTargets.map((target) => (
<option key={target.id} value={target.id}>
{target.name}
</option>
))}
</select>
</label>
<button type="button" onClick={onRunVectorIntersect}>
Run intersect
</button>
<h4 className="dataset-tool-heading">Buffer vector</h4>
<p className="dataset-tool-helper">Create a 25m buffer using the existing vector operation defaults.</p>
<div className="dataset-tool-action-row">
<button type="button" onClick={onRunVectorBuffer}>
Run buffer (25m)
</button>
</div>
</div>
<div className="dataset-tool-group">
<h4 className="dataset-tool-heading">Intersect vector</h4>
<p className="dataset-tool-helper">Intersect with another persisted vector dataset.</p>
<div className="dataset-tool-grid">
<label className="dataset-tool-field">
<span className="dataset-tool-label">Intersect target</span>
<select value={selectedIntersectTargetId} onChange={(event) => onSetSelectedIntersectTargetId(event.target.value)}>
<option value="">auto first vector</option>
{availableVectorTargets.map((target) => (
<option key={target.id} value={target.id}>
{target.name}
</option>
))}
</select>
<span className="dataset-tool-helper">Leave automatic to use the first available vector target.</span>
</label>
</div>
<div className="dataset-tool-action-row">
<button type="button" onClick={onRunVectorIntersect}>
Run intersect
</button>
</div>
</div>
</div>
)
+53
View File
@@ -2148,16 +2148,69 @@ button.entity-card {
background: #ffffff;
}
.dataset-tool-heading {
margin: 0;
font-size: 0.92rem;
line-height: 1.25;
}
.dataset-tool-helper {
margin: 0;
color: var(--muted);
font-size: 0.82rem;
line-height: 1.38;
overflow-wrap: anywhere;
}
.dataset-tool-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(9rem, 1fr));
gap: 0.52rem;
}
.dataset-tool-group label {
min-width: 0;
}
.dataset-tool-field {
display: grid;
gap: 0.22rem;
min-width: 0;
}
.dataset-tool-label {
color: var(--text);
font-size: 0.78rem;
font-weight: 800;
line-height: 1.25;
}
.dataset-tool-group input,
.dataset-tool-group select {
width: 100%;
min-width: 0;
}
.dataset-tool-error {
margin: 0;
border: 1px solid rgba(153, 27, 27, 0.24);
border-radius: 8px;
padding: 0.52rem 0.62rem;
background: rgba(153, 27, 27, 0.07);
color: var(--danger);
font-size: 0.82rem;
font-weight: 750;
line-height: 1.35;
overflow-wrap: anywhere;
}
.dataset-tool-action-row {
display: flex;
flex-wrap: wrap;
gap: 0.5rem;
align-items: center;
}
.dataset-tool-group button {
width: fit-content;
max-width: 100%;