From e654aa002b6dac4cecaed011c7b1d479d394c8a6 Mon Sep 17 00:00:00 2001 From: Carter Date: Fri, 27 Jun 2025 11:33:43 -0600 Subject: [PATCH 01/13] changed default spacing value back to 0, fixed some pageContainers using padding instead of spacing --- src/pages/Bin.tsx | 2 +- src/pages/PageContainer.tsx | 2 +- src/pages/Team.tsx | 2 +- src/pages/Teams.tsx | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/pages/Bin.tsx b/src/pages/Bin.tsx index fc9463f..fcc1117 100644 --- a/src/pages/Bin.tsx +++ b/src/pages/Bin.tsx @@ -1107,7 +1107,7 @@ export default function Bin(props: Props) { }; return ( - + {showPlenumError()} = (props: Props) => { const { children, fullViewport, isCenterCenter, sx } = props; // let fullViewport = false // let isCenterCenter = false - const spacing = props.spacing ? props.spacing : 2; + const spacing = props.spacing ?? 0; return ( + {title()} diff --git a/src/pages/Teams.tsx b/src/pages/Teams.tsx index 2e74628..90e3401 100644 --- a/src/pages/Teams.tsx +++ b/src/pages/Teams.tsx @@ -5,7 +5,7 @@ import TeamList from "teams/TeamList"; export default function Teams() { const isMobile = useMobile() return ( - + ); From 24d27121d169b155604b01e9707fb0ff096c2ede Mon Sep 17 00:00:00 2001 From: Carter Date: Thu, 3 Jul 2025 13:41:09 -0600 Subject: [PATCH 02/13] added signup callback page that runs an api call to finish signup --- src/app/main.tsx | 4 +- src/navigation/Router.tsx | 3 ++ src/pages/SignupCallback.tsx | 87 ++++++++++++++++++++++++++++++++++ src/providers/auth.tsx | 49 ++++++++++++------- src/providers/pond/userAPI.tsx | 20 +++++++- 5 files changed, 142 insertions(+), 21 deletions(-) create mode 100644 src/pages/SignupCallback.tsx diff --git a/src/app/main.tsx b/src/app/main.tsx index e2acdd9..2d8615c 100644 --- a/src/app/main.tsx +++ b/src/app/main.tsx @@ -19,7 +19,7 @@ if ('serviceWorker' in navigator) { } createRoot(document.getElementById('root')!).render( - + // - , + // , ) diff --git a/src/navigation/Router.tsx b/src/navigation/Router.tsx index 29362ad..f6badb1 100644 --- a/src/navigation/Router.tsx +++ b/src/navigation/Router.tsx @@ -9,6 +9,7 @@ import { getWhitelabel } from "services/whiteLabel"; import Ventilation from "pages/VentEditor"; import { useGlobalState } from "providers"; import DeviceSupport from "pages/DeviceSupport"; +import SignupCallback from "pages/SignupCallback"; const DeviceHistory = lazy(() => import("pages/DeviceHistory")); const DevicePage = lazy(() => import("pages/Device")); @@ -302,6 +303,8 @@ export default function Router() { {/* Redirects */} } /> + {/* } /> */} + } /> {/* } /> */} {/* } /> */} {/* } /> */} diff --git a/src/pages/SignupCallback.tsx b/src/pages/SignupCallback.tsx new file mode 100644 index 0000000..78215f4 --- /dev/null +++ b/src/pages/SignupCallback.tsx @@ -0,0 +1,87 @@ +import { useDeviceAPI, useUserAPI } from "hooks"; +import PageContainer from "./PageContainer"; +import { useEffect, useState } from "react"; +import { Button, CircularProgress, Dialog, DialogActions, DialogContent, DialogContentText, DialogTitle, Divider } from "@mui/material"; +import { useBinAPI, useTeamAPI } from "providers"; +import { CheckCircleOutline } from "@mui/icons-material"; +import { green } from "@mui/material/colors"; + +interface Props { + +} + +export default function SignupCallback (props: Props) { + const userAPI = useUserAPI(); + const teamAPI = useTeamAPI() + const deviceAPI = useDeviceAPI(); + const binAPI = useBinAPI(); + const [loading, setLoading] = useState(false) + const [signupDialogOpen, setSignupDialogOpen] = useState(true) + const [hasTeams, setHasTeams] = useState(false) + const [hasDevices, setHasDevices] = useState(false) + const [hasBins, setHasBins] = useState(false) + + const [doneBins, setDoneBins] = useState(false) + const [doneTeams, setDoneTeams] = useState(false) + const [doneDevices, setDoneDevices] = useState(false) + + useEffect(() => { + console.log(hasTeams) + }, [hasTeams]) + + useEffect(() => { + setLoading(true) + userAPI.completeSignup().then(resp => { + console.log(resp) + teamAPI.listTeams(1, 0).then(resp => { + setHasTeams(resp.data.teams.length > 0) + }).finally(() => setDoneTeams(true)) + deviceAPI.list(1, 0).then(resp => { + setHasDevices(resp.data.devices.length > 0) + }).finally(() => setDoneDevices(true)) + binAPI.listBins(1, 0).then(resp => { + setHasBins(resp.data.bins.length > 0) + }).finally(() => setDoneBins(true)) + }).finally(() => setLoading(false)) + }, []) + + const loadingComplete = () => { + return loading === false && doneBins && doneTeams && doneDevices + } + + const content = () => { + return ( + + { loadingComplete() ? + + : + + } + + {hasTeams && "Teams found!"} + {hasDevices && "Devices found!"} + {hasBins && "Bins found!"} + + + ) + } + + return ( + + + + Finishing Signup + + + { content() } + + {loadingComplete() && + + } + + + + ) +} \ No newline at end of file diff --git a/src/providers/auth.tsx b/src/providers/auth.tsx index 22ac76d..f7ee585 100644 --- a/src/providers/auth.tsx +++ b/src/providers/auth.tsx @@ -9,28 +9,30 @@ interface Props extends PropsWithChildren{ export default function AuthWrapper(props: Props) { const { children, setToken } = props; - const { isLoading, loginWithRedirect, isAuthenticated, getAccessTokenSilently } = useAuth0(); + const { isLoading, loginWithRedirect, isAuthenticated, getAccessTokenSilently, getIdTokenClaims } = useAuth0(); useEffect(() => { const url = new URL(window.location.href); - const isManualLoginPage = url.pathname.includes("/login"); + const options: RedirectLoginOptions = { + authorizationParams: {} // Initialize here + }; - if (isManualLoginPage) { - const options: RedirectLoginOptions = { - authorizationParams: {} // Initialize here - }; - + if (isManualLoginPage && options.authorizationParams) { + let parsed = queryString.parse(url.search); if (parsed.signup === "true") { - options.authorizationParams!.screen_hint = "signup"; - options.authorizationParams!.prompt = "login"; + options.authorizationParams.screen_hint = "signUp"; + // options.authorizationParams.m + // options.authorizationParams.prompt = "login"; } if (parsed.email && parsed.email !== "") { - options.authorizationParams!.login_hint = parsed.email.toString(); // prefill email + options.authorizationParams.login_hint = parsed.email.toString(); // prefill email } + + options.authorizationParams.redirect_uri = "http://localhost:5173/signupCallback" loginWithRedirect(options) @@ -40,15 +42,28 @@ export default function AuthWrapper(props: Props) { }, [isAuthenticated, loginWithRedirect, isLoading]); useEffect(() => { - if (isAuthenticated) getAccessTokenSilently().then(t => { - if (t.length < 1) { + const url = new URL(window.location.href); + const isSignupCallback = url.pathname.includes("/signupCallback"); + if (isAuthenticated) { + getAccessTokenSilently().then(t => { + if (t.length < 1) { + loginWithRedirect() + } + setToken(t) + }).catch(() => { + // No token, go back to login loginWithRedirect() + }) + if (isSignupCallback) { + console.log("IS SIGNUP") + getIdTokenClaims().then(id_claims => { + console.log(id_claims) + console.log(id_claims?.nickname) + if (id_claims) console.log(Object.keys(id_claims)) + }) } - setToken(t) - }).catch(() => { - // No token, go back to login - loginWithRedirect() - }) + + } }, [setToken, isAuthenticated]) return children; diff --git a/src/providers/pond/userAPI.tsx b/src/providers/pond/userAPI.tsx index 247f77c..21befb2 100644 --- a/src/providers/pond/userAPI.tsx +++ b/src/providers/pond/userAPI.tsx @@ -28,6 +28,7 @@ export interface IUserAPIContext { prefixSearch?: string, ) => Promise; listProfiles: () => Promise; + completeSignup: () => Promise; listObjectUsers: (scope: Scope) => Promise; updateObjectUsers: (scope: Scope, users: pond.IUser[]) => Promise; getUser: (id: string, scope?: Scope) => Promise; @@ -55,8 +56,22 @@ export const UserAPIContext = createContext({} as IUserAPIConte export default function UserProvider(props: PropsWithChildren) { const { children } = props; - const { get, put } = useHTTP(); - const [{ as }] = useGlobalState(); + const { get, put, post } = useHTTP(); + const [{ as, user }] = useGlobalState(); + + const completeSignup = (): Promise => { + return new Promise((resolve, reject) => { + let request = pond.CompleteSignupUserRequest.create(); + request.avatar = user.settings.avatar + request.email = user.settings.email + request.id = user.id() + request.name = user.name() + post(pondURL("/signups"), request).then(res => { + console.log(res) + resolve(pond.CompleteSignupUserResponse.create()) + }) + }) + } const listUsers = ( limit: number, @@ -234,6 +249,7 @@ export default function UserProvider(props: PropsWithChildren) { return ( Date: Mon, 7 Jul 2025 11:57:53 -0600 Subject: [PATCH 03/13] added dark streamline logo, minor styling adjustments --- public/Streamline/stream-logo-dark.png | Bin 0 -> 26259 bytes src/device/SyncDevice.tsx | 5 ++++- src/pages/SignupCallback.tsx | 10 +++------- 3 files changed, 7 insertions(+), 8 deletions(-) create mode 100644 public/Streamline/stream-logo-dark.png diff --git a/public/Streamline/stream-logo-dark.png b/public/Streamline/stream-logo-dark.png new file mode 100644 index 0000000000000000000000000000000000000000..a486c1e727ee6ec98c99138cca3cb6ebee18898e GIT binary patch literal 26259 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 { + close() + }) }; return ( diff --git a/src/pages/SignupCallback.tsx b/src/pages/SignupCallback.tsx index 78215f4..00b2c51 100644 --- a/src/pages/SignupCallback.tsx +++ b/src/pages/SignupCallback.tsx @@ -1,4 +1,4 @@ -import { useDeviceAPI, useUserAPI } from "hooks"; +import { useDeviceAPI, useMobile, useUserAPI } from "hooks"; import PageContainer from "./PageContainer"; import { useEffect, useState } from "react"; import { Button, CircularProgress, Dialog, DialogActions, DialogContent, DialogContentText, DialogTitle, Divider } from "@mui/material"; @@ -15,6 +15,7 @@ export default function SignupCallback (props: Props) { const teamAPI = useTeamAPI() const deviceAPI = useDeviceAPI(); const binAPI = useBinAPI(); + const isMobile = useMobile() const [loading, setLoading] = useState(false) const [signupDialogOpen, setSignupDialogOpen] = useState(true) const [hasTeams, setHasTeams] = useState(false) @@ -25,14 +26,9 @@ export default function SignupCallback (props: Props) { const [doneTeams, setDoneTeams] = useState(false) const [doneDevices, setDoneDevices] = useState(false) - useEffect(() => { - console.log(hasTeams) - }, [hasTeams]) - useEffect(() => { setLoading(true) userAPI.completeSignup().then(resp => { - console.log(resp) teamAPI.listTeams(1, 0).then(resp => { setHasTeams(resp.data.teams.length > 0) }).finally(() => setDoneTeams(true)) @@ -68,7 +64,7 @@ export default function SignupCallback (props: Props) { return ( - + Finishing Signup From 4f8b0254bb29b02bf9908e4147d5b65217c09b07 Mon Sep 17 00:00:00 2001 From: Carter Date: Tue, 8 Jul 2025 14:07:01 -0600 Subject: [PATCH 04/13] added a welcome tour to the signupCallback page --- package-lock.json | 56 ++++++++++++ package.json | 1 + src/common/Tour.tsx | 30 +++++-- src/navigation/SideNavigator.tsx | 8 +- src/pages/Firmware.tsx | 4 +- src/pages/SignupCallback.tsx | 148 +++++++++++++++++++++++++++++-- src/pages/Users.tsx | 2 +- src/providers/auth.tsx | 27 +++--- src/providers/pond/userAPI.tsx | 1 - src/user/UserMenu.tsx | 2 +- 10 files changed, 247 insertions(+), 32 deletions(-) diff --git a/package-lock.json b/package-lock.json index 65ae1cc..840ea14 100644 --- a/package-lock.json +++ b/package-lock.json @@ -49,6 +49,7 @@ "react-color": "^2.19.3", "react-day-picker": "^9.6.5", "react-dom": "^18.3.1", + "react-emoji-render": "^2.0.1", "react-error-boundary": "^5.0.0", "react-full-screen": "^1.1.1", "react-horizontal-scrolling-menu": "^7.1.1", @@ -9188,6 +9189,15 @@ "node": ">=12" } }, + "node_modules/invariant": { + "version": "2.2.4", + "resolved": "https://registry.npmjs.org/invariant/-/invariant-2.2.4.tgz", + "integrity": "sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.0.0" + } + }, "node_modules/is-array-buffer": { "version": "3.0.5", "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.5.tgz", @@ -9971,6 +9981,18 @@ "resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz", "integrity": "sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==" }, + "node_modules/lodash.flatten": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/lodash.flatten/-/lodash.flatten-4.4.0.tgz", + "integrity": "sha512-C5N2Z3DgnnKr0LOpv/hKCgKdb7ZZwafIrsesve6lmzvZIRZRGaZ/l6Q8+2W7NaT+ZwO3fFlSCzCzrDCFdJfZ4g==", + "license": "MIT" + }, + "node_modules/lodash.isstring": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz", + "integrity": "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==", + "license": "MIT" + }, "node_modules/lodash.memoize": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz", @@ -11107,6 +11129,29 @@ "react": "^18.3.1" } }, + "node_modules/react-emoji-render": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/react-emoji-render/-/react-emoji-render-2.0.1.tgz", + "integrity": "sha512-SKtsdwgEf2BFNiE9y4UBFZBWjkRcyWmhMprVly52+J77/zxThcfaQ3sCA0+2LtGJIRMpm4DGWSvyLg72fd1rXQ==", + "license": "MIT", + "dependencies": { + "classnames": "^2.2.5", + "emoji-regex": "^8.0.0", + "lodash.flatten": "^4.4.0", + "prop-types": "^15.5.8", + "string-replace-to-array": "^1.0.1" + }, + "peerDependencies": { + "react": ">=0.14.0", + "react-dom": ">=0.14.0" + } + }, + "node_modules/react-emoji-render/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "license": "MIT" + }, "node_modules/react-error-boundary": { "version": "5.0.0", "integrity": "sha512-tnjAxG+IkpLephNcePNA7v6F/QpWLH8He65+DmedchDwg162JZqx4NmbXj0mlAYVVEd81OW7aFhmbsScYfiAFQ==", @@ -12408,6 +12453,17 @@ "safe-buffer": "~5.2.0" } }, + "node_modules/string-replace-to-array": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/string-replace-to-array/-/string-replace-to-array-1.0.3.tgz", + "integrity": "sha512-QIdKvmfXbdFvblXAAz6IIjR7A+C6SU6m2A+e7fE/0EYDC5yfeWNMJQ193fPsW7nG+9q52dv/UjnVrDaNVZXpmQ==", + "license": "MIT", + "dependencies": { + "invariant": "^2.2.1", + "lodash.flatten": "^4.2.0", + "lodash.isstring": "^4.0.1" + } + }, "node_modules/string.prototype.matchall": { "version": "4.0.12", "resolved": "https://registry.npmjs.org/string.prototype.matchall/-/string.prototype.matchall-4.0.12.tgz", diff --git a/package.json b/package.json index 67d05d8..afae078 100644 --- a/package.json +++ b/package.json @@ -61,6 +61,7 @@ "react-color": "^2.19.3", "react-day-picker": "^9.6.5", "react-dom": "^18.3.1", + "react-emoji-render": "^2.0.1", "react-error-boundary": "^5.0.0", "react-full-screen": "^1.1.1", "react-horizontal-scrolling-menu": "^7.1.1", diff --git a/src/common/Tour.tsx b/src/common/Tour.tsx index 4781e21..9d328a7 100644 --- a/src/common/Tour.tsx +++ b/src/common/Tour.tsx @@ -1,4 +1,4 @@ -import Joyride, { Step, CallBackProps, STATUS, TooltipRenderProps, Locale } from "react-joyride"; +import Joyride, { Step, CallBackProps, STATUS, TooltipRenderProps, Locale, ACTIONS, EVENTS } from "react-joyride"; import { Button, Card, @@ -10,12 +10,13 @@ import { Tooltip, IconButton, Theme, - useTheme + useTheme, } from "@mui/material"; import { KeyboardArrowLeft, KeyboardArrowRight, Close as CloseIcon } from "@mui/icons-material"; import classNames from "classnames"; import { grey } from "@mui/material/colors"; import { makeStyles } from "@mui/styles"; +import { CSSProperties } from "react"; const useStyles = makeStyles((theme: Theme) => { return ({ @@ -46,6 +47,10 @@ const useStyles = makeStyles((theme: Theme) => { }) }); +export interface TourStep extends Step { + onNext?: () => void; // Define onNext as an optional function +} + const StepContainer = (props: TooltipRenderProps, numSteps: number, skip: () => void) => { const { continuous, @@ -114,18 +119,30 @@ const StepContainer = (props: TooltipRenderProps, numSteps: number, skip: () => interface Props { run: boolean; - steps: Step[]; + steps: TourStep[]; endTourCallback: () => void; + spotlightStyle?: CSSProperties, + setStepIndex?: React.Dispatch>, } export default function Tour(props: Props) { - const { run, steps, endTourCallback } = props; + const { run, steps, endTourCallback, setStepIndex, spotlightStyle } = props; const theme = useTheme(); const handleTourCallback = (data: CallBackProps) => { - const { status } = data; + const { action, index, status, type } = data; const finishedStatuses: string[] = [STATUS.FINISHED, STATUS.SKIPPED]; + if (setStepIndex) setStepIndex(index) + + if (action === ACTIONS.NEXT && type === EVENTS.STEP_AFTER) { + // Safely access the step and call onNext if it exists + const currentStep = steps[index]; + if (currentStep.onNext) { + currentStep.onNext(); + } + } + if (finishedStatuses.includes(status)) { endTourCallback(); } @@ -150,6 +167,9 @@ export default function Tour(props: Props) { primaryColor: theme.palette.primary.main, textColor: theme.palette.text.primary, zIndex: theme.zIndex.modal + }, + spotlight: { + ...spotlightStyle } }} /> diff --git a/src/navigation/SideNavigator.tsx b/src/navigation/SideNavigator.tsx index 38a10cb..f71ca9c 100644 --- a/src/navigation/SideNavigator.tsx +++ b/src/navigation/SideNavigator.tsx @@ -221,7 +221,7 @@ export default function SideNavigator(props: Props) { {(isAg || isStreamline || user.hasFeature("admin")) && ( goTo("/bins")} classes={getClasses("/bin")} > @@ -277,7 +277,7 @@ export default function SideNavigator(props: Props) { {(isMiVent || user.hasFeature("admin")) && ( goTo("/mines")} classes={getClasses("/mine")} > @@ -330,7 +330,7 @@ export default function SideNavigator(props: Props) { } goTo("/teams")} classes={getClasses("/team")} > @@ -342,7 +342,7 @@ export default function SideNavigator(props: Props) { goTo("/users")} classes={getClasses("/user")} > diff --git a/src/pages/Firmware.tsx b/src/pages/Firmware.tsx index b3a7c58..1ae97af 100644 --- a/src/pages/Firmware.tsx +++ b/src/pages/Firmware.tsx @@ -12,7 +12,7 @@ import { TypeBackground } from "@mui/material"; import { blue, green, orange, red } from "@mui/material/colors"; -import { createStyles, makeStyles } from "@mui/styles"; +import { makeStyles } from "@mui/styles"; //import { TypeBackground } from "@material-ui/core/styles/createPalette"; import { ArrowUpward, @@ -267,7 +267,7 @@ export default function Firmware() { if (selected) { firmwareAPI .removeFirmware(selected.settings.platform, selected.settings.version) - .then((response: any) => { + .then(() => { success("Successfully removed firmware"); }) .catch((err: any) => { diff --git a/src/pages/SignupCallback.tsx b/src/pages/SignupCallback.tsx index 00b2c51..a22dbfc 100644 --- a/src/pages/SignupCallback.tsx +++ b/src/pages/SignupCallback.tsx @@ -1,26 +1,36 @@ import { useDeviceAPI, useMobile, useUserAPI } from "hooks"; import PageContainer from "./PageContainer"; import { useEffect, useState } from "react"; -import { Button, CircularProgress, Dialog, DialogActions, DialogContent, DialogContentText, DialogTitle, Divider } from "@mui/material"; -import { useBinAPI, useTeamAPI } from "providers"; +import { Button, CircularProgress, Dialog, DialogActions, DialogContent, DialogContentText, DialogTitle, Divider, Typography } from "@mui/material"; +import { useBinAPI, useGlobalState, useTeamAPI } from "providers"; import { CheckCircleOutline } from "@mui/icons-material"; import { green } from "@mui/material/colors"; +import Tour, { TourStep } from "common/Tour"; +import Emoji from "react-emoji-render"; +import { getWhitelabel, IsAdaptiveAgriculture } from "services/whiteLabel"; +// import { color } from "framer-motion"; -interface Props { +// interface Props { -} +// } -export default function SignupCallback (props: Props) { +// export default function SignupCallback (props: Props) { +export default function SignupCallback () { const userAPI = useUserAPI(); const teamAPI = useTeamAPI() const deviceAPI = useDeviceAPI(); const binAPI = useBinAPI(); const isMobile = useMobile() + const [{ user }] = useGlobalState(); + const whiteLabel = getWhitelabel() + const [loading, setLoading] = useState(false) const [signupDialogOpen, setSignupDialogOpen] = useState(true) const [hasTeams, setHasTeams] = useState(false) const [hasDevices, setHasDevices] = useState(false) const [hasBins, setHasBins] = useState(false) + const [isTourRunning, setIsTourRunning] = useState(false); + const [stepIndex, setStepIndex] = useState(0) const [doneBins, setDoneBins] = useState(false) const [doneTeams, setDoneTeams] = useState(false) @@ -28,7 +38,7 @@ export default function SignupCallback (props: Props) { useEffect(() => { setLoading(true) - userAPI.completeSignup().then(resp => { + userAPI.completeSignup().then(() => { teamAPI.listTeams(1, 0).then(resp => { setHasTeams(resp.data.teams.length > 0) }).finally(() => setDoneTeams(true)) @@ -55,13 +65,131 @@ export default function SignupCallback (props: Props) { } {hasTeams && "Teams found!"} +
{hasDevices && "Devices found!"} +
{hasBins && "Bins found!"}
) } + const getTourSteps = (): TourStep[] => { + let steps: TourStep[] = [ + { + title: ( + <> + Welcome to {whiteLabel.name} + + + ), + content: ( + <> + + {"Hello " + user.name()} + + + + Thank you for signing up! Let me give you the tour + + + + ), + target: "body", + placement: "center", + disableBeacon: true, + }, + { + title: "User Menu", + content: ( + <> + + In the user menu, you can make changes to your profile, and adjust unit preferences. + +
+ + If you are part of a team, you can select it here as well. + + + ), + target: "#tour-user-menu", + placement: "bottom", + disableBeacon: true, + // onNext: () => setDoneFirst(true) + }, + // { + // title: + // } + ]; + if (hasTeams) steps.push({ + title: "Teams", + content: ( + <> + + Here, you can view your team(s), access their settings, and choose your favourite. + + + ), + target: "#tour-teams", + placement: "right", + // spotlightPadding: 50, + }) + steps.push({ + title: "Devices", + content: ( + <> + + You can view your devices and their readings here. + +
+ + If you are viewing as a team, your team's devices will be displayed here instead. + + + ), + target: "#tour-dashboard", + placement: "right" + }) + if (IsAdaptiveAgriculture()) steps.push({ + title: "Bins", + content: ( + <> + + You can view your bins and their inventory here. + +
+ + If you are viewing as a team, your team's bins will be displayed here instead. + + + ), + target: "#tour-bins", + placement: "right", + disableBeacon: true, + }) + return steps; + }; + + const endTour = () => { + setIsTourRunning(false); + // if (user) { + // user.status.finishedBinIntro = moment().toJSON(); + // userAPI + // .updateUser(userID, user.protobuf()) + // .then(() => dispatch({ key: "user", value: user })) + // .catch((err: any) => error(err)); + // } + }; + + const closeDialog = () => { + setSignupDialogOpen(false) + setIsTourRunning(true) + } + + useEffect(() => { + console.log(stepIndex) + }, [stepIndex]) + return ( @@ -72,12 +200,18 @@ export default function SignupCallback (props: Props) { { content() } {loadingComplete() && - } + ) } \ No newline at end of file diff --git a/src/pages/Users.tsx b/src/pages/Users.tsx index 00ef9f7..296c60a 100644 --- a/src/pages/Users.tsx +++ b/src/pages/Users.tsx @@ -349,7 +349,7 @@ export default function Users() { } return ( - + title="Users" rows={rows} diff --git a/src/providers/auth.tsx b/src/providers/auth.tsx index f7ee585..2a8f6bb 100644 --- a/src/providers/auth.tsx +++ b/src/providers/auth.tsx @@ -9,7 +9,13 @@ interface Props extends PropsWithChildren{ export default function AuthWrapper(props: Props) { const { children, setToken } = props; - const { isLoading, loginWithRedirect, isAuthenticated, getAccessTokenSilently, getIdTokenClaims } = useAuth0(); + const { + isLoading, + loginWithRedirect, + isAuthenticated, + getAccessTokenSilently, + // getIdTokenClaims + } = useAuth0(); useEffect(() => { @@ -42,8 +48,8 @@ export default function AuthWrapper(props: Props) { }, [isAuthenticated, loginWithRedirect, isLoading]); useEffect(() => { - const url = new URL(window.location.href); - const isSignupCallback = url.pathname.includes("/signupCallback"); + // const url = new URL(window.location.href); + // const isSignupCallback = url.pathname.includes("/signupCallback"); if (isAuthenticated) { getAccessTokenSilently().then(t => { if (t.length < 1) { @@ -54,14 +60,13 @@ export default function AuthWrapper(props: Props) { // No token, go back to login loginWithRedirect() }) - if (isSignupCallback) { - console.log("IS SIGNUP") - getIdTokenClaims().then(id_claims => { - console.log(id_claims) - console.log(id_claims?.nickname) - if (id_claims) console.log(Object.keys(id_claims)) - }) - } + // if (isSignupCallback) { + // getIdTokenClaims().then(id_claims => { + // console.log(id_claims) + // console.log(id_claims?.nickname) + // if (id_claims) console.log(Object.keys(id_claims)) + // }) + // } } }, [setToken, isAuthenticated]) diff --git a/src/providers/pond/userAPI.tsx b/src/providers/pond/userAPI.tsx index 21befb2..0d33b2e 100644 --- a/src/providers/pond/userAPI.tsx +++ b/src/providers/pond/userAPI.tsx @@ -67,7 +67,6 @@ export default function UserProvider(props: PropsWithChildren) { request.id = user.id() request.name = user.name() post(pondURL("/signups"), request).then(res => { - console.log(res) resolve(pond.CompleteSignupUserResponse.create()) }) }) diff --git a/src/user/UserMenu.tsx b/src/user/UserMenu.tsx index 2b41a6f..c7b8ab3 100644 --- a/src/user/UserMenu.tsx +++ b/src/user/UserMenu.tsx @@ -228,7 +228,7 @@ export default function UserMenu() { */} {hasTeams && ( - + From 0127eeefc75d40e450eb6ab525a779f94a669191 Mon Sep 17 00:00:00 2001 From: Carter Date: Wed, 16 Jul 2025 13:32:08 -0600 Subject: [PATCH 05/13] view as team notification pops up if they don't have devices or bins but have a team --- src/pages/Bins.tsx | 76 +++++++++++++++++++++++++------------ src/pages/DeviceSupport.tsx | 5 ++- src/pages/Devices.tsx | 26 ++++++++++++- src/teams/TeamDialog.tsx | 58 +++++++++++++++++++++++++--- 4 files changed, 130 insertions(+), 35 deletions(-) diff --git a/src/pages/Bins.tsx b/src/pages/Bins.tsx index 8f77613..476b66f 100644 --- a/src/pages/Bins.tsx +++ b/src/pages/Bins.tsx @@ -18,7 +18,6 @@ import { ImageList, ImageListItem, InputLabel, - lighten, ListItemIcon, ListItemText, Menu, @@ -26,8 +25,6 @@ import { Select, Skeleton, Theme, - ToggleButton, - ToggleButtonGroup, Typography, useTheme, } from "@mui/material"; @@ -61,7 +58,8 @@ import { // useBinYardAPI, useGlobalState, useInteractionsAPI, - useSnackbar + useSnackbar, + useTeamAPI } from "providers"; import React, { SetStateAction, useCallback, useEffect, useState } from "react"; // import { getThemeType } from "theme"; @@ -72,7 +70,7 @@ import ObjectTable from "objects/ObjectTable"; import ResponsiveDialog from "common/ResponsiveDialog"; import { describeMeasurement } from "pbHelpers/MeasurementDescriber"; import { green, yellow } from "@mui/material/colors"; -import { makeStyles, styled } from "@mui/styles"; +import { makeStyles } from "@mui/styles"; import { useNavigate } from "react-router-dom"; import BinSettings from "bin/BinSettings"; import AddBinFab from "bin/AddBinFab"; @@ -82,6 +80,7 @@ import GrainBagSettings from "grainBag/grainBagSettings"; import GrainBagList from "grainBag/grainBagList"; import BinYards from "bin/BinYards"; import ButtonGroup from "common/ButtonGroup"; +import TeamDialog from "teams/TeamDialog"; // import { useHistory } from "react-router"; const useStyles = makeStyles((theme: Theme) => { @@ -141,11 +140,13 @@ export default function Bins(props: Props) { const classes = useStyles(); const isMobile = useMobile(); const binAPI = useBinAPI(); + const teamAPI = useTeamAPI(); // const binyardAPI = useBinYardAPI(); const interactionsAPI = useInteractionsAPI(); const [{ user, as }] = useGlobalState(); const [allLoading, setAllLoading] = useState(false); const [binsLoading, setBinsLoading] = useState(false); + const [doneLoadingPage, setDoneLoadingPage] = useState(false) const [addBinOpen, setAddBinOpen] = useState(false); const [paginatedBins, setPaginatedBins] = useState({ bins: [], @@ -188,13 +189,16 @@ export default function Bins(props: Props) { const [qrKeyList, setQrKeyList] = useState([]); const [totalFanControllers, setTotalFanControllers] = useState(0); const [totalFanControllersOn, setTotalFanControllersOn] = useState(0); - const [yardsLoading, setYardsLoading] = useState(false); + // const [yardsLoading, setYardsLoading] = useState(false); const [openAddAlerts, setOpenAddAlerts] = useState(false); const [alertBins, setAlertBins] = useState([]); const { openSnack } = useSnackbar(); // const history = useHistory(); const navigate = useNavigate() const [cardValDisplay, setCardValDisplay] = useState<"high" | "low" | "average">("average"); + + const [teamsDialog, setTeamsDialog] = useState(false) + // const [scrollTranslations, setScrollTranslations] = useState({ // bins: 0, // empty: 0, @@ -252,6 +256,18 @@ export default function Bins(props: Props) { // selected: {} // })); + useEffect(() => { + if (doneLoadingPage && grainBins.length < 1 && grainBags.length < 1) { + teamAPI.listTeams(1, 0).then(resp => { + // console.log(resp.data.teams) + if (resp.data.teams.length > 0) { + // console.log("should?") + setTeamsDialog(true) + } + }) + } + }, [doneLoadingPage, grainBins, grainBags]) + useEffect(() => { let ebt = sessionStorage.getItem("expandBinTotal"); if (ebt === "true") { @@ -400,7 +416,7 @@ export default function Bins(props: Props) { const bags = resp.data.bags.map(b => GrainBag.create(b)); setGrainBags(bags); }) - .catch(err => { + .catch(() => { setPaginatedBins({ bins: [], binsOffset: 0, binsTotal: 0 }); setBinMetrics(pond.BinMetrics.create()); setYards([]); @@ -408,6 +424,7 @@ export default function Bins(props: Props) { .finally(() => { setAllLoading(false); setBinsLoading(false); + setDoneLoadingPage(true) }); }, [binAPI, yardFilter, order, orderBy, as, loadingAs, loadingFilter]); // eslint-disable-line react-hooks/exhaustive-deps @@ -454,7 +471,7 @@ export default function Bins(props: Props) { setFertilizerBins([...fert]); setQrKeyList([...newKeyList]); }) - .catch(err => { + .catch(() => { setPaginatedBins({ bins: [], binsOffset: 0, binsTotal: 0 }); }) .finally(() => { @@ -462,13 +479,13 @@ export default function Bins(props: Props) { }); }; - const searchYards = ( - search?: string, - limit?: number, - offset?: number, - order?: "asc" | "desc" - ) => { - setYardsLoading(true); + // const searchYards = ( + // search?: string, + // limit?: number, + // offset?: number, + // order?: "asc" | "desc" + // ) => { + // setYardsLoading(true); // binyardAPI.listBinYards(limit ?? binsLimit, offset ?? 0, order, "name", search).then(resp => { // let y: pond.BinYardSettings[] = []; // let p: Dictionary = {}; @@ -493,7 +510,7 @@ export default function Bins(props: Props) { // } // setYardsLoading(false); // }); - }; + // }; useEffect(() => { loadBins(); @@ -515,7 +532,7 @@ export default function Bins(props: Props) { } const duplicateBin = (bin: Bin) => { - binAPI.addBin(bin.settings, as).then(resp => { + binAPI.addBin(bin.settings, as).then(() => { loadBins(); }); }; @@ -556,7 +573,7 @@ export default function Bins(props: Props) { .then(resp => { openSnack("Successfully added " + resp.data.numAlertsSet + " alerts"); }) - .catch(err => { + .catch(() => { console.log("errors found"); }); }; @@ -1080,7 +1097,7 @@ export default function Bins(props: Props) { control={ { + onChange={(_, checked) => { setDisplayGrain(checked); }} /> @@ -1093,7 +1110,7 @@ export default function Bins(props: Props) { control={ { + onChange={(_, checked) => { setDisplayFert(checked); }} /> @@ -1106,7 +1123,7 @@ export default function Bins(props: Props) { control={ { + onChange={(_, checked) => { setDisplayEmpty(checked); }} /> @@ -1392,15 +1409,17 @@ export default function Bins(props: Props) { {addALertsDialog()} { - searchYards(search, limit, offset); - }} + // loadYards={(search, limit, offset) => { + // searchYards(search, limit, offset); + // }} + loadYards={() => {}} grainBags={grainBags} setShowing={(val: "all" | "bins" | "bags") => { setShowing(val); @@ -1473,6 +1492,13 @@ export default function Bins(props: Props) { /> {binMenu()} setAddBinOpen(true)} pulse={binsTotal < 1 && !allLoading} /> +
); }; diff --git a/src/pages/DeviceSupport.tsx b/src/pages/DeviceSupport.tsx index a6a19de..6c9a0db 100644 --- a/src/pages/DeviceSupport.tsx +++ b/src/pages/DeviceSupport.tsx @@ -3,7 +3,7 @@ import PageContainer from "./PageContainer"; import { pond } from "protobuf-ts/pond"; import LogsDisplay from "common/LogsDisplay"; import { useEffect, useState } from "react"; -import { useDeviceAPI, useSnackbar } from "hooks"; +import { useDeviceAPI, useMobile, useSnackbar } from "hooks"; import { useLocation, useParams } from "react-router-dom"; import { Component, Device, Interaction } from "models"; import { getContextKeys, getContextTypes } from "pbHelpers/Context"; @@ -43,6 +43,7 @@ export default function DeviceSupport() { const deviceAPI = useDeviceAPI() const snackbar = useSnackbar() const theme = useTheme() + const isMobile = useMobile() const { state } = useLocation(); const [{ user, as }] = useGlobalState() @@ -188,7 +189,7 @@ export default function DeviceSupport() { } return ( - + diff --git a/src/pages/Devices.tsx b/src/pages/Devices.tsx index fe47b3c..7721ec1 100644 --- a/src/pages/Devices.tsx +++ b/src/pages/Devices.tsx @@ -5,7 +5,7 @@ import { makeStyles } from "@mui/styles"; import ResponsiveTable, { Column } from "common/ResponsiveTable"; import ProvisionDevice from "device/ProvisionDevice"; import { pond } from "protobuf-ts/pond"; -import { useDeviceAPI, useGlobalState, useGroupAPI } from "providers"; +import { useDeviceAPI, useGlobalState, useGroupAPI, useTeamAPI } from "providers"; import { useCallback, useEffect, useState } from "react"; import PageContainer from "./PageContainer"; import { useMobile } from "hooks"; @@ -28,6 +28,7 @@ import StatusDust from "common/StatusDust"; import StatusGas from "common/StatusGas"; import { cloneDeep } from "lodash"; import LoadingScreen from "app/LoadingScreen"; +import TeamDialog from "teams/TeamDialog"; const useStyles = makeStyles((theme: Theme) => { return ({ @@ -98,9 +99,12 @@ export default function Devices() { const location = useLocation(); const deviceAPI = useDeviceAPI(); const groupAPI = useGroupAPI(); + const teamAPI = useTeamAPI(); const [devicesLoading, setDevicesLoading] = useState(false) const [limit, setLimit] = useState(10); const [page, setPage] = useState(0); + const [doneLoading, setDoneLoading] = useState(false) + const [teamsDialog, setTeamsDialog] = useState(false) const [{ user }] = useGlobalState() @@ -186,7 +190,7 @@ export default function Devices() { const [preferencesAnchor, setPreferencesAnchor] = useState(null) - const [{ as }] = useGlobalState() + const [{ as, team }] = useGlobalState() const updateGroups = (newGroups: Group[]) => { setGroups(newGroups); @@ -327,6 +331,7 @@ export default function Devices() { setTotal(resp.data.total) }).finally(() => { setDevicesLoading(false) + setDoneLoading(true) }) } @@ -338,6 +343,16 @@ export default function Devices() { loadGroups() }, [groupLimit, groupPage, orderGroup, orderGroupBy, searchGroup, as]) + useEffect(() => { + if (doneLoading && devices.length < 1 && groups.length < 1) { + teamAPI.listTeams(1, 0).then(resp => { + if (resp.data.teams.length > 0 && team.key().length < 1) { + setTeamsDialog(true); + } + }) + } + }, [doneLoading, devices, groups]) + const handleChange = (event: any) => { setLimit(event.target.value); }; @@ -826,6 +841,13 @@ export default function Devices() { addGroupCallback={addGroupCallback} /> {preferencesMenu()} + ) } \ No newline at end of file diff --git a/src/teams/TeamDialog.tsx b/src/teams/TeamDialog.tsx index 25ebb9c..edc2124 100644 --- a/src/teams/TeamDialog.tsx +++ b/src/teams/TeamDialog.tsx @@ -1,6 +1,9 @@ import { AppBar, Button, + Checkbox, + FormControlLabel, + FormGroup, IconButton, Theme, Toolbar, @@ -18,6 +21,9 @@ import { makeStyles } from "@mui/styles"; interface Props { open: boolean; setOpen: (value: React.SetStateAction) => void; + title?: string; + info?: string; + hideKey?: string; } const useStyles = makeStyles((theme: Theme) => { @@ -37,16 +43,22 @@ const useStyles = makeStyles((theme: Theme) => { }); export default function TeamDialog(props: Props) { - const { open, setOpen } = props; + const { open, setOpen, title, info, hideKey } = props; const teamAPI = useTeamAPI(); const userAPI = useUserAPI(); const [, dispatch] = useGlobalState(); const [teamKey, setTeamKey] = useState(""); const [{ team, user }] = useGlobalState(); + const [viewAsTeam, setViewAsTeam] = useState(user.settings.useTeam) + const [doNotShow, setDoNotShow] = useState(false) const classes = useStyles(); + const shouldHide = hideKey ? + (localStorage.getItem(hideKey) === "true" ? true : false) + : false + const setTeam = () => { - setOpen(false); + onClose() dispatch({ key: "as", value: teamKey }); if (teamKey === "" || teamKey.includes("auth")) { dispatch({ key: "team", value: Team.create() }); @@ -55,13 +67,19 @@ export default function TeamDialog(props: Props) { teamAPI.getTeam(teamKey).then(team => { dispatch({ key: "team", value: team }); user.settings.defaultTeam = team.key(); - user.settings.useTeam = true; //when selecting a team now set to view as team by default + user.settings.useTeam = viewAsTeam; //when selecting a team now set to view as team by default userAPI.updateUser(user.id(), user.protobuf()); }); }; + const onClose = () => { + console.log("close") + setOpen(false) + if (hideKey) localStorage.setItem(hideKey, ""+doNotShow) + } + return ( - setOpen(false)}> + - Select a Team to View As + { title ? title : "Select a Team to View As"} - @@ -84,12 +102,40 @@ export default function TeamDialog(props: Props) { Viewing as: {team.name()} )} + {info && + + {info} + + } + {/* setViewAsTeam(!viewAsTeam)} /> */} + + setViewAsTeam(!viewAsTeam)} + /> + } + label="View as team" + /> + + {hideKey && + setDoNotShow(!doNotShow)} + /> + } + label="Do not show again" + /> + } ); } From cd96d783ed3d8dcbd0d5176863fda8b775b9c312 Mon Sep 17 00:00:00 2001 From: csawatzky Date: Fri, 18 Jul 2025 11:33:30 -0600 Subject: [PATCH 06/13] qick fix for the duplicate bin functionality on the dasboard, for admin users, so that it stop going to the bin page when you click on the duplicate button --- src/bin/BinsList.tsx | 8 ++++++-- src/pages/Bins.tsx | 2 +- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/src/bin/BinsList.tsx b/src/bin/BinsList.tsx index 6779258..af14b88 100644 --- a/src/bin/BinsList.tsx +++ b/src/bin/BinsList.tsx @@ -123,7 +123,9 @@ export default function BinsList(props: Props) { }} key={i} className={classes.gridListTile} - onClick={() => goToBin(i)}> + onClick={() => { + !duplicate && goToBin(i) + }}> goToBin(i)}> + onClick={() => { + !duplicate && goToBin(i) + }}> { binAPI.addBin(bin.settings, as).then(resp => { - loadBins(); + loadMoreBins(contentFilter); }); }; From 6f02f9e562dca7a1ec72ea5a82d533ce500cc105 Mon Sep 17 00:00:00 2001 From: Carter Date: Fri, 18 Jul 2025 13:15:55 -0600 Subject: [PATCH 07/13] setting auth redirect to the url dynamically instead of just local host --- src/providers/auth.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/providers/auth.tsx b/src/providers/auth.tsx index 2a8f6bb..f539ff3 100644 --- a/src/providers/auth.tsx +++ b/src/providers/auth.tsx @@ -38,7 +38,7 @@ export default function AuthWrapper(props: Props) { options.authorizationParams.login_hint = parsed.email.toString(); // prefill email } - options.authorizationParams.redirect_uri = "http://localhost:5173/signupCallback" + options.authorizationParams.redirect_uri = url + "/signupCallback" loginWithRedirect(options) From bc1cf27ad43d58cb38ae3c4aaebff895c55f938d Mon Sep 17 00:00:00 2001 From: Carter Date: Fri, 18 Jul 2025 14:31:48 -0600 Subject: [PATCH 08/13] debug output :) --- src/providers/auth.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/src/providers/auth.tsx b/src/providers/auth.tsx index f539ff3..9138061 100644 --- a/src/providers/auth.tsx +++ b/src/providers/auth.tsx @@ -39,6 +39,7 @@ export default function AuthWrapper(props: Props) { } options.authorizationParams.redirect_uri = url + "/signupCallback" + console.log(options.authorizationParams.redirect_uri) loginWithRedirect(options) From b9d2635f99bd2b9d488b86afb0be8270361c7455 Mon Sep 17 00:00:00 2001 From: Carter Date: Fri, 18 Jul 2025 15:05:49 -0600 Subject: [PATCH 09/13] using hostname instead of url --- src/providers/auth.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/providers/auth.tsx b/src/providers/auth.tsx index 9138061..4b49e5d 100644 --- a/src/providers/auth.tsx +++ b/src/providers/auth.tsx @@ -38,7 +38,7 @@ export default function AuthWrapper(props: Props) { options.authorizationParams.login_hint = parsed.email.toString(); // prefill email } - options.authorizationParams.redirect_uri = url + "/signupCallback" + options.authorizationParams.redirect_uri = url.hostname + "/signupCallback" console.log(options.authorizationParams.redirect_uri) loginWithRedirect(options) From 84ed63a7659d4de5c67f2e27c3c7e7b35aac067e Mon Sep 17 00:00:00 2001 From: Carter Date: Fri, 18 Jul 2025 15:28:41 -0600 Subject: [PATCH 10/13] added https:// to the hostname to make a valid uri --- src/providers/auth.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/providers/auth.tsx b/src/providers/auth.tsx index 4b49e5d..bc116ff 100644 --- a/src/providers/auth.tsx +++ b/src/providers/auth.tsx @@ -38,7 +38,7 @@ export default function AuthWrapper(props: Props) { options.authorizationParams.login_hint = parsed.email.toString(); // prefill email } - options.authorizationParams.redirect_uri = url.hostname + "/signupCallback" + options.authorizationParams.redirect_uri = "https://" + url.hostname + "/signupCallback" console.log(options.authorizationParams.redirect_uri) loginWithRedirect(options) From ed8ba4b3e4217c085a3b14aaa748c0ea0e0dd590 Mon Sep 17 00:00:00 2001 From: csawatzky Date: Mon, 21 Jul 2025 13:21:19 -0600 Subject: [PATCH 11/13] added dry beans --- package-lock.json | 4 ++-- package.json | 2 +- src/grain/GrainDescriber.ts | 31 +++++++++++++++++++++++++++++++ 3 files changed, 34 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index af61e6b..222fcf3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -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#beans", "query-string": "^9.2.1", "react": "^18.3.1", "react-beautiful-dnd": "^13.1.1", @@ -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#4523ffef42032eb4a923bd50d4cfcccbb1397dbd", + "resolved": "git+https://gitlab+deploy-token-50627:hv8mB4WkyvtjBpJKU1rN@gitlab.com/brandx/protobuf-ts.git#f09196cbc38911bab69fb35c16b1c8f691a4413f", "dependencies": { "protobufjs": "^6.8.8" } diff --git a/package.json b/package.json index fc7a1ac..5a27ce1 100644 --- a/package.json +++ b/package.json @@ -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#beans", "query-string": "^9.2.1", "react": "^18.3.1", "react-beautiful-dnd": "^13.1.1", diff --git a/src/grain/GrainDescriber.ts b/src/grain/GrainDescriber.ts index 00f9bb0..d112e90 100644 --- a/src/grain/GrainDescriber.ts +++ b/src/grain/GrainDescriber.ts @@ -475,6 +475,37 @@ export const GrainExtensions: Map = new Map([ weightConversionKg: 27.2158214642112, bushelsPerTonne: 36.744 } + ], + [ + pond.Grain.GRAIN_DRY_BEANS_RED, + { + name: "Dry Beans Red", + group: "Dry Beans", + equation: Equation.halsey, + a: 4.2669, + b: -0.013382, + c: 1.6933, + setTempC: defaultSetTemp, + targetMC: 15.0, + colour: "red", + weightConversionKg: 27.2155, + bushelsPerTonne: 36.744 + } + ],[ + pond.Grain.GRAIN_CUSTOM, + { + name: "Dry Beans Black", + group: "Dry Beans", + equation: Equation.halsey, + a: 5.2003, + b: -0.022685, + c: 1.9656, + setTempC: defaultSetTemp, + targetMC: 15.0, + colour: "grey", + weightConversionKg: 27.2155, + bushelsPerTonne: 36.744 + } ] ]); From 0266776e77ac25df51ce2a15ed3fae9e32748d8f Mon Sep 17 00:00:00 2001 From: csawatzky Date: Mon, 21 Jul 2025 13:21:30 -0600 Subject: [PATCH 12/13] missed change --- src/grain/GrainDescriber.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/grain/GrainDescriber.ts b/src/grain/GrainDescriber.ts index d112e90..ce394b3 100644 --- a/src/grain/GrainDescriber.ts +++ b/src/grain/GrainDescriber.ts @@ -492,7 +492,7 @@ export const GrainExtensions: Map = new Map([ bushelsPerTonne: 36.744 } ],[ - pond.Grain.GRAIN_CUSTOM, + pond.Grain.GRAIN_DRY_BEANS_BLACK, { name: "Dry Beans Black", group: "Dry Beans", From 2d5f21c9b5cf53584b936fe78fdcfae4fe25f4fe Mon Sep 17 00:00:00 2001 From: csawatzky Date: Tue, 22 Jul 2025 10:42:55 -0600 Subject: [PATCH 13/13] set the detect I2C feature to not be available yet as we have changed how it works on dev so will want to wait for those changes before it goes live --- src/models/Device.ts | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/src/models/Device.ts b/src/models/Device.ts index 6311981..e45f7aa 100644 --- a/src/models/Device.ts +++ b/src/models/Device.ts @@ -36,17 +36,17 @@ const featureVersions: Map = new Map([ ],[ "detectI2C", { - photon: "NA", - electron: "NA", - v2Wifi: "2.1.6", - v2Cell: "2.1.6", - v2WifiS3: "2.1.6", - v2CellS3: "2.1.6", - v2CellBlack: "2.1.6", - v2CellGreen: "2.1.6", - v2WifiBlue: "2.1.6", - v2CellBlue: "2.1.6", - v2EthBlue: "2.1.6" + photon: "N/A", + electron: "N/A", + v2Wifi: "N/A", + v2Cell: "N/A", + v2WifiS3: "N/A", + v2CellS3: "N/A", + v2CellBlack: "N/A", + v2CellGreen: "N/A", + v2WifiBlue: "N/A", + v2CellBlue: "N/A", + v2EthBlue: "N/A" } ] ]);