diff --git a/client/src/components/shop-info/shop-info.component.jsx b/client/src/components/shop-info/shop-info.component.jsx index 8dd04843e..c74427724 100644 --- a/client/src/components/shop-info/shop-info.component.jsx +++ b/client/src/components/shop-info/shop-info.component.jsx @@ -1,6 +1,7 @@ import { useTreatmentsWithConfig } from "@splitsoftware/splitio-react"; import { Button, Card, Tabs } from "antd"; import queryString from "query-string"; +import { useRef } from "react"; import { useTranslation } from "react-i18next"; import { connect } from "react-redux"; import { useLocation, useNavigate } from "react-router-dom"; @@ -21,6 +22,7 @@ import ShopInfoResponsibilityCenterComponent from "./shop-info.responsibilitycen import ShopInfoRoGuard from "./shop-info.roguard.component"; import ShopInfoROStatusComponent from "./shop-info.rostatus.component"; import ShopInfoSchedulingComponent from "./shop-info.scheduling.component"; +import ShopInfoSectionNavigator from "./shop-info.section-navigator.component.jsx"; import ShopInfoSpeedPrint from "./shop-info.speedprint.component"; import ShopInfoTaskPresets from "./shop-info.task-presets.component"; import ShopInfoIntellipay from "./shop-intellipay-config.component"; @@ -47,6 +49,7 @@ export function ShopInfoComponent({ bodyshop, form, saveLoading }) { const history = useNavigate(); const location = useLocation(); const search = queryString.parse(location.search); + const tabsRef = useRef(null); const tabItems = [ { @@ -154,23 +157,28 @@ export function ShopInfoComponent({ bodyshop, form, saveLoading }) { ] : []) ]; + const activeTabKey = search.subtab || tabItems[0]?.key; + return ( } extra={ } > - - history({ - search: `?tab=${search.tab}&subtab=${key}` - }) - } - items={tabItems} - /> +
+ + history({ + search: `?tab=${search.tab}&subtab=${key}` + }) + } + items={tabItems} + /> +
); } diff --git a/client/src/components/shop-info/shop-info.section-navigator.component.jsx b/client/src/components/shop-info/shop-info.section-navigator.component.jsx new file mode 100644 index 000000000..69fd36e85 --- /dev/null +++ b/client/src/components/shop-info/shop-info.section-navigator.component.jsx @@ -0,0 +1,137 @@ +import { Select } from "antd"; +import { useEffect, useRef, useState } from "react"; +import { useTranslation } from "react-i18next"; +import "./shop-info.section-navigator.styles.scss"; + +const HIGHLIGHT_CLASS = "shop-info-section-navigator__target--active"; + +export default function ShopInfoSectionNavigator({ tabsRef, activeTabKey }) { + const { t } = useTranslation(); + const targetMapRef = useRef(new Map()); + const highlightedTargetRef = useRef(null); + const [options, setOptions] = useState([]); + const [selectedSection, setSelectedSection] = useState(undefined); + + useEffect(() => { + const tabsContainer = tabsRef.current; + if (!tabsContainer) return undefined; + + let animationFrameId = 0; + + const refreshOptions = () => { + const activePane = tabsContainer.querySelector(".ant-tabs-tabpane-active"); + if (!activePane) { + targetMapRef.current = new Map(); + setOptions([]); + return; + } + + const nextTargetMap = new Map(); + const nextOptions = Array.from(activePane.querySelectorAll(".imex-form-row")) + .filter((card) => { + const titleNode = getOwnCardTitleNode(card); + if (!titleNode?.textContent?.trim()) return false; + + const ancestorCard = card.parentElement?.closest(".imex-form-row"); + return !ancestorCard || !activePane.contains(ancestorCard); + }) + .map((card, index) => { + const label = getOwnCardTitleNode(card)?.textContent?.trim(); + const value = `${activeTabKey}-shop-info-section-${index}`; + + nextTargetMap.set(value, card); + + return { + label, + value + }; + }); + + targetMapRef.current = nextTargetMap; + setOptions((currentOptions) => (areOptionsEqual(currentOptions, nextOptions) ? currentOptions : nextOptions)); + }; + + const scheduleRefresh = () => { + cancelAnimationFrame(animationFrameId); + animationFrameId = requestAnimationFrame(refreshOptions); + }; + + scheduleRefresh(); + + const observer = new MutationObserver(scheduleRefresh); + observer.observe(tabsContainer, { + childList: true, + subtree: true, + characterData: true, + attributes: true, + attributeFilter: ["class"] + }); + + return () => { + cancelAnimationFrame(animationFrameId); + observer.disconnect(); + }; + }, [activeTabKey, tabsRef]); + + useEffect(() => { + clearHighlightedTarget(highlightedTargetRef); + setSelectedSection(undefined); + }, [activeTabKey]); + + const handleSectionChange = (value) => { + setSelectedSection(value); + + clearHighlightedTarget(highlightedTargetRef); + if (!value) return; + + const target = targetMapRef.current.get(value); + if (target) { + target.classList.add(HIGHLIGHT_CLASS); + highlightedTargetRef.current = target; + target.scrollIntoView({ + behavior: "smooth", + block: "start" + }); + } + + window.setTimeout(() => { + setSelectedSection(undefined); + }, 0); + }; + + return ( +
+