finished implementing the custom grain functionality on the grains page and the basic desktop view, and also am now using permissions to disable the buttons ro add/update/remove

This commit is contained in:
csawatzky 2025-12-31 13:58:38 -06:00
parent 2f8897d8ed
commit 586c971795
4 changed files with 244 additions and 21 deletions

2
package-lock.json generated
View file

@ -10953,7 +10953,7 @@
},
"node_modules/protobuf-ts": {
"version": "1.0.0",
"resolved": "git+https://gitlab+deploy-token-50627:hv8mB4WkyvtjBpJKU1rN@gitlab.com/brandx/protobuf-ts.git#2aa771366c36285eac26e4e0143c1759380e2fc9",
"resolved": "git+https://gitlab+deploy-token-50627:hv8mB4WkyvtjBpJKU1rN@gitlab.com/brandx/protobuf-ts.git#917c54113cbb7e14d5037a647b9f6784bf3a8aad",
"dependencies": {
"protobufjs": "^6.8.8"
}

View file

@ -30,6 +30,7 @@ export default function CustomGrainForm(props: Props) {
setConstantC(grainSettings.c.toString())
setKgPerBushel(grainSettings.kgPerBushel.toString())
setBushelsPerTonne(grainSettings.bushelsPerTonne.toString())
setNewGrainSettings(grainSettings)
}
},[grainSettings])

View file

@ -1,28 +1,144 @@
import { useState } from "react";
import { useCallback, useEffect, useState } from "react";
import PageContainer from "./PageContainer";
import CustomGrainForm from "grain/CustomGrainForm";
import { Box, Button, Grid2, Typography } from "@mui/material";
import { Box, Button, DialogActions, DialogContent, DialogTitle, Grid2, Typography } from "@mui/material";
import { pond } from "protobuf-ts/pond";
import ButtonGroup from "common/ButtonGroup";
import { useMobile } from "hooks";
import { useMobile, useSnackbar, useUserAPI } from "hooks";
import { useGrainAPI } from "providers/pond/grainAPI";
import ResponsiveTable from "common/ResponsiveTable";
import { GrainExtension } from "grain";
import ResponsiveDialog from "common/ResponsiveDialog";
import { useGlobalState } from "providers";
import { Scope } from "models";
export default function Grains() {
const [currentCustomGrain, setCurrentCustomGrain] = useState<pond.GrainSettings>()
const [formValid, setFormValid] = useState(false)
const [selectedGrain, setSelectedGrain] = useState<pond.GrainObject | undefined>()
const [displayCustom, setDisplayCustom] = useState(false)
const [grainTableData, setGrainTableData] = useState()
const { openSnack } = useSnackbar()
const [tableSize, setTableSize] = useState(5)
const [tablePage, setTablePage] = useState(0)
const [tableTotal, setTableTotal] = useState(0)
const grainAPI = useGrainAPI()
const userAPI = useUserAPI()
const [{as, user}] = useGlobalState();
const [permissions, setPermissions] = useState<pond.Permission[]>([])
const [openRemoveConfirmation, setOpenRemoveConfirmation] = useState(false)
const [openUpdateConfirmation, setOpenUpdateConfirmation] = useState(false)
const [grainObjectTableData, setGrainObjectTableData] = useState<pond.GrainObject[]>([])
const [supportedGrainTableData, setSupportedGrainTableData] = useState<GrainExtension[]>([])
const isMobile = useMobile()
//function to load the first set of custom grains
const loadCustomGrain = () => {}
const loadCustomGrain = useCallback(() => {
grainAPI.listGrains(tableSize, tablePage * tableSize, "asc", "name").then(resp => {
setGrainObjectTableData(resp.data.grains)
setTableTotal(resp.data.total)
}).catch(err => {
openSnack("There was an issue loading your custom grains")
})
},[tableSize, tablePage])
useEffect(() => {
if(displayCustom){
loadCustomGrain()
}
//if the user is viewing as a team get their permissions to the team, if they are not then we will get their permission to the grain typ individually when a type is selected
if(as){
userAPI.getUser(user.id(), { key: as, kind: "team" } as Scope).then(resp => {
setPermissions(resp.permissions);
});
}
},[loadCustomGrain, tableSize, tablePage, displayCustom, as])
const equationName = (enumVal: number) => {
switch(enumVal){
case pond.MoistureEquation.MOISTURE_EQUATION_CHUNG_PFOST:
return "Chung-Pfost"
case pond.MoistureEquation.MOISTURE_EQUATION_HALSEY:
return "Halsey"
case pond.MoistureEquation.MOISTURE_EQUATION_HENDERSON:
return "Henderson"
case pond.MoistureEquation.MOISTURE_EQUATION_OSWIN:
return "Oswin"
default:
return "None"
}
}
//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 = () => {
//table that will display the custom grains created by the user
const grainObjectTable = () => {
return (
<ResponsiveTable<pond.GrainObject>
rows={grainObjectTableData}
onRowClick={(grainObj) => {
setCurrentCustomGrain(grainObj.settings ?? pond.GrainSettings.create())
setSelectedGrain(grainObj)
setFormValid(true)
/** if they are viewin as a team leave the permissions alone, if not then use the users permissions to the grain type,
* for the moment grain types are not shareable so this will be unused, but this will cover our bases if we want to implement sharing in the future
* */
if (!as) {
userAPI.getUser(user.id(), { key: grainObj.key, kind: "grain" } as Scope).then(resp => {
setPermissions(resp.permissions);
});
}
}}
columns={[
{
title: "Name",
render: row => <Box>
<Typography>
{row.name}
</Typography>
</Box>
},
{
title: "Group",
render: row => <Box>
<Typography>
{row.settings?.group}
</Typography>
</Box>
},
{
title: "Equation",
render: row => <Box>
<Typography>
{equationName(row.settings?.equation ?? 0)}
</Typography>
</Box>
}
]}
page={tablePage}
pageSize={tableSize}
handleRowsPerPageChange={(e) => {
setTableSize(e.target.value)
}}
setPage={(page) => {
setTablePage(page)
}}
total={tableTotal}
/>
)
}
//table that will display the grain extensions provided by us
const supportedGrainTable = () => {
return (
<ResponsiveTable<GrainExtension>
rows={supportedGrainTableData}
page={tablePage}
pageSize={tableSize}
handleRowsPerPageChange={() => {}}
setPage={() => {}}
total={tableTotal}
/>
)
}
//the grain display for supported grain type to show the rest of the data such as what constants are used in the formula
@ -32,11 +148,101 @@ export default function Grains() {
)
}
const updateGrain = () => {
if(selectedGrain && currentCustomGrain){
grainAPI.updateGrain(selectedGrain.key, currentCustomGrain).then(resp => {
openSnack("Grain type updated")
}).catch(err => {
openSnack("There was an issue updating the selected grain type")
})
}
}
const updateGrainConfirmation = () => {
return (
<ResponsiveDialog open={openUpdateConfirmation} onClose={() => {setOpenUpdateConfirmation(false)}}>
<DialogTitle>Update Custom Grain</DialogTitle>
<DialogContent>
Update existing custom grain type?
<br/>
Note that updating the grain here will not change any bins or components this type was used for, those will retain the old settings.
</DialogContent>
<DialogActions>
<Button variant="contained" color="primary" onClick={()=>{setOpenUpdateConfirmation(false)}}>Cancel</Button>
<Button variant="contained" color="primary" onClick={()=>{updateGrain()}}>Confirm</Button>
</DialogActions>
</ResponsiveDialog>
)
}
const addNewGrain = () => {
if(currentCustomGrain){
grainAPI.addGrain(currentCustomGrain).then(resp => {
//new grain added
openSnack("New grain type added")
}).catch(err => {
openSnack("There was a problem creating the new grain type")
})
}
}
const removeGrain = () => {
if(selectedGrain){
grainAPI.removeGrain(selectedGrain.key).then(resp => {
openSnack("Grain type removed")
}).catch(err => {
openSnack("There was a problem removing the selected grain type")
})
}
}
const removeConfirmation = () => {
return (
<ResponsiveDialog open={openRemoveConfirmation} onClose={() => {setOpenRemoveConfirmation(false)}}>
<DialogTitle>Remove Custom Grain</DialogTitle>
<DialogContent>
Remove custom grain type?
<br />
Note that removing the grain here will not change any bins or components this type was used for, those will retain the old settings.
</DialogContent>
<DialogActions>
<Button variant="contained" color="primary" onClick={()=>{setOpenRemoveConfirmation(false)}}>Cancel</Button>
<Button variant="contained" color="primary" onClick={()=>{removeGrain()}}>Confirm</Button>
</DialogActions>
</ResponsiveDialog>
)
}
const customGrainActions = () => {
return (
<Box display="flex" justifyContent="space-between">
<Box>
{selectedGrain &&
<Button disabled={!permissions.includes(pond.Permission.PERMISSION_WRITE)} variant="contained" color="error" onClick={() => {setOpenRemoveConfirmation(true)}}>Remove Grain</Button>
}
</Box>
<Box>
{selectedGrain &&
<Button sx={{marginRight: 1}} onClick={() => setOpenUpdateConfirmation(true)} disabled={!formValid || !permissions.includes(pond.Permission.PERMISSION_WRITE)} variant="contained" color="primary">Update</Button>
}
<Button onClick={() => addNewGrain()} disabled={!formValid || !permissions.includes(pond.Permission.PERMISSION_WRITE)} variant="contained" color="primary">Save New</Button>
</Box>
</Box>
)
}
//the grain display for custom grain the user has defined
const customGrainDisplay = () => {
return (
<Box padding={2}>
<CustomGrainForm grainSettings={currentCustomGrain} onGrainSettingsChange={()=>{}}/>
<Box>
<CustomGrainForm
grainSettings={currentCustomGrain}
onGrainSettingsChange={(newSettings, formValid)=>{
setCurrentCustomGrain(newSettings)
setFormValid(formValid)
}}
/>
{customGrainActions()}
</Box>
)
}
@ -44,10 +250,10 @@ export default function Grains() {
const desktopView = () => {
return (
<Grid2 container>
<Grid2 size={7}>
<Grid2 size={7} padding={2}>
{displayCustom ? grainObjectTable() : supportedGrainTable()}
</Grid2>
<Grid2 size={5}>
<Grid2 size={5} padding={2}>
{displayCustom ? customGrainDisplay() : supportedDisplay()}
</Grid2>
</Grid2>
@ -73,20 +279,22 @@ export default function Grains() {
title: "Supported",
function: () => {
setDisplayCustom(false)
setTablePage(0)
}
},
{
title: "Custom",
function: () => {
setDisplayCustom(true)
setTablePage(0)
}
}
]}
/>
</Box>
{!isMobile ? desktopView() : mobileView()}
{/* <Button onClick={() => {setCurrentCustomGrain(pond.GrainSettings.create({name: "grain one"}))}}>CG1</Button>
<Button onClick={() => {setCurrentCustomGrain(pond.GrainSettings.create({name: "grain two"}))}}>CG2</Button> */}
{updateGrainConfirmation()}
{removeConfirmation()}
</PageContainer>
)
}

View file

@ -22,6 +22,7 @@ export interface IGrainInterface {
types?: string[],
otherTeam?: string,
) => Promise<AxiosResponse<pond.ListGrainsResponse>>;
updateGrain: (key: string, settings: pond.GrainSettings, otherTeam?: string) => Promise<AxiosResponse<pond.UpdateGrainResponse>>
removeGrain: (key: string, otherTeam?: string) => Promise<AxiosResponse<pond.RemoveGrainResponse>>;
}
@ -31,7 +32,7 @@ interface Props {}
export default function GrainProvider(props: PropsWithChildren<Props>) {
const { children } = props;
const { get, del, post } = useHTTP();
const { get, del, post, put } = useHTTP();
const [{ as }] = useGlobalState();
//add
@ -79,7 +80,6 @@ export default function GrainProvider(props: PropsWithChildren<Props>) {
get<pond.ListGrainsResponse>(
pondURL(
"/grains?limit=" +
"?limit=" +
limit +
"&offset=" +
offset +
@ -98,6 +98,19 @@ export default function GrainProvider(props: PropsWithChildren<Props>) {
})
})
};
//update
const updateGrain = (key: string, settings: pond.GrainSettings, otherTeam?: string) => {
const view = otherTeam ? otherTeam : as
if (view) {
return put<pond.UpdateGrainResponse>(
pondURL("/grains/"+ key + "?&as=" + view),
settings
);
}
return put<pond.UpdateGrainResponse>(pondURL("/grains/" + key), settings);
}
//remove
const removeGrain = (key: string, otherTeam?: string) => {
const view = otherTeam ? otherTeam : as
@ -113,6 +126,7 @@ export default function GrainProvider(props: PropsWithChildren<Props>) {
addGrain,
getGrain,
listGrains,
updateGrain,
removeGrain
}}>
{children}