feature/IO-3624-Shop-Config-UX-Refresh - Add Quick Select Jump
This commit is contained in:
@@ -1,6 +1,7 @@
|
|||||||
import { useTreatmentsWithConfig } from "@splitsoftware/splitio-react";
|
import { useTreatmentsWithConfig } from "@splitsoftware/splitio-react";
|
||||||
import { Button, Card, Tabs } from "antd";
|
import { Button, Card, Tabs } from "antd";
|
||||||
import queryString from "query-string";
|
import queryString from "query-string";
|
||||||
|
import { useRef } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { connect } from "react-redux";
|
import { connect } from "react-redux";
|
||||||
import { useLocation, useNavigate } from "react-router-dom";
|
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 ShopInfoRoGuard from "./shop-info.roguard.component";
|
||||||
import ShopInfoROStatusComponent from "./shop-info.rostatus.component";
|
import ShopInfoROStatusComponent from "./shop-info.rostatus.component";
|
||||||
import ShopInfoSchedulingComponent from "./shop-info.scheduling.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 ShopInfoSpeedPrint from "./shop-info.speedprint.component";
|
||||||
import ShopInfoTaskPresets from "./shop-info.task-presets.component";
|
import ShopInfoTaskPresets from "./shop-info.task-presets.component";
|
||||||
import ShopInfoIntellipay from "./shop-intellipay-config.component";
|
import ShopInfoIntellipay from "./shop-intellipay-config.component";
|
||||||
@@ -47,6 +49,7 @@ export function ShopInfoComponent({ bodyshop, form, saveLoading }) {
|
|||||||
const history = useNavigate();
|
const history = useNavigate();
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
const search = queryString.parse(location.search);
|
const search = queryString.parse(location.search);
|
||||||
|
const tabsRef = useRef(null);
|
||||||
|
|
||||||
const tabItems = [
|
const tabItems = [
|
||||||
{
|
{
|
||||||
@@ -154,23 +157,28 @@ export function ShopInfoComponent({ bodyshop, form, saveLoading }) {
|
|||||||
]
|
]
|
||||||
: [])
|
: [])
|
||||||
];
|
];
|
||||||
|
const activeTabKey = search.subtab || tabItems[0]?.key;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card
|
<Card
|
||||||
|
title={<ShopInfoSectionNavigator tabsRef={tabsRef} activeTabKey={activeTabKey} />}
|
||||||
extra={
|
extra={
|
||||||
<Button type="primary" loading={saveLoading} onClick={() => form.submit()} id="shop-info-save-button">
|
<Button type="primary" loading={saveLoading} onClick={() => form.submit()} id="shop-info-save-button">
|
||||||
{t("general.actions.save")}
|
{t("general.actions.save")}
|
||||||
</Button>
|
</Button>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<Tabs
|
<div ref={tabsRef}>
|
||||||
defaultActiveKey={search.subtab}
|
<Tabs
|
||||||
onChange={(key) =>
|
activeKey={activeTabKey}
|
||||||
history({
|
onChange={(key) =>
|
||||||
search: `?tab=${search.tab}&subtab=${key}`
|
history({
|
||||||
})
|
search: `?tab=${search.tab}&subtab=${key}`
|
||||||
}
|
})
|
||||||
items={tabItems}
|
}
|
||||||
/>
|
items={tabItems}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</Card>
|
</Card>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 (
|
||||||
|
<div className="shop-info-section-navigator">
|
||||||
|
<Select
|
||||||
|
allowClear
|
||||||
|
showSearch={{ optionFilterProp: "label" }}
|
||||||
|
value={selectedSection}
|
||||||
|
placeholder={t("bodyshop.labels.jump_to_section")}
|
||||||
|
options={options}
|
||||||
|
popupMatchSelectWidth={false}
|
||||||
|
disabled={options.length === 0}
|
||||||
|
onChange={handleSectionChange}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function getOwnCardTitleNode(card) {
|
||||||
|
const headNode = Array.from(card.children).find((child) => child.classList?.contains("ant-card-head"));
|
||||||
|
return headNode?.querySelector(".ant-card-head-title");
|
||||||
|
}
|
||||||
|
|
||||||
|
function clearHighlightedTarget(highlightedTargetRef) {
|
||||||
|
if (highlightedTargetRef.current) {
|
||||||
|
highlightedTargetRef.current.classList.remove(HIGHLIGHT_CLASS);
|
||||||
|
highlightedTargetRef.current = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function areOptionsEqual(currentOptions, nextOptions) {
|
||||||
|
if (currentOptions.length !== nextOptions.length) return false;
|
||||||
|
|
||||||
|
return currentOptions.every((option, index) => {
|
||||||
|
const nextOption = nextOptions[index];
|
||||||
|
return option.label === nextOption.label && option.value === nextOption.value;
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
.shop-info-section-navigator {
|
||||||
|
max-width: 360px;
|
||||||
|
width: min(360px, 100%);
|
||||||
|
|
||||||
|
.ant-select {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.imex-form-row.shop-info-section-navigator__target--active.ant-card {
|
||||||
|
border-color: color-mix(
|
||||||
|
in srgb,
|
||||||
|
var(--ant-colorPrimary, #1890ff) 65%,
|
||||||
|
var(--imex-form-surface-border)
|
||||||
|
);
|
||||||
|
background: color-mix(in srgb, var(--ant-colorPrimary, #1890ff) 7%, var(--imex-form-surface));
|
||||||
|
box-shadow: 0 0 0 3px color-mix(in srgb, var(--ant-colorPrimary, #1890ff) 24%, transparent);
|
||||||
|
transition: border-color 0.2s ease,
|
||||||
|
background-color 0.2s ease,
|
||||||
|
box-shadow 0.2s ease;
|
||||||
|
|
||||||
|
.ant-card-head {
|
||||||
|
background: color-mix(in srgb, var(--ant-colorPrimary, #1890ff) 12%, var(--imex-form-surface-head));
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-card-body {
|
||||||
|
background: color-mix(in srgb, var(--ant-colorPrimary, #1890ff) 7%, var(--imex-form-surface));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -765,6 +765,7 @@
|
|||||||
"notification_options": "Notification Options",
|
"notification_options": "Notification Options",
|
||||||
"notemplatesavailable": "No templates available to add.",
|
"notemplatesavailable": "No templates available to add.",
|
||||||
"notespresets": "Notes Presets",
|
"notespresets": "Notes Presets",
|
||||||
|
"jump_to_section": "Jump to section",
|
||||||
"notifications": {
|
"notifications": {
|
||||||
"followers": "Notifications"
|
"followers": "Notifications"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -765,6 +765,7 @@
|
|||||||
"notification_options": "",
|
"notification_options": "",
|
||||||
"notemplatesavailable": "",
|
"notemplatesavailable": "",
|
||||||
"notespresets": "",
|
"notespresets": "",
|
||||||
|
"jump_to_section": "",
|
||||||
"notifications": {
|
"notifications": {
|
||||||
"followers": ""
|
"followers": ""
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -765,6 +765,7 @@
|
|||||||
"notification_options": "",
|
"notification_options": "",
|
||||||
"notemplatesavailable": "",
|
"notemplatesavailable": "",
|
||||||
"notespresets": "",
|
"notespresets": "",
|
||||||
|
"jump_to_section": "",
|
||||||
"notifications": {
|
"notifications": {
|
||||||
"followers": ""
|
"followers": ""
|
||||||
},
|
},
|
||||||
|
|||||||
Reference in New Issue
Block a user