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 # 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) ## 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. - 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: Next recommended pass:
- Continue with form-level validation/readability polish for dense raster/vector operation panels. - 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 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 export preview readability polish for large JSON/GeoJSON handoff artifacts.
- [x] Add accessibility focus polish for primary workbench keyboard navigation. - [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 ## 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. 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 ## Scope implemented
- API client layer (`src/services/api`) - API client layer (`src/services/api`)
- Project and area list/create flows - Project and area list/create flows
@@ -133,29 +133,39 @@ export function RasterControls({
</div> </div>
) : null} ) : null}
<div className="dataset-tool-group"> <div className="dataset-tool-group">
<label> <h4 className="dataset-tool-heading">Reproject raster</h4>
Reproject CRS <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 <input
value={rasterReprojectCrs} value={rasterReprojectCrs}
onChange={(event) => onSetRasterReprojectCrs(event.target.value)} onChange={(event) => onSetRasterReprojectCrs(event.target.value)}
placeholder="EPSG:31370" placeholder="EPSG:31370"
/> />
<span className="dataset-tool-helper">Target CRS for the derived raster artifact.</span>
</label> </label>
<label> <label className="dataset-tool-field">
Resampling <span className="dataset-tool-label">Resampling</span>
<select value={rasterReprojectResampling} onChange={(event) => onSetRasterReprojectResampling(event.target.value)}> <select value={rasterReprojectResampling} onChange={(event) => onSetRasterReprojectResampling(event.target.value)}>
<option value="nearest">nearest</option> <option value="nearest">nearest</option>
<option value="bilinear">bilinear</option> <option value="bilinear">bilinear</option>
<option value="cubic">cubic</option> <option value="cubic">cubic</option>
</select> </select>
<span className="dataset-tool-helper">Nearest preserves classes; bilinear/cubic smooth continuous rasters.</span>
</label> </label>
</div>
<div className="dataset-tool-action-row">
<button type="button" onClick={onRunRasterReproject}> <button type="button" onClick={onRunRasterReproject}>
Reproject raster Reproject raster
</button> </button>
</div> </div>
</div>
<div className="dataset-tool-group"> <div className="dataset-tool-group">
<label> <h4 className="dataset-tool-heading">Clip raster</h4>
Clip area <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)}> <select value={selectedClipAreaId} onChange={(event) => onSetSelectedClipAreaId(event.target.value)}>
{areas.map((area) => ( {areas.map((area) => (
<option key={area.id} value={area.id}> <option key={area.id} value={area.id}>
@@ -164,43 +174,54 @@ export function RasterControls({
))} ))}
</select> </select>
</label> </label>
<div className="dataset-tool-action-row">
<button type="button" onClick={onRunRasterClip} disabled={areas.length === 0}> <button type="button" onClick={onRunRasterClip} disabled={areas.length === 0}>
Clip raster by area Clip raster by area
</button> </button>
{areas.length === 0 ? <p className="error">Create an area before raster clipping.</p> : null} </div>
{areas.length === 0 ? <p className="dataset-tool-error">Create an area before raster clipping.</p> : null}
</div> </div>
<div className="dataset-tool-group"> <div className="dataset-tool-group">
<label> <h4 className="dataset-tool-heading">Generate tiles</h4>
Tile size <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 <input
type="number" type="number"
min={1} min={1}
value={rasterTileSize} value={rasterTileSize}
onChange={(event) => onSetRasterTileSize(Number(event.target.value))} onChange={(event) => onSetRasterTileSize(Number(event.target.value))}
/> />
<span className="dataset-tool-helper">{'Tile size must be > 0.'}</span>
</label> </label>
<label> <label className="dataset-tool-field">
Overlap <span className="dataset-tool-label">Overlap</span>
<input <input
type="number" type="number"
min={0} min={0}
value={rasterTileOverlap} value={rasterTileOverlap}
onChange={(event) => onSetRasterTileOverlap(Number(event.target.value))} onChange={(event) => onSetRasterTileOverlap(Number(event.target.value))}
/> />
<span className="dataset-tool-helper">Overlap must be smaller than tile size.</span>
</label> </label>
<label> <label className="dataset-tool-field">
Tile basename <span className="dataset-tool-label">Tile basename</span>
<input <input
value={rasterTileOutputName} value={rasterTileOutputName}
onChange={(event) => onSetRasterTileOutputName(event.target.value)} onChange={(event) => onSetRasterTileOutputName(event.target.value)}
placeholder="optional" placeholder="optional"
/> />
<span className="dataset-tool-helper">Optional artifact name prefix for generated tiles.</span>
</label> </label>
</div>
<div className="dataset-tool-action-row">
<button type="button" onClick={onRunRasterTile} disabled={!isRasterTileInputValid}> <button type="button" onClick={onRunRasterTile} disabled={!isRasterTileInputValid}>
Generate tiles Generate tiles
</button> </button>
</div>
{!isRasterTileInputValid ? ( {!isRasterTileInputValid ? (
<p className="error"> <p className="dataset-tool-error">
Tile size must be {'>'} 0 and overlap must be {'>='} 0 and smaller than tile size. Tile size must be {'>'} 0 and overlap must be {'>='} 0 and smaller than tile size.
</p> </p>
) : null} ) : null}
@@ -209,48 +230,63 @@ export function RasterControls({
<div> <div>
<p>Use available band indexes from the raster file (1-based).</p> <p>Use available band indexes from the raster file (1-based).</p>
<div className="dataset-tool-group"> <div className="dataset-tool-group">
<p>NDVI</p> <h4 className="dataset-tool-heading">NDVI</h4>
<label> <p className="dataset-tool-helper">Vegetation index from NIR and red bands.</p>
Nir band <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))} /> <input type="number" min={1} value={ndviNirBand} onChange={(event) => onSetNdviNirBand(Number(event.target.value))} />
</label> </label>
<label> <label className="dataset-tool-field">
Red band <span className="dataset-tool-label">Red band</span>
<input type="number" min={1} value={ndviRedBand} onChange={(event) => onSetNdviRedBand(Number(event.target.value))} /> <input type="number" min={1} value={ndviRedBand} onChange={(event) => onSetNdviRedBand(Number(event.target.value))} />
</label> </label>
</div>
<div className="dataset-tool-action-row">
<button type="button" onClick={onRunRasterNdvi}> <button type="button" onClick={onRunRasterNdvi}>
Compute NDVI Compute NDVI
</button> </button>
</div> </div>
</div>
<div className="dataset-tool-group"> <div className="dataset-tool-group">
<p>NDWI</p> <h4 className="dataset-tool-heading">NDWI</h4>
<label> <p className="dataset-tool-helper">Water index from NIR and green bands.</p>
Nir band <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))} /> <input type="number" min={1} value={ndwiNirBand} onChange={(event) => onSetNdwiNirBand(Number(event.target.value))} />
</label> </label>
<label> <label className="dataset-tool-field">
Green band <span className="dataset-tool-label">Green band</span>
<input type="number" min={1} value={ndwiGreenBand} onChange={(event) => onSetNdwiGreenBand(Number(event.target.value))} /> <input type="number" min={1} value={ndwiGreenBand} onChange={(event) => onSetNdwiGreenBand(Number(event.target.value))} />
</label> </label>
</div>
<div className="dataset-tool-action-row">
<button type="button" onClick={onRunRasterNdwi}> <button type="button" onClick={onRunRasterNdwi}>
Compute NDWI Compute NDWI
</button> </button>
</div> </div>
</div>
<div className="dataset-tool-group"> <div className="dataset-tool-group">
<p>NDBI</p> <h4 className="dataset-tool-heading">NDBI</h4>
<label> <p className="dataset-tool-helper">Built-up index from SWIR and NIR bands.</p>
Swir band <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))} /> <input type="number" min={1} value={ndbiSwirBand} onChange={(event) => onSetNdbiSwirBand(Number(event.target.value))} />
</label> </label>
<label> <label className="dataset-tool-field">
Nir band <span className="dataset-tool-label">NIR band</span>
<input type="number" min={1} value={ndbiNirBand} onChange={(event) => onSetNdbiNirBand(Number(event.target.value))} /> <input type="number" min={1} value={ndbiNirBand} onChange={(event) => onSetNdbiNirBand(Number(event.target.value))} />
</label> </label>
</div>
<div className="dataset-tool-action-row">
<button type="button" onClick={onRunRasterNdbi}> <button type="button" onClick={onRunRasterNdbi}>
Compute NDBI Compute NDBI
</button> </button>
</div> </div>
</div> </div>
</div> </div>
</div>
) )
} }
@@ -27,8 +27,11 @@ export function VectorControls({
<div className="dataset-tool-panel vector-tool-panel"> <div className="dataset-tool-panel vector-tool-panel">
<h3>Vector operations</h3> <h3>Vector operations</h3>
<div className="dataset-tool-group"> <div className="dataset-tool-group">
<label> <h4 className="dataset-tool-heading">Clip vector</h4>
Clip area <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)}> <select value={selectedClipAreaId} onChange={(event) => onSetSelectedClipAreaId(event.target.value)}>
{areas.map((area) => ( {areas.map((area) => (
<option key={area.id} value={area.id}> <option key={area.id} value={area.id}>
@@ -37,16 +40,28 @@ export function VectorControls({
))} ))}
</select> </select>
</label> </label>
</div>
<div className="dataset-tool-action-row">
<button type="button" onClick={onRunVectorClip} disabled={areas.length === 0}> <button type="button" onClick={onRunVectorClip} disabled={areas.length === 0}>
Run clip Run clip
</button> </button>
</div> </div>
</div>
<div className="dataset-tool-group">
<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}> <button type="button" onClick={onRunVectorBuffer}>
Run buffer (25m) Run buffer (25m)
</button> </button>
</div>
</div>
<div className="dataset-tool-group"> <div className="dataset-tool-group">
<label> <h4 className="dataset-tool-heading">Intersect vector</h4>
Intersect target <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)}> <select value={selectedIntersectTargetId} onChange={(event) => onSetSelectedIntersectTargetId(event.target.value)}>
<option value="">auto first vector</option> <option value="">auto first vector</option>
{availableVectorTargets.map((target) => ( {availableVectorTargets.map((target) => (
@@ -55,11 +70,15 @@ export function VectorControls({
</option> </option>
))} ))}
</select> </select>
<span className="dataset-tool-helper">Leave automatic to use the first available vector target.</span>
</label> </label>
</div>
<div className="dataset-tool-action-row">
<button type="button" onClick={onRunVectorIntersect}> <button type="button" onClick={onRunVectorIntersect}>
Run intersect Run intersect
</button> </button>
</div> </div>
</div> </div>
</div>
) )
} }
+53
View File
@@ -2148,16 +2148,69 @@ button.entity-card {
background: #ffffff; 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 { .dataset-tool-group label {
min-width: 0; 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 input,
.dataset-tool-group select { .dataset-tool-group select {
width: 100%; width: 100%;
min-width: 0; 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 { .dataset-tool-group button {
width: fit-content; width: fit-content;
max-width: 100%; max-width: 100%;