changed the component used in the bin and component settings to the grain selector and split out the form into another component that handles just the form and passing up a grainSettings object from its fields

This commit is contained in:
csawatzky 2025-12-23 10:16:31 -06:00
parent 49aa08f232
commit 27590da78f
7 changed files with 225 additions and 151 deletions

View file

@ -65,7 +65,7 @@ import { getDistanceUnit, or, getTemperatureUnit, getGrainUnit } from "utils";
import { makeStyles } from "@mui/styles"; import { makeStyles } from "@mui/styles";
import { useNavigate } from "react-router-dom"; import { useNavigate } from "react-router-dom";
import BinSelector from "./BinSelector"; import BinSelector from "./BinSelector";
import CustomGrainForm from "grain/CustomGrainForm"; import CustomGrainSelector from "grain/CustomGrainSelector";
// import BinSelector from "./BinSelector"; // import BinSelector from "./BinSelector";
const useStyles = makeStyles((theme: Theme) => { const useStyles = makeStyles((theme: Theme) => {
@ -1054,7 +1054,7 @@ export default function BinSettings(props: Props) {
</React.Fragment> </React.Fragment>
: :
<Box sx={{border: "1px solid white", borderRadius: 2, padding: 2, marginBottom: 2}}> <Box sx={{border: "1px solid white", borderRadius: 2, padding: 2, marginBottom: 2}}>
<CustomGrainForm initialGrain={customGrain} onGrainSettingsChange={(newGrainSettings) => {setCustomGrain(newGrainSettings)}}/> <CustomGrainSelector initialGrain={customGrain} onGrainSettingsChange={(newGrainSettings) => {setCustomGrain(newGrainSettings)}}/>
</Box> </Box>
} }
</React.Fragment> </React.Fragment>

View file

@ -70,7 +70,7 @@ import Edit from "@mui/icons-material/Edit";
import { makeStyles } from "@mui/styles"; import { makeStyles } from "@mui/styles";
import ButtonGroup from "common/ButtonGroup"; import ButtonGroup from "common/ButtonGroup";
import ModeChangeDialog from "./conditioning/modeChangeDialog"; import ModeChangeDialog from "./conditioning/modeChangeDialog";
import CustomGrainForm from "grain/CustomGrainForm"; import CustomGrainSelector from "grain/CustomGrainSelector";
const useStyles = makeStyles((theme: Theme) => { const useStyles = makeStyles((theme: Theme) => {
return ({ return ({
@ -861,7 +861,7 @@ export default function BinVisualizer(props: Props) {
className={classes.bottomSpacing} className={classes.bottomSpacing}
/> />
)} */} )} */}
<CustomGrainForm initialGrain={bin.customGrain()} onGrainSettingsChange={settings => {setNewGrainSettings(settings)}}/> <CustomGrainSelector initialGrain={bin.customGrain()} onGrainSettingsChange={settings => {setNewGrainSettings(settings)}}/>
</React.Fragment> </React.Fragment>
) : ( ) : (
<TextField <TextField

View file

@ -48,7 +48,7 @@ import { getDistanceUnit } from "utils";
import { GrainOptions } from "grain"; import { GrainOptions } from "grain";
import CompModes from "component/ComponentMode.json"; import CompModes from "component/ComponentMode.json";
import FanPicker from "fans/fanPicker"; import FanPicker from "fans/fanPicker";
import CustomGrainForm from "grain/CustomGrainForm"; import CustomGrainSelector from "grain/CustomGrainSelector";
const useStyles = makeStyles((theme: Theme) => { const useStyles = makeStyles((theme: Theme) => {
return ({ return ({
@ -654,7 +654,7 @@ export default function ComponentForm(props: Props) {
/> />
{useCustomGrain ? {useCustomGrain ?
<Box sx={{border: "1px solid white", borderRadius: 2, padding: 2, marginBottom: 2}}> <Box sx={{border: "1px solid white", borderRadius: 2, padding: 2, marginBottom: 2}}>
<CustomGrainForm onGrainSettingsChange={updateCustomGrain} initialGrain={form.component.customGrainProps()}/> <CustomGrainSelector onGrainSettingsChange={updateCustomGrain} initialGrain={form.component.customGrainProps()}/>
</Box> </Box>
: :
<SearchSelect <SearchSelect

View file

@ -1,74 +1,37 @@
import { ExpandMore } from "@mui/icons-material" import {MenuItem, TextField } from "@mui/material"
import { Accordion, AccordionDetails, AccordionSummary, Box, Button, MenuItem, TextField, Typography } from "@mui/material"
import SearchSelect, { Option } from "common/SearchSelect"
import { cloneDeep } from "lodash" import { cloneDeep } from "lodash"
import { pond } from "protobuf-ts/pond" import { pond } from "protobuf-ts/pond"
import { useGlobalState } from "providers"
import { useGrainAPI } from "providers/pond/grainAPI"
import React, { useEffect, useState } from "react" import React, { useEffect, useState } from "react"
interface Props { interface Props {
initialGrain?: pond.GrainSettings grainSettings?: pond.GrainSettings
onGrainSettingsChange: (settings: pond.GrainSettings) => void onGrainSettingsChange: (settings: pond.GrainSettings, formValid: boolean) => void
} }
export default function CustomGrainForm(props: Props) { export default function CustomGrainForm(props: Props) {
const {initialGrain, onGrainSettingsChange} = props const {grainSettings, onGrainSettingsChange} = props
const grainAPI = useGrainAPI() const [newGrainSettings, setNewGrainSettings] = useState(pond.GrainSettings.create())
const [{as}] = useGlobalState() const [name, setName] = useState("")
const [name, setName] = useState(initialGrain ? initialGrain.name : "") const [group, setGroup] = useState("")
const [group, setGroup] = useState(initialGrain ? initialGrain.group : "") const [equation, setEquation] = useState<pond.MoistureEquation>(pond.MoistureEquation.MOISTURE_EQUATION_NONE)
const [equation, setEquation] = useState<pond.MoistureEquation>(initialGrain ? initialGrain.equation : pond.MoistureEquation.MOISTURE_EQUATION_NONE) const [constantA, setConstantA] = useState("0")
const [constantA, setConstantA] = useState(initialGrain ? initialGrain.a.toString() : "0") const [constantB, setConstantB] = useState("0")
const [constantB, setConstantB] = useState(initialGrain ? initialGrain.b.toString() : "0") const [constantC, setConstantC] = useState("0")
const [constantC, setConstantC] = useState(initialGrain ? initialGrain.c.toString() : "0") const [kgPerBushel, setKgPerBushel] = useState("0")
const [kgPerBushel, setKgPerBushel] = useState(initialGrain ? initialGrain.kgPerBushel.toString() : "0") const [bushelsPerTonne, setBushelsPerTonne] = useState("0")
const [bushelsPerTonne, setBushelsPerTonne] = useState(initialGrain ? initialGrain.bushelsPerTonne.toString() : "0")
const [newGrainSettings, setNewGrainSettings] = useState(initialGrain ?? pond.GrainSettings.create())
const [grainMap, setGrainMap] = useState<Map<string, pond.GrainObject>>(
initialGrain ?
new Map([
["Active", pond.GrainObject.create({key: "Active", name: initialGrain.name, settings: initialGrain})]
])
:
new Map())
const [grainOptions, setGrainOptions] = useState<Option[]>(
initialGrain ?
[
{
label: initialGrain.name,
value: "Active",
group: "Active"
}
]
:
[]
)
const [isNew, setIsNew] = useState(false)
const [selectedOption, setSelectedOption] = useState<Option | null>(initialGrain ? {
label: initialGrain.name,
value: "Active",
group: "Active"
} : null)
useEffect(()=>{ useEffect(()=>{
grainAPI.listGrains(0, 0, "asc", "name", undefined, undefined, undefined, as).then(resp => { if(grainSettings){
let options: Option[] = grainOptions setName(grainSettings.name)
let map: Map<string, pond.GrainObject> = grainMap setGroup(grainSettings.group)
if (resp.data.grains){ setEquation(grainSettings.equation)
resp.data.grains.forEach(grain => { setConstantA(grainSettings.a.toString())
map.set(grain.key, grain) setConstantB(grainSettings.b.toString())
options.push({ setConstantC(grainSettings.c.toString())
label: grain.name, setKgPerBushel(grainSettings.kgPerBushel.toString())
value: grain.key, setBushelsPerTonne(grainSettings.bushelsPerTonne.toString())
group: grain.settings?.group,
})
})
} }
setGrainOptions([...options]) },[grainSettings])
setGrainMap(map)
})
},[grainAPI])
const invalid = () => { const invalid = () => {
if (name === "") return true if (name === "") return true
@ -81,21 +44,12 @@ export default function CustomGrainForm(props: Props) {
return false return false
} }
const saveGrain = () => {
// console.log(newGrainSettings)
grainAPI.addGrain(newGrainSettings, as).then(resp => {
console.log("grain added")
}).catch(err => {
console.log("err")
})
}
const settingsChanged = (newSettings: pond.GrainSettings) => { const settingsChanged = (newSettings: pond.GrainSettings) => {
setNewGrainSettings(newSettings) setNewGrainSettings(newSettings)
onGrainSettingsChange(newSettings) onGrainSettingsChange(newSettings, !invalid())
} }
const newGrain = () => { const grainForm = () => {
return ( return (
<React.Fragment> <React.Fragment>
<TextField <TextField
@ -106,7 +60,6 @@ export default function CustomGrainForm(props: Props) {
onChange={(e) => { onChange={(e) => {
let name = e.target.value let name = e.target.value
setName(name) setName(name)
setIsNew(true)
let settings = cloneDeep(newGrainSettings) let settings = cloneDeep(newGrainSettings)
settings.name = name settings.name = name
settingsChanged(settings) settingsChanged(settings)
@ -119,7 +72,6 @@ export default function CustomGrainForm(props: Props) {
onChange={(e) => { onChange={(e) => {
let group = e.target.value let group = e.target.value
setGroup(group) setGroup(group)
setIsNew(true)
let settings = cloneDeep(newGrainSettings) let settings = cloneDeep(newGrainSettings)
settings.group = group settings.group = group
settingsChanged(settings) settingsChanged(settings)
@ -133,7 +85,6 @@ export default function CustomGrainForm(props: Props) {
onChange={(e) => { onChange={(e) => {
let enumVal = parseFloat(e.target.value) let enumVal = parseFloat(e.target.value)
setEquation(enumVal) setEquation(enumVal)
setIsNew(true)
let settings = cloneDeep(newGrainSettings) let settings = cloneDeep(newGrainSettings)
settings.equation = enumVal settings.equation = enumVal
settingsChanged(settings) settingsChanged(settings)
@ -165,7 +116,6 @@ export default function CustomGrainForm(props: Props) {
onChange={(e) => { onChange={(e) => {
let val = e.target.value let val = e.target.value
setConstantA(val) setConstantA(val)
setIsNew(true)
if(!isNaN(parseFloat(val))){ if(!isNaN(parseFloat(val))){
let settings = cloneDeep(newGrainSettings) let settings = cloneDeep(newGrainSettings)
settings.a = parseFloat(val) settings.a = parseFloat(val)
@ -183,7 +133,6 @@ export default function CustomGrainForm(props: Props) {
onChange={(e) => { onChange={(e) => {
let val = e.target.value let val = e.target.value
setConstantB(val) setConstantB(val)
setIsNew(true)
if(!isNaN(parseFloat(val))){ if(!isNaN(parseFloat(val))){
let settings = cloneDeep(newGrainSettings) let settings = cloneDeep(newGrainSettings)
settings.b = parseFloat(val) settings.b = parseFloat(val)
@ -201,7 +150,6 @@ export default function CustomGrainForm(props: Props) {
onChange={(e) => { onChange={(e) => {
let val = e.target.value let val = e.target.value
setConstantC(val) setConstantC(val)
setIsNew(true)
if(!isNaN(parseFloat(val))){ if(!isNaN(parseFloat(val))){
let settings = cloneDeep(newGrainSettings) let settings = cloneDeep(newGrainSettings)
settings.c = parseFloat(val) settings.c = parseFloat(val)
@ -219,7 +167,6 @@ export default function CustomGrainForm(props: Props) {
onChange={(e) => { onChange={(e) => {
let val = e.target.value let val = e.target.value
setKgPerBushel(val) setKgPerBushel(val)
setIsNew(true)
if(!isNaN(parseFloat(val))){ if(!isNaN(parseFloat(val))){
let settings = cloneDeep(newGrainSettings) let settings = cloneDeep(newGrainSettings)
settings.kgPerBushel = parseFloat(val) settings.kgPerBushel = parseFloat(val)
@ -237,7 +184,6 @@ export default function CustomGrainForm(props: Props) {
onChange={(e) => { onChange={(e) => {
let val = e.target.value let val = e.target.value
setBushelsPerTonne(val) setBushelsPerTonne(val)
setIsNew(true)
if(!isNaN(parseFloat(val))){ if(!isNaN(parseFloat(val))){
let settings = cloneDeep(newGrainSettings) let settings = cloneDeep(newGrainSettings)
settings.bushelsPerTonne = parseFloat(val) settings.bushelsPerTonne = parseFloat(val)
@ -245,77 +191,13 @@ export default function CustomGrainForm(props: Props) {
} }
}} }}
label="Bushels per Tonne"/> label="Bushels per Tonne"/>
{isNew &&
<Box display='flex' justifyContent="flex-end">
<Button
variant="contained"
disabled={invalid()}
onClick={() => {
saveGrain()
setIsNew(false)
}}
color="primary">
Save Custom Grain
</Button>
</Box>
}
</React.Fragment> </React.Fragment>
) )
} }
const grainAccordion = () => {
return (
<Accordion>
<AccordionSummary expandIcon={<ExpandMore />}>Custom Grain Properties</AccordionSummary>
<AccordionDetails>
{newGrain()}
</AccordionDetails>
</Accordion>
)
}
return ( return (
<React.Fragment> <React.Fragment>
{grainOptions.length === 0 ? {grainForm()}
<Box>
<Typography sx={{marginBottom: 1}}>Custom Grain Properties</Typography>
{newGrain()}
</Box>
:
<Box>
<Box sx={{marginBottom: 2}}>
<SearchSelect
label="My Grains"
selected={selectedOption}
changeSelection={option => {
setIsNew(false)
setSelectedOption(option)
//when an option is selected set all of the state variables controlling the form entries
let grain = grainMap.get(option?.value)
if(grain && grain.settings){
//set the form values
setName(grain.name)
setGroup(grain.settings.group)
setEquation(grain.settings.equation)
setConstantA(grain.settings.a.toString())
setConstantB(grain.settings.b.toString())
setConstantC(grain.settings.c.toString())
setKgPerBushel(grain.settings.kgPerBushel.toString())
setBushelsPerTonne(grain.settings.bushelsPerTonne.toString())
//the the new grain settings object
setNewGrainSettings(grain.settings)
//pass that settings object back up
settingsChanged(grain.settings)
}
}}
group
options={grainOptions}
/>
</Box>
{grainAccordion()}
</Box>
}
</React.Fragment> </React.Fragment>
) )
} }

View file

@ -0,0 +1,155 @@
import { ExpandMore } from "@mui/icons-material"
import { Accordion, AccordionDetails, AccordionSummary, Box, Button, Typography } from "@mui/material"
import SearchSelect, { Option } from "common/SearchSelect"
import { pond } from "protobuf-ts/pond"
import { useGlobalState } from "providers"
import { useGrainAPI } from "providers/pond/grainAPI"
import React, { useEffect, useState } from "react"
import CustomGrainForm from "./CustomGrainForm"
interface Props {
initialGrain?: pond.GrainSettings
onGrainSettingsChange: (settings: pond.GrainSettings) => void
}
export default function CustomGrainSelector(props: Props) {
const {initialGrain, onGrainSettingsChange} = props
const grainAPI = useGrainAPI()
const [{as}] = useGlobalState()
const [newGrainSettings, setNewGrainSettings] = useState(initialGrain ?? pond.GrainSettings.create())
const [formValid, setFormValid] = useState(true)
const [grainMap, setGrainMap] = useState<Map<string, pond.GrainObject>>(
initialGrain ?
new Map([
["Active", pond.GrainObject.create({key: "Active", name: initialGrain.name, settings: initialGrain})]
])
:
new Map())
const [grainOptions, setGrainOptions] = useState<Option[]>(
initialGrain ?
[
{
label: initialGrain.name,
value: "Active",
group: "Active"
}
]
:
[]
)
const [isNew, setIsNew] = useState(false)
const [selectedOption, setSelectedOption] = useState<Option | null>(initialGrain ? {
label: initialGrain.name,
value: "Active",
group: "Active"
} : null)
useEffect(()=>{
grainAPI.listGrains(0, 0, "asc", "name", undefined, undefined, undefined, as).then(resp => {
let options: Option[] = grainOptions
let map: Map<string, pond.GrainObject> = grainMap
if (resp.data.grains){
resp.data.grains.forEach(grain => {
map.set(grain.key, grain)
options.push({
label: grain.name,
value: grain.key,
group: grain.settings?.group,
})
})
}
setGrainOptions([...options])
setGrainMap(map)
})
},[grainAPI])
const saveGrain = () => {
// console.log(newGrainSettings)
grainAPI.addGrain(newGrainSettings, as).then(resp => {
console.log("grain added")
}).catch(err => {
console.log("err")
})
}
const settingsChanged = (newSettings: pond.GrainSettings, formValid: boolean) => {
setNewGrainSettings(newSettings)
setIsNew(true)
setFormValid(formValid)
onGrainSettingsChange(newSettings)
}
const grainAccordion = () => {
return (
<Accordion>
<AccordionSummary expandIcon={<ExpandMore />}>Custom Grain Properties</AccordionSummary>
<AccordionDetails>
<CustomGrainForm grainSettings={newGrainSettings} onGrainSettingsChange={settingsChanged}/>
{isNew &&
<Box display='flex' justifyContent="flex-end">
<Button
variant="contained"
disabled={!formValid}
onClick={() => {
setIsNew(false)
saveGrain()
}}
color="primary">
Save Custom Grain
</Button>
</Box>
}
</AccordionDetails>
</Accordion>
)
}
return (
<React.Fragment>
{grainOptions.length === 0 ?
<Box>
<Typography sx={{marginBottom: 1}}>Custom Grain Properties</Typography>
<CustomGrainForm grainSettings={newGrainSettings} onGrainSettingsChange={settingsChanged}/>
{isNew &&
<Box display='flex' justifyContent="flex-end">
<Button
variant="contained"
disabled={!formValid}
onClick={() => {
setIsNew(false)
}}
color="primary">
Save Custom Grain
</Button>
</Box>
}
</Box>
:
<Box>
<Box sx={{marginBottom: 2}}>
<SearchSelect
label="My Grains"
selected={selectedOption}
changeSelection={option => {
setIsNew(false)
setSelectedOption(option)
//when an option is selected set all of the state variables controlling the form entries
let grain = grainMap.get(option?.value)
if(grain && grain.settings){
//the the new grain settings object
setNewGrainSettings(grain.settings)
//pass that settings object back up
settingsChanged(grain.settings, true)
}
}}
group
options={grainOptions}
/>
</Box>
{grainAccordion()}
</Box>
}
</React.Fragment>
)
}

View file

@ -48,6 +48,7 @@ const JohnDeere = lazy(() => import("pages/JohnDeere"));
const CNHi = lazy(() => import("pages/CNHi")); const CNHi = lazy(() => import("pages/CNHi"));
const LibraCart = lazy(() => import("pages/LibraCart")); const LibraCart = lazy(() => import("pages/LibraCart"));
const FieldPage = lazy(()=>import("pages/Field")); const FieldPage = lazy(()=>import("pages/Field"));
const GrainsPage = lazy(()=>import("pages/Grains"));
export const appendToUrl = (appendage: number | string) => { export const appendToUrl = (appendage: number | string) => {
const basePath = location.pathname.replace(/\/$/, ""); const basePath = location.pathname.replace(/\/$/, "");
@ -360,6 +361,7 @@ export default function Router() {
{user.hasFeature("libra-cart") && {user.hasFeature("libra-cart") &&
<Route path="libracart" element={<LibraCart />} /> <Route path="libracart" element={<LibraCart />} />
} }
<Route path="grains" element={<GrainsPage />} />
{/* Map routes */} {/* Map routes */}
<Route path="visualFarm" element={<FieldMap />} /> <Route path="visualFarm" element={<FieldMap />} />
<Route path="aviationMap" element={<AviationMap />} /> <Route path="aviationMap" element={<AviationMap />} />

35
src/pages/Grains.tsx Normal file
View file

@ -0,0 +1,35 @@
import { useState } from "react";
import PageContainer from "./PageContainer";
import CustomGrainForm from "grain/CustomGrainForm";
import { Button } from "@mui/material";
import { pond } from "protobuf-ts/pond";
export default function Grains() {
const [currentCustomGrain, setCurrentCustomGrain] = useState<pond.GrainSettings>()
//function to load the first set of custom grains
const loadCustomGrain = () => {}
//function to update
//tabs to select supported and custom
const grainTab = () => {}
//table that will display the grains (both for the supported grain and custom for the user)
const grainTable = () => {}
//the grain display for supported grain type to show the rest of the data such as what constants are used in the formula
const supportedDisplay = () => {}
//the grain display for custom grain the user has defined
const customGrainDisplay = () => {}
return (
<PageContainer>
GRAIN PAGE
<CustomGrainForm grainSettings={currentCustomGrain} onGrainSettingsChange={()=>{}}/>
<Button onClick={() => {setCurrentCustomGrain(pond.GrainSettings.create({name: "grain one"}))}}>CG1</Button>
<Button onClick={() => {setCurrentCustomGrain(pond.GrainSettings.create({name: "grain two"}))}}>CG2</Button>
</PageContainer>
)
}