From 4db21a3d0c9d0da2a5a3f00ce3aa977dad14adf8 Mon Sep 17 00:00:00 2001 From: csawatzky Date: Tue, 26 Aug 2025 14:33:57 -0600 Subject: [PATCH] finished designing the list and mobile view of the field list --- package-lock.json | 2 +- src/component/ComponentCard.tsx | 1 - src/field/FieldList.tsx | 252 ++++++++++++++++++++------- src/field/Fieldminimap.tsx | 9 +- src/field/ShareAllFields.tsx | 43 +++++ src/maps/mapLayers/geoMapLayer.tsx | 8 +- src/models/Field.ts | 10 +- src/providers/pond/fieldAPI.tsx | 74 ++++---- src/providers/pond/permissionAPI.tsx | 1 - 9 files changed, 287 insertions(+), 113 deletions(-) create mode 100644 src/field/ShareAllFields.tsx diff --git a/package-lock.json b/package-lock.json index 6518d2b..6292c47 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10911,7 +10911,7 @@ }, "node_modules/protobuf-ts": { "version": "1.0.0", - "resolved": "git+https://gitlab+deploy-token-50627:hv8mB4WkyvtjBpJKU1rN@gitlab.com/brandx/protobuf-ts.git#1b6076608722fff5290457b077604bb0b38d5499", + "resolved": "git+https://gitlab+deploy-token-50627:hv8mB4WkyvtjBpJKU1rN@gitlab.com/brandx/protobuf-ts.git#70351a87aa4a83bd6274adf8e3b7937090ab4cc2", "dependencies": { "protobufjs": "^6.8.8" } diff --git a/src/component/ComponentCard.tsx b/src/component/ComponentCard.tsx index bf83696..6d430d5 100644 --- a/src/component/ComponentCard.tsx +++ b/src/component/ComponentCard.tsx @@ -285,7 +285,6 @@ export default function ComponentCard(props: Props) { // ) : ( diff --git a/src/field/FieldList.tsx b/src/field/FieldList.tsx index f30d6dd..7362fd8 100644 --- a/src/field/FieldList.tsx +++ b/src/field/FieldList.tsx @@ -1,7 +1,13 @@ import { + Accordion, + AccordionDetails, + AccordionSummary, Box, Button, + Grid2, + IconButton, Paper, + Stack, Tab, Table, TableBody, @@ -13,15 +19,20 @@ import { Typography } from "@mui/material"; import React, { useCallback, useEffect, useState } from "react"; -import { useMobile, useSnackbar } from "hooks"; -import { useGlobalState, useFieldAPI } from "providers"; +import { useMobile, useSnackbar, useUserAPI } from "hooks"; +import { useGlobalState, useFieldAPI, useJohnDeereProxyAPI, useCNHiProxyAPI } from "providers"; //import HarvestTable from "harvestPlan/HarvestTable"; -import { Field } from "models"; +import { Field, fieldScope, teamScope } from "models"; import GrainDescriber from "grain/GrainDescriber"; import { pond } from "protobuf-ts/pond"; import HarvestSettings from "harvestPlan/HarvestSettings"; import ResponsiveTable, { Column } from "common/ResponsiveTable"; import FieldMinimap from "./Fieldminimap"; +import FieldActions from "./FieldActions"; +import FieldSettings from "./FieldSettings"; +import { ExpandMore, Settings } from "@mui/icons-material"; +import { cloneDeep } from "lodash"; +import ShareAllFields from "./ShareAllFields"; interface TabPanelProps { children?: React.ReactNode; @@ -29,61 +40,73 @@ interface TabPanelProps { value: any; } -function TabPanelMine(props: TabPanelProps) { - const { children, value, index, ...other } = props; - - return ( - - ); -} - export default function FieldList() { const [{ as }] = useGlobalState(); const isMobile = useMobile(); const fieldAPI = useFieldAPI(); + const jdAPI = useJohnDeereProxyAPI(); + const cnhAPI = useCNHiProxyAPI(); const { openSnack } = useSnackbar(); - const [value, setValue] = React.useState(0); + const [tabValue, setTabValue] = React.useState(0); const [openHarvestSettings, setOpenHarvestSettings] = useState(false); - const [fieldForPlan, setFieldForPlan] = useState(Field.create()); - const [fields, setFields] = useState([]); //all fields - const [adaptiveFields, setAdaptiveFields] = useState([]) - const [jdFields, setJDFields] = useState([]) - const [cnhFields, setCNHFields] = useState([]) + // const [fieldForPlan, setFieldForPlan] = useState(Field.create()); + const [fields, setFields] = useState([]); + const [total, setTotal] = useState(0) + const [rowsPerPage, setRowsPerPage] = useState(5) + const [page, setPage] = useState(0) + const [selectedField, setSelectedField] = useState() + const [openFieldSettings, setOpenFieldSettings] = useState(false) + const [shareOpen, setShareOpen] = useState(false) const handleChange = (event: React.ChangeEvent<{}>, newValue: number) => { - setValue(newValue); + setTabValue(newValue); + setPage(0) }; - const loadFields = useCallback(() => { + const loadAdaptiveFields = useCallback(() => { fieldAPI - .listFields(500, 0, "asc", "fieldName", undefined, as) + .listFields(rowsPerPage, page*rowsPerPage, "asc", "fieldName", undefined, as) .then(resp => { - setAdaptiveFields(resp.data.fields.map(f => Field.any(f))); - // resp.data.fields.forEach(f => { - // if (f.settings) { - // fieldAPI.updateField(f.settings.key, f.settings); - // } - // }); + setFields(resp.data.fields.map(f => Field.create(f))); + setTotal(resp.data.total) }) .catch(err => { - openSnack("Failed to load Field Mapping"); + openSnack("Failed to load Fields"); }); - }, [fieldAPI, as, openSnack]); + }, [fieldAPI, as, openSnack, page, rowsPerPage]); + + const loadJDFields = useCallback(() => { + jdAPI.listFields(rowsPerPage, page*rowsPerPage, as) + .then(resp => { + resp.data.fields ? setFields(resp.data.fields.map(f => Field.create(f))) : setFields([]) + }).catch(err => { + openSnack("Failed to load John Deere Fields"); + }) + },[jdAPI, as, openSnack, page, rowsPerPage]) + + const loadCNHFields = useCallback(() => { + cnhAPI.listFields(rowsPerPage, page*rowsPerPage, as) + .then(resp => { + resp.data.fields ? setFields(resp.data.fields.map(f => Field.create(f))) : setFields([]) + }).catch(err => { + openSnack("Failed to load John Deere Fields"); + }) + },[cnhAPI, as, openSnack, page, rowsPerPage]) useEffect(() => { - loadFields(); - }, [loadFields]); - - useEffect(()=>{ - setFields(adaptiveFields.concat(jdFields, cnhFields)) - },[adaptiveFields,jdFields,cnhFields]) + //based on the current tab load the correct fields + switch(tabValue){ + case 0: + loadAdaptiveFields(); + break; + case 1: + loadJDFields(); + break; + case 2: + loadCNHFields(); + break; + } + }, [loadAdaptiveFields, loadJDFields, loadCNHFields, tabValue]); const calcTotalAcres = (fields: Field[]) => { let totalAcres = 0; @@ -94,6 +117,19 @@ export default function FieldList() { return totalAcres; }; + const fieldActions = (field: Field) => { + return ( + + { + setSelectedField(field) + setOpenFieldSettings(true) + }}> + {}}/> + + ) + } + const columns: Column[] = [ { title: "View", @@ -108,26 +144,25 @@ const columns: Column[] = [ { title: "Field Name", render: row => { - return {row.name()} + return {row.name()} } }, { title: "Current Crop", render: row => { - return {row.crop()} + return {GrainDescriber(row.crop()).name} } }, { title: "Acres", render: row => { - return {row.acres()} + return {row.acres()} } }, { title: "Actions", - render: row => { - return Field Actions Here - } + hidden: tabValue > 0, + render: row => fieldActions(row) } ] @@ -136,43 +171,126 @@ const columns: Column[] = [ columns={columns} rows={fields} - setPage={()=>{}} - handleRowsPerPageChange={()=>{}} - page={0} - pageSize={1} - total={fields.length} + setPage={(newPage)=>{ + setPage(newPage) + }} + handleRowsPerPageChange={(e)=>{setRowsPerPage(e.target.value)}} + page={page} + pageSize={rowsPerPage} + total={total} + resizeable + loadMore={() => { + let currentFields = cloneDeep(fields) + switch(tabValue){ + case 0: + fieldAPI.listFields(rowsPerPage, currentFields.length, "asc", "fieldName", undefined, as).then(resp => { + setFields(currentFields.concat(resp.data.fields.map(f => Field.create(f)))) + }).catch(err => { + openSnack("Failed to load more fields") + }) + break; + case 1: + jdAPI.listFields(rowsPerPage, currentFields.length, as).then(resp => { + resp.data.fields ? setFields(currentFields.concat(resp.data.fields.map(f => Field.create(f)))) : setFields([]) + }).catch(err => { + openSnack("Failed to load more fields") + }) + break; + case 2: + cnhAPI.listFields(rowsPerPage, currentFields.length, as).then(resp => { + resp.data.fields ? setFields(currentFields.concat(resp.data.fields.map(f => Field.create(f)))) : setFields([]) + }).catch(err => { + openSnack("Failed to load more fields") + }) + break; + } + }} + renderMobile={(row, index) => { + return ( + + }> + + + + {row.name()} + + + + {fieldActions(row)} + + + + + + + + + + + + + {row.grainName()} + {row.acres()} Acres + + + + + + ) + }} /> ) } return ( + {isMobile && + + } - + {!isMobile && + + } - - {fieldTable(fields)} - - - {fieldTable(adaptiveFields)} - - - {fieldTable(jdFields)} - - - {fieldTable(cnhFields)} - - setOpenHarvestSettings(false)} field={fieldForPlan} + /> */} + { + //will need to re-load the fields after one is deleted + loadAdaptiveFields() + }} + open={openFieldSettings} + onClose={() => { + setOpenFieldSettings(false); + }} /> + {setShareOpen(false)}}/> ); } \ No newline at end of file diff --git a/src/field/Fieldminimap.tsx b/src/field/Fieldminimap.tsx index 762a622..0752610 100644 --- a/src/field/Fieldminimap.tsx +++ b/src/field/Fieldminimap.tsx @@ -40,16 +40,17 @@ export default function FieldMinimap(props: Props) { return geoCollection ? ( {}} /> ) diff --git a/src/field/ShareAllFields.tsx b/src/field/ShareAllFields.tsx new file mode 100644 index 0000000..366ee16 --- /dev/null +++ b/src/field/ShareAllFields.tsx @@ -0,0 +1,43 @@ +import { Dialog, DialogActions, DialogContent, DialogTitle } from "@mui/material"; +import ResponsiveDialog from "common/ResponsiveDialog"; + +interface Props { + open: boolean + close: () => void +} + +export default function ShareAllFields(props: Props) { + const {open, close} = props + + const share = () => { + //if sharing to team use shareAllByKey + + //if sharing to user use shareAll + } + + const target = () => { + // toggle for whether sharing to user or team + // if sharing to user have a text field + // if sharing to team have the team selector + } + + const permissions = () => { + // the checkboxes of permissions + } + + const actions = () => { + //the buttons to shaare or cancel + } + + return ( + + Share All Fields + + Content for selecting target and permissions + + + Buttons for cancel and confirm + + + ) +} \ No newline at end of file diff --git a/src/maps/mapLayers/geoMapLayer.tsx b/src/maps/mapLayers/geoMapLayer.tsx index 5b90854..1923b64 100644 --- a/src/maps/mapLayers/geoMapLayer.tsx +++ b/src/maps/mapLayers/geoMapLayer.tsx @@ -6,7 +6,7 @@ interface Props { objectCollection: FeatureCollection; measurementCollection?: FeatureCollection; showTitle?: boolean; - setInteractiveLayers: (layers: string[]) => void; + setInteractiveLayers?: (layers: string[]) => void; } export default function GeoMapLayer(props: Props) { @@ -18,7 +18,9 @@ export default function GeoMapLayer(props: Props) { // }, [measurementCollection, objectCollection]); useEffect(() => { - setInteractiveLayers(["shapefill", "shapeborder", "measurementBorder"]); + if(setInteractiveLayers){ + setInteractiveLayers(["shapefill", "shapeborder", "measurementBorder"]); + } }, []); // eslint-disable-line react-hooks/exhaustive-deps return ( @@ -60,6 +62,7 @@ export default function GeoMapLayer(props: Props) { /> )} + {measurementCollection && + } ); } diff --git a/src/models/Field.ts b/src/models/Field.ts index 3816fce..d2061d1 100644 --- a/src/models/Field.ts +++ b/src/models/Field.ts @@ -10,14 +10,16 @@ import { LngLat, LngLatBounds } from "mapbox-gl"; export class Field { public settings: pond.FieldSettings = pond.FieldSettings.create(); public status: pond.FieldStatus = pond.FieldStatus.create(); - public permissions: pond.Permission[] = []; + public permissions: pond.Permission[] = []; public static create(pf?: pond.Field): Field { let my = new Field(); if (pf) { - my.settings = pond.FieldSettings.fromObject(cloneDeep(or(pf.settings, {}))); - my.status = pond.FieldStatus.fromObject(cloneDeep(or(pf.status, {}))); - my.permissions = pf.fieldPermissions; + //the from object is required here to deal with a protobuf quirk where enums would be seen as strings at runtime, but the code sees them as the actual value + let field = pond.Field.fromObject(cloneDeep(or(pf, pond.Field.create()))) + my.settings = field.settings ? field.settings : pond.FieldSettings.create() + my.status = field.status ? field.status : pond.FieldStatus.create() + my.permissions = field.fieldPermissions } return my; } diff --git a/src/providers/pond/fieldAPI.tsx b/src/providers/pond/fieldAPI.tsx index 17d188e..93309df 100644 --- a/src/providers/pond/fieldAPI.tsx +++ b/src/providers/pond/fieldAPI.tsx @@ -5,6 +5,7 @@ import { useGlobalState } from "providers"; import React, { createContext, PropsWithChildren, useContext } from "react"; import { or } from "utils"; import { pondURL } from "./pond"; +import { permissionToString } from "pbHelpers/Permission"; export interface IFieldAPIContext { addField: (field: pond.FieldSettings, otherTeam?: string) => Promise; @@ -18,14 +19,6 @@ export interface IFieldAPIContext { otherTeam?: string, asRoot?: boolean ) => Promise>; - fieldsPageData: ( - limit: number, - offset: number, - order?: "asc" | "desc", - orderBy?: string, - search?: string, - otherTeam?: string, - ) => Promise> removeField: (key: string, otherTeam?: string) => Promise>; updateField: ( key: string, @@ -33,6 +26,14 @@ export interface IFieldAPIContext { asRoot?: true, otherTeam?: string ) => Promise>; + shareAll: ( + email: string, + permissions: pond.Permission[], + ) => Promise> + shareAllByKey: ( + key: string, + permissions: pond.Permission[] + ) => Promise> } export const FieldAPIContext = createContext({} as IFieldAPIContext); @@ -88,30 +89,6 @@ export default function FieldProvider(props: PropsWithChildren) { ); }; - const fieldsPageData = ( - limit: number, - offset: number, - order?: "asc" | "desc", - orderBy?: string, - search?: string, - otherTeam?: string, - ) => { -const view = otherTeam ? otherTeam : as - return get( - pondURL( - "/fieldsPage" + - "?limit=" + - limit + - "&offset=" + - offset + - ("&order=" + or(order, "asc")) + - ("&by=" + or(orderBy, "key")) + - (view ? "&as=" + view : "") + - (search ? "&search=" + search : "") - ) - ); - } - const updateField = (key: string, field: pond.FieldSettings, asRoot?: boolean, otherTeam?: string) => { const view = otherTeam ? otherTeam : as if (view) @@ -125,6 +102,36 @@ const view = otherTeam ? otherTeam : as ); }; + const shareAll = (email: string, permissions: pond.Permission[]) => { + let url = pondURL("/fields/shareAll") + if (as) url = url + "?as=" + as + return new Promise((resolve, reject) => { + post(url, { + email: email, + permissions: permissions.map(permission => permissionToString(permission)) + }).then(resp => { + return resolve(resp) + }).catch(err => { + return reject(err) + }) + }) + } + + const shareAllByKey = (key: string, permissions: pond.Permission[]) => { + let url = pondURL("/fields/shareAllByKey") + if (as) url = url + "?as=" + as + return new Promise((resolve, reject) => { + post(url, { + key: key, + permissions: permissions.map(permission => permissionToString(permission)) + }).then(resp => { + return resolve(resp) + }).catch(err => { + return reject(err) + }) + }) + } + return ( {children} diff --git a/src/providers/pond/permissionAPI.tsx b/src/providers/pond/permissionAPI.tsx index 2311afd..91b7d6a 100644 --- a/src/providers/pond/permissionAPI.tsx +++ b/src/providers/pond/permissionAPI.tsx @@ -135,7 +135,6 @@ export default function PermissionProvider(props: PropsWithChildren) { let url = pondURL("/" + scope.kind + "s/" + scope.key + "/shareByKey") if (as) url = pondURL(`/${scope.kind}s/${scope.key}/shareByKey?as=${as}`) return new Promise((resolve, reject) => { - post(url, { key: key, permissions: permissions.map(permission => permissionToString(permission))