moved all of the libracart suff from the old frontend into the new new, also added libracart as an option alongside jd and cnh to skip the auth0 callback when redirected back to us from there

This commit is contained in:
csawatzky 2025-07-17 13:42:11 -06:00
parent 67cafbe2ed
commit a0a54bee2a
14 changed files with 528 additions and 12 deletions

4
package-lock.json generated
View file

@ -42,7 +42,7 @@
"mui-tel-input": "^7.0.0",
"notistack": "^3.0.1",
"openweathermap-ts": "^1.2.10",
"protobuf-ts": "git+https://gitlab+deploy-token-50627:hv8mB4WkyvtjBpJKU1rN@gitlab.com/brandx/protobuf-ts.git#staging",
"protobuf-ts": "git+https://gitlab+deploy-token-50627:hv8mB4WkyvtjBpJKU1rN@gitlab.com/brandx/protobuf-ts.git#libracart",
"query-string": "^9.2.1",
"react": "^18.3.1",
"react-beautiful-dnd": "^13.1.1",
@ -10889,7 +10889,7 @@
},
"node_modules/protobuf-ts": {
"version": "1.0.0",
"resolved": "git+https://gitlab+deploy-token-50627:hv8mB4WkyvtjBpJKU1rN@gitlab.com/brandx/protobuf-ts.git#4523ffef42032eb4a923bd50d4cfcccbb1397dbd",
"resolved": "git+https://gitlab+deploy-token-50627:hv8mB4WkyvtjBpJKU1rN@gitlab.com/brandx/protobuf-ts.git#174342d58e3d36bda2f2efc08aa3c95a217125e1",
"dependencies": {
"protobufjs": "^6.8.8"
}

View file

@ -54,7 +54,7 @@
"mui-tel-input": "^7.0.0",
"notistack": "^3.0.1",
"openweathermap-ts": "^1.2.10",
"protobuf-ts": "git+https://gitlab+deploy-token-50627:hv8mB4WkyvtjBpJKU1rN@gitlab.com/brandx/protobuf-ts.git#staging",
"protobuf-ts": "git+https://gitlab+deploy-token-50627:hv8mB4WkyvtjBpJKU1rN@gitlab.com/brandx/protobuf-ts.git#libracart",
"query-string": "^9.2.1",
"react": "^18.3.1",
"react-beautiful-dnd": "^13.1.1",

View file

@ -50,7 +50,8 @@ function App() {
const skipCallbacks = [
"/johndeere",
"/cnhi"
"/cnhi",
"/libracart"
]
return (

Binary file not shown.

After

Width:  |  Height:  |  Size: 44 KiB

View file

@ -0,0 +1,178 @@
import {
Avatar,
Box,
Button,
DialogActions,
DialogContent,
DialogTitle,
Grid,
MenuItem,
TextField
} from "@mui/material";
import ResponsiveDialog from "common/ResponsiveDialog";
import { teamScope, User } from "models";
import { pond } from "protobuf-ts/pond";
import { useSnackbar, useUserAPI } from "providers";
import { useLibraCartProxyAPI } from "providers/pond/libracartProxyAPI";
import React, { useEffect, useState } from "react";
import { useLocation } from "react-router";
import TeamSearch from "teams/TeamSearch";
export default function LibraCartAccess() {
const [openDialog, setOpenDialog] = useState(false);
const [teamKey, setTeamKey] = useState("");
const [teamUsers, setTeamUsers] = useState<User[]>([]);
const [primaryUser, setPrimaryUser] = useState("");
const [libracartUsername, setLibraCartUserName] = useState("");
const [libracartCode, setLibraCartCode] = useState<string | null>("");
//const [activeStep, setActiveStep] = useState<number>(0);
const userAPI = useUserAPI();
const libracartAPI = useLibraCartProxyAPI();
//const [dataOps, setDataOps] = useState<pond.DataOption[]>([]);
const { openSnack } = useSnackbar();
const search = useLocation().search;
const searchParams = new URLSearchParams(search);
//const [{ as }] = useGlobalState();
// const integration_url = process.env.REACT_APP_LIBRACART_INTEGRATION_URL;
const integration_url = import.meta.env.VITE_LIBRACART_INTEGRATION_URL;
const submitNewOrganization = () => {
if (libracartCode) {
libracartAPI
.addAccount(teamKey, primaryUser, libracartCode, libracartUsername)
.then(resp => {
//the code was used so remove it from local storage
if (localStorage.getItem("code")) {
localStorage.removeItem("code");
}
openSnack("Added New Libra Cart Account Link");
})
.catch(err => {
openSnack("Failed to Add New Libra Cart Account Link");
});
setOpenDialog(false);
}
};
useEffect(() => {
if (teamKey !== "") {
userAPI.listObjectUsers(teamScope(teamKey)).then(resp => {
setTeamUsers(resp.data.users.map((u: pond.User) => User.any(u)));
});
}
}, [teamKey, userAPI]);
useEffect(() => {
let code = localStorage.getItem("state");
if (code) {
setLibraCartCode(code);
setOpenDialog(true);
}
}, [searchParams, libracartCode]);
const validate = () => {
let invalid = false;
if (libracartUsername === "" || teamKey === "" || primaryUser === "" || libracartCode === "") {
invalid = true;
}
return invalid;
};
const general = () => {
return (
<Box>
<TextField
margin="dense"
label="Libra Cart Email"
fullWidth
variant="outlined"
value={libracartUsername}
onChange={e => setLibraCartUserName(e.target.value)}
/>
<TeamSearch label="Team" setTeamCallback={setTeamKey} />
<TextField
disabled={teamKey === ""}
margin="dense"
label="Primary User"
select
fullWidth
variant="outlined"
value={primaryUser}
onChange={e => setPrimaryUser(e.target.value)}>
{teamUsers.map(u => (
<MenuItem key={u.id()} value={u.id()}>
<Grid container direction="row" alignItems="center">
<Grid item>
<Avatar
alt={u.name()}
src={
u.settings.avatar && u.settings.avatar !== "" ? u.settings.avatar : undefined
}
/>
</Grid>
<Grid item>
<Box marginLeft={2}>{u.name()}</Box>
</Grid>
</Grid>
</MenuItem>
))}
</TextField>
<TextField
margin="dense"
label="code"
disabled
fullWidth
variant="outlined"
value={libracartCode}
onChange={e => setLibraCartCode(e.target.value)}
/>
</Box>
);
};
const newOrgDialog = () => {
return (
<ResponsiveDialog
open={openDialog}
onClose={() => {
setOpenDialog(false);
}}>
<DialogTitle>Enter New Integration Link</DialogTitle>
<DialogContent>{general()}</DialogContent>
<DialogActions>
<Button
onClick={() => {
setOpenDialog(false);
}}>
Close
</Button>
<Button
disabled={validate()}
onClick={submitNewOrganization}
variant="contained"
color="primary">
Submit
</Button>
</DialogActions>
</ResponsiveDialog>
);
};
return (
<Box style={{ padding: 10 }}>
<Button
variant="contained"
color="primary"
onClick={e => {
e.preventDefault();
window.open(`${integration_url}`, "_blank");
}}>
Link Libra Cart Account
</Button>
To integrate with Libra Cart, navigate to Integrations page on Libra Cart and "Connect" to
Adaptive Agriculture App: https://staging.cloud.agrimatics.com/grain/integrations
{newOrgDialog()}
</Box>
);
}

View file

@ -45,6 +45,7 @@ const Contracts = lazy(() => import("pages/Contracts"));
const Contract = lazy(() => import("pages/Contract"));
const JohnDeere = lazy(() => import("pages/JohnDeere"));
const CNHi = lazy(() => import("pages/CNHi"));
const LibraCart = lazy(() => import("pages/LibraCart"));
export const appendToUrl = (appendage: number | string) => {
const basePath = location.pathname.replace(/\/$/, "");
@ -337,6 +338,9 @@ export default function Router() {
{user.hasFeature("cnhi") &&
<Route path="cnhi" element={<CNHi />} />
}
{user.hasFeature("libra-cart") &&
<Route path="libracart" element={<LibraCart />} />
}
{/* Map routes */}
<Route path="visualFarm" element={<FieldMap />} />
<Route path="aviationMap" element={<AviationMap />} />

View file

@ -48,6 +48,7 @@ import DataDuckIcon from "products/Bindapt/DataDuckIcon";
import ContractsIcon from "products/CommonIcons/contractIcon";
import JohnDeereIcon from "products/CommonIcons/johnDeereIcon";
import CNHiIcon from "products/CommonIcons/cnhiIcon";
import LibraCartIcon from "products/CommonIcons/libracartIcon";
const drawerWidth = 230;
@ -477,6 +478,20 @@ export default function SideNavigator(props: Props) {
{open && <ListItemText primary="Case New Holland" />}
</ListItemButton>
</Tooltip>
}
{user.hasFeature("libra-cart") &&
<Tooltip title="LibraCart" placement="right">
<ListItemButton
id="tour-libraCart"
onClick={() => goTo("/libracart")}
classes={getClasses("/libracart")}
>
<ListItemIcon>
<LibraCartIcon />
</ListItemIcon>
{open && <ListItemText primary="LibraCart" />}
</ListItemButton>
</Tooltip>
}
</List>
)

View file

@ -20,7 +20,7 @@ import PageContainer from "./PageContainer";
export default function CNHi() {
const [currentOrg, setCurrentOrg] = useState("");
const cnhiAPI = useCNHiProxyAPI();
const [organizations, setOrganizations] = useState<Map<string, pond.JDAccount>>(new Map());
const [organizations, setOrganizations] = useState<Map<string, pond.CNHiAccount>>(new Map());
const [fieldAccordion, setFieldAccordion] = useState(false);
const [dataOptions, setDataOptions] = useState<pond.DataOption[]>([]);
const [{ as }] = useGlobalState();

148
src/pages/LibraCart.tsx Normal file
View file

@ -0,0 +1,148 @@
import {
Accordion,
AccordionDetails,
AccordionSummary,
Box,
Button,
Checkbox,
FormControlLabel,
Grid2 as Grid,
MenuItem,
Select
} from "@mui/material";
import { ExpandMore } from "@mui/icons-material";
import LibraCartAccess from "integrations/LibraCart/LibraCartAccess";
import { pond } from "protobuf-ts/pond";
import { useGlobalState } from "providers";
import { useLibraCartProxyAPI } from "providers/pond/libracartProxyAPI";
import React, { useEffect, useState } from "react";
import PageContainer from "./PageContainer";
export default function LibraCart() {
const [currentOrg, setCurrentOrg] = useState("");
const libracartAPI = useLibraCartProxyAPI();
const [organizations, setOrganizations] = useState<Map<string, pond.LibraCartAccount>>(new Map());
const [fieldAccordion, setFieldAccordion] = useState(false);
const [dataOptions, setDataOptions] = useState<pond.DataOption[]>([]);
const [{ as }] = useGlobalState();
//load organizations for the user
useEffect(() => {
setCurrentOrg("");
libracartAPI
.listAccounts(0, 0, as)
.then(resp => {
let tempOrgs: Map<string, pond.JDAccount> = new Map();
resp.data.accounts.forEach(org => {
let organization = pond.JDAccount.fromObject(org);
tempOrgs.set(organization.key, organization);
});
setOrganizations(tempOrgs);
})
.catch(err => {});
}, [libracartAPI, as]);
useEffect(() => {
let organization = organizations.get(currentOrg);
if (organization) {
let currentOptions: pond.DataOption[] = organization.options ?? [];
setDataOptions(currentOptions);
}
}, [currentOrg, organizations]);
const updateOrgData = (checked: boolean, option: pond.DataOption) => {
let currentOps: pond.DataOption[] = dataOptions;
if (checked && !currentOps.includes(option)) {
currentOps.push(option);
} else if (!checked && currentOps.includes(option)) {
currentOps.splice(currentOps.indexOf(option), 1);
}
setDataOptions([...currentOps]);
};
const submit = () => {
libracartAPI.updateAccount(currentOrg, dataOptions, as ?? undefined).then(resp => {
//update the organization in the map to have the correct dataOptions
let org = organizations.get(currentOrg);
if (org) {
org.options = dataOptions;
}
});
};
const fieldOptions = () => {
return (
<React.Fragment>
<Accordion
expanded={fieldAccordion}
onChange={(_, expanded) => {
setFieldAccordion(expanded);
}}>
<AccordionSummary expandIcon={<ExpandMore />}>Field Data</AccordionSummary>
<AccordionDetails>
<Grid container direction="row" spacing={2} alignItems="center" alignContent="center">
<Grid size={6}>
<FormControlLabel
label="Field Boundaries"
control={
<Checkbox
checked={dataOptions.includes(pond.DataOption.DATA_OPTION_FIELDS)}
onChange={(_, checked) => {
//setFields(!fields);
updateOrgData(checked, pond.DataOption.DATA_OPTION_FIELDS);
}}
/>
}
/>
</Grid>
<Grid size={6}>
View your data from Libra Cart on your visual Farm
</Grid>
</Grid>
</AccordionDetails>
</Accordion>
</React.Fragment>
);
};
return (
<PageContainer>
<LibraCartAccess />
<Grid
container
direction="row"
justifyContent="space-between"
alignContent="center"
alignItems="center"
style={{ padding: 10 }}>
<Grid>
<Select
//style={{ maxWidth: 110 }}
title="John Deer Account"
displayEmpty
disableUnderline={true}
value={currentOrg}
onChange={(event: any) => {
setCurrentOrg(event.target.value);
}}>
<MenuItem key={""} value={""}>
Select account to adjust accessable data
</MenuItem>
{Array.from(organizations.values()).map(org => {
return (
<MenuItem key={org.key} value={org.key}>
{org.username}
</MenuItem>
);
})}
</Select>
</Grid>
<Grid>
<Button onClick={submit} variant="contained" color="primary">
Update
</Button>
</Grid>
</Grid>
<Box style={{ padding: 10 }}>{fieldOptions()}</Box>
</PageContainer>
);
}

View file

@ -214,7 +214,7 @@ export default function TeamPage() {
}
return (
<PageContainer padding={isMobile ? 0 : 2}>
<PageContainer spacing={isMobile ? 0 : 2}>
<Box className={classes.container}>
<Box className={classes.leftBox}>
{title()}

View file

@ -0,0 +1,24 @@
// update logo images when they share official black and white
import LibraCartLogoWhite from "assets/marketplaceImages/LibraCartGrey.png";
import LibraCartLogoBlack from "assets/marketplaceImages/LibraCartGrey.png";
import { ImgIcon } from "common/ImgIcon";
import { useThemeType } from "hooks";
interface Props {
type?: "light" | "dark";
}
export default function LibraCartIcon(props: Props) {
const themeType = useThemeType();
const { type } = props;
const src = () => {
if (type) {
return type === "light" ? LibraCartLogoWhite : LibraCartLogoBlack;
}
return themeType === "light" ? LibraCartLogoBlack : LibraCartLogoWhite;
};
return <ImgIcon alt="libra-cart" src={src()} />;
}

View file

@ -0,0 +1,133 @@
import { AxiosResponse } from "axios";
import { useHTTP } from "hooks";
import { pond } from "protobuf-ts/pond";
import React, { createContext, PropsWithChildren, useContext } from "react";
//import { or } from "utils";
import { pondURL } from "./pond";
export interface ILibraCartProxyAPIContext {
//add new organization
addAccount: (
teamKey: string,
userID: string,
libracartCode: string,
libracartUsername: string
) => Promise<AxiosResponse<pond.AddLibraCartAccountResponse>>;
//list organizations
listAccounts: (
limit: number,
offset: number,
as?: string,
keys?: string[],
types?: string[]
) => Promise<AxiosResponse<pond.ListLibraCartAccountsResponse>>;
updateAccount: (
key: string,
options: pond.DataOption[],
as?: string
) => Promise<AxiosResponse<pond.UpdateLibraCartAccountResponse>>;
listFields: (
limit: number,
offset: number,
// order?: "asc" | "desc",
// orderBy?: string,
// search?: string,
as?: string,
asRoot?: boolean
) => Promise<AxiosResponse<pond.ListLibraCartFieldsResponse>>;
}
export const LibraCartProxyAPIContext = createContext<ILibraCartProxyAPIContext>(
{} as ILibraCartProxyAPIContext
);
interface Props {}
export default function LibraCartProvider(props: PropsWithChildren<Props>) {
const { children } = props;
const { post, get, put } = useHTTP();
const addAccount = (
teamKey: string,
userID: string,
libracartCode: string,
libracartUsername: string
) => {
return post<pond.AddLibraCartAccountResponse>(
pondURL(
"/libracartAccounts?team=" +
teamKey +
"&user=" +
userID +
"&code=" +
libracartCode +
"&libracartUsername=" +
libracartUsername
)
);
};
const listAccounts = (
limit: number,
offset: number,
as?: string,
keys?: string[],
types?: string[]
) => {
return get<pond.ListLibraCartAccountsResponse>(
pondURL(
"/libracartAccounts?limit=" +
limit +
"&offset=" +
offset +
(as ? "&as=" + as : "") +
(keys ? "&keys=" + keys.join(",") : "") +
(types ? "&types=" + types.join(",") : "")
)
);
};
const listFields = (
limit: number,
offset: number,
// order?: "asc" | "desc",
// orderBy?: string,
// search?: string,
as?: string,
asRoot?: boolean
) => {
return get<pond.ListFieldsResponse>(
pondURL(
"/libracartFields" +
"?limit=" +
limit +
"&offset=" +
offset +
(as ? "&as=" + as : "") +
(asRoot ? "&asRoot=" + asRoot.toString() : "")
)
);
};
const updateAccount = (key: string, options: pond.DataOption[], as?: string) => {
return put<pond.UpdateLibraCartAccountResponse>(
pondURL(
"/libracartAccounts/" + key + "?options=" + options.toString() + (as ? "&as=" + as : "")
)
);
};
return (
<LibraCartProxyAPIContext.Provider
value={{
addAccount,
listAccounts,
updateAccount,
listFields
}}>
{children}
</LibraCartProxyAPIContext.Provider>
);
}
export const useLibraCartProxyAPI = () => useContext(LibraCartProxyAPIContext);

View file

@ -35,6 +35,7 @@ import ContractProvider, { useContractAPI } from "./contractAPI";
import UsageProvider, { useUsageAPI } from "./usageAPI";
import KeyManagerProvider, { useKeyManagerAPI } from "./keyManagerAPI";
import JohnDeereProvider, { useJohnDeereProxyAPI } from "./johnDeereProxyAPI";
import LibraCartProvider, { useLibraCartProxyAPI } from "./libracartProxyAPI";
import CNHiProvider, { useCNHiProxyAPI } from "./cnhiProxyAPI";
// import NoteProvider from "providers/noteAPI";
@ -86,11 +87,13 @@ export default function PondProvider(props: PropsWithChildren<any>) {
<ContractProvider>
<JohnDeereProvider>
<CNHiProvider>
<UsageProvider>
<KeyManagerProvider>
{children}
</KeyManagerProvider>
</UsageProvider>
<LibraCartProvider>
<UsageProvider>
<KeyManagerProvider>
{children}
</KeyManagerProvider>
</UsageProvider>
</LibraCartProvider>
</CNHiProvider>
</JohnDeereProvider>
</ContractProvider>
@ -163,5 +166,6 @@ export {
useUsageAPI,
useKeyManagerAPI,
useJohnDeereProxyAPI,
useCNHiProxyAPI
useCNHiProxyAPI,
useLibraCartProxyAPI
};

View file

@ -5,6 +5,7 @@ import { useSnackbar, useUserAPI } from "hooks";
import React, { useEffect, useState } from "react";
import JohnDeereIcon from "products/CommonIcons/johnDeereIcon";
import CNHiIcon from "products/CommonIcons/cnhiIcon";
import LibraCartIcon from "products/CommonIcons/libracartIcon";
//import AgLogo from "assets/whitelabels/AdaptiveAgriculture/AGLogoSquare.png";
import {
IsAdaptiveAgriculture
@ -60,6 +61,14 @@ const agFeatureList: ProductDetails[] = [
"Integrate with the Case New Holland Industrial Center to bring your data into our platform"
// bulletPoints: ["bullet one", "bullet two"],
// questions: [],
},
{
featureName: "libra-cart",
featureLogo: <LibraCartIcon />,
featureCost: 0,
cardImage: FeatureImageTest,
featureTitle: "Libra Cart",
longDescription: "Integrate with Libra Cart to bring your data into our platform"
}
];