Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Morecast: Validation on editable fields #3919

Merged
merged 23 commits into from
Sep 16, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@
"cffdrs",
"colour",
"cutline",
"CWFIS",
"determinates",
"excinfo",
"fastapi",
Expand Down
4 changes: 3 additions & 1 deletion web/src/app/rootReducer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import fireZoneElevationInfoSlice from 'features/fba/slices/fireZoneElevationInf
import stationGroupsSlice from 'commonSlices/stationGroupsSlice'
import selectedStationGroupsMembersSlice from 'commonSlices/selectedStationGroupMembers'
import dataSlice from 'features/moreCast2/slices/dataSlice'
import morecastInputValidSlice from 'features/moreCast2/slices/validInputSlice'
import selectedStationsSlice from 'features/moreCast2/slices/selectedStationsSlice'
import provincialSummarySlice from 'features/fba/slices/provincialSummarySlice'
import fireCentreTPIStatsSlice from 'features/fba/slices/fireCentreTPIStatsSlice'
Expand Down Expand Up @@ -44,7 +45,8 @@ const rootReducer = combineReducers({
stationGroupsMembers: selectedStationGroupsMembersSlice,
weatherIndeterminates: dataSlice,
selectedStations: selectedStationsSlice,
provincialSummary: provincialSummarySlice
provincialSummary: provincialSummarySlice,
morecastInputValid: morecastInputValidSlice
})

// Infer whatever gets returned from rootReducer and use it as the type of the root state
Expand Down
77 changes: 63 additions & 14 deletions web/src/features/moreCast2/components/ColumnDefBuilder.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,9 @@
GridCellParams,
GridColDef,
GridColumnHeaderParams,
GridPreProcessEditCellProps,
GridRenderCellParams,
GridRenderEditCellParams,
GridValueFormatterParams,
GridValueGetterParams,
GridValueSetterParams
Expand All @@ -12,6 +14,7 @@
import { modelColorClass, modelHeaderColorClass } from 'app/theme'
import { GridComponentRenderer } from 'features/moreCast2/components/GridComponentRenderer'
import { ColumnClickHandlerProps } from 'features/moreCast2/components/TabbedDataGrid'
import { EditInputCell } from '@/features/moreCast2/components/EditInputCell'

export const DEFAULT_COLUMN_WIDTH = 80
export const DEFAULT_FORECAST_COLUMN_WIDTH = 145
Expand Down Expand Up @@ -44,17 +47,29 @@

export interface ForecastColDefGenerator {
getField: () => string
generateForecastColDef: (columnClickHandlerProps: ColumnClickHandlerProps, headerName?: string) => GridColDef
generateForecastSummaryColDef: (columnClickHandlerProps: ColumnClickHandlerProps) => GridColDef
generateForecastColDef: (
columnClickHandlerProps: ColumnClickHandlerProps,
headerName?: string,
validator?: (value: string) => string
) => GridColDef
generateForecastSummaryColDef: (
columnClickHandlerProps: ColumnClickHandlerProps,
validator?: (value: string) => string
) => GridColDef
}

export interface ColDefGenerator {
getField: () => string
generateColDef: (columnClickHandlerProps: ColumnClickHandlerProps, headerName?: string) => GridColDef
generateColDef: (
columnClickHandlerProps: ColumnClickHandlerProps,
headerName?: string,
validator?: (value: string) => string
) => GridColDef
generateColDefs: (
columnClickHandlerProps: ColumnClickHandlerProps,
headerName?: string,
includeBiasFields?: boolean
includeBiasFields?: boolean,
validator?: (value: string) => string
) => GridColDef[]
}

Expand All @@ -73,34 +88,48 @@
return this.generateColDefWith(this.field, this.headerName, this.precision, DEFAULT_COLUMN_WIDTH)
}

public generateForecastColDef = (columnClickHandlerProps: ColumnClickHandlerProps, headerName?: string) => {
private renderEditCell(params: GridRenderEditCellParams) {
return <EditInputCell {...params} />

Check warning on line 92 in web/src/features/moreCast2/components/ColumnDefBuilder.tsx

View check run for this annotation

Codecov / codecov/patch

web/src/features/moreCast2/components/ColumnDefBuilder.tsx#L92

Added line #L92 was not covered by tests
}

public generateForecastColDef = (
columnClickHandlerProps: ColumnClickHandlerProps,
headerName?: string,
validator?: (value: string) => string
) => {
return this.generateForecastColDefWith(
`${this.field}${WeatherDeterminate.FORECAST}`,
headerName ?? this.headerName,
this.precision,
columnClickHandlerProps,
DEFAULT_FORECAST_COLUMN_WIDTH
DEFAULT_FORECAST_COLUMN_WIDTH,
validator
)
}

public generateForecastSummaryColDef = (columnClickHandlerProps: ColumnClickHandlerProps) => {
public generateForecastSummaryColDef = (
columnClickHandlerProps: ColumnClickHandlerProps,
validator?: (value: string) => string
) => {
return this.generateForecastSummaryColDefWith(
`${this.field}${WeatherDeterminate.FORECAST}`,
this.headerName,
this.precision,
columnClickHandlerProps,
DEFAULT_FORECAST_SUMMARY_COLUMN_WIDTH
DEFAULT_FORECAST_SUMMARY_COLUMN_WIDTH,
validator
)
}

public generateColDefs = (
columnClickHandlerProps: ColumnClickHandlerProps,
headerName?: string,
includeBiasFields = true
includeBiasFields = true,
validator?: (value: string) => string
) => {
const gridColDefs: GridColDef[] = []
// Forecast columns have unique requirement (eg. column header menu, editable, etc.)
const forecastColDef = this.generateForecastColDef(columnClickHandlerProps, headerName)
const forecastColDef = this.generateForecastColDef(columnClickHandlerProps, headerName, validator)
gridColDefs.push(forecastColDef)

for (const colDef of this.generateNonForecastColDefs(includeBiasFields)) {
Expand All @@ -119,7 +148,13 @@
)
}

public generateColDefWith = (field: string, headerName: string, precision: number, width?: number) => {
public generateColDefWith = (
field: string,
headerName: string,
precision: number,
width?: number,
validator?: (value: string) => string
) => {
return {
field,
disableColumnMenu: true,
Expand All @@ -129,6 +164,10 @@
sortable: false,
type: 'number',
width: width ?? DEFAULT_COLUMN_WIDTH,
renderEditCell: this.renderEditCell,
preProcessEditCellProps: (params: GridPreProcessEditCellProps) => {
return { ...params.props, error: validator ? validator(params.props.value) : '' }
},
cellClassName: (params: Pick<GridCellParams, 'colDef' | 'field'>) => {
return modelColorClass(params)
},
Expand All @@ -154,7 +193,8 @@
headerName: string,
precision: number,
columnClickHandlerProps: ColumnClickHandlerProps,
width?: number
width?: number,
validator?: (value: string) => string
) => {
const isGrassField = field.includes('grass')
const isCalcField = field.includes('Calc')
Expand All @@ -171,6 +211,10 @@
sortable: false,
type: 'number',
width: width ?? DEFAULT_FORECAST_COLUMN_WIDTH,
renderEditCell: this.renderEditCell,
preProcessEditCellProps: (params: GridPreProcessEditCellProps) => {
return { ...params.props, error: validator ? validator(params.props.value) : '' }
},
renderHeader: (params: GridColumnHeaderParams) => {
return isCalcField || isGrassField
? this.gridComponentRenderer.renderHeaderWith(params)
Expand All @@ -179,7 +223,7 @@
renderCell: (params: Pick<GridRenderCellParams, 'row' | 'formattedValue'>) => {
return isCalcField
? this.gridComponentRenderer.renderCellWith(params)
: this.gridComponentRenderer.renderForecastCellWith(params, field)
: this.gridComponentRenderer.renderForecastCellWith(params, field, validator)
},
valueFormatter: (params: Pick<GridValueFormatterParams, 'value'>) => {
return this.valueFormatterWith(params, precision)
Expand All @@ -196,7 +240,8 @@
headerName: string,
precision: number,
columnClickHandlerProps: ColumnClickHandlerProps,
width?: number
width?: number,
validator?: (value: string) => string
) => {
const isGrassField = field.includes('grass')
const isCalcField = field.includes('Calc')
Expand All @@ -213,6 +258,10 @@
sortable: false,
type: 'number',
width: width ?? DEFAULT_FORECAST_SUMMARY_COLUMN_WIDTH,
preProcessEditCellProps: (params: GridPreProcessEditCellProps) => {
return { ...params.props, error: validator ? validator(params.props.value) : '' }
},
renderEditCell: this.renderEditCell,
renderHeader: (params: GridColumnHeaderParams) => {
return isCalcField || isGrassField
? this.gridComponentRenderer.renderHeaderWith(params)
Expand Down
78 changes: 78 additions & 0 deletions web/src/features/moreCast2/components/EditInputCell.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
import { GridRenderEditCellParams, useGridApiContext } from '@mui/x-data-grid-pro'
import React, { useRef, useEffect } from 'react'
import { TextField } from '@mui/material'
import { theme } from '@/app/theme'
import { isEmpty } from 'lodash'
import { AppDispatch } from '@/app/store'
import { useDispatch } from 'react-redux'
import { setInputValid } from '@/features/moreCast2/slices/validInputSlice'
import InvalidCellToolTip from '@/features/moreCast2/components/InvalidCellToolTip'

export const EditInputCell = (props: GridRenderEditCellParams) => {
const { id, value, field, hasFocus, error } = props
const apiRef = useGridApiContext()
const inputRef = useRef<HTMLInputElement | null>(null)
const dispatch: AppDispatch = useDispatch()

dispatch(setInputValid(isEmpty(error)))

useEffect(() => {
if (hasFocus && inputRef.current) {
inputRef.current.focus()
}
}, [hasFocus])

const handleValueChange = (event: React.ChangeEvent<HTMLInputElement>) => {
const newValue = event.target.value
apiRef.current.setEditCellValue({ id, field, value: newValue })
}

const handleBlur = () => {
apiRef.current.stopCellEditMode({ id, field })
}

const handleKeyDown = (event: React.KeyboardEvent) => {
if (event.key === 'Escape') {
event.stopPropagation()
if (isEmpty(error)) {
apiRef.current.stopCellEditMode({ id, field })
} else {
event.stopPropagation()
}
}
}

return (
<InvalidCellToolTip error={error}>
<TextField
data-testid="forecast-edit-cell"
type="number"
inputMode="numeric"
inputRef={inputRef}
size="small"
InputLabelProps={{
shrink: true
}}
sx={{
'& .MuiOutlinedInput-root': {
'& fieldset': {
borderColor: error ? theme.palette.error.main : '#737373',
borderWidth: '2px'
},
'&:hover fieldset': {
borderColor: error ? theme.palette.error.main : '#737373'
},
'&.Mui-focused fieldset': {
borderColor: error ? theme.palette.error.main : '#737373',
borderWidth: '2px'
}
}
}}
value={value}
onChange={handleValueChange}
onBlur={handleBlur}
onKeyDown={handleKeyDown}
/>
</InvalidCellToolTip>
)
}
34 changes: 6 additions & 28 deletions web/src/features/moreCast2/components/ForecastCell.tsx
Original file line number Diff line number Diff line change
@@ -1,19 +1,20 @@
import React from 'react'
import { Grid, TextField, Tooltip } from '@mui/material'
import { Grid, Tooltip } from '@mui/material'
import { GridRenderCellParams } from '@mui/x-data-grid-pro'
import RemoveCircleIcon from '@mui/icons-material/RemoveCircle'
import AddBoxIcon from '@mui/icons-material/AddBox'
import { MEDIUM_GREY, theme } from 'app/theme'
import { MEDIUM_GREY } from 'app/theme'
import ValidatedForecastCell from '@/features/moreCast2/components/ValidatedForecastCell'

interface ForecastCellProps {
disabled: boolean
label: string
showGreaterThan: boolean
showLessThan: boolean
value: Pick<GridRenderCellParams, 'formattedValue'>
validator?: (value: string) => string
}

const ForecastCell = ({ disabled, label, showGreaterThan, showLessThan, value }: ForecastCellProps) => {
const ForecastCell = ({ disabled, label, showGreaterThan, showLessThan, value, validator }: ForecastCellProps) => {
// We should never display both less than and greater than icons at the same time
if (showGreaterThan && showLessThan) {
throw Error('ForecastCell cannot show both greater than and less than icons at the same time.')
Expand All @@ -31,30 +32,7 @@ const ForecastCell = ({ disabled, label, showGreaterThan, showLessThan, value }:
)}
</Grid>
<Grid item xs={8}>
<TextField
data-testid="forecast-cell-text-field"
disabled={disabled}
size="small"
label={label}
InputLabelProps={{
shrink: true
}}
sx={{
'& .MuiOutlinedInput-root': {
backgroundColor: `${theme.palette.common.white}`,
'& fieldset': {
borderColor: '#737373',
borderWidth: '2px'
}
},
'& .Mui-disabled': {
'& fieldset': {
borderWidth: '1px'
}
}
}}
value={value}
></TextField>
<ValidatedForecastCell disabled={disabled} label={label} value={value} validator={validator} />
</Grid>
<Grid item xs={2} sx={{ marginLeft: 'auto' }}>
{showGreaterThan && (
Expand Down
22 changes: 10 additions & 12 deletions web/src/features/moreCast2/components/GridComponentRenderer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import ForecastHeader from 'features/moreCast2/components/ForecastHeader'
import { ColumnClickHandlerProps } from 'features/moreCast2/components/TabbedDataGrid'
import { cloneDeep, isNumber } from 'lodash'
import ForecastCell from 'features/moreCast2/components/ForecastCell'
import ValidatedForecastCell from '@/features/moreCast2/components/ValidatedForecastCell'

export const NOT_AVAILABLE = 'N/A'
export const NOT_REPORTING = 'N/R'
Expand Down Expand Up @@ -92,7 +93,11 @@ export class GridComponentRenderer {
} else return isNaN(value) ? noDataField : Number(value).toFixed(precision)
}

public renderForecastCellWith = (params: Pick<GridRenderCellParams, 'row' | 'formattedValue'>, field: string) => {
public renderForecastCellWith = (
params: Pick<GridRenderCellParams, 'row' | 'formattedValue'>,
field: string,
validator?: (value: string) => string
) => {
// If a single cell in a row contains an Actual, no Forecast will be entered into the row anymore, so we can disable the whole row.
const isActual = rowContainsActual(params.row)
// We can disable a cell if an Actual exists or the forDate is before today.
Expand All @@ -115,20 +120,12 @@ export class GridComponentRenderer {
// The grass curing 'forecast' field is rendered differently
if (isGrassField) {
return (
<TextField
sx={{
'& .MuiOutlinedInput-root': {
backgroundColor: `${theme.palette.common.white}`
}
}}
<ValidatedForecastCell
disabled={isActual || isPreviousDate}
size="small"
label={label}
InputLabelProps={{
shrink: true
}}
value={params.formattedValue}
></TextField>
validator={validator}
/>
)
} else {
// Forecast fields (except wind direction) have plus and minus icons indicating if the forecast was
Expand All @@ -140,6 +137,7 @@ export class GridComponentRenderer {
showGreaterThan={showGreaterThan}
showLessThan={showLessThan}
value={params.formattedValue}
validator={validator}
/>
)
}
Expand Down
Loading
Loading