From 2de4b81ccfd11b5141c0fe3101cf1ec178c00233 Mon Sep 17 00:00:00 2001 From: Maurovic Cachia Date: Sat, 25 Apr 2026 03:27:30 +0200 Subject: [PATCH 01/16] Part 2 for eslint 9 change --- front/src/App.tsx | 9 ++-- .../components/HeaderMenu/BaseSwitcher.tsx | 10 ++-- .../components/QrReaderMultiBoxContainer.tsx | 12 ++--- front/src/components/Timeline/Timeline.tsx | 2 +- .../hooks/useLoadAndSetGlobalPreferences.ts | 51 +++++++++++++------ front/src/tests/test-utils.tsx | 10 ++-- .../custom-graphs/BarChartCenterAxis.tsx | 10 ++-- shared-components/tests/testUtils.tsx | 9 ++-- shared-front/src/App.tsx | 15 ++++-- 9 files changed, 81 insertions(+), 47 deletions(-) diff --git a/front/src/App.tsx b/front/src/App.tsx index 1c89cd1bba..ed668279e7 100644 --- a/front/src/App.tsx +++ b/front/src/App.tsx @@ -105,11 +105,10 @@ function App() { const hasExecutedInitialFetchOfBoxes = useRef(false); // store previous location to return to if you are not authorized - useEffect(() => { - const regex = /^\/bases\/\d+\//; - // only store previous location if a base is selected - if (regex.test(location.pathname)) setPrevLocation(location.pathname); - }, [location]); + // only store previous location if a base is selected + if (/^\/bases\/\d+\//.test(location.pathname) && location.pathname !== prevLocation) { + setPrevLocation(location.pathname); + } if (error) { return ; diff --git a/front/src/components/HeaderMenu/BaseSwitcher.tsx b/front/src/components/HeaderMenu/BaseSwitcher.tsx index aec440a141..c3e0f2ca5d 100644 --- a/front/src/components/HeaderMenu/BaseSwitcher.tsx +++ b/front/src/components/HeaderMenu/BaseSwitcher.tsx @@ -11,7 +11,7 @@ import { RadioGroup, Stack, } from "@chakra-ui/react"; -import { useEffect, useState } from "react"; +import { useState } from "react"; import { useNavigate, useLocation, useParams } from "react-router-dom"; import { useAtomValue } from "jotai"; import { availableBasesAtom, selectedBaseIdAtom } from "stores/globalPreferenceStore"; @@ -25,11 +25,13 @@ function BaseSwitcher({ isOpen, onClose }: { isOpen: boolean; onClose: () => voi const currentOrganisationBases = availableBases.filter((base) => base.id !== baseId); const firstAvailableBaseId = currentOrganisationBases.find((base) => base)?.id; const [value, setValue] = useState(firstAvailableBaseId); + const [prevFirstAvailableBaseId, setPrevFirstAvailableBaseId] = useState(firstAvailableBaseId); - // Need to set this as soon as we have this value available to set the default radio selection. - useEffect(() => { + // Need to reset the default radio selection whenever the available bases change. + if (firstAvailableBaseId !== prevFirstAvailableBaseId) { + setPrevFirstAvailableBaseId(firstAvailableBaseId); setValue(firstAvailableBaseId); - }, [firstAvailableBaseId, baseId]); + } const switchBase = () => { const currentPath = pathname.split(`/bases/${urlBaseId}`)[1]; diff --git a/front/src/components/QrReader/components/QrReaderMultiBoxContainer.tsx b/front/src/components/QrReader/components/QrReaderMultiBoxContainer.tsx index 096f985561..0b9245296d 100644 --- a/front/src/components/QrReader/components/QrReaderMultiBoxContainer.tsx +++ b/front/src/components/QrReader/components/QrReaderMultiBoxContainer.tsx @@ -1,4 +1,4 @@ -import { useCallback, useEffect, useMemo, useState } from "react"; +import { useCallback, useMemo, useState } from "react"; import { useQuery } from "@apollo/client"; import { useAtomValue } from "jotai"; import { GET_SCANNED_BOXES } from "queries/local-only"; @@ -168,11 +168,11 @@ function QrReaderMultiBoxContainer() { }, [baseId, hasShipmentPermission, optionsQueryResult.data]); // Assign To Shipment is default MultiBoxAction if there are shipments - useEffect(() => { - if (shipmentOptions.length > 0) { - setMultiBoxAction(IMultiBoxAction.assignShipment); - } - }, [shipmentOptions]); + const [prevShipmentOptionsLength, setPrevShipmentOptionsLength] = useState(0); + if (shipmentOptions.length > 0 && prevShipmentOptionsLength === 0) { + setPrevShipmentOptionsLength(shipmentOptions.length); + setMultiBoxAction(IMultiBoxAction.assignShipment); + } const notInStockBoxes = useMemo( () => scannedBoxesQueryResult.data?.scannedBoxes.filter((box) => box.state !== "InStock") ?? [], diff --git a/front/src/components/Timeline/Timeline.tsx b/front/src/components/Timeline/Timeline.tsx index a3c497673c..981dc53d28 100644 --- a/front/src/components/Timeline/Timeline.tsx +++ b/front/src/components/Timeline/Timeline.tsx @@ -5,7 +5,7 @@ import { User } from "../../../../graphql/types"; export interface ITimelineEntry { action: string; createdOn: Date; - createdBy: User; + createdBy: Partial | null | undefined; } export interface IGroupedRecordEntry { diff --git a/front/src/hooks/useLoadAndSetGlobalPreferences.ts b/front/src/hooks/useLoadAndSetGlobalPreferences.ts index 3d442d75e6..3ea631d09e 100644 --- a/front/src/hooks/useLoadAndSetGlobalPreferences.ts +++ b/front/src/hooks/useLoadAndSetGlobalPreferences.ts @@ -1,4 +1,4 @@ -import { useEffect, useState } from "react"; +import { useEffect, useMemo } from "react"; import { useAtom, useAtomValue, useSetAtom } from "jotai"; import { useAuth0 } from "@auth0/auth0-react"; import { useLazyQuery } from "@apollo/client"; @@ -17,7 +17,6 @@ export const useLoadAndSetGlobalPreferences = () => { const { user } = useAuth0(); const authorize = useAuthorization(); const location = useLocation(); - const [error, setError] = useState(); const setOrganisation = useSetAtom(organisationAtom); const [selectedBase, setSelectedBase] = useAtom(selectedBaseAtom); const [availableBases, setAvailableBases] = useAtom(availableBasesAtom); @@ -32,15 +31,25 @@ export const useLoadAndSetGlobalPreferences = () => { authorize({ requiredAbps: ["create_shareable_link"] }).toString(), ); - // validate if base Ids are set in auth0 id token - if (!user || (!isGod && !user[JWT_AVAILABLE_BASES]?.length)) - setError("You do not have access to any bases."); - const [ runOrganisationAndBasesQuery, { loading: isOrganisationAndBasesQueryLoading, data: organisationAndBaseData }, ] = useLazyQuery(ORGANISATION_AND_BASES_QUERY); + const error = useMemo(() => { + if (!user || (!isGod && !user[JWT_AVAILABLE_BASES]?.length)) { + return "You do not have access to any bases."; + } else { + const urlBaseIdInput = location.pathname.match(/\/bases\/(\d+)(\/)?/); + const urlBaseId = urlBaseIdInput?.length && urlBaseIdInput[1]; + if (urlBaseId && !isGod && !user[JWT_AVAILABLE_BASES].map(String).includes(urlBaseId)) { + return "The requested base is not available to you."; + } + } + + return undefined; + }, [isGod, location.pathname, user]); + // run query only if // - the access token is in the request header from the apollo client and // - the base Name is not set @@ -74,8 +83,6 @@ export const useLoadAndSetGlobalPreferences = () => { // only overwrite the selected base ID if the id is different from the existing one. setSelectedBase({ id: urlBaseId }); } - } else { - setError("The requested base is not available to you."); } } } @@ -110,14 +117,8 @@ export const useLoadAndSetGlobalPreferences = () => { setSelectedBase({ id: matchingBase.id, name: matchingBase.name }); // set organisation for selected base setOrganisation(matchingBase.organisation); - } else { - // this error is set if the requested base is not part of the available bases - setError("The requested base is not available to you."); } } - } else { - // this error is set if the bases query returned an empty array for bases - setError("There are no available bases."); } } }, [ @@ -129,9 +130,29 @@ export const useLoadAndSetGlobalPreferences = () => { setSelectedBase, ]); + const finalError = useMemo(() => { + const basesWithOrgData = organisationAndBaseData?.bases; + const bases = basesWithOrgData?.map((base) => ({ + id: base.id, + name: base.name, + })); + + if (!bases || bases.length <= 0) { + return "There are no available bases."; + } else if (selectedBase?.id) { + const matchingBase = basesWithOrgData?.find((base) => base.id === selectedBase.id); + + if (!matchingBase) { + return "The requested base is not available to you."; + } + } + + return error; + }, [error, organisationAndBaseData?.bases, selectedBase?.id]); + const isLoading = !selectedBase?.name || isOrganisationAndBasesQueryLoading; const isInitialized = selectedBaseId !== "0"; - return { isLoading, error, isInitialized }; + return { isLoading, error: finalError, isInitialized }; }; diff --git a/front/src/tests/test-utils.tsx b/front/src/tests/test-utils.tsx index 997830ae35..4d81f25831 100644 --- a/front/src/tests/test-utils.tsx +++ b/front/src/tests/test-utils.tsx @@ -1,4 +1,3 @@ -/* eslint-disable import/export */ // TODO: Investigate possible render function overload. import React, { ReactNode } from "react"; @@ -177,5 +176,10 @@ function StorybookApolloProvider({ children }: { children: ReactNode }) { return {children}; } -export * from "@testing-library/react"; -export { render, StorybookApolloProvider }; +import * as reactLib from "@testing-library/react"; + +export const finalExport = { + ...reactLib, + render, + StorybookApolloProvider, +}; diff --git a/shared-components/statviz/components/custom-graphs/BarChartCenterAxis.tsx b/shared-components/statviz/components/custom-graphs/BarChartCenterAxis.tsx index d98fd90213..626aebeeb2 100644 --- a/shared-components/statviz/components/custom-graphs/BarChartCenterAxis.tsx +++ b/shared-components/statviz/components/custom-graphs/BarChartCenterAxis.tsx @@ -74,7 +74,7 @@ export default function BarChartCenterAxis(chart: IBarChartCenterAxis) { tooltipOpen: false, }); - let tooltipTimeout: number; + const tooltipTimeoutRef = useRef(undefined); if (!fields.settings) { fields.settings = {}; @@ -174,10 +174,10 @@ export default function BarChartCenterAxis(chart: IBarChartCenterAxis) { x={x} y={Math.round(y - barHight / 2)} onMouseLeave={() => { - tooltipTimeout = window.setTimeout(() => hideTooltip(), 300); + tooltipTimeoutRef.current = window.setTimeout(() => hideTooltip(), 300); }} onMouseMove={(event) => { - if (tooltipTimeout) clearTimeout(tooltipTimeout); + if (tooltipTimeoutRef.current) clearTimeout(tooltipTimeoutRef.current); const localY = localPoint(event)?.y ?? 0; showTooltip({ tooltipData: tooltip, @@ -205,10 +205,10 @@ export default function BarChartCenterAxis(chart: IBarChartCenterAxis) { y={Math.round(y - barHight / 2)} fill={fields.colorBarRight} onMouseLeave={() => { - tooltipTimeout = window.setTimeout(() => hideTooltip(), 300); + tooltipTimeoutRef.current = window.setTimeout(() => hideTooltip(), 300); }} onMouseMove={(event) => { - if (tooltipTimeout) clearTimeout(tooltipTimeout); + if (tooltipTimeoutRef.current) clearTimeout(tooltipTimeoutRef.current); const localY = localPoint(event)?.y ?? 0; showTooltip({ diff --git a/shared-components/tests/testUtils.tsx b/shared-components/tests/testUtils.tsx index 2e46bb25a1..b80fe208ee 100644 --- a/shared-components/tests/testUtils.tsx +++ b/shared-components/tests/testUtils.tsx @@ -1,4 +1,3 @@ -/* eslint-disable import/export */ // TODO: Investigate possible render function overload. import React from "react"; @@ -91,5 +90,9 @@ function render( }); } -export * from "@testing-library/react"; -export { render }; +import * as reactLib from "@testing-library/react"; + +export const finalExport = { + ...reactLib, + render, +}; diff --git a/shared-front/src/App.tsx b/shared-front/src/App.tsx index d1a67482f0..035c67e6ce 100644 --- a/shared-front/src/App.tsx +++ b/shared-front/src/App.tsx @@ -106,6 +106,16 @@ function App() { } }, [data?.resolveLink?.data]); + // Redirect to full URL with view param once link data has loaded + useEffect(() => { + if (data && !view) { + const urlParams = data?.resolveLink?.urlParameters ?? "nofilters=true"; + const hasBoiParam = urlParams.includes("boi="); + const boiParam = hasBoiParam ? "" : `&boi=${boxesOrItemsFilterValues[0].urlId}`; + window.location.search = `view=${data?.resolveLink?.view.toLowerCase()}&${urlParams}${boiParam}&code=${code}`; + } + }, [data, view, code]); + if (error) { return {matchErrorMessage(error.message)}; } @@ -133,11 +143,6 @@ function App() { // Prepend Search Params with fetched link data params and reload the page while displaying a skeleton loader. if (!view) { - const urlParams = data?.resolveLink?.urlParameters ?? "nofilters=true"; - const hasBoiParam = urlParams.includes("boi="); - const boiParam = hasBoiParam ? "" : `&boi=${boxesOrItemsFilterValues[0].urlId}`; - location.search = `view=${data?.resolveLink?.view.toLowerCase()}&${urlParams}${boiParam}&code=${code}`; - return ( <> From 6bbc3aaf5b547e2df7a00af41b373ccd98bf10c2 Mon Sep 17 00:00:00 2001 From: Maurovic Cachia Date: Sat, 25 Apr 2026 19:40:17 +0200 Subject: [PATCH 02/16] Box Create Updates and Fixes to initial load hook --- .../hooks/useLoadAndSetGlobalPreferences.ts | 44 +++++++++++-------- front/src/views/BoxCreate/BoxCreateView.tsx | 11 ++--- .../views/BoxCreate/components/BoxCreate.tsx | 35 ++++++--------- 3 files changed, 43 insertions(+), 47 deletions(-) diff --git a/front/src/hooks/useLoadAndSetGlobalPreferences.ts b/front/src/hooks/useLoadAndSetGlobalPreferences.ts index 3ea631d09e..9a11abc86c 100644 --- a/front/src/hooks/useLoadAndSetGlobalPreferences.ts +++ b/front/src/hooks/useLoadAndSetGlobalPreferences.ts @@ -75,7 +75,8 @@ export const useLoadAndSetGlobalPreferences = () => { // validate that // - the selected base ID is part of the available base IDs from Auth0 or // - that the user is a Boxtribute God - if (urlBaseId) { + // - do not overwrite if the base already has a name (set by the query result) + if (urlBaseId && !selectedBase?.name) { if (isGod) { setSelectedBase({ id: urlBaseId }); } else if (user[JWT_AVAILABLE_BASES].map(String).includes(urlBaseId)) { @@ -89,12 +90,13 @@ export const useLoadAndSetGlobalPreferences = () => { }, [ availableBases.length, error, + isGod, location.pathname, + selectedBase?.name, + selectedBaseId, setAvailableBases, setSelectedBase, user, - isGod, - selectedBaseId, ]); // handle additional base information being returned from the query @@ -131,24 +133,28 @@ export const useLoadAndSetGlobalPreferences = () => { ]); const finalError = useMemo(() => { - const basesWithOrgData = organisationAndBaseData?.bases; - const bases = basesWithOrgData?.map((base) => ({ - id: base.id, - name: base.name, - })); - - if (!bases || bases.length <= 0) { - return "There are no available bases."; - } else if (selectedBase?.id) { - const matchingBase = basesWithOrgData?.find((base) => base.id === selectedBase.id); - - if (!matchingBase) { - return "The requested base is not available to you."; + if (organisationAndBaseData) { + const basesWithOrgData = organisationAndBaseData.bases; + const bases = basesWithOrgData?.map((base) => ({ + id: base.id, + name: base.name, + })); + + if (!bases || bases.length <= 0) { + return "There are no available bases."; + } else if (selectedBase?.id) { + const matchingBase = basesWithOrgData?.find((base) => base.id === selectedBase.id); + + if (!matchingBase) { + return "The requested base is not available to you."; + } } - } - return error; - }, [error, organisationAndBaseData?.bases, selectedBase?.id]); + return error; + } else { + return ""; + } + }, [error, organisationAndBaseData, selectedBase?.id]); const isLoading = !selectedBase?.name || isOrganisationAndBasesQueryLoading; diff --git a/front/src/views/BoxCreate/BoxCreateView.tsx b/front/src/views/BoxCreate/BoxCreateView.tsx index 95c077e31c..ae91303b2c 100644 --- a/front/src/views/BoxCreate/BoxCreateView.tsx +++ b/front/src/views/BoxCreate/BoxCreateView.tsx @@ -160,13 +160,10 @@ function BoxCreateView() { })) .sort((a, b) => Number(a?.seq) - Number(b?.seq)); - useEffect(() => { - // Disable form submission if no warehouse location or products associated with base, but only if the query response is available - if (allLocations !== undefined && allLocations.length < 1) setNoLocation(true); - else if (noLocation) setNoLocation(false); - if (allProducts !== undefined && allProducts.length < 1) setNoProducts(true); - else if (noProducts) setNoProducts(false); - }, [allLocations, allProducts, noLocation, noProducts]); + if (allLocations !== undefined && allLocations.length < 1 && !noLocation) setNoLocation(true); + else if (noLocation) setNoLocation(false); + if (allProducts !== undefined && allProducts.length < 1 && !noProducts) setNoProducts(true); + else if (noProducts) setNoProducts(false); // check data for form useEffect(() => { diff --git a/front/src/views/BoxCreate/components/BoxCreate.tsx b/front/src/views/BoxCreate/components/BoxCreate.tsx index 1462af5e6e..33fa39d4ce 100644 --- a/front/src/views/BoxCreate/components/BoxCreate.tsx +++ b/front/src/views/BoxCreate/components/BoxCreate.tsx @@ -1,7 +1,7 @@ import { Box, Button, FormLabel, Heading, Input, List, ListItem, Stack } from "@chakra-ui/react"; -import { useEffect, useState } from "react"; -import { SubmitHandler, useForm } from "react-hook-form"; +import { useEffect } from "react"; +import { SubmitHandler, useForm, useWatch } from "react-hook-form"; import { useNavigate, useParams } from "react-router-dom"; import { useAtomValue } from "jotai"; @@ -130,42 +130,35 @@ export function BoxCreate({ control, register, resetField, - watch, setValue, formState: { errors, isSubmitting }, } = useForm({ resolver: zodResolver(CreateBoxFormDataSchema), }); - const [sizesOptionsForCurrentProduct, setSizesOptionsForCurrentProduct] = useState< - IDropdownOption[] - >([]); + const productId = useWatch({ control, name: "productId" }); - const productId = watch("productId"); + const productAndSizeDataForCurrentProduct = productId + ? productAndSizesData.find((p) => p.id === productId.value) + : undefined; + const sizesOptionsForCurrentProduct: IDropdownOption[] = + productAndSizeDataForCurrentProduct?.sizeRange?.sizes?.map((s) => ({ + label: s.label, + value: s.id, + })) ?? []; useEffect(() => { if (productId != null) { - const productAndSizeDataForCurrentProduct = productAndSizesData.find( - (p) => p.id === productId.value, - ); - setSizesOptionsForCurrentProduct( - () => - productAndSizeDataForCurrentProduct?.sizeRange?.sizes?.map((s) => ({ - label: s.label, - value: s.id, - })) || [], - ); - resetField("sizeId"); // Put a default value for sizeId when there's only one option if (productAndSizeDataForCurrentProduct?.sizeRange?.sizes?.length === 1) { setValue("sizeId", { - label: productAndSizeDataForCurrentProduct?.sizeRange?.sizes[0].label, - value: productAndSizeDataForCurrentProduct?.sizeRange?.sizes[0].id, + label: productAndSizeDataForCurrentProduct.sizeRange.sizes[0].label, + value: productAndSizeDataForCurrentProduct.sizeRange.sizes[0].id, }); } } - }, [productId, productAndSizesData, resetField, setValue]); + }, [productId, productAndSizeDataForCurrentProduct, resetField, setValue]); return ( From 659f096b2e575b72ffc25dcb987eda48aa0ac9ac Mon Sep 17 00:00:00 2001 From: Maurovic Cachia Date: Sat, 25 Apr 2026 19:49:12 +0200 Subject: [PATCH 03/16] Hotfix --- front/src/tests/test-utils.tsx | 10 +++------- shared-components/tests/testUtils.tsx | 9 +++------ 2 files changed, 6 insertions(+), 13 deletions(-) diff --git a/front/src/tests/test-utils.tsx b/front/src/tests/test-utils.tsx index 4d81f25831..997830ae35 100644 --- a/front/src/tests/test-utils.tsx +++ b/front/src/tests/test-utils.tsx @@ -1,3 +1,4 @@ +/* eslint-disable import/export */ // TODO: Investigate possible render function overload. import React, { ReactNode } from "react"; @@ -176,10 +177,5 @@ function StorybookApolloProvider({ children }: { children: ReactNode }) { return {children}; } -import * as reactLib from "@testing-library/react"; - -export const finalExport = { - ...reactLib, - render, - StorybookApolloProvider, -}; +export * from "@testing-library/react"; +export { render, StorybookApolloProvider }; diff --git a/shared-components/tests/testUtils.tsx b/shared-components/tests/testUtils.tsx index b80fe208ee..2e46bb25a1 100644 --- a/shared-components/tests/testUtils.tsx +++ b/shared-components/tests/testUtils.tsx @@ -1,3 +1,4 @@ +/* eslint-disable import/export */ // TODO: Investigate possible render function overload. import React from "react"; @@ -90,9 +91,5 @@ function render( }); } -import * as reactLib from "@testing-library/react"; - -export const finalExport = { - ...reactLib, - render, -}; +export * from "@testing-library/react"; +export { render }; From 0602bdb837d949fdac24c262b51b0e2f5a60bb63 Mon Sep 17 00:00:00 2001 From: Maurovic Cachia Date: Sat, 25 Apr 2026 20:33:56 +0200 Subject: [PATCH 04/16] Fixes --- .../components/HeaderMenu/BaseSwitcher.tsx | 15 +- .../hooks/useLoadAndSetGlobalPreferences.ts | 138 +++++++++++------- 2 files changed, 90 insertions(+), 63 deletions(-) diff --git a/front/src/components/HeaderMenu/BaseSwitcher.tsx b/front/src/components/HeaderMenu/BaseSwitcher.tsx index c3e0f2ca5d..85783d02f4 100644 --- a/front/src/components/HeaderMenu/BaseSwitcher.tsx +++ b/front/src/components/HeaderMenu/BaseSwitcher.tsx @@ -23,21 +23,20 @@ function BaseSwitcher({ isOpen, onClose }: { isOpen: boolean; onClose: () => voi const baseId = useAtomValue(selectedBaseIdAtom); const availableBases = useAtomValue(availableBasesAtom); const currentOrganisationBases = availableBases.filter((base) => base.id !== baseId); - const firstAvailableBaseId = currentOrganisationBases.find((base) => base)?.id; + const firstAvailableBaseId = currentOrganisationBases[0]?.id; const [value, setValue] = useState(firstAvailableBaseId); - const [prevFirstAvailableBaseId, setPrevFirstAvailableBaseId] = useState(firstAvailableBaseId); - - // Need to reset the default radio selection whenever the available bases change. - if (firstAvailableBaseId !== prevFirstAvailableBaseId) { - setPrevFirstAvailableBaseId(firstAvailableBaseId); - setValue(firstAvailableBaseId); - } const switchBase = () => { const currentPath = pathname.split(`/bases/${urlBaseId}`)[1]; navigate(`/bases/${value}${currentPath}`); onClose(); + + // Need to reset the default radio selection whenever the available bases change. + + const currentOrganisationBases = availableBases.filter((base) => base.id !== value); + const newFirstAvailableBaseId = currentOrganisationBases[0]?.id; + setValue(newFirstAvailableBaseId); }; return ( diff --git a/front/src/hooks/useLoadAndSetGlobalPreferences.ts b/front/src/hooks/useLoadAndSetGlobalPreferences.ts index 9a11abc86c..9cf2d61bee 100644 --- a/front/src/hooks/useLoadAndSetGlobalPreferences.ts +++ b/front/src/hooks/useLoadAndSetGlobalPreferences.ts @@ -61,28 +61,54 @@ export const useLoadAndSetGlobalPreferences = () => { // setting auth atoms initially from auth0 useEffect(() => { - if (!error && user && (user[JWT_AVAILABLE_BASES] || isGod)) { - // set available bases from auth0 id token only if they are not set yet. - // Otherwise, it would overwrite the names queried from the BE. - if (!availableBases.length && !isGod) { - setAvailableBases(user[JWT_AVAILABLE_BASES].map((id: string) => ({ id }))); - } - - // extract the current/selected base ID from the URL, default to "0" until a valid base ID is set - const urlBaseIdInput = location.pathname.match(/\/bases\/(\d+)(\/)?/); - const urlBaseId = urlBaseIdInput?.length && urlBaseIdInput[1]; - - // validate that - // - the selected base ID is part of the available base IDs from Auth0 or - // - that the user is a Boxtribute God - // - do not overwrite if the base already has a name (set by the query result) - if (urlBaseId && !selectedBase?.name) { - if (isGod) { - setSelectedBase({ id: urlBaseId }); - } else if (user[JWT_AVAILABLE_BASES].map(String).includes(urlBaseId)) { - if (selectedBaseId !== urlBaseId) { - // only overwrite the selected base ID if the id is different from the existing one. - setSelectedBase({ id: urlBaseId }); + if (!isOrganisationAndBasesQueryLoading && organisationAndBaseData !== undefined) { + if (!error && user && (user[JWT_AVAILABLE_BASES] || isGod)) { + const basesWithOrgData = organisationAndBaseData.bases; + const bases = basesWithOrgData.map((base) => ({ + id: base.id, + name: base.name, + })); + if (bases.length > 0) { + setAvailableBases(bases); + // set available bases from auth0 id token only if they are not set yet. + // Otherwise, it would overwrite the names queried from the BE. + // if (!availableBases.length && !isGod) { + // setAvailableBases(user[JWT_AVAILABLE_BASES].map((id: string) => ({ id }))); + // } + + // extract the current/selected base ID from the URL, default to "0" until a valid base ID is set + const urlBaseIdInput = location.pathname.match(/\/bases\/(\d+)(\/)?/); + const urlBaseId = urlBaseIdInput?.length && urlBaseIdInput[1]; + + // validate that + // - the selected base ID is part of the available base IDs from Auth0 or + // - that the user is a Boxtribute God + if (urlBaseId) { + if (isGod) { + // setSelectedBase({ id: urlBaseId }); + const matchingBase = basesWithOrgData.find((base) => base.id === urlBaseId); + + if (matchingBase) { + // set selected base + setSelectedBase({ id: matchingBase.id, name: matchingBase.name }); + // set organisation for selected base + setOrganisation(matchingBase.organisation); + } + } else if (user[JWT_AVAILABLE_BASES].map(String).includes(urlBaseId)) { + if (selectedBaseId !== urlBaseId) { + // only overwrite the selected base ID if the id is different from the existing one. + // setSelectedBase({ id: urlBaseId }); + + const matchingBase = basesWithOrgData.find((base) => base.id === urlBaseId); + + if (matchingBase) { + // set selected base + setSelectedBase({ id: matchingBase.id, name: matchingBase.name }); + // set organisation for selected base + setOrganisation(matchingBase.organisation); + } + } + } } } } @@ -91,46 +117,48 @@ export const useLoadAndSetGlobalPreferences = () => { availableBases.length, error, isGod, + isOrganisationAndBasesQueryLoading, location.pathname, - selectedBase?.name, + organisationAndBaseData, selectedBaseId, setAvailableBases, + setOrganisation, setSelectedBase, user, ]); - // handle additional base information being returned from the query - useEffect(() => { - if (!isOrganisationAndBasesQueryLoading && organisationAndBaseData !== undefined) { - const basesWithOrgData = organisationAndBaseData.bases; - const bases = basesWithOrgData.map((base) => ({ - id: base.id, - name: base.name, - })); - - if (bases.length > 0) { - setAvailableBases(bases); - - if (selectedBase?.id) { - const matchingBase = basesWithOrgData.find((base) => base.id === selectedBase.id); - - if (matchingBase) { - // set selected base - setSelectedBase({ id: matchingBase.id, name: matchingBase.name }); - // set organisation for selected base - setOrganisation(matchingBase.organisation); - } - } - } - } - }, [ - isOrganisationAndBasesQueryLoading, - organisationAndBaseData, - selectedBase?.id, - setAvailableBases, - setOrganisation, - setSelectedBase, - ]); + // // handle additional base information being returned from the query + // useEffect(() => { + // if (!isOrganisationAndBasesQueryLoading && organisationAndBaseData !== undefined) { + // const basesWithOrgData = organisationAndBaseData.bases; + // const bases = basesWithOrgData.map((base) => ({ + // id: base.id, + // name: base.name, + // })); + + // if (bases.length > 0) { + // setAvailableBases(bases); + + // if (selectedBase?.id) { + // const matchingBase = basesWithOrgData.find((base) => base.id === selectedBase.id); + + // if (matchingBase) { + // // set selected base + // setSelectedBase({ id: matchingBase.id, name: matchingBase.name }); + // // set organisation for selected base + // setOrganisation(matchingBase.organisation); + // } + // } + // } + // } + // }, [ + // isOrganisationAndBasesQueryLoading, + // organisationAndBaseData, + // selectedBase?.id, + // setAvailableBases, + // setOrganisation, + // setSelectedBase, + // ]); const finalError = useMemo(() => { if (organisationAndBaseData) { From 7c113437c9139d53c4568b0cad6a8f3b56dff471 Mon Sep 17 00:00:00 2001 From: Maurovic Cachia Date: Thu, 30 Apr 2026 01:11:53 +0200 Subject: [PATCH 05/16] Fix for BoxCreate --- .../views/BoxCreate/components/BoxCreate.tsx | 54 +++++++++++-------- pnpm-lock.yaml | 24 ++++----- 2 files changed, 45 insertions(+), 33 deletions(-) diff --git a/front/src/views/BoxCreate/components/BoxCreate.tsx b/front/src/views/BoxCreate/components/BoxCreate.tsx index 33fa39d4ce..3d49f9e0d5 100644 --- a/front/src/views/BoxCreate/components/BoxCreate.tsx +++ b/front/src/views/BoxCreate/components/BoxCreate.tsx @@ -1,7 +1,7 @@ import { Box, Button, FormLabel, Heading, Input, List, ListItem, Stack } from "@chakra-ui/react"; -import { useEffect } from "react"; -import { SubmitHandler, useForm, useWatch } from "react-hook-form"; +import { useEffect, useMemo } from "react"; +import { SubmitHandler, useForm } from "react-hook-form"; import { useNavigate, useParams } from "react-router-dom"; import { useAtomValue } from "jotai"; @@ -130,35 +130,47 @@ export function BoxCreate({ control, register, resetField, + watch, setValue, formState: { errors, isSubmitting }, } = useForm({ resolver: zodResolver(CreateBoxFormDataSchema), }); - const productId = useWatch({ control, name: "productId" }); + const productId = watch("productId"); - const productAndSizeDataForCurrentProduct = productId - ? productAndSizesData.find((p) => p.id === productId.value) - : undefined; - const sizesOptionsForCurrentProduct: IDropdownOption[] = - productAndSizeDataForCurrentProduct?.sizeRange?.sizes?.map((s) => ({ - label: s.label, - value: s.id, - })) ?? []; + const productAndSizeDataForCurrentProduct = useMemo(() => { + if (productId != null) { + return productAndSizesData.find((p) => p.id === productId.value); + } else { + return; + } + }, [productAndSizesData, productId]); + + const sizesOptionsForCurrentProduct = useMemo(() => { + return ( + productAndSizeDataForCurrentProduct?.sizeRange?.sizes?.map((s) => ({ + label: s.label, + value: s.id, + })) || [] + ); + }, [productAndSizeDataForCurrentProduct?.sizeRange?.sizes]); useEffect(() => { - if (productId != null) { - resetField("sizeId"); - // Put a default value for sizeId when there's only one option - if (productAndSizeDataForCurrentProduct?.sizeRange?.sizes?.length === 1) { - setValue("sizeId", { - label: productAndSizeDataForCurrentProduct.sizeRange.sizes[0].label, - value: productAndSizeDataForCurrentProduct.sizeRange.sizes[0].id, - }); - } + resetField("sizeId"); + // Put a default value for sizeId when there's only one option + if (productAndSizeDataForCurrentProduct?.sizeRange?.sizes?.length === 1) { + setValue("sizeId", { + label: productAndSizeDataForCurrentProduct?.sizeRange?.sizes[0].label, + value: productAndSizeDataForCurrentProduct?.sizeRange?.sizes[0].id, + }); } - }, [productId, productAndSizeDataForCurrentProduct, resetField, setValue]); + }, [ + productAndSizeDataForCurrentProduct?.sizeRange?.sizes, + productAndSizesData, + resetField, + setValue, + ]); return ( diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index c95e5adb72..63c38fc456 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -200,7 +200,7 @@ importers: version: 6.1.1(typescript@5.9.3)(vite@6.4.2(@types/node@25.6.0)(yaml@2.8.2)) vitest: specifier: ^3.2.4 - version: 3.2.4(@types/node@25.6.0)(@vitest/ui@3.2.4)(jsdom@26.1.0)(msw@2.13.4(@types/node@25.6.0)(typescript@5.9.3))(yaml@2.8.2) + version: 3.2.4(@types/node@25.6.0)(@vitest/ui@3.2.4)(jsdom@26.1.0)(msw@2.14.2(@types/node@25.6.0)(typescript@5.9.3))(yaml@2.8.2) front: dependencies: @@ -242,8 +242,8 @@ importers: specifier: ^7.7.20 version: 7.7.20 msw: - specifier: ^2.13.4 - version: 2.13.4(@types/node@25.6.0)(typescript@5.9.3) + specifier: ^2.13.6 + version: 2.14.2(@types/node@25.6.0)(typescript@5.9.3) mutationobserver-shim: specifier: ^0.3.7 version: 0.3.7 @@ -3703,8 +3703,8 @@ packages: ms@2.1.3: resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} - msw@2.13.4: - resolution: {integrity: sha512-fPlKBeFe+8rpcyR3umUmmHuNwu6gc6T3STvkgEa9WDX/HEgal9wDeflpCUAIRtmvaLZM2igfI5y1bZ9G5J26KA==} + msw@2.14.2: + resolution: {integrity: sha512-D2bTe0tpuf9nw4DA39wFaqUD/hRPKj0DKpo2lAqu+A47Ifg4+h0hbfn6QxVOsiUY2uhgEN6TTpGSHDsc+ysYNg==} engines: {node: '>=18'} hasBin: true peerDependencies: @@ -7255,7 +7255,7 @@ snapshots: std-env: 3.9.0 test-exclude: 7.0.1 tinyrainbow: 2.0.0 - vitest: 3.2.4(@types/node@25.6.0)(@vitest/ui@3.2.4)(jsdom@26.1.0)(msw@2.13.4(@types/node@25.6.0)(typescript@5.9.3))(yaml@2.8.2) + vitest: 3.2.4(@types/node@25.6.0)(@vitest/ui@3.2.4)(jsdom@26.1.0)(msw@2.14.2(@types/node@25.6.0)(typescript@5.9.3))(yaml@2.8.2) transitivePeerDependencies: - supports-color @@ -7267,13 +7267,13 @@ snapshots: chai: 5.2.0 tinyrainbow: 2.0.0 - '@vitest/mocker@3.2.4(msw@2.13.4(@types/node@25.6.0)(typescript@5.9.3))(vite@6.4.2(@types/node@25.6.0)(yaml@2.8.2))': + '@vitest/mocker@3.2.4(msw@2.14.2(@types/node@25.6.0)(typescript@5.9.3))(vite@6.4.2(@types/node@25.6.0)(yaml@2.8.2))': dependencies: '@vitest/spy': 3.2.4 estree-walker: 3.0.3 magic-string: 0.30.17 optionalDependencies: - msw: 2.13.4(@types/node@25.6.0)(typescript@5.9.3) + msw: 2.14.2(@types/node@25.6.0)(typescript@5.9.3) vite: 6.4.2(@types/node@25.6.0)(yaml@2.8.2) '@vitest/pretty-format@3.2.4': @@ -7305,7 +7305,7 @@ snapshots: sirv: 3.0.1 tinyglobby: 0.2.14 tinyrainbow: 2.0.0 - vitest: 3.2.4(@types/node@25.6.0)(@vitest/ui@3.2.4)(jsdom@26.1.0)(msw@2.13.4(@types/node@25.6.0)(typescript@5.9.3))(yaml@2.8.2) + vitest: 3.2.4(@types/node@25.6.0)(@vitest/ui@3.2.4)(jsdom@26.1.0)(msw@2.14.2(@types/node@25.6.0)(typescript@5.9.3))(yaml@2.8.2) '@vitest/utils@3.2.4': dependencies: @@ -9166,7 +9166,7 @@ snapshots: ms@2.1.3: {} - msw@2.13.4(@types/node@25.6.0)(typescript@5.9.3): + msw@2.14.2(@types/node@25.6.0)(typescript@5.9.3): dependencies: '@inquirer/confirm': 6.0.12(@types/node@25.6.0) '@mswjs/interceptors': 0.41.4 @@ -10471,11 +10471,11 @@ snapshots: fsevents: 2.3.3 yaml: 2.8.2 - vitest@3.2.4(@types/node@25.6.0)(@vitest/ui@3.2.4)(jsdom@26.1.0)(msw@2.13.4(@types/node@25.6.0)(typescript@5.9.3))(yaml@2.8.2): + vitest@3.2.4(@types/node@25.6.0)(@vitest/ui@3.2.4)(jsdom@26.1.0)(msw@2.14.2(@types/node@25.6.0)(typescript@5.9.3))(yaml@2.8.2): dependencies: '@types/chai': 5.2.2 '@vitest/expect': 3.2.4 - '@vitest/mocker': 3.2.4(msw@2.13.4(@types/node@25.6.0)(typescript@5.9.3))(vite@6.4.2(@types/node@25.6.0)(yaml@2.8.2)) + '@vitest/mocker': 3.2.4(msw@2.14.2(@types/node@25.6.0)(typescript@5.9.3))(vite@6.4.2(@types/node@25.6.0)(yaml@2.8.2)) '@vitest/pretty-format': 3.2.4 '@vitest/runner': 3.2.4 '@vitest/snapshot': 3.2.4 From 762cb2dd45e271b24b7b56e97334348087c734c2 Mon Sep 17 00:00:00 2001 From: Maurovic Cachia Date: Thu, 30 Apr 2026 01:34:49 +0200 Subject: [PATCH 06/16] Fixes for useLoadAndSetGlobalPreferences --- front/src/App.tsx | 8 +-- .../hooks/useLoadAndSetGlobalPreferences.ts | 59 ++++--------------- .../views/BoxCreate/components/BoxCreate.tsx | 5 +- 3 files changed, 19 insertions(+), 53 deletions(-) diff --git a/front/src/App.tsx b/front/src/App.tsx index ed668279e7..7da41e792b 100644 --- a/front/src/App.tsx +++ b/front/src/App.tsx @@ -110,15 +110,15 @@ function App() { setPrevLocation(location.pathname); } - if (error) { - return ; - } - // selectedBaseId not set yet if (!isInitialized) { return; } + if (error) { + return ; + } + return ( diff --git a/front/src/hooks/useLoadAndSetGlobalPreferences.ts b/front/src/hooks/useLoadAndSetGlobalPreferences.ts index 9cf2d61bee..3fbd91e47e 100644 --- a/front/src/hooks/useLoadAndSetGlobalPreferences.ts +++ b/front/src/hooks/useLoadAndSetGlobalPreferences.ts @@ -95,19 +95,19 @@ export const useLoadAndSetGlobalPreferences = () => { setOrganisation(matchingBase.organisation); } } else if (user[JWT_AVAILABLE_BASES].map(String).includes(urlBaseId)) { - if (selectedBaseId !== urlBaseId) { - // only overwrite the selected base ID if the id is different from the existing one. - // setSelectedBase({ id: urlBaseId }); - - const matchingBase = basesWithOrgData.find((base) => base.id === urlBaseId); - - if (matchingBase) { - // set selected base - setSelectedBase({ id: matchingBase.id, name: matchingBase.name }); - // set organisation for selected base - setOrganisation(matchingBase.organisation); - } + // if (selectedBaseId !== urlBaseId) { + // only overwrite the selected base ID if the id is different from the existing one. + // setSelectedBase({ id: urlBaseId }); + + const matchingBase = basesWithOrgData.find((base) => base.id === urlBaseId); + + if (matchingBase) { + // set selected base + setSelectedBase({ id: matchingBase.id, name: matchingBase.name }); + // set organisation for selected base + setOrganisation(matchingBase.organisation); } + // } } } } @@ -127,39 +127,6 @@ export const useLoadAndSetGlobalPreferences = () => { user, ]); - // // handle additional base information being returned from the query - // useEffect(() => { - // if (!isOrganisationAndBasesQueryLoading && organisationAndBaseData !== undefined) { - // const basesWithOrgData = organisationAndBaseData.bases; - // const bases = basesWithOrgData.map((base) => ({ - // id: base.id, - // name: base.name, - // })); - - // if (bases.length > 0) { - // setAvailableBases(bases); - - // if (selectedBase?.id) { - // const matchingBase = basesWithOrgData.find((base) => base.id === selectedBase.id); - - // if (matchingBase) { - // // set selected base - // setSelectedBase({ id: matchingBase.id, name: matchingBase.name }); - // // set organisation for selected base - // setOrganisation(matchingBase.organisation); - // } - // } - // } - // } - // }, [ - // isOrganisationAndBasesQueryLoading, - // organisationAndBaseData, - // selectedBase?.id, - // setAvailableBases, - // setOrganisation, - // setSelectedBase, - // ]); - const finalError = useMemo(() => { if (organisationAndBaseData) { const basesWithOrgData = organisationAndBaseData.bases; @@ -180,7 +147,7 @@ export const useLoadAndSetGlobalPreferences = () => { return error; } else { - return ""; + return "The requested base is not available to you"; } }, [error, organisationAndBaseData, selectedBase?.id]); diff --git a/front/src/views/BoxCreate/components/BoxCreate.tsx b/front/src/views/BoxCreate/components/BoxCreate.tsx index 3d49f9e0d5..06dcaace6a 100644 --- a/front/src/views/BoxCreate/components/BoxCreate.tsx +++ b/front/src/views/BoxCreate/components/BoxCreate.tsx @@ -1,7 +1,7 @@ import { Box, Button, FormLabel, Heading, Input, List, ListItem, Stack } from "@chakra-ui/react"; import { useEffect, useMemo } from "react"; -import { SubmitHandler, useForm } from "react-hook-form"; +import { SubmitHandler, useForm, useWatch } from "react-hook-form"; import { useNavigate, useParams } from "react-router-dom"; import { useAtomValue } from "jotai"; @@ -130,14 +130,13 @@ export function BoxCreate({ control, register, resetField, - watch, setValue, formState: { errors, isSubmitting }, } = useForm({ resolver: zodResolver(CreateBoxFormDataSchema), }); - const productId = watch("productId"); + const productId = useWatch({ control, name: "productId" }); const productAndSizeDataForCurrentProduct = useMemo(() => { if (productId != null) { From b988221be6b7db76ab9b7177e92a6b15cd50937e Mon Sep 17 00:00:00 2001 From: Maurovic Cachia Date: Thu, 30 Apr 2026 02:00:38 +0200 Subject: [PATCH 07/16] Qr Reader Changes --- .../components/QrReaderMultiBoxContainer.tsx | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/front/src/components/QrReader/components/QrReaderMultiBoxContainer.tsx b/front/src/components/QrReader/components/QrReaderMultiBoxContainer.tsx index 0b9245296d..983a8441ad 100644 --- a/front/src/components/QrReader/components/QrReaderMultiBoxContainer.tsx +++ b/front/src/components/QrReader/components/QrReaderMultiBoxContainer.tsx @@ -1,4 +1,4 @@ -import { useCallback, useMemo, useState } from "react"; +import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import { useQuery } from "@apollo/client"; import { useAtomValue } from "jotai"; import { GET_SCANNED_BOXES } from "queries/local-only"; @@ -167,12 +167,14 @@ function QrReaderMultiBoxContainer() { })); }, [baseId, hasShipmentPermission, optionsQueryResult.data]); - // Assign To Shipment is default MultiBoxAction if there are shipments - const [prevShipmentOptionsLength, setPrevShipmentOptionsLength] = useState(0); - if (shipmentOptions.length > 0 && prevShipmentOptionsLength === 0) { - setPrevShipmentOptionsLength(shipmentOptions.length); - setMultiBoxAction(IMultiBoxAction.assignShipment); - } + // Assign To Shipment is default MultiBoxAction if there are shipments (set once on first load) + const hasSetDefaultShipmentAction = useRef(false); + useEffect(() => { + if (shipmentOptions.length > 0 && !hasSetDefaultShipmentAction.current) { + hasSetDefaultShipmentAction.current = true; + setMultiBoxAction(IMultiBoxAction.assignShipment); + } + }, [shipmentOptions.length]); const notInStockBoxes = useMemo( () => scannedBoxesQueryResult.data?.scannedBoxes.filter((box) => box.state !== "InStock") ?? [], From b637fdd1c05271034c9637a0c489008ba3d4e15f Mon Sep 17 00:00:00 2001 From: Maurovic Cachia Date: Thu, 30 Apr 2026 22:26:51 +0200 Subject: [PATCH 08/16] BoxCreateView --- front/src/views/BoxCreate/BoxCreateView.tsx | 20 +++++++------------- 1 file changed, 7 insertions(+), 13 deletions(-) diff --git a/front/src/views/BoxCreate/BoxCreateView.tsx b/front/src/views/BoxCreate/BoxCreateView.tsx index ae91303b2c..00dae8e6c1 100644 --- a/front/src/views/BoxCreate/BoxCreateView.tsx +++ b/front/src/views/BoxCreate/BoxCreateView.tsx @@ -1,4 +1,4 @@ -import { useEffect, useState } from "react"; +import { useEffect } from "react"; import { useLazyQuery, useMutation, useQuery } from "@apollo/client"; import { graphql } from "../../../../graphql/graphql"; import { Center } from "@chakra-ui/react"; @@ -92,10 +92,6 @@ function BoxCreateView() { const baseId = useAtomValue(selectedBaseIdAtom); const baseName = selectedBase?.name; - // no warehouse location or products associated with base - const [noLocation, setNoLocation] = useState(false); - const [noProducts, setNoProducts] = useState(false); - // variables in URL const qrCode = useParams<{ qrCode: string }>().qrCode!; @@ -160,11 +156,6 @@ function BoxCreateView() { })) .sort((a, b) => Number(a?.seq) - Number(b?.seq)); - if (allLocations !== undefined && allLocations.length < 1 && !noLocation) setNoLocation(true); - else if (noLocation) setNoLocation(false); - if (allProducts !== undefined && allProducts.length < 1 && !noProducts) setNoProducts(true); - else if (noProducts) setNoProducts(false); - // check data for form useEffect(() => { if (!allFormOptions.loading) { @@ -284,12 +275,12 @@ function BoxCreateView() { return (
- {noLocation && ( + {allLocations !== undefined && allLocations.length < 1 && ( warehouse location before boxes can be created!`} /> )} - {noProducts && ( + {allProducts !== undefined && allProducts.length < 1 && ( @@ -300,7 +291,10 @@ function BoxCreateView() { onSubmitBoxCreateForm={onSubmitBoxCreateForm} onSubmitBoxCreateFormAndCreateAnother={onSubmitBoxCreateFormAndCreateAnother} allTags={allTags} - disableSubmission={noLocation || noProducts} + disableSubmission={ + (allLocations !== undefined && allLocations.length < 1) || + (allProducts !== undefined && allProducts.length < 1) + } />
); From 608828fd216efbacdac3d8a730168a87fed7e606 Mon Sep 17 00:00:00 2001 From: Maurovic Cachia Date: Thu, 30 Apr 2026 22:27:37 +0200 Subject: [PATCH 09/16] Fixes --- shared-front/src/App.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/shared-front/src/App.tsx b/shared-front/src/App.tsx index 035c67e6ce..1e873e8ca9 100644 --- a/shared-front/src/App.tsx +++ b/shared-front/src/App.tsx @@ -108,7 +108,7 @@ function App() { // Redirect to full URL with view param once link data has loaded useEffect(() => { - if (data && !view) { + if (data && !view && data?.resolveLink?.view) { const urlParams = data?.resolveLink?.urlParameters ?? "nofilters=true"; const hasBoiParam = urlParams.includes("boi="); const boiParam = hasBoiParam ? "" : `&boi=${boxesOrItemsFilterValues[0].urlId}`; From b549f3606a13bc482bb007e7e6ee41be1d80b563 Mon Sep 17 00:00:00 2001 From: Maurovic Cachia Date: Thu, 30 Apr 2026 22:37:23 +0200 Subject: [PATCH 10/16] Timeout fixes --- front/src/views/BoxCreate/BoxCreateView.test.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/front/src/views/BoxCreate/BoxCreateView.test.tsx b/front/src/views/BoxCreate/BoxCreateView.test.tsx index efbce1db3e..3666b135d1 100644 --- a/front/src/views/BoxCreate/BoxCreateView.test.tsx +++ b/front/src/views/BoxCreate/BoxCreateView.test.tsx @@ -227,7 +227,7 @@ describe("BoxCreateView", () => { // Verify navigation to box details page expect(mockNavigate).toHaveBeenCalledWith("/bases/1/boxes/12345"); - }); + }, 10000); it("successfully creates a box with QR label and navigates to box details", async () => { const user = userEvent.setup(); From b4d5614b7a6fc24268068dd96c0c5f2b2739df27 Mon Sep 17 00:00:00 2001 From: Maurovic Cachia Date: Thu, 30 Apr 2026 22:44:53 +0200 Subject: [PATCH 11/16] Timeout fix 2 --- front/src/views/BoxCreate/BoxCreateView.test.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/front/src/views/BoxCreate/BoxCreateView.test.tsx b/front/src/views/BoxCreate/BoxCreateView.test.tsx index 3666b135d1..7a65bb06e4 100644 --- a/front/src/views/BoxCreate/BoxCreateView.test.tsx +++ b/front/src/views/BoxCreate/BoxCreateView.test.tsx @@ -15,7 +15,7 @@ import { product1, productBasic1 } from "mocks/products"; import { location1 } from "mocks/locations"; import { tag1, tag2 } from "mocks/tags"; -vi.setConfig({ testTimeout: 20_000 }); +vi.setConfig({ testTimeout: 40_000 }); vi.mock("@auth0/auth0-react"); const mockedUseAuth0 = vi.mocked(useAuth0); @@ -227,7 +227,7 @@ describe("BoxCreateView", () => { // Verify navigation to box details page expect(mockNavigate).toHaveBeenCalledWith("/bases/1/boxes/12345"); - }, 10000); + }); it("successfully creates a box with QR label and navigates to box details", async () => { const user = userEvent.setup(); From cedf8a036054bb0e2232073dccc58ae329005b5f Mon Sep 17 00:00:00 2001 From: Maurovic Cachia Date: Thu, 30 Apr 2026 23:04:49 +0200 Subject: [PATCH 12/16] Hotfix --- front/src/views/Tags/TagsOverview/TagsView.test.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/front/src/views/Tags/TagsOverview/TagsView.test.tsx b/front/src/views/Tags/TagsOverview/TagsView.test.tsx index 1809da7567..41781b9a7e 100644 --- a/front/src/views/Tags/TagsOverview/TagsView.test.tsx +++ b/front/src/views/Tags/TagsOverview/TagsView.test.tsx @@ -9,7 +9,7 @@ import { mockAuthenticatedUser } from "mocks/hooks"; import { cache, tableConfigsVar } from "queries/cache"; import { DELETE_TAGS } from "hooks/useDeleteTags"; -vi.setConfig({ testTimeout: 20_000 }); +vi.setConfig({ testTimeout: 40_000 }); vi.mock("@auth0/auth0-react"); const mockedUseAuth0 = vi.mocked(useAuth0); From 077f04435de755790618b7800867ea0444e33fa9 Mon Sep 17 00:00:00 2001 From: Maurovic Cachia Date: Fri, 1 May 2026 01:29:06 +0200 Subject: [PATCH 13/16] Copilot Review Updates --- .../components/HeaderMenu/BaseSwitcher.tsx | 32 ++++++++++++++----- .../hooks/useLoadAndSetGlobalPreferences.ts | 25 ++++++++------- graphql/generated/graphql-env.d.ts | 12 +++---- .../custom-graphs/BarChartCenterAxis.tsx | 8 ++++- 4 files changed, 51 insertions(+), 26 deletions(-) diff --git a/front/src/components/HeaderMenu/BaseSwitcher.tsx b/front/src/components/HeaderMenu/BaseSwitcher.tsx index 85783d02f4..1419688acd 100644 --- a/front/src/components/HeaderMenu/BaseSwitcher.tsx +++ b/front/src/components/HeaderMenu/BaseSwitcher.tsx @@ -11,7 +11,7 @@ import { RadioGroup, Stack, } from "@chakra-ui/react"; -import { useState } from "react"; +import { useMemo, useState } from "react"; import { useNavigate, useLocation, useParams } from "react-router-dom"; import { useAtomValue } from "jotai"; import { availableBasesAtom, selectedBaseIdAtom } from "stores/globalPreferenceStore"; @@ -22,19 +22,30 @@ function BaseSwitcher({ isOpen, onClose }: { isOpen: boolean; onClose: () => voi const { pathname } = useLocation(); const baseId = useAtomValue(selectedBaseIdAtom); const availableBases = useAtomValue(availableBasesAtom); - const currentOrganisationBases = availableBases.filter((base) => base.id !== baseId); - const firstAvailableBaseId = currentOrganisationBases[0]?.id; - const [value, setValue] = useState(firstAvailableBaseId); + + const currentOrganisationBases = useMemo( + () => availableBases.filter((base) => base.id !== baseId), + [availableBases, baseId], + ); + + const firstAvailableBaseId = useMemo( + () => currentOrganisationBases[0]?.id, + [currentOrganisationBases], + ); + + const [value, setValue] = useState(""); const switchBase = () => { const currentPath = pathname.split(`/bases/${urlBaseId}`)[1]; - navigate(`/bases/${value}${currentPath}`); + const actValue = value || firstAvailableBaseId; + + navigate(`/bases/${actValue}${currentPath}`); onClose(); // Need to reset the default radio selection whenever the available bases change. - const currentOrganisationBases = availableBases.filter((base) => base.id !== value); + const currentOrganisationBases = availableBases.filter((base) => base.id !== actValue); const newFirstAvailableBaseId = currentOrganisationBases[0]?.id; setValue(newFirstAvailableBaseId); }; @@ -47,7 +58,7 @@ function BaseSwitcher({ isOpen, onClose }: { isOpen: boolean; onClose: () => voi Switch Base to - + {currentOrganisationBases?.map((base) => ( @@ -61,7 +72,12 @@ function BaseSwitcher({ isOpen, onClose }: { isOpen: boolean; onClose: () => voi - diff --git a/front/src/hooks/useLoadAndSetGlobalPreferences.ts b/front/src/hooks/useLoadAndSetGlobalPreferences.ts index 3fbd91e47e..ad81816051 100644 --- a/front/src/hooks/useLoadAndSetGlobalPreferences.ts +++ b/front/src/hooks/useLoadAndSetGlobalPreferences.ts @@ -33,10 +33,10 @@ export const useLoadAndSetGlobalPreferences = () => { const [ runOrganisationAndBasesQuery, - { loading: isOrganisationAndBasesQueryLoading, data: organisationAndBaseData }, + { loading: isOrganisationAndBasesQueryLoading, data: organisationAndBaseData, error }, ] = useLazyQuery(ORGANISATION_AND_BASES_QUERY); - const error = useMemo(() => { + const localError = useMemo(() => { if (!user || (!isGod && !user[JWT_AVAILABLE_BASES]?.length)) { return "You do not have access to any bases."; } else { @@ -54,22 +54,24 @@ export const useLoadAndSetGlobalPreferences = () => { // - the access token is in the request header from the apollo client and // - the base Name is not set useEffect(() => { - if (user && !selectedBase?.name && !error) { + if (user && !selectedBase?.name && !localError) { runOrganisationAndBasesQuery(); } - }, [runOrganisationAndBasesQuery, user, selectedBase?.name, error]); + }, [runOrganisationAndBasesQuery, user, selectedBase?.name, localError]); // setting auth atoms initially from auth0 useEffect(() => { if (!isOrganisationAndBasesQueryLoading && organisationAndBaseData !== undefined) { - if (!error && user && (user[JWT_AVAILABLE_BASES] || isGod)) { + if (!localError && user && (user[JWT_AVAILABLE_BASES] || isGod)) { const basesWithOrgData = organisationAndBaseData.bases; const bases = basesWithOrgData.map((base) => ({ id: base.id, name: base.name, })); if (bases.length > 0) { - setAvailableBases(bases); + if (JSON.stringify(availableBases) !== JSON.stringify(bases)) { + setAvailableBases(bases); + } // set available bases from auth0 id token only if they are not set yet. // Otherwise, it would overwrite the names queried from the BE. // if (!availableBases.length && !isGod) { @@ -114,13 +116,12 @@ export const useLoadAndSetGlobalPreferences = () => { } } }, [ - availableBases.length, - error, + availableBases, + localError, isGod, isOrganisationAndBasesQueryLoading, location.pathname, organisationAndBaseData, - selectedBaseId, setAvailableBases, setOrganisation, setSelectedBase, @@ -145,11 +146,13 @@ export const useLoadAndSetGlobalPreferences = () => { } } - return error; + return localError; + } else if (error) { + return "Failed getting information " + error.message; } else { return "The requested base is not available to you"; } - }, [error, organisationAndBaseData, selectedBase?.id]); + }, [error, localError, organisationAndBaseData, selectedBase?.id]); const isLoading = !selectedBase?.name || isOrganisationAndBasesQueryLoading; diff --git a/graphql/generated/graphql-env.d.ts b/graphql/generated/graphql-env.d.ts index 87bcd06316..43f619088e 100644 --- a/graphql/generated/graphql-env.d.ts +++ b/graphql/generated/graphql-env.d.ts @@ -189,16 +189,16 @@ export type introspection_types = { */ export type introspection = { name: never; - query: "Query"; - mutation: "Mutation"; + query: 'Query'; + mutation: 'Mutation'; subscription: never; types: introspection_types; }; -import * as gqlTada from "gql.tada"; +import * as gqlTada from 'gql.tada'; -declare module "gql.tada" { +declare module 'gql.tada' { interface setupSchema { - introspection: introspection; + introspection: introspection } -} +} \ No newline at end of file diff --git a/shared-components/statviz/components/custom-graphs/BarChartCenterAxis.tsx b/shared-components/statviz/components/custom-graphs/BarChartCenterAxis.tsx index 626aebeeb2..2ef80140f0 100644 --- a/shared-components/statviz/components/custom-graphs/BarChartCenterAxis.tsx +++ b/shared-components/statviz/components/custom-graphs/BarChartCenterAxis.tsx @@ -58,7 +58,13 @@ export default function BarChartCenterAxis(chart: IBarChartCenterAxis) { if (firstRender && chart.rendered) { chart.rendered(firstRender); } - }); + + return () => { + if (tooltipTimeoutRef.current) { + clearTimeout(tooltipTimeoutRef.current); + } + }; + }, [chart]); const fields = { ...chart }; From 405ac5110a0cba5dfdd51f8eb64382ebe7e831f6 Mon Sep 17 00:00:00 2001 From: Maurovic Cachia Date: Fri, 1 May 2026 01:36:31 +0200 Subject: [PATCH 14/16] Timoeut fixes --- front/src/views/Tags/UpdateTag/UpdateTagView.test.tsx | 2 +- front/src/views/Tags/components/TagForm.test.tsx | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/front/src/views/Tags/UpdateTag/UpdateTagView.test.tsx b/front/src/views/Tags/UpdateTag/UpdateTagView.test.tsx index 17ec52e05d..3d68fab1ce 100644 --- a/front/src/views/Tags/UpdateTag/UpdateTagView.test.tsx +++ b/front/src/views/Tags/UpdateTag/UpdateTagView.test.tsx @@ -213,7 +213,7 @@ describe("UpdateTagView", () => { // Verify navigation back to tags list expect(mockNavigate).toHaveBeenCalledWith(".."); - }, 30000); + }); it("handles tag with null/undefined description", async () => { const tagQueryNullDescription = { diff --git a/front/src/views/Tags/components/TagForm.test.tsx b/front/src/views/Tags/components/TagForm.test.tsx index 26421399b2..59d1544b97 100644 --- a/front/src/views/Tags/components/TagForm.test.tsx +++ b/front/src/views/Tags/components/TagForm.test.tsx @@ -4,7 +4,7 @@ import { userEvent } from "@testing-library/user-event"; import { selectOptionInSelectField } from "tests/helpers"; import { TagForm, nameErrorText, applicationErrorText } from "./TagForm"; -vi.setConfig({ testTimeout: 20_000 }); +vi.setConfig({ testTimeout: 40_000 }); // Mock useNavigate vi.mock("react-router-dom", async () => { @@ -112,7 +112,7 @@ describe("TagForm", () => { expect.anything(), // form event ); }); - }, 30000); + }); it("submits form without optional description", async () => { const user = userEvent.setup(); From 7937257fe7c84a43930c1aa8646a3a62c243825e Mon Sep 17 00:00:00 2001 From: Maurovic Cachia Date: Fri, 1 May 2026 07:52:52 +0200 Subject: [PATCH 15/16] More Copilot suggestions --- .../components/HeaderMenu/BaseSwitcher.tsx | 4 ++++ .../hooks/useLoadAndSetGlobalPreferences.ts | 24 +++++++++++++------ 2 files changed, 21 insertions(+), 7 deletions(-) diff --git a/front/src/components/HeaderMenu/BaseSwitcher.tsx b/front/src/components/HeaderMenu/BaseSwitcher.tsx index 1419688acd..a3bdff3fee 100644 --- a/front/src/components/HeaderMenu/BaseSwitcher.tsx +++ b/front/src/components/HeaderMenu/BaseSwitcher.tsx @@ -40,6 +40,10 @@ function BaseSwitcher({ isOpen, onClose }: { isOpen: boolean; onClose: () => voi const actValue = value || firstAvailableBaseId; + if (!actValue) { + return; + } + navigate(`/bases/${actValue}${currentPath}`); onClose(); diff --git a/front/src/hooks/useLoadAndSetGlobalPreferences.ts b/front/src/hooks/useLoadAndSetGlobalPreferences.ts index ad81816051..43a4527023 100644 --- a/front/src/hooks/useLoadAndSetGlobalPreferences.ts +++ b/front/src/hooks/useLoadAndSetGlobalPreferences.ts @@ -25,11 +25,13 @@ export const useLoadAndSetGlobalPreferences = () => { // Boxtribute God user const isGod: boolean = (user && user[JWT_ROLE]?.includes("boxtribute_god")) || false; - // Set in localStore if current user can Share Public Dashboard Views - localStorage.setItem( - "canShareLink", - authorize({ requiredAbps: ["create_shareable_link"] }).toString(), - ); + useEffect(() => { + // Set in localStore if current user can Share Public Dashboard Views + localStorage.setItem( + "canShareLink", + authorize({ requiredAbps: ["create_shareable_link"] }).toString(), + ); + }, [authorize]); const [ runOrganisationAndBasesQuery, @@ -149,10 +151,18 @@ export const useLoadAndSetGlobalPreferences = () => { return localError; } else if (error) { return "Failed getting information " + error.message; - } else { + } else if (!isOrganisationAndBasesQueryLoading) { return "The requested base is not available to you"; + } else { + return; } - }, [error, localError, organisationAndBaseData, selectedBase?.id]); + }, [ + error, + isOrganisationAndBasesQueryLoading, + localError, + organisationAndBaseData, + selectedBase?.id, + ]); const isLoading = !selectedBase?.name || isOrganisationAndBasesQueryLoading; From 4485ea6e369ef9b5d71ac6dcff77dc9b18f059d8 Mon Sep 17 00:00:00 2001 From: Maurovic Cachia Date: Fri, 1 May 2026 08:43:50 +0200 Subject: [PATCH 16/16] More Copilot review changes --- front/src/App.tsx | 15 +++++++++------ front/src/components/HeaderMenu/BaseSwitcher.tsx | 2 +- .../components/QrReaderMultiBoxContainer.tsx | 8 ++++---- front/src/hooks/useLoadAndSetGlobalPreferences.ts | 7 ++++--- .../Box/BoxViewReconciliationOverlay.test.tsx | 4 +++- 5 files changed, 21 insertions(+), 15 deletions(-) diff --git a/front/src/App.tsx b/front/src/App.tsx index 7da41e792b..9bdb1e1c13 100644 --- a/front/src/App.tsx +++ b/front/src/App.tsx @@ -1,5 +1,5 @@ import "regenerator-runtime/runtime"; -import { ReactElement, Suspense, useEffect, useRef, useState } from "react"; +import { ReactElement, Suspense, useEffect, useMemo, useRef } from "react"; import { Navigate, Outlet, Route, Routes, useLocation } from "react-router-dom"; import { useLoadAndSetGlobalPreferences } from "hooks/useLoadAndSetGlobalPreferences"; import Layout from "components/Layout"; @@ -99,23 +99,26 @@ function DropappRedirect({ path }: DropappRedirectProps) { function App() { const { error, isInitialized } = useLoadAndSetGlobalPreferences(); const location = useLocation(); - const [prevLocation, setPrevLocation] = useState(undefined); // For BoxesView to reduce number of expensive Boxes queries // when navigating between boxes and other views. const hasExecutedInitialFetchOfBoxes = useRef(false); // store previous location to return to if you are not authorized // only store previous location if a base is selected - if (/^\/bases\/\d+\//.test(location.pathname) && location.pathname !== prevLocation) { - setPrevLocation(location.pathname); - } + const prevLocation = useMemo(() => { + if (/^\/bases\/\d+\//.test(location.pathname)) { + return location.pathname; + } else { + return; + } + }, [location.pathname]); // selectedBaseId not set yet if (!isInitialized) { return; } - if (error) { + if (error && isInitialized) { return ; } diff --git a/front/src/components/HeaderMenu/BaseSwitcher.tsx b/front/src/components/HeaderMenu/BaseSwitcher.tsx index a3bdff3fee..3c916edb5d 100644 --- a/front/src/components/HeaderMenu/BaseSwitcher.tsx +++ b/front/src/components/HeaderMenu/BaseSwitcher.tsx @@ -51,7 +51,7 @@ function BaseSwitcher({ isOpen, onClose }: { isOpen: boolean; onClose: () => voi const currentOrganisationBases = availableBases.filter((base) => base.id !== actValue); const newFirstAvailableBaseId = currentOrganisationBases[0]?.id; - setValue(newFirstAvailableBaseId); + setValue(newFirstAvailableBaseId || ""); }; return ( diff --git a/front/src/components/QrReader/components/QrReaderMultiBoxContainer.tsx b/front/src/components/QrReader/components/QrReaderMultiBoxContainer.tsx index 983a8441ad..4cb3a6e013 100644 --- a/front/src/components/QrReader/components/QrReaderMultiBoxContainer.tsx +++ b/front/src/components/QrReader/components/QrReaderMultiBoxContainer.tsx @@ -168,13 +168,13 @@ function QrReaderMultiBoxContainer() { }, [baseId, hasShipmentPermission, optionsQueryResult.data]); // Assign To Shipment is default MultiBoxAction if there are shipments (set once on first load) - const hasSetDefaultShipmentAction = useRef(false); + const defaultShipmentActionForBaseId = useRef(""); useEffect(() => { - if (shipmentOptions.length > 0 && !hasSetDefaultShipmentAction.current) { - hasSetDefaultShipmentAction.current = true; + if (shipmentOptions.length > 0 && defaultShipmentActionForBaseId.current !== baseId) { + defaultShipmentActionForBaseId.current = baseId; setMultiBoxAction(IMultiBoxAction.assignShipment); } - }, [shipmentOptions.length]); + }, [baseId, shipmentOptions.length]); const notInStockBoxes = useMemo( () => scannedBoxesQueryResult.data?.scannedBoxes.filter((box) => box.state !== "InStock") ?? [], diff --git a/front/src/hooks/useLoadAndSetGlobalPreferences.ts b/front/src/hooks/useLoadAndSetGlobalPreferences.ts index 43a4527023..b1365114e0 100644 --- a/front/src/hooks/useLoadAndSetGlobalPreferences.ts +++ b/front/src/hooks/useLoadAndSetGlobalPreferences.ts @@ -35,7 +35,7 @@ export const useLoadAndSetGlobalPreferences = () => { const [ runOrganisationAndBasesQuery, - { loading: isOrganisationAndBasesQueryLoading, data: organisationAndBaseData, error }, + { loading: isOrganisationAndBasesQueryLoading, data: organisationAndBaseData, error, called }, ] = useLazyQuery(ORGANISATION_AND_BASES_QUERY); const localError = useMemo(() => { @@ -151,12 +151,13 @@ export const useLoadAndSetGlobalPreferences = () => { return localError; } else if (error) { return "Failed getting information " + error.message; - } else if (!isOrganisationAndBasesQueryLoading) { + } else if (!isOrganisationAndBasesQueryLoading && called) { return "The requested base is not available to you"; } else { return; } }, [ + called, error, isOrganisationAndBasesQueryLoading, localError, @@ -166,7 +167,7 @@ export const useLoadAndSetGlobalPreferences = () => { const isLoading = !selectedBase?.name || isOrganisationAndBasesQueryLoading; - const isInitialized = selectedBaseId !== "0"; + const isInitialized = !isLoading && selectedBaseId !== "0"; return { isLoading, error: finalError, isInitialized }; }; diff --git a/front/src/views/Box/BoxViewReconciliationOverlay.test.tsx b/front/src/views/Box/BoxViewReconciliationOverlay.test.tsx index 1963b197ef..cba80c0d50 100644 --- a/front/src/views/Box/BoxViewReconciliationOverlay.test.tsx +++ b/front/src/views/Box/BoxViewReconciliationOverlay.test.tsx @@ -14,6 +14,8 @@ import { mockMatchMediaQuery } from "mocks/functions"; import { mockAuthenticatedUser } from "mocks/hooks"; import BTBox from "./BoxView"; +vi.setConfig({ testTimeout: 40_000 }); + vi.mock("@auth0/auth0-react"); const mockedUseAuth0 = vi.mocked(useAuth0); @@ -160,4 +162,4 @@ it("4.7.4.1 - Reconciliation dialog automatically appears when box state equals }, { timeout: 5000 }, ); -}, 20000); +});