From fa64151163c8d0e8b4fec559fbd215a672920024 Mon Sep 17 00:00:00 2001 From: Maurovic Cachia Date: Thu, 6 Aug 2026 22:51:42 +0200 Subject: [PATCH 1/4] Walkthrough update to Eslint 9 + a number of smaller refactors around the system --- .../components/BoxReconciliationAccordion.tsx | 25 +++++----- .../components/MatchProductsForm.tsx | 50 ++++++++----------- .../components/ReceiveLocationForm.tsx | 5 +- .../Walkthrough/desktop/TourOverlay.tsx | 38 +++++++------- .../desktop/WalkthroughContext.tsx | 41 +++++++++------ .../mobile/MobileWalkthroughContext.tsx | 17 +++---- front/src/views/Box/BoxView.tsx | 12 ++--- .../Box/components/AddItemsToBoxOverlay.tsx | 5 +- .../components/TakeItemsFromBoxOverlay.tsx | 7 ++- .../components/ShipmentContent.tsx | 2 +- .../components/ShipmentExportButton.tsx | 1 + pnpm-lock.yaml | 2 +- 12 files changed, 100 insertions(+), 105 deletions(-) diff --git a/front/src/components/BoxReconciliationOverlay/components/BoxReconciliationAccordion.tsx b/front/src/components/BoxReconciliationOverlay/components/BoxReconciliationAccordion.tsx index 6a6c232ae4..d3ce6b4075 100644 --- a/front/src/components/BoxReconciliationOverlay/components/BoxReconciliationAccordion.tsx +++ b/front/src/components/BoxReconciliationOverlay/components/BoxReconciliationAccordion.tsx @@ -15,7 +15,7 @@ import { RiQuestionFill } from "react-icons/ri"; import { ILocationData, IProductWithSizeRangeData } from "./BoxReconciliationView"; import { MatchProductsFormData, MatchProductsForm } from "./MatchProductsForm"; import { IReceiveLocationFormData, ReceiveLocationForm } from "./ReceiveLocationForm"; -import { useAtom, useAtomValue, useSetAtom } from "jotai"; +import { useAtomValue, useSetAtom } from "jotai"; import { reconciliationMatchProductAtom, reconciliationReceiveLocationAtom, @@ -55,27 +55,25 @@ export function BoxReconcilationAccordion({ onBoxUndelivered, onBoxDelivered, }: IBoxReconcilationAccordionProps) { - const [reconciliationMatchProductCache, setReconciliationMatchProductCache] = useAtom( - reconciliationMatchProductAtom, - ); - const cachedReconciliationMatchProduct = useAtomValue(reconciliationMatchProductAtom); + const reconciliationMatchProductCache = useAtomValue(reconciliationMatchProductAtom); + const setReconciliationMatchProductCache = useSetAtom(reconciliationMatchProductAtom); const setReconciliationReceiveLocationCache = useSetAtom(reconciliationReceiveLocationAtom); // source side const sourceProductId = shipmentDetail.sourceProduct?.id; const sourceSizeId = shipmentDetail.sourceSize?.id; const isSourceProductInCache = - sourceProductId && !!cachedReconciliationMatchProduct[sourceProductId]; + sourceProductId && !!reconciliationMatchProductCache[sourceProductId]; // target side const isTargetProductAutoMatched = !!shipmentDetail?.autoMatchingTargetProduct; const targetProductName = isSourceProductInCache - ? cachedReconciliationMatchProduct[sourceProductId].productId.label + ? reconciliationMatchProductCache[sourceProductId].productId.label : isTargetProductAutoMatched && shipmentDetail?.autoMatchingTargetProduct ? shipmentDetail.autoMatchingTargetProduct.name : undefined; const targetProductId = isSourceProductInCache - ? cachedReconciliationMatchProduct[sourceProductId].productId.value + ? reconciliationMatchProductCache[sourceProductId].productId.value : isTargetProductAutoMatched && shipmentDetail?.autoMatchingTargetProduct ? shipmentDetail.autoMatchingTargetProduct.id : undefined; @@ -86,12 +84,12 @@ export function BoxReconcilationAccordion({ return sourceSizeId && possibleSizeIds.includes(sourceSizeId); }); const targetSizeName = isSourceProductInCache - ? cachedReconciliationMatchProduct[sourceProductId].sizeId.label + ? reconciliationMatchProductCache[sourceProductId].sizeId.label : isTargetProductAutoMatched && isSourceSizeInRangeOfTargetProduct && shipmentDetail?.sourceSize ? shipmentDetail.sourceSize.label : undefined; const targetSizeId = isSourceProductInCache - ? cachedReconciliationMatchProduct[sourceProductId].sizeId.value + ? reconciliationMatchProductCache[sourceProductId].sizeId.value : isTargetProductAutoMatched && isSourceSizeInRangeOfTargetProduct && shipmentDetail?.sourceSize ? shipmentDetail.sourceSize.id : undefined; @@ -187,9 +185,10 @@ export function BoxReconcilationAccordion({ setAccordionIndex(1); if (shipmentDetail.sourceProduct?.id) { - reconciliationMatchProductCache[shipmentDetail.sourceProduct.id] = - matchedProductsFormData; - setReconciliationMatchProductCache(reconciliationMatchProductCache); + setReconciliationMatchProductCache({ + ...reconciliationMatchProductCache, + [shipmentDetail.sourceProduct.id]: matchedProductsFormData, + }); } setProductFormData({ diff --git a/front/src/components/BoxReconciliationOverlay/components/MatchProductsForm.tsx b/front/src/components/BoxReconciliationOverlay/components/MatchProductsForm.tsx index da13328a7e..3dc4b35231 100644 --- a/front/src/components/BoxReconciliationOverlay/components/MatchProductsForm.tsx +++ b/front/src/components/BoxReconciliationOverlay/components/MatchProductsForm.tsx @@ -1,9 +1,9 @@ -import { useEffect, useRef, useState } from "react"; +import { useEffect, useMemo, useRef } from "react"; import { Box, Button, Flex, Text, Wrap, WrapItem } from "@chakra-ui/react"; import { z } from "zod"; import { zodResolver } from "@hookform/resolvers/zod"; import { groupBy } from "lodash"; -import { useForm } from "react-hook-form"; +import { useForm, useWatch } from "react-hook-form"; import { BiSubdirectoryRight } from "react-icons/bi"; import { BsFillCheckCircleFill } from "react-icons/bs"; import { IProductWithSizeRangeData } from "./BoxReconciliationView"; @@ -69,7 +69,6 @@ export function MatchProductsForm({ const { handleSubmit, control, - watch, resetField, formState: { errors, isSubmitting }, } = useForm({ @@ -78,39 +77,32 @@ export function MatchProductsForm({ }); // needed for updating size select field for new product - const productId = watch("productId"); - const sizeId = watch("sizeId"); + const productId = useWatch({ control, name: "productId" }); + const sizeId = useWatch({ control, name: "sizeId" }); const productRef = useRef(); // sizes reset depending on selected product - const [sizesOptionsForCurrentProduct, setSizesOptionsForCurrentProduct] = useState< - IDropdownOption[] - >([]); + const sizesOptionsForCurrentProduct: IDropdownOption[] = useMemo( + () => + productId + ? (productAndSizesData + .find((p) => p.id === productId.value) + ?.sizeRange?.sizes?.map((s) => ({ label: s.label, value: s.id })) ?? []) + : [], + [productId, productAndSizesData], + ); useEffect(() => { - if (productId != null) { - const productAndSizeDataForCurrentProduct = productAndSizesData.find( - (p) => p.id === productId.value, - ); - const prepSizesOptionsForCurrentProduct = - productAndSizeDataForCurrentProduct?.sizeRange?.sizes?.map((s) => ({ - label: s.label, - value: s.id, - })) || []; - setSizesOptionsForCurrentProduct(() => prepSizesOptionsForCurrentProduct); - - // Reset size if the product reference is different than the currently selected product - if (productRef.current !== productId.value) { - productRef.current = productId.value; - // if there is only one option select it directly - if (prepSizesOptionsForCurrentProduct.length === 1) { - resetField("sizeId", { defaultValue: prepSizesOptionsForCurrentProduct[0] }); - } else { - resetField("sizeId", { defaultValue: undefined }); - } + if (productId != null && productRef.current !== productId.value) { + productRef.current = productId.value; + // if there is only one option select it directly + if (sizesOptionsForCurrentProduct.length === 1) { + resetField("sizeId", { defaultValue: sizesOptionsForCurrentProduct[0] }); + } else { + resetField("sizeId", { defaultValue: undefined }); } } - }, [productId, productAndSizesData, resetField]); + }, [productId, sizesOptionsForCurrentProduct, resetField]); // Option Preparations for select fields const productsGroupedByCategory: Record = groupBy( diff --git a/front/src/components/BoxReconciliationOverlay/components/ReceiveLocationForm.tsx b/front/src/components/BoxReconciliationOverlay/components/ReceiveLocationForm.tsx index c2354005f8..50459d35ef 100644 --- a/front/src/components/BoxReconciliationOverlay/components/ReceiveLocationForm.tsx +++ b/front/src/components/BoxReconciliationOverlay/components/ReceiveLocationForm.tsx @@ -2,7 +2,7 @@ import { Button, Flex } from "@chakra-ui/react"; import { zodResolver } from "@hookform/resolvers/zod"; import SelectField from "components/Form/SelectField"; import { useEffect } from "react"; -import { useForm } from "react-hook-form"; +import { useForm, useWatch } from "react-hook-form"; import { BsFillCheckCircleFill } from "react-icons/bs"; import { FaWarehouse } from "react-icons/fa"; import { z } from "zod"; @@ -57,7 +57,6 @@ export function ReceiveLocationForm({ // react-hook-form const { control, - watch, handleSubmit, formState: { errors, isSubmitting }, } = useForm({ @@ -65,7 +64,7 @@ export function ReceiveLocationForm({ defaultValues, }); - const locationId = watch("locationId"); + const locationId = useWatch({ control, name: "locationId" }); useEffect(() => { if ( diff --git a/front/src/components/Walkthrough/desktop/TourOverlay.tsx b/front/src/components/Walkthrough/desktop/TourOverlay.tsx index 79db980c74..fd5d9c8ac1 100644 --- a/front/src/components/Walkthrough/desktop/TourOverlay.tsx +++ b/front/src/components/Walkthrough/desktop/TourOverlay.tsx @@ -8,7 +8,6 @@ import { STATUS, Step, TooltipRenderProps, - TourData, } from "react-joyride"; import { Box, Button, Flex, IconButton, Progress, Text } from "@chakra-ui/react"; import { BiChevronLeft } from "react-icons/bi"; @@ -34,8 +33,7 @@ export const PATHS: Record = { // Chakra v2 generates the AccordionButton's id as `accordion-button-{AccordionItem id}`, // so we target it directly rather than querying inside the AccordionItem element. function makeExpandGroupHook(groupName: string): BeforeHook { - // eslint-disable-next-line no-unused-vars - return async (_data: TourData) => { + return async () => { const groupId = nameToNavId(groupName); const btn = document.getElementById(`accordion-button-${groupId}`) as HTMLButtonElement | null; if (!btn || btn.getAttribute("aria-expanded") === "true") return; @@ -161,14 +159,13 @@ function TourOverlay() { backToPathSelection, } = useWalkthrough(); const visiblePaths = useVisiblePaths(); + const [stepIndex, setStepIndex] = useState(0); const [run, setRun] = useState(false); const isActive = isWalkthroughActive && currentStep === "tour" && activePath != null; const pathDef = activePath ? PATHS[activePath] : null; - // Compute the step list once when the path changes so Joyride never receives a - // new array reference on every render (which would reset its internal state). - // buildJoyrideSteps also filters out steps whose targets are absent from the DOM. + // eslint-disable-next-line react-hooks/exhaustive-deps const steps = useMemo(() => (pathDef ? buildJoyrideSteps(pathDef.steps) : []), [activePath]); const totalSteps = steps.length; @@ -180,24 +177,29 @@ function TourOverlay() { [activePath, visiblePaths, completedPaths], ); - // Reset step index whenever the active path changes useEffect(() => { - if (!isActive) { - setRun(false); - return; - } - setStepIndex(0); + if (!isActive) return; + // Small delay to allow DOM to settle before joyride starts const t = setTimeout(() => setRun(true), 100); - return () => clearTimeout(t); - }, [isActive, activePath]); + return () => { + clearTimeout(t); + setRun(false); // Clean up state when component unmounts or path changes + }; + }, [isActive]); // Only depend on isActive starting/stopping const handleEvent = useCallback( (data: EventData) => { const { status, action, index, type } = data; - if (status === STATUS.FINISHED || status === STATUS.SKIPPED) { + // Helper to reset the tour state completely + const resetTourState = () => { setRun(false); + setStepIndex(0); + }; + + if (status === STATUS.FINISHED || status === STATUS.SKIPPED) { + resetTourState(); if (activePath) completePath(activePath); return; } @@ -206,11 +208,9 @@ function TourOverlay() { if (type === EVENTS.TARGET_NOT_FOUND) { const nextIndex = index + 1; if (nextIndex < steps.length) { - // Skip to next step if available setStepIndex(nextIndex); } else { - // No more steps, go back to path selection - setRun(false); + resetTourState(); backToPathSelection(); } return; @@ -222,7 +222,7 @@ function TourOverlay() { } else if (action === ACTIONS.PREV) { setStepIndex(Math.max(0, index - 1)); } else if (action === ACTIONS.CLOSE || action === ACTIONS.SKIP) { - setRun(false); + resetTourState(); backToPathSelection(); } } diff --git a/front/src/components/Walkthrough/desktop/WalkthroughContext.tsx b/front/src/components/Walkthrough/desktop/WalkthroughContext.tsx index d1da295cbc..72385b2394 100644 --- a/front/src/components/Walkthrough/desktop/WalkthroughContext.tsx +++ b/front/src/components/Walkthrough/desktop/WalkthroughContext.tsx @@ -1,4 +1,4 @@ -import React, { createContext, useCallback, useContext, useEffect, useMemo, useState } from "react"; +import React, { createContext, useCallback, useContext, useMemo, useState } from "react"; import { useAuth0 } from "@auth0/auth0-react"; import { PathId } from "./paths/types"; import { useVisiblePaths } from "./useVisiblePaths"; @@ -66,22 +66,36 @@ function saveState(userId: string, state: WalkthroughState) { export function WalkthroughProvider({ children }: { children: React.ReactNode }) { const { user } = useAuth0(); const userId = user?.sub ?? "anonymous"; + const visiblePaths = useVisiblePaths(); + + const [prevUserId, setPrevUserId] = useState(userId); + + const [isWalkthroughActive, setIsWalkthroughActive] = useState(() => { + const state = loadState(userId); + return !state.hasSeenWelcome; + }); + + const [currentStep, setCurrentStep] = useState(() => { + const state = loadState(userId); + return !state.hasSeenWelcome ? "welcome" : "pathSelection"; + }); + + const [completedPaths, setCompletedPaths] = useState>(() => { + const state = loadState(userId); + return new Set(state.completedPaths); + }); - const [isWalkthroughActive, setIsWalkthroughActive] = useState(false); - const [currentStep, setCurrentStep] = useState("welcome"); const [activePath, setActivePath] = useState(null); - const [completedPaths, setCompletedPaths] = useState>(new Set()); - const visiblePaths = useVisiblePaths(); - // Load persisted state and auto-show welcome for new users - useEffect(() => { + if (userId !== prevUserId) { + setPrevUserId(userId); + const state = loadState(userId); setCompletedPaths(new Set(state.completedPaths)); - if (!state.hasSeenWelcome) { - setIsWalkthroughActive(true); - setCurrentStep("welcome"); - } - }, [userId]); + setIsWalkthroughActive(!state.hasSeenWelcome); + setCurrentStep(!state.hasSeenWelcome ? "welcome" : "pathSelection"); + setActivePath(null); + } const persistCompletedPaths = useCallback( (paths: Set) => { @@ -93,11 +107,9 @@ export function WalkthroughProvider({ children }: { children: React.ReactNode }) const goToPathSelection = useCallback(() => { setIsWalkthroughActive(true); - // Mark welcome as seen const state = loadState(userId); saveState(userId, { ...state, hasSeenWelcome: true }); - // short-cut to only path if (visiblePaths.length === 1) { setActivePath(visiblePaths[0].id); setCurrentStep("tour"); @@ -110,7 +122,6 @@ export function WalkthroughProvider({ children }: { children: React.ReactNode }) const closeWalkthrough = useCallback(() => { setIsWalkthroughActive(false); setActivePath(null); - // Mark welcome as seen when user skips/closes const state = loadState(userId); saveState(userId, { ...state, hasSeenWelcome: true }); }, [userId]); diff --git a/front/src/components/Walkthrough/mobile/MobileWalkthroughContext.tsx b/front/src/components/Walkthrough/mobile/MobileWalkthroughContext.tsx index be7cdcb8a5..f0a274a623 100644 --- a/front/src/components/Walkthrough/mobile/MobileWalkthroughContext.tsx +++ b/front/src/components/Walkthrough/mobile/MobileWalkthroughContext.tsx @@ -1,4 +1,4 @@ -import React, { createContext, useCallback, useContext, useEffect, useMemo, useState } from "react"; +import React, { createContext, useCallback, useContext, useMemo, useState } from "react"; import { useAuth0 } from "@auth0/auth0-react"; import { JWT_ROLE } from "utils/constants"; import { isCoordinatorOrAbove } from "../roles"; @@ -63,16 +63,13 @@ export function MobileWalkthroughProvider({ children }: { children: React.ReactN const roles: string[] = user?.[JWT_ROLE] ?? []; const isCoordinator = isCoordinatorOrAbove(roles); - const [step, setStep] = useState("idle"); - const [slideIndex, setSlideIndex] = useState(0); - - // Auto-show welcome for first-time mobile users - useEffect(() => { + // Initialize state lazily based on local storage + const [step, setStep] = useState(() => { const state = loadState(userId); - if (!state.hasSeenWelcome) { - setStep("welcome"); - } - }, [userId]); + return state.hasSeenWelcome ? "idle" : "welcome"; + }); + + const [slideIndex, setSlideIndex] = useState(0); const markSeen = useCallback(() => { saveState(userId, { hasSeenWelcome: true }); diff --git a/front/src/views/Box/BoxView.tsx b/front/src/views/Box/BoxView.tsx index 1277debc3e..2e277a9026 100644 --- a/front/src/views/Box/BoxView.tsx +++ b/front/src/views/Box/BoxView.tsx @@ -1,4 +1,4 @@ -import { useCallback, useEffect, useMemo, useState } from "react"; +import { useCallback, useEffect, useMemo } from "react"; import { useMutation, useQuery, NetworkStatus } from "@apollo/client"; import { graphql } from "gql.tada"; import { @@ -210,7 +210,6 @@ function BTBox() { const { createToast } = useNotification(); const labelIdentifier = useParams<{ labelIdentifier: string }>().labelIdentifier!; const baseId = useAtomValue(selectedBaseIdAtom); - const [currentBoxState, setCurrentState] = useState(); const { isOpen: isHistoryOpen, onOpen: onHistoryOpen, onClose: onHistoryClose } = useDisclosure(); const { assignBoxesToShipment, @@ -231,10 +230,6 @@ function BTBox() { }, ); - const boxInTransit = currentBoxState - ? ["Receiving", "MarkedForShipment", "InTransit"].includes(currentBoxState) - : false; - // map over each box HistoryEntry to compile its timeline records const boxLogs: ITimelineEntry[] = allData.data?.box?.history?.flatMap((histories) => _.compact([ @@ -314,8 +309,11 @@ function BTBox() { const boxData = allData.data?.box; + const boxInTransit = boxData?.state + ? ["Receiving", "MarkedForShipment", "InTransit"].includes(boxData.state) + : false; + useEffect(() => { - setCurrentState(boxData?.state); const shipmentId = boxData?.shipmentDetail?.shipment.id; // open reconciliation overlay if the box state is receiving and if we're on the receiving side if ( diff --git a/front/src/views/Box/components/AddItemsToBoxOverlay.tsx b/front/src/views/Box/components/AddItemsToBoxOverlay.tsx index 4e8a3d5673..7abe1624ed 100644 --- a/front/src/views/Box/components/AddItemsToBoxOverlay.tsx +++ b/front/src/views/Box/components/AddItemsToBoxOverlay.tsx @@ -10,7 +10,7 @@ import { ModalOverlay, Spacer, } from "@chakra-ui/react"; -import { useForm } from "react-hook-form"; +import { useForm, useWatch } from "react-hook-form"; import { IChangeNumberOfItemsBoxData } from "../BoxView"; import { NumberField } from "@boxtribute/shared-components"; @@ -30,14 +30,13 @@ function AddItemsToBoxOverlay({ const { control, handleSubmit, - watch, formState: { isSubmitting, errors }, } = useForm({ defaultValues: { numberOfItems: 1, }, }); - const numberOfItems = watch("numberOfItems"); + const numberOfItems = useWatch({ control, name: "numberOfItems" }); return ( diff --git a/front/src/views/Box/components/TakeItemsFromBoxOverlay.tsx b/front/src/views/Box/components/TakeItemsFromBoxOverlay.tsx index 0776b9e117..3bd89284ec 100644 --- a/front/src/views/Box/components/TakeItemsFromBoxOverlay.tsx +++ b/front/src/views/Box/components/TakeItemsFromBoxOverlay.tsx @@ -15,7 +15,7 @@ import { VStack, } from "@chakra-ui/react"; import SelectField, { IDropdownOption } from "components/Form/SelectField"; -import { useForm } from "react-hook-form"; +import { useForm, useWatch } from "react-hook-form"; import { IChangeNumberOfItemsBoxData } from "../BoxView"; import { useAuthorization } from "hooks/useAuthorization"; import { NumberField } from "@boxtribute/shared-components"; @@ -44,7 +44,6 @@ function TakeItemsFromBoxOverlay({ const { handleSubmit, control, - watch, formState: { isSubmitting, errors }, } = useForm({ defaultValues: { @@ -54,8 +53,8 @@ function TakeItemsFromBoxOverlay({ }); const authorize = useAuthorization(); - const selectedLocationId = watch("locationId"); - const numberOfItems = watch("numberOfItems"); + const selectedLocationId = useWatch({ control, name: "locationId" }); + const numberOfItems = useWatch({ control, name: "numberOfItems" }); const onSubmit = (data: ITakeItemsFromBoxData) => { if (data.locationId?.value && onSubmitCreateBoxFromBox) { diff --git a/front/src/views/Transfers/ShipmentView/components/ShipmentContent.tsx b/front/src/views/Transfers/ShipmentView/components/ShipmentContent.tsx index 07842b4aec..7a0510c8be 100644 --- a/front/src/views/Transfers/ShipmentView/components/ShipmentContent.tsx +++ b/front/src/views/Transfers/ShipmentView/components/ShipmentContent.tsx @@ -202,7 +202,7 @@ function ShipmentContent({ }} onClick={ !isExpanded && !isLoadingMutation - ? () => onBulkRemoveBox(item.boxes.map((b) => b?.labelIdentifier!)) + ? () => onBulkRemoveBox(item.boxes.map((b) => b?.labelIdentifier ?? "")) : undefined } /> diff --git a/front/src/views/Transfers/ShipmentsOverview/components/ShipmentExportButton.tsx b/front/src/views/Transfers/ShipmentsOverview/components/ShipmentExportButton.tsx index 5cacb45f58..f7aba89098 100644 --- a/front/src/views/Transfers/ShipmentsOverview/components/ShipmentExportButton.tsx +++ b/front/src/views/Transfers/ShipmentsOverview/components/ShipmentExportButton.tsx @@ -239,6 +239,7 @@ const ShipmentExportButton: React.FC = ({ filteredRow } } } catch (err) { + console.error(err); createToast({ type: "error", message: "An error occurred while exporting shipments. Please try again.", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index fe987ed070..45a97ff2a7 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -218,7 +218,7 @@ importers: front: dependencies: '@auth0/auth0-react': - specifier: ^2.22.0 + specifier: ^2.22.1 version: 2.22.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@boxtribute/shared-components': specifier: workspace:* From cf75b85acb5731e90fd841654b4351d64b66aaa8 Mon Sep 17 00:00:00 2001 From: Maurovic Cachia Date: Thu, 17 Sep 2026 10:57:30 +0200 Subject: [PATCH 2/4] More Testing and fixes for some types --- .circleci/config.yml | 8 ++++ .../statviz/dashboard/StockOverview.test.tsx | 40 +++++++++---------- shared-components/tests/setupTests.ts | 27 +++++++++++++ shared-front/src/App.test.tsx | 8 ++-- shared-front/src/tests/setupTests.ts | 27 +++++++++++++ 5 files changed, 86 insertions(+), 24 deletions(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index 58b92f88ba..f59c77e8b5 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -119,6 +119,10 @@ jobs: FRONT_SENTRY_TRACES_SAMPLE_RATE=${SENTRY_TRACES_SAMPLE_RATE} FRONT_ENVIRONMENT=${ENVIRONMENT} " > .env + - run: + name: Run type check + command: | + pnpm -C front tsc:check && pnpm -C shared-front tsc:check - run: name: Run style checks command: | @@ -154,6 +158,10 @@ jobs: SHARED_FRONT_SENTRY_TRACES_SAMPLE_RATE=${SENTRY_TRACES_SAMPLE_RATE} SHARED_FRONT_ENVIRONMENT=${ENVIRONMENT} " > .env + - run: + name: Run type check + command: | + pnpm -C shared-components tsc:check - run: name: Run style checks command: | diff --git a/shared-components/statviz/dashboard/StockOverview.test.tsx b/shared-components/statviz/dashboard/StockOverview.test.tsx index cb60949d54..10614ff226 100644 --- a/shared-components/statviz/dashboard/StockOverview.test.tsx +++ b/shared-components/statviz/dashboard/StockOverview.test.tsx @@ -57,8 +57,8 @@ vi.mock("../components/nivo/CalendarChart", () => ({ // --------------------------------------------------------------------------- const products: IProductOption[] = [ - { id: 1, name: "Shoes", gender: "Male" }, - { id: 2, name: "Shirts", gender: "Female" }, + { id: 1, name: "Shoes", gender: "Men" }, + { id: 2, name: "Shirts", gender: "Women" }, { id: 3, name: "Pants", gender: null }, ]; @@ -86,7 +86,7 @@ const fiveMonthsAgo = format(subMonths(new Date(), 5), "yyyy-MM-dd"); const tenMonthsAgo = format(subMonths(new Date(), 10), "yyyy-MM-dd"); const fifteenMonthsAgo = format(subMonths(new Date(), 15), "yyyy-MM-dd"); // outside 1-year window -// STOCK_QUERY mock: 3 InStock facts (Shoes/Male, Shirts/Female, Pants/null) + Donated + Lost +// STOCK_QUERY mock: 3 InStock facts (Shoes/Men, Shirts/Female, Pants/null) + Donated + Lost // NOTE: dimension ids are Int (not ID string) per the GraphQL schema's BasicDimensionInfo type const stockMock = { request: { @@ -101,7 +101,7 @@ const stockMock = { // productName is lowercase to match the backend convention (used with p.name.toLowerCase() in the filter) productName: "shoes", categoryId: 1, - gender: "Male", + gender: "Men", boxesCount: 5, itemsCount: 10, sizeId: 1, @@ -135,7 +135,7 @@ const stockMock = { { productName: "shoes", categoryId: 1, - gender: "Male", + gender: "Men", boxesCount: 2, itemsCount: 4, sizeId: 1, @@ -197,7 +197,7 @@ const createdBoxesMock = { categoryId: 1, createdOn: oneMonthAgo, tagIds: [1], - gender: "Male", + gender: "Men", itemsCount: 10, }, // 5 months ago – within 1-year window @@ -217,7 +217,7 @@ const createdBoxesMock = { categoryId: 1, createdOn: tenMonthsAgo, tagIds: [1], - gender: "Male", + gender: "Men", itemsCount: 14, }, // 15 months ago – OUTSIDE the default 1-year window, should be filtered out @@ -233,8 +233,8 @@ const createdBoxesMock = { ], dimensions: { product: [ - { id: 1, name: "Shoes", gender: "Male" }, - { id: 2, name: "Shirts", gender: "Female" }, + { id: 1, name: "Shoes", gender: "Men" }, + { id: 2, name: "Shirts", gender: "Women" }, { id: 3, name: "Pants", gender: null }, ], category: [ @@ -330,7 +330,7 @@ describe("StockOverview", () => { describe("product filter", () => { it("shows only the matching product in ring, bar, and calendar; displays filter chip", async () => { - // sp=1 → product id 1 (Shoes, Male) + // sp=1 → product id 1 (Shoes, Men) renderStockOverview("?sp=1"); // Ring chart: only InStock Shoes fact (cat=Footwear, boxesCount=5) @@ -344,8 +344,8 @@ describe("StockOverview", () => { // Calendar: 2 facts match productId=1 (1 month + 10 months ago) expect(screen.getAllByTestId("calendar-day")).toHaveLength(2); - // Filter chip shows "Shoes (Male)" - expect(screen.getByText("Shoes (Male)")).toBeInTheDocument(); + // Filter chip shows "Shoes (Men)" + expect(screen.getByText("Shoes (Men)")).toBeInTheDocument(); expect(screen.getByTestId("stock-clear-all-filters-button")).toBeInTheDocument(); }); }); @@ -356,21 +356,21 @@ describe("StockOverview", () => { describe("gender filter", () => { it("shows only the matching gender in ring, bar, and calendar; displays filter chip", async () => { - // sg=Male - renderStockOverview("?sg=Male"); + // sg=Men + renderStockOverview("?sg=Men"); - // Ring chart: only Male InStock (Shoes, Footwear, boxesCount=5) + // Ring chart: only Men InStock (Shoes, Footwear, boxesCount=5) expect(await screen.findByText("Footwear: 5")).toBeInTheDocument(); expect(screen.getAllByTestId("pie-slice")).toHaveLength(1); // Bar chart: only Footwear expect(screen.getAllByTestId("bar-category")).toHaveLength(1); - // Calendar: 2 facts have gender=Male (1 month + 10 months ago) + // Calendar: 2 facts have gender=Men (1 month + 10 months ago) expect(screen.getAllByTestId("calendar-day")).toHaveLength(2); // Filter chip - expect(screen.getByText("Male")).toBeInTheDocument(); + expect(screen.getByText("Men")).toBeInTheDocument(); }); }); @@ -515,7 +515,7 @@ describe("StockOverview", () => { describe("too many filters", () => { it("shows no data information if selected filters don't match any data", async () => { // sc=2 → category id 2 (Clothes) - renderStockOverview("?sg=Male&sc=2"); + renderStockOverview("?sg=Men&sc=2"); // "No data" information shown (should be 3 though?) expect(await screen.findAllByText(/no data available/i)).toHaveLength(2); @@ -527,7 +527,7 @@ describe("StockOverview", () => { // Filter chips expect(screen.getByTestId("stock-filter-chip-close-category-2")).toBeInTheDocument(); - expect(screen.getByText("Male")).toBeInTheDocument(); + expect(screen.getByText("Men")).toBeInTheDocument(); }); }); @@ -544,7 +544,7 @@ describe("StockOverview", () => { await screen.findByText("Footwear: 5"); expect(screen.getAllByTestId("pie-slice")).toHaveLength(1); - // Click the close button on the "Shoes (Male)" chip + // Click the close button on the "Shoes (Men)" chip const closeBtn = screen.getByTestId("stock-filter-chip-close-product-1"); await user.click(closeBtn); diff --git a/shared-components/tests/setupTests.ts b/shared-components/tests/setupTests.ts index ca1482f466..923faac3be 100644 --- a/shared-components/tests/setupTests.ts +++ b/shared-components/tests/setupTests.ts @@ -19,3 +19,30 @@ Object.defineProperty(window, "matchMedia", { dispatchEvent: vi.fn(), })), }); + +// needed to avoid "running clear() on undefined" when executing `pnpm test` outside docker or CI +const mockLocalStorage = (() => { + let store = {} as Storage; + + return { + getItem(key: string) { + return store[key]; + }, + + setItem(key: string, value: string) { + store[key] = value; + }, + + removeItem(key: string) { + delete store[key]; + }, + + clear() { + store = {} as Storage; + }, + }; +})(); + +Object.defineProperty(window, "localStorage", { + value: mockLocalStorage, +}); diff --git a/shared-front/src/App.test.tsx b/shared-front/src/App.test.tsx index 85879552c5..1176887e62 100644 --- a/shared-front/src/App.test.tsx +++ b/shared-front/src/App.test.tsx @@ -57,7 +57,7 @@ const stockOverviewData = { { productName: "shoes", categoryId: 1, - gender: "Male", + gender: "Men", boxesCount: 5, itemsCount: 10, sizeId: 1, @@ -68,7 +68,7 @@ const stockOverviewData = { { productName: "shirts", categoryId: 2, - gender: "Female", + gender: "Women", boxesCount: 3, itemsCount: 6, sizeId: 2, @@ -203,8 +203,8 @@ describe("Shared FE", () => { expect(await screen.findByTestId("pie-chart")).toBeInTheDocument(); expect(screen.getByTestId("pie-center")).toHaveTextContent("8 boxes"); - expect(screen.getByText("Male: 5")).toBeInTheDocument(); - expect(screen.getByText("Female: 3")).toBeInTheDocument(); + expect(screen.getByText("Men: 5")).toBeInTheDocument(); + expect(screen.getByText("Women: 3")).toBeInTheDocument(); }); it("groups by category when srg=cn", async () => { diff --git a/shared-front/src/tests/setupTests.ts b/shared-front/src/tests/setupTests.ts index ca1482f466..923faac3be 100644 --- a/shared-front/src/tests/setupTests.ts +++ b/shared-front/src/tests/setupTests.ts @@ -19,3 +19,30 @@ Object.defineProperty(window, "matchMedia", { dispatchEvent: vi.fn(), })), }); + +// needed to avoid "running clear() on undefined" when executing `pnpm test` outside docker or CI +const mockLocalStorage = (() => { + let store = {} as Storage; + + return { + getItem(key: string) { + return store[key]; + }, + + setItem(key: string, value: string) { + store[key] = value; + }, + + removeItem(key: string) { + delete store[key]; + }, + + clear() { + store = {} as Storage; + }, + }; +})(); + +Object.defineProperty(window, "localStorage", { + value: mockLocalStorage, +}); From e151900ab351c817fa413eeaa27a59eef67ec51e Mon Sep 17 00:00:00 2001 From: Maurovic Cachia Date: Thu, 17 Sep 2026 11:04:39 +0200 Subject: [PATCH 3/4] Hotfix for commands --- .circleci/config.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index f59c77e8b5..afc5220646 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -122,7 +122,7 @@ jobs: - run: name: Run type check command: | - pnpm -C front tsc:check && pnpm -C shared-front tsc:check + pnpm tsc:check - run: name: Run style checks command: | @@ -161,7 +161,7 @@ jobs: - run: name: Run type check command: | - pnpm -C shared-components tsc:check + pnpm tsc:check && pnpm -C ../shared-components tsc:check - run: name: Run style checks command: | From f5dfe9d829f17efbe39628b812e3ce88562f99bd Mon Sep 17 00:00:00 2001 From: Maurovic Cachia Date: Sun, 27 Sep 2026 18:12:29 +0200 Subject: [PATCH 4/4] Further Clean up --- .../statviz/dashboard/StockOverview.test.tsx | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/shared-components/statviz/dashboard/StockOverview.test.tsx b/shared-components/statviz/dashboard/StockOverview.test.tsx index 10614ff226..1eacf1e3af 100644 --- a/shared-components/statviz/dashboard/StockOverview.test.tsx +++ b/shared-components/statviz/dashboard/StockOverview.test.tsx @@ -86,7 +86,7 @@ const fiveMonthsAgo = format(subMonths(new Date(), 5), "yyyy-MM-dd"); const tenMonthsAgo = format(subMonths(new Date(), 10), "yyyy-MM-dd"); const fifteenMonthsAgo = format(subMonths(new Date(), 15), "yyyy-MM-dd"); // outside 1-year window -// STOCK_QUERY mock: 3 InStock facts (Shoes/Men, Shirts/Female, Pants/null) + Donated + Lost +// STOCK_QUERY mock: 3 InStock facts (Shoes/Men, Shirts/Women, Pants/null) + Donated + Lost // NOTE: dimension ids are Int (not ID string) per the GraphQL schema's BasicDimensionInfo type const stockMock = { request: { @@ -112,7 +112,7 @@ const stockMock = { { productName: "shirts", categoryId: 2, - gender: "Female", + gender: "Women", boxesCount: 3, itemsCount: 6, sizeId: 1, @@ -147,7 +147,7 @@ const stockMock = { { productName: "shirts", categoryId: 2, - gender: "Female", + gender: "Women", boxesCount: 1, itemsCount: 2, sizeId: 1, @@ -207,7 +207,7 @@ const createdBoxesMock = { categoryId: 2, createdOn: fiveMonthsAgo, tagIds: [2], - gender: "Female", + gender: "Women", itemsCount: 6, }, // 10 months ago – within 1-year window