feature/IO-3701-Harness-Replacement - Implement
This commit is contained in:
@@ -1,184 +0,0 @@
|
||||
import { ApolloProvider } from "@apollo/client/react";
|
||||
import * as Sentry from "@sentry/react";
|
||||
import { SplitFactoryProvider, useSplitClient } from "@splitsoftware/splitio-react";
|
||||
import { ConfigProvider, Grid } from "antd";
|
||||
import enLocale from "antd/es/locale/en_US";
|
||||
import { useEffect, useMemo } from "react";
|
||||
import { CookiesProvider } from "react-cookie";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useDispatch, useSelector } from "react-redux";
|
||||
import GlobalLoadingBar from "../components/global-loading-bar/global-loading-bar.component";
|
||||
import { setDarkMode } from "../redux/application/application.actions";
|
||||
import { selectDarkMode } from "../redux/application/application.selectors";
|
||||
import { selectCurrentUser } from "../redux/user/user.selectors.js";
|
||||
import { signOutStart } from "../redux/user/user.actions";
|
||||
import client from "../utils/GraphQLClient";
|
||||
import App from "./App";
|
||||
import getTheme from "./themeProvider";
|
||||
|
||||
// Base Split configuration
|
||||
const config = {
|
||||
core: {
|
||||
authorizationKey: import.meta.env.VITE_APP_SPLIT_API,
|
||||
key: "anon"
|
||||
}
|
||||
};
|
||||
|
||||
function SplitClientProvider({ children }) {
|
||||
const imexshopid = useSelector((state) => state.user.imexshopid);
|
||||
const splitClient = useSplitClient({ key: imexshopid || "anon" });
|
||||
|
||||
useEffect(() => {
|
||||
if (import.meta.env.DEV && splitClient && imexshopid) {
|
||||
console.log(`Split client initialized with key: ${imexshopid}, isReady: ${splitClient.isReady}`);
|
||||
}
|
||||
}, [splitClient, imexshopid]);
|
||||
|
||||
return children;
|
||||
}
|
||||
|
||||
function AppContainer() {
|
||||
const { t } = useTranslation();
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const currentUser = useSelector(selectCurrentUser);
|
||||
const isDarkMode = useSelector(selectDarkMode);
|
||||
const screens = Grid.useBreakpoint();
|
||||
const isPhone = !screens.md;
|
||||
const isUltraWide = Boolean(screens.xxxl);
|
||||
|
||||
const theme = useMemo(() => {
|
||||
const baseTheme = getTheme(isDarkMode);
|
||||
|
||||
return {
|
||||
...baseTheme,
|
||||
token: {
|
||||
...(baseTheme.token || {}),
|
||||
screenXXXL: 2160
|
||||
},
|
||||
components: {
|
||||
...(baseTheme.components || {}),
|
||||
Table: {
|
||||
...(baseTheme.components?.Table || {}),
|
||||
cellFontSizeSM: isPhone ? 12 : 13,
|
||||
cellFontSizeMD: isPhone ? 13 : isUltraWide ? 15 : 14,
|
||||
cellFontSize: isUltraWide ? 15 : 14,
|
||||
cellPaddingInlineSM: isPhone ? 8 : 10,
|
||||
cellPaddingInlineMD: isPhone ? 10 : 14,
|
||||
cellPaddingInline: isUltraWide ? 20 : 16,
|
||||
cellPaddingBlockSM: isPhone ? 8 : 10,
|
||||
cellPaddingBlockMD: isPhone ? 10 : 12,
|
||||
cellPaddingBlock: isUltraWide ? 14 : 12,
|
||||
selectionColumnWidth: isPhone ? 44 : 52
|
||||
}
|
||||
}
|
||||
};
|
||||
}, [isDarkMode, isPhone, isUltraWide]);
|
||||
|
||||
const antdInput = useMemo(() => ({ autoComplete: "new-password" }), []);
|
||||
const antdTable = useMemo(() => ({ scroll: { x: "max-content" } }), []);
|
||||
const antdPagination = useMemo(
|
||||
() => ({
|
||||
showSizeChanger: !isPhone,
|
||||
totalBoundaryShowSizeChanger: 100
|
||||
}),
|
||||
[isPhone]
|
||||
);
|
||||
|
||||
const antdForm = useMemo(
|
||||
() => ({
|
||||
validateMessages: {
|
||||
required: t("general.validation.required", { label: "${label}" })
|
||||
}
|
||||
}),
|
||||
[t]
|
||||
);
|
||||
|
||||
// Global seamless logout listener with redirect to /signin
|
||||
useEffect(() => {
|
||||
const handleSeamlessLogout = (event) => {
|
||||
if (event.data?.type !== "seamlessLogoutRequest") return;
|
||||
|
||||
// Only accept messages from the parent window
|
||||
if (event.source !== window.parent) return;
|
||||
|
||||
const targetOrigin = event.origin || "*";
|
||||
|
||||
if (currentUser?.authorized !== true) {
|
||||
window.parent?.postMessage({ type: "seamlessLogoutResponse", status: "already_logged_out" }, targetOrigin);
|
||||
return;
|
||||
}
|
||||
|
||||
dispatch(signOutStart());
|
||||
window.parent?.postMessage({ type: "seamlessLogoutResponse", status: "logged_out" }, targetOrigin);
|
||||
};
|
||||
|
||||
window.addEventListener("message", handleSeamlessLogout);
|
||||
return () => {
|
||||
window.removeEventListener("message", handleSeamlessLogout);
|
||||
};
|
||||
}, [dispatch, currentUser?.authorized]);
|
||||
|
||||
// Update data-theme attribute (no cleanup to avoid transient style churn)
|
||||
useEffect(() => {
|
||||
document.documentElement.dataset.theme = isDarkMode ? "dark" : "light";
|
||||
}, [isDarkMode]);
|
||||
|
||||
// Sync darkMode with localStorage
|
||||
useEffect(() => {
|
||||
const uid = currentUser?.uid;
|
||||
|
||||
if (!uid) {
|
||||
dispatch(setDarkMode(false));
|
||||
return;
|
||||
}
|
||||
|
||||
const key = `dark-mode-${uid}`;
|
||||
const raw = localStorage.getItem(key);
|
||||
|
||||
if (raw == null) {
|
||||
dispatch(setDarkMode(false));
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
dispatch(setDarkMode(Boolean(JSON.parse(raw))));
|
||||
} catch {
|
||||
dispatch(setDarkMode(false));
|
||||
}
|
||||
}, [currentUser?.uid, dispatch]);
|
||||
|
||||
// Persist darkMode
|
||||
useEffect(() => {
|
||||
const uid = currentUser?.uid;
|
||||
if (!uid) return;
|
||||
|
||||
localStorage.setItem(`dark-mode-${uid}`, JSON.stringify(isDarkMode));
|
||||
}, [isDarkMode, currentUser?.uid]);
|
||||
|
||||
return (
|
||||
<CookiesProvider>
|
||||
<ApolloProvider client={client}>
|
||||
<ConfigProvider
|
||||
input={antdInput}
|
||||
locale={enLocale}
|
||||
theme={theme}
|
||||
form={antdForm}
|
||||
table={antdTable}
|
||||
pagination={antdPagination}
|
||||
componentSize={isPhone ? "small" : isUltraWide ? "large" : "middle"}
|
||||
popupOverflow="viewport"
|
||||
>
|
||||
<GlobalLoadingBar />
|
||||
<SplitFactoryProvider config={config}>
|
||||
<SplitClientProvider>
|
||||
<App />
|
||||
</SplitClientProvider>
|
||||
</SplitFactoryProvider>
|
||||
</ConfigProvider>
|
||||
</ApolloProvider>
|
||||
</CookiesProvider>
|
||||
);
|
||||
}
|
||||
|
||||
export default Sentry.withProfiler(AppContainer);
|
||||
@@ -1,6 +1,6 @@
|
||||
import { ApolloProvider } from "@apollo/client/react";
|
||||
import * as Sentry from "@sentry/react";
|
||||
import { SplitFactoryProvider, useSplitClient } from "@splitsoftware/splitio-react";
|
||||
import { FeatureFlagProvider, useFeatureFlagClient } from "../feature-flags/splitio-react-replacement";
|
||||
import { ConfigProvider } from "antd";
|
||||
import enLocale from "antd/es/locale/en_US";
|
||||
import { useEffect, useMemo } from "react";
|
||||
@@ -16,23 +16,21 @@ import client from "../utils/GraphQLClient";
|
||||
import App from "./App";
|
||||
import getTheme from "./themeProvider";
|
||||
|
||||
// Base Split configuration
|
||||
const config = {
|
||||
core: {
|
||||
authorizationKey: import.meta.env.VITE_APP_SPLIT_API,
|
||||
key: "anon"
|
||||
}
|
||||
};
|
||||
|
||||
function SplitClientProvider({ children }) {
|
||||
function FeatureFlagClientProvider({ children }) {
|
||||
const imexshopid = useSelector((state) => state.user.imexshopid);
|
||||
const splitClient = useSplitClient({ key: imexshopid || "anon" });
|
||||
const featureFlagClient = useFeatureFlagClient({ key: imexshopid || "anon" });
|
||||
|
||||
useEffect(() => {
|
||||
if (import.meta.env.DEV && splitClient && imexshopid) {
|
||||
console.log(`Split client initialized with key: ${imexshopid}, isReady: ${splitClient.isReady}`);
|
||||
if (import.meta.env.DEV && featureFlagClient && imexshopid) {
|
||||
console.log(`Feature flag client initialized with key: ${imexshopid}, isReady: ${featureFlagClient.isReady}`);
|
||||
}
|
||||
}, [splitClient, imexshopid]);
|
||||
}, [featureFlagClient, imexshopid]);
|
||||
|
||||
return children;
|
||||
}
|
||||
@@ -124,11 +122,11 @@ function AppContainer() {
|
||||
<ApolloProvider client={client}>
|
||||
<ConfigProvider input={antdInput} locale={enLocale} theme={theme} form={antdForm}>
|
||||
<GlobalLoadingBar />
|
||||
<SplitFactoryProvider config={config}>
|
||||
<SplitClientProvider>
|
||||
<FeatureFlagProvider config={config}>
|
||||
<FeatureFlagClientProvider>
|
||||
<App />
|
||||
</SplitClientProvider>
|
||||
</SplitFactoryProvider>
|
||||
</FeatureFlagClientProvider>
|
||||
</FeatureFlagProvider>
|
||||
</ConfigProvider>
|
||||
</ApolloProvider>
|
||||
</CookiesProvider>
|
||||
|
||||
@@ -1,184 +0,0 @@
|
||||
import { ApolloProvider } from "@apollo/client/react";
|
||||
import * as Sentry from "@sentry/react";
|
||||
import { SplitFactoryProvider, useSplitClient } from "@splitsoftware/splitio-react";
|
||||
import { ConfigProvider, Grid } from "antd";
|
||||
import enLocale from "antd/es/locale/en_US";
|
||||
import { useEffect, useMemo } from "react";
|
||||
import { CookiesProvider } from "react-cookie";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useDispatch, useSelector } from "react-redux";
|
||||
import GlobalLoadingBar from "../components/global-loading-bar/global-loading-bar.component";
|
||||
import { setDarkMode } from "../redux/application/application.actions";
|
||||
import { selectDarkMode } from "../redux/application/application.selectors";
|
||||
import { selectCurrentUser } from "../redux/user/user.selectors.js";
|
||||
import { signOutStart } from "../redux/user/user.actions";
|
||||
import client from "../utils/GraphQLClient";
|
||||
import App from "./App";
|
||||
import getTheme from "./themeProvider";
|
||||
|
||||
// Base Split configuration
|
||||
const config = {
|
||||
core: {
|
||||
authorizationKey: import.meta.env.VITE_APP_SPLIT_API,
|
||||
key: "anon"
|
||||
}
|
||||
};
|
||||
|
||||
function SplitClientProvider({ children }) {
|
||||
const imexshopid = useSelector((state) => state.user.imexshopid);
|
||||
const splitClient = useSplitClient({ key: imexshopid || "anon" });
|
||||
|
||||
useEffect(() => {
|
||||
if (import.meta.env.DEV && splitClient && imexshopid) {
|
||||
console.log(`Split client initialized with key: ${imexshopid}, isReady: ${splitClient.isReady}`);
|
||||
}
|
||||
}, [splitClient, imexshopid]);
|
||||
|
||||
return children;
|
||||
}
|
||||
|
||||
function AppContainer() {
|
||||
const { t } = useTranslation();
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const currentUser = useSelector(selectCurrentUser);
|
||||
const isDarkMode = useSelector(selectDarkMode);
|
||||
const screens = Grid.useBreakpoint();
|
||||
const isPhone = !screens.md;
|
||||
const isUltraWide = Boolean(screens.xxxl);
|
||||
|
||||
const theme = useMemo(() => {
|
||||
const baseTheme = getTheme(isDarkMode);
|
||||
|
||||
return {
|
||||
...baseTheme,
|
||||
token: {
|
||||
...(baseTheme.token || {}),
|
||||
screenXXXL: 2160
|
||||
},
|
||||
components: {
|
||||
...(baseTheme.components || {}),
|
||||
Table: {
|
||||
...(baseTheme.components?.Table || {}),
|
||||
cellFontSizeSM: isPhone ? 12 : 13,
|
||||
cellFontSizeMD: isPhone ? 13 : isUltraWide ? 15 : 14,
|
||||
cellFontSize: isUltraWide ? 15 : 14,
|
||||
cellPaddingInlineSM: isPhone ? 8 : 10,
|
||||
cellPaddingInlineMD: isPhone ? 10 : 14,
|
||||
cellPaddingInline: isUltraWide ? 20 : 16,
|
||||
cellPaddingBlockSM: isPhone ? 8 : 10,
|
||||
cellPaddingBlockMD: isPhone ? 10 : 12,
|
||||
cellPaddingBlock: isUltraWide ? 14 : 12,
|
||||
selectionColumnWidth: isPhone ? 44 : 52
|
||||
}
|
||||
}
|
||||
};
|
||||
}, [isDarkMode, isPhone, isUltraWide]);
|
||||
|
||||
const antdInput = useMemo(() => ({ autoComplete: "new-password" }), []);
|
||||
const antdTable = useMemo(() => ({ scroll: { x: "max-content" } }), []);
|
||||
const antdPagination = useMemo(
|
||||
() => ({
|
||||
showSizeChanger: !isPhone,
|
||||
totalBoundaryShowSizeChanger: 100
|
||||
}),
|
||||
[isPhone]
|
||||
);
|
||||
|
||||
const antdForm = useMemo(
|
||||
() => ({
|
||||
validateMessages: {
|
||||
required: t("general.validation.required", { label: "${label}" })
|
||||
}
|
||||
}),
|
||||
[t]
|
||||
);
|
||||
|
||||
// Global seamless logout listener with redirect to /signin
|
||||
useEffect(() => {
|
||||
const handleSeamlessLogout = (event) => {
|
||||
if (event.data?.type !== "seamlessLogoutRequest") return;
|
||||
|
||||
// Only accept messages from the parent window
|
||||
if (event.source !== window.parent) return;
|
||||
|
||||
const targetOrigin = event.origin || "*";
|
||||
|
||||
if (currentUser?.authorized !== true) {
|
||||
window.parent?.postMessage({ type: "seamlessLogoutResponse", status: "already_logged_out" }, targetOrigin);
|
||||
return;
|
||||
}
|
||||
|
||||
dispatch(signOutStart());
|
||||
window.parent?.postMessage({ type: "seamlessLogoutResponse", status: "logged_out" }, targetOrigin);
|
||||
};
|
||||
|
||||
window.addEventListener("message", handleSeamlessLogout);
|
||||
return () => {
|
||||
window.removeEventListener("message", handleSeamlessLogout);
|
||||
};
|
||||
}, [dispatch, currentUser?.authorized]);
|
||||
|
||||
// Update data-theme attribute (no cleanup to avoid transient style churn)
|
||||
useEffect(() => {
|
||||
document.documentElement.dataset.theme = isDarkMode ? "dark" : "light";
|
||||
}, [isDarkMode]);
|
||||
|
||||
// Sync darkMode with localStorage
|
||||
useEffect(() => {
|
||||
const uid = currentUser?.uid;
|
||||
|
||||
if (!uid) {
|
||||
dispatch(setDarkMode(false));
|
||||
return;
|
||||
}
|
||||
|
||||
const key = `dark-mode-${uid}`;
|
||||
const raw = localStorage.getItem(key);
|
||||
|
||||
if (raw == null) {
|
||||
dispatch(setDarkMode(false));
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
dispatch(setDarkMode(Boolean(JSON.parse(raw))));
|
||||
} catch {
|
||||
dispatch(setDarkMode(false));
|
||||
}
|
||||
}, [currentUser?.uid, dispatch]);
|
||||
|
||||
// Persist darkMode
|
||||
useEffect(() => {
|
||||
const uid = currentUser?.uid;
|
||||
if (!uid) return;
|
||||
|
||||
localStorage.setItem(`dark-mode-${uid}`, JSON.stringify(isDarkMode));
|
||||
}, [isDarkMode, currentUser?.uid]);
|
||||
|
||||
return (
|
||||
<CookiesProvider>
|
||||
<ApolloProvider client={client}>
|
||||
<ConfigProvider
|
||||
input={antdInput}
|
||||
locale={enLocale}
|
||||
theme={theme}
|
||||
form={antdForm}
|
||||
table={antdTable}
|
||||
pagination={antdPagination}
|
||||
componentSize={isPhone ? "small" : isUltraWide ? "large" : "middle"}
|
||||
popupOverflow="viewport"
|
||||
>
|
||||
<GlobalLoadingBar />
|
||||
<SplitFactoryProvider config={config}>
|
||||
<SplitClientProvider>
|
||||
<App />
|
||||
</SplitClientProvider>
|
||||
</SplitFactoryProvider>
|
||||
</ConfigProvider>
|
||||
</ApolloProvider>
|
||||
</CookiesProvider>
|
||||
);
|
||||
}
|
||||
|
||||
export default Sentry.withProfiler(AppContainer);
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useSplitClient } from "@splitsoftware/splitio-react";
|
||||
import { useSplitClient } from "../feature-flags/splitio-react-replacement";
|
||||
import { Button, Result } from "antd";
|
||||
import LogRocket from "logrocket";
|
||||
import { lazy, Suspense, useEffect, useState } from "react";
|
||||
@@ -225,13 +225,22 @@ export function App({
|
||||
path="/parts/*"
|
||||
element={
|
||||
<ErrorBoundary>
|
||||
<PrivateRoute isAuthorized={currentUser.authorized} />
|
||||
<SocketProvider bodyshop={bodyshop} navigate={navigate} currentUser={currentUser}>
|
||||
<PrivateRoute isAuthorized={currentUser.authorized} />
|
||||
</SocketProvider>
|
||||
</ErrorBoundary>
|
||||
}
|
||||
>
|
||||
<Route path="*" element={<SimplifiedPartsPageContainer />} />
|
||||
</Route>
|
||||
<Route path="/edit/*" element={<PrivateRoute isAuthorized={currentUser.authorized} />}>
|
||||
<Route
|
||||
path="/edit/*"
|
||||
element={
|
||||
<SocketProvider bodyshop={bodyshop} navigate={navigate} currentUser={currentUser}>
|
||||
<PrivateRoute isAuthorized={currentUser.authorized} />
|
||||
</SocketProvider>
|
||||
}
|
||||
>
|
||||
<Route path="*" element={<DocumentEditorContainer />} />
|
||||
</Route>
|
||||
</Routes>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useApolloClient, useMutation } from "@apollo/client/react";
|
||||
import { useTreatmentsWithConfig } from "@splitsoftware/splitio-react";
|
||||
import { useTreatmentsWithConfig } from "../../feature-flags/splitio-react-replacement";
|
||||
import { Button, Checkbox, Divider, Form, Modal, Space } from "antd";
|
||||
import _ from "lodash";
|
||||
import { useEffect, useMemo, useRef, useState } from "react";
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import Icon, { UploadOutlined } from "@ant-design/icons";
|
||||
import { useApolloClient } from "@apollo/client/react";
|
||||
import { useTreatmentsWithConfig } from "@splitsoftware/splitio-react";
|
||||
import { useTreatmentsWithConfig } from "../../feature-flags/splitio-react-replacement";
|
||||
import { Alert, Divider, Form, Input, Select, Space, Statistic, Switch, Upload } from "antd";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useLazyQuery, useQuery } from "@apollo/client/react";
|
||||
import { useTreatmentsWithConfig } from "@splitsoftware/splitio-react";
|
||||
import { useTreatmentsWithConfig } from "../../feature-flags/splitio-react-replacement";
|
||||
import { connect } from "react-redux";
|
||||
import { createStructuredSelector } from "reselect";
|
||||
import { QUERY_OUTSTANDING_INVENTORY } from "../../graphql/inventory.queries";
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { DeleteFilled, DollarCircleFilled } from "@ant-design/icons";
|
||||
import { useTreatmentsWithConfig } from "@splitsoftware/splitio-react";
|
||||
import { useTreatmentsWithConfig } from "../../feature-flags/splitio-react-replacement";
|
||||
import { Button, Checkbox, Form, Input, InputNumber, Select, Space, Switch, Table, Tooltip } from "antd";
|
||||
import { useRef } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
@@ -8,7 +8,7 @@ import { selectBodyshop } from "../../redux/user/user.selectors";
|
||||
import GlobalSearch from "../global-search/global-search.component";
|
||||
import GlobalSearchOs from "../global-search/global-search-os.component";
|
||||
import "./breadcrumbs.styles.scss";
|
||||
import { useTreatmentsWithConfig } from "@splitsoftware/splitio-react";
|
||||
import { useTreatmentsWithConfig } from "../../feature-flags/splitio-react-replacement";
|
||||
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
breadcrumbs: selectBreadcrumbs,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { PictureFilled } from "@ant-design/icons";
|
||||
import { useQuery } from "@apollo/client/react";
|
||||
import { useTreatmentsWithConfig } from "@splitsoftware/splitio-react";
|
||||
import { useTreatmentsWithConfig } from "../../feature-flags/splitio-react-replacement";
|
||||
import { Badge, Popover } from "antd";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
@@ -5,7 +5,7 @@ import { useTranslation } from "react-i18next";
|
||||
import { connect } from "react-redux";
|
||||
import { createStructuredSelector } from "reselect";
|
||||
import { selectBodyshop, selectCurrentUser } from "../../redux/user/user.selectors";
|
||||
import { useTreatmentsWithConfig } from "@splitsoftware/splitio-react";
|
||||
import { useTreatmentsWithConfig } from "../../feature-flags/splitio-react-replacement";
|
||||
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
currentUser: selectCurrentUser,
|
||||
|
||||
@@ -26,7 +26,7 @@ import DmsCdkMakesRefetch from "../dms-cdk-makes/dms-cdk-makes.refetch.component
|
||||
import DateTimePicker from "../form-date-time-picker/form-date-time-picker.component.jsx";
|
||||
import CurrencyInput from "../form-items-formatted/currency-form-item.component";
|
||||
import { DMS_MAP } from "../../utils/dmsUtils";
|
||||
import { useTreatmentsWithConfig } from "@splitsoftware/splitio-react";
|
||||
import { useTreatmentsWithConfig } from "../../feature-flags/splitio-react-replacement";
|
||||
|
||||
/**
|
||||
* CDK-like DMS post form:
|
||||
|
||||
@@ -9,7 +9,7 @@ import AlertComponent from "../alert/alert.component";
|
||||
import JobDocumentsGalleryExternal from "../jobs-documents-gallery/jobs-documents-gallery.external.component";
|
||||
import JobsDocumentsLocalGalleryExternalComponent from "../jobs-documents-local-gallery/jobs-documents-local-gallery.external.component";
|
||||
import LoadingSpinner from "../loading-spinner/loading-spinner.component";
|
||||
import { useTreatmentsWithConfig } from "@splitsoftware/splitio-react";
|
||||
import { useTreatmentsWithConfig } from "../../feature-flags/splitio-react-replacement";
|
||||
import JobsDocumentImgproxyGalleryExternal from "../jobs-documents-imgproxy-gallery/jobs-documents-imgproxy-gallery.external.component";
|
||||
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
|
||||
@@ -5,6 +5,7 @@ import { connect } from "react-redux";
|
||||
import { Link } from "react-router-dom";
|
||||
import { createStructuredSelector } from "reselect";
|
||||
import WssStatusDisplayComponent from "../../components/wss-status-display/wss-status-display.component.jsx";
|
||||
import { useTreatment } from "../../feature-flags/splitio-react-replacement.jsx";
|
||||
import { selectIsPartsEntry } from "../../redux/application/application.selectors.js";
|
||||
import InstanceRenderManager from "../../utils/instanceRenderMgr.js";
|
||||
|
||||
@@ -16,6 +17,12 @@ const mapStateToProps = createStructuredSelector({
|
||||
|
||||
export function GlobalFooter({ isPartsEntry }) {
|
||||
const { t } = useTranslation();
|
||||
const testFlagTreatment = useTreatment({ name: "TEST_FLAG" });
|
||||
const testFlagEnabled = testFlagTreatment === "on";
|
||||
|
||||
const testFlagIndicator = testFlagEnabled ? (
|
||||
<div style={{ fontWeight: 600, marginTop: 4 }}>Test Feature Flag Enabled</div>
|
||||
) : null;
|
||||
|
||||
if (isPartsEntry) {
|
||||
return (
|
||||
@@ -38,6 +45,7 @@ export function GlobalFooter({ isPartsEntry }) {
|
||||
<Link to="/disclaimer" target="_blank" style={{ color: "#ccc" }}>
|
||||
Disclaimer & Notices
|
||||
</Link>
|
||||
{testFlagIndicator}
|
||||
</div>
|
||||
</Footer>
|
||||
);
|
||||
@@ -74,6 +82,7 @@ export function GlobalFooter({ isPartsEntry }) {
|
||||
<Link to="/disclaimer" target="_blank" style={{ color: "#ccc" }}>
|
||||
Disclaimer & Notices
|
||||
</Link>
|
||||
{testFlagIndicator}
|
||||
</div>
|
||||
</Footer>
|
||||
);
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
import { BellFilled } from "@ant-design/icons";
|
||||
import { useQuery } from "@apollo/client/react";
|
||||
import { useTreatmentsWithConfig } from "@splitsoftware/splitio-react";
|
||||
import { useTreatmentsWithConfig } from "../../feature-flags/splitio-react-replacement";
|
||||
import { Badge, Layout, Menu, Spin, Tooltip } from "antd";
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useQuery } from "@apollo/client/react";
|
||||
import { useTreatmentsWithConfig } from "@splitsoftware/splitio-react";
|
||||
import { useTreatmentsWithConfig } from "../../feature-flags/splitio-react-replacement";
|
||||
import { connect } from "react-redux";
|
||||
import { createStructuredSelector } from "reselect";
|
||||
import { GET_LINE_TICKET_BY_PK } from "../../graphql/jobs-lines.queries";
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useApolloClient } from "@apollo/client/react";
|
||||
import { useTreatmentsWithConfig } from "@splitsoftware/splitio-react";
|
||||
import { useTreatmentsWithConfig } from "../../feature-flags/splitio-react-replacement";
|
||||
import { Button, Popconfirm } from "antd";
|
||||
import { useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
@@ -30,7 +30,7 @@ import JobLinesBillRefernece from "../job-lines-bill-reference/job-lines-bill-re
|
||||
// import AllocationsAssignmentContainer from "../allocations-assignment/allocations-assignment.container";
|
||||
// import AllocationsBulkAssignmentContainer from "../allocations-bulk-assignment/allocations-bulk-assignment.container";
|
||||
// import AllocationsEmployeeLabelContainer from "../allocations-employee-label/allocations-employee-label.container";
|
||||
import { useTreatmentsWithConfig } from "@splitsoftware/splitio-react";
|
||||
import { useTreatmentsWithConfig } from "../../feature-flags/splitio-react-replacement";
|
||||
import _ from "lodash";
|
||||
import { FaTasks } from "react-icons/fa";
|
||||
import { selectAuthLevel, selectBodyshop } from "../../redux/user/user.selectors";
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useTreatmentsWithConfig } from "@splitsoftware/splitio-react";
|
||||
import { useTreatmentsWithConfig } from "../../feature-flags/splitio-react-replacement";
|
||||
import { Form, Input, InputNumber, Modal, Select, Switch } from "antd";
|
||||
import { useEffect } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useMutation } from "@apollo/client/react";
|
||||
import { useTreatmentsWithConfig } from "@splitsoftware/splitio-react";
|
||||
import { useTreatmentsWithConfig } from "../../feature-flags/splitio-react-replacement";
|
||||
import Axios from "axios";
|
||||
import Dinero from "dinero.js";
|
||||
import { useState } from "react";
|
||||
|
||||
@@ -16,7 +16,7 @@ import DataLabel from "../data-label/data-label.component";
|
||||
import PaymentExpandedRowComponent from "../payment-expanded-row/payment-expanded-row.component";
|
||||
import PaymentsGenerateLink from "../payments-generate-link/payments-generate-link.component";
|
||||
import PrintWrapperComponent from "../print-wrapper/print-wrapper.component";
|
||||
import { useTreatmentsWithConfig } from "@splitsoftware/splitio-react";
|
||||
import { useTreatmentsWithConfig } from "../../feature-flags/splitio-react-replacement";
|
||||
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
bodyshop: selectBodyshop
|
||||
|
||||
@@ -4,7 +4,7 @@ import { ADD_JOB_WATCHER, GET_JOB_WATCHERS, REMOVE_JOB_WATCHER } from "../../gra
|
||||
import { connect } from "react-redux";
|
||||
import { createStructuredSelector } from "reselect";
|
||||
import { selectBodyshop, selectCurrentUser } from "../../redux/user/user.selectors.js";
|
||||
import { useTreatmentsWithConfig } from "@splitsoftware/splitio-react";
|
||||
import { useTreatmentsWithConfig } from "../../feature-flags/splitio-react-replacement";
|
||||
import JobWatcherToggleComponent from "./job-watcher-toggle.component.jsx";
|
||||
import { useIsEmployee } from "../../utils/useIsEmployee.js";
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useApolloClient, useLazyQuery, useMutation, useQuery } from "@apollo/client/react";
|
||||
import { gql } from "@apollo/client";
|
||||
import { useTreatmentsWithConfig } from "@splitsoftware/splitio-react";
|
||||
import { useTreatmentsWithConfig } from "../../feature-flags/splitio-react-replacement";
|
||||
import { Col, Row } from "antd";
|
||||
import Axios from "axios";
|
||||
import _ from "lodash";
|
||||
|
||||
@@ -6,7 +6,7 @@ import { useCallback, useMemo, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { connect } from "react-redux";
|
||||
import { createStructuredSelector } from "reselect";
|
||||
import { useTreatmentsWithConfig } from "@splitsoftware/splitio-react";
|
||||
import { useTreatmentsWithConfig } from "../../feature-flags/splitio-react-replacement";
|
||||
import { CONVERT_JOB_TO_RO } from "../../graphql/jobs.queries";
|
||||
import { insertAuditTrail } from "../../redux/application/application.actions";
|
||||
import { selectJobReadOnly } from "../../redux/application/application.selectors";
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { DownCircleFilled } from "@ant-design/icons";
|
||||
import { useApolloClient, useMutation, useQuery } from "@apollo/client/react";
|
||||
import { useTreatmentsWithConfig } from "@splitsoftware/splitio-react";
|
||||
import { useTreatmentsWithConfig } from "../../feature-flags/splitio-react-replacement";
|
||||
import { Button, Card, Dropdown, Form, Input, Modal, Popover, Select, Space } from "antd";
|
||||
import axios from "axios";
|
||||
import parsePhoneNumber from "libphonenumber-js";
|
||||
|
||||
@@ -6,7 +6,7 @@ import LaborAllocationsTableComponent from "../labor-allocations-table/labor-all
|
||||
import TimeTicketList from "../time-ticket-list/time-ticket-list.component";
|
||||
import PayrollLaborAllocationsTable from "../labor-allocations-table/labor-allocations-table.payroll.component";
|
||||
import { selectBodyshop } from "../../redux/user/user.selectors";
|
||||
import { useTreatmentsWithConfig } from "@splitsoftware/splitio-react";
|
||||
import { useTreatmentsWithConfig } from "../../feature-flags/splitio-react-replacement";
|
||||
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
jobRO: selectJobReadOnly,
|
||||
|
||||
@@ -6,7 +6,7 @@ import { logImEXEvent } from "../../firebase/firebase.utils";
|
||||
import cleanAxios from "../../utils/CleanAxios";
|
||||
import formatBytes from "../../utils/formatbytes";
|
||||
//import yauzl from "yauzl";
|
||||
import { useTreatmentsWithConfig } from "@splitsoftware/splitio-react";
|
||||
import { useTreatmentsWithConfig } from "../../feature-flags/splitio-react-replacement";
|
||||
|
||||
import { connect } from "react-redux";
|
||||
import { createStructuredSelector } from "reselect";
|
||||
|
||||
@@ -7,7 +7,7 @@ import JobDocuments from "./jobs-documents-gallery.component";
|
||||
import { connect } from "react-redux";
|
||||
import { createStructuredSelector } from "reselect";
|
||||
import { selectBodyshop } from "../../redux/user/user.selectors";
|
||||
import { useTreatmentsWithConfig } from "@splitsoftware/splitio-react";
|
||||
import { useTreatmentsWithConfig } from "../../feature-flags/splitio-react-replacement";
|
||||
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
bodyshop: selectBodyshop
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { DeleteFilled, DownOutlined, WarningFilled } from "@ant-design/icons";
|
||||
import { useTreatmentsWithConfig } from "@splitsoftware/splitio-react";
|
||||
import { useTreatmentsWithConfig } from "../../feature-flags/splitio-react-replacement";
|
||||
import { Button, Checkbox, Divider, Dropdown, Form, Input, InputNumber, Radio, Select, Space, Tag } from "antd";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { connect } from "react-redux";
|
||||
|
||||
@@ -19,7 +19,7 @@ import { TemplateList } from "../../utils/TemplateConstants";
|
||||
import AlertComponent from "../alert/alert.component";
|
||||
import PartsOrderModalComponent from "./parts-order-modal.component";
|
||||
import axios from "axios";
|
||||
import { useTreatmentsWithConfig } from "@splitsoftware/splitio-react";
|
||||
import { useTreatmentsWithConfig } from "../../feature-flags/splitio-react-replacement";
|
||||
import _ from "lodash";
|
||||
import { UPDATE_JOB } from "../../graphql/jobs.queries";
|
||||
import { useNotification } from "../../contexts/Notifications/notificationContext.jsx";
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useTreatmentsWithConfig } from "@splitsoftware/splitio-react";
|
||||
import { useTreatmentsWithConfig } from "../../feature-flags/splitio-react-replacement";
|
||||
import { Form, Input, Radio, Select } from "antd";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { connect } from "react-redux";
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useTreatmentsWithConfig } from "@splitsoftware/splitio-react";
|
||||
import { useTreatmentsWithConfig } from "../../feature-flags/splitio-react-replacement";
|
||||
import { Button, Card, Col, Row, Space, Tooltip, Typography } from "antd";
|
||||
import { connect } from "react-redux";
|
||||
import { createStructuredSelector } from "reselect";
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useTreatmentsWithConfig } from "@splitsoftware/splitio-react";
|
||||
import { useTreatmentsWithConfig } from "../../feature-flags/splitio-react-replacement";
|
||||
import { Card, Col, Input, Row, Space, Typography } from "antd";
|
||||
import _ from "lodash";
|
||||
import { useState } from "react";
|
||||
|
||||
@@ -11,7 +11,7 @@ import {
|
||||
import { QUERY_KANBAN_SETTINGS } from "../../graphql/user.queries";
|
||||
import { selectBodyshop, selectCurrentUser } from "../../redux/user/user.selectors";
|
||||
import ProductionBoardKanbanComponent from "./production-board-kanban.component";
|
||||
import { useTreatmentsWithConfig } from "@splitsoftware/splitio-react";
|
||||
import { useTreatmentsWithConfig } from "../../feature-flags/splitio-react-replacement";
|
||||
import { useSocket } from "../../contexts/SocketIO/useSocket.js";
|
||||
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
|
||||
@@ -5,7 +5,7 @@ import { connect } from "react-redux";
|
||||
import { createStructuredSelector } from "reselect";
|
||||
import { selectTechnician } from "../../redux/tech/tech.selectors";
|
||||
import { selectBodyshop } from "../../redux/user/user.selectors";
|
||||
import { useTreatmentsWithConfig } from "@splitsoftware/splitio-react";
|
||||
import { useTreatmentsWithConfig } from "../../feature-flags/splitio-react-replacement";
|
||||
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
technician: selectTechnician,
|
||||
|
||||
@@ -6,7 +6,7 @@ import { useTranslation } from "react-i18next";
|
||||
import { UPDATE_ACTIVE_PROD_LIST_VIEW } from "../../graphql/associations.queries";
|
||||
import { UPDATE_SHOP } from "../../graphql/bodyshop.queries";
|
||||
import ProductionListColumns from "../production-list-columns/production-list-columns.data";
|
||||
import { useTreatmentsWithConfig } from "@splitsoftware/splitio-react";
|
||||
import { useTreatmentsWithConfig } from "../../feature-flags/splitio-react-replacement";
|
||||
import { logImEXEvent } from "../../firebase/firebase.utils";
|
||||
import { isFunction } from "lodash";
|
||||
import { useNotification } from "../../contexts/Notifications/notificationContext.jsx";
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { HolderOutlined, SyncOutlined } from "@ant-design/icons";
|
||||
import { PageHeader } from "@ant-design/pro-layout";
|
||||
import { useTreatmentsWithConfig } from "@splitsoftware/splitio-react";
|
||||
import { useTreatmentsWithConfig } from "../../feature-flags/splitio-react-replacement";
|
||||
import { Button, Dropdown, Input, Space, Statistic, Table } from "antd";
|
||||
import _ from "lodash";
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||
|
||||
@@ -9,7 +9,7 @@ import {
|
||||
} from "../../graphql/jobs.queries";
|
||||
import ProductionListTable from "./production-list-table.component";
|
||||
import _ from "lodash";
|
||||
import { useTreatmentsWithConfig } from "@splitsoftware/splitio-react";
|
||||
import { useTreatmentsWithConfig } from "../../feature-flags/splitio-react-replacement";
|
||||
import { useSocket } from "../../contexts/SocketIO/useSocket.js";
|
||||
|
||||
export default function ProductionListTableContainer({ bodyshop, subscriptionType = "direct" }) {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useLazyQuery } from "@apollo/client/react";
|
||||
import { useTreatmentsWithConfig } from "@splitsoftware/splitio-react";
|
||||
import { useTreatmentsWithConfig } from "../../feature-flags/splitio-react-replacement";
|
||||
import { Button, Card, Col, DatePicker, Form, Input, Radio, Row, Typography } from "antd";
|
||||
import _ from "lodash";
|
||||
import { useState } from "react";
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { DeleteFilled } from "@ant-design/icons";
|
||||
import { useApolloClient, useMutation, useQuery } from "@apollo/client/react";
|
||||
import { useTreatmentsWithConfig } from "@splitsoftware/splitio-react";
|
||||
import { useTreatmentsWithConfig } from "../../feature-flags/splitio-react-replacement";
|
||||
import { Button, Card, Col, Form, Input, InputNumber, Row, Select, Space, Switch } from "antd";
|
||||
import ResponsiveTable from "../responsive-table/responsive-table.component";
|
||||
import queryString from "query-string";
|
||||
|
||||
@@ -33,7 +33,7 @@ vi.mock("@apollo/client/react", async () => {
|
||||
};
|
||||
});
|
||||
|
||||
vi.mock("@splitsoftware/splitio-react", () => ({
|
||||
vi.mock("../../feature-flags/splitio-react-replacement", () => ({
|
||||
useTreatmentsWithConfig: () => ({
|
||||
treatments: {
|
||||
Enhanced_Payroll: {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useTreatmentsWithConfig } from "@splitsoftware/splitio-react";
|
||||
import { useTreatmentsWithConfig } from "../../feature-flags/splitio-react-replacement";
|
||||
import { Button, Card, Tabs } from "antd";
|
||||
import queryString from "query-string";
|
||||
import { useRef } from "react";
|
||||
|
||||
@@ -4,7 +4,7 @@ import LayoutFormRow from "../layout-form-row/layout-form-row.component";
|
||||
import { connect } from "react-redux";
|
||||
import { createStructuredSelector } from "reselect";
|
||||
import { selectBodyshop } from "../../redux/user/user.selectors";
|
||||
import { useTreatmentsWithConfig } from "@splitsoftware/splitio-react";
|
||||
import { useTreatmentsWithConfig } from "../../feature-flags/splitio-react-replacement";
|
||||
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
bodyshop: selectBodyshop
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useTreatmentsWithConfig } from "@splitsoftware/splitio-react";
|
||||
import { useTreatmentsWithConfig } from "../../feature-flags/splitio-react-replacement";
|
||||
import { Form, InputNumber } from "antd";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { connect } from "react-redux";
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { DeleteFilled } from "@ant-design/icons";
|
||||
import { useTreatmentsWithConfig } from "@splitsoftware/splitio-react";
|
||||
import { useTreatmentsWithConfig } from "../../feature-flags/splitio-react-replacement";
|
||||
import { Button, Col, DatePicker, Divider, Form, Input, InputNumber, Radio, Row, Select, Space, Switch } from "antd";
|
||||
import { useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
@@ -16,7 +16,7 @@ import { DEFAULT_TRANSLUCENT_CARD_COLOR, getTintedCardSurfaceStyles } from "./sh
|
||||
import { connect } from "react-redux";
|
||||
import { createStructuredSelector } from "reselect";
|
||||
import { selectBodyshop } from "../../redux/user/user.selectors";
|
||||
import { useTreatmentsWithConfig } from "@splitsoftware/splitio-react";
|
||||
import { useTreatmentsWithConfig } from "../../feature-flags/splitio-react-replacement";
|
||||
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
bodyshop: selectBodyshop
|
||||
|
||||
@@ -7,7 +7,7 @@ import { selectBodyshop } from "../../redux/user/user.selectors";
|
||||
import JobSearchSelect from "../job-search-select/job-search-select.component";
|
||||
import JobsDetailLaborContainer from "../jobs-detail-labor/jobs-detail-labor.container";
|
||||
import LayoutFormRow from "../layout-form-row/layout-form-row.component";
|
||||
import { useTreatmentsWithConfig } from "@splitsoftware/splitio-react";
|
||||
import { useTreatmentsWithConfig } from "../../feature-flags/splitio-react-replacement";
|
||||
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
bodyshop: selectBodyshop,
|
||||
|
||||
@@ -12,7 +12,7 @@ import { selectTechnician } from "../../redux/tech/tech.selectors";
|
||||
import { selectBodyshop, selectCurrentUser } from "../../redux/user/user.selectors";
|
||||
import TechJobPrintTickets from "../tech-job-print-tickets/tech-job-print-tickets.component";
|
||||
import TechClockInComponent from "./tech-job-clock-in-form.component";
|
||||
import { useTreatmentsWithConfig } from "@splitsoftware/splitio-react";
|
||||
import { useTreatmentsWithConfig } from "../../feature-flags/splitio-react-replacement";
|
||||
import { useNotification } from "../../contexts/Notifications/notificationContext.jsx";
|
||||
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
|
||||
@@ -14,7 +14,7 @@ import { selectBodyshop } from "../../redux/user/user.selectors";
|
||||
import { CalculateAllocationsTotals } from "../labor-allocations-table/labor-allocations-table.utility";
|
||||
import TechJobClockoutDelete from "../tech-job-clock-out-delete/tech-job-clock-out-delete.component";
|
||||
import { LaborAllocationContainer } from "../time-ticket-modal/time-ticket-modal.component";
|
||||
import { useTreatmentsWithConfig } from "@splitsoftware/splitio-react";
|
||||
import { useTreatmentsWithConfig } from "../../feature-flags/splitio-react-replacement";
|
||||
import { useNotification } from "../../contexts/Notifications/notificationContext.jsx";
|
||||
import { bodyshopHasDmsKey } from "../../utils/dmsUtils.js";
|
||||
|
||||
|
||||
@@ -11,7 +11,7 @@ import { createStructuredSelector } from "reselect";
|
||||
import { techLogout } from "../../redux/tech/tech.actions";
|
||||
import { selectTechnician } from "../../redux/tech/tech.selectors";
|
||||
import { BsKanban } from "react-icons/bs";
|
||||
import { useTreatmentsWithConfig } from "@splitsoftware/splitio-react";
|
||||
import { useTreatmentsWithConfig } from "../../feature-flags/splitio-react-replacement";
|
||||
import { selectBodyshop } from "../../redux/user/user.selectors";
|
||||
import { setModalContext } from "../../redux/modals/modals.actions";
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { EditFilled, SyncOutlined } from "@ant-design/icons";
|
||||
import { useTreatmentsWithConfig } from "@splitsoftware/splitio-react";
|
||||
import { useTreatmentsWithConfig } from "../../feature-flags/splitio-react-replacement";
|
||||
import { Button, Card, Checkbox, Space } from "antd";
|
||||
import ResponsiveTable from "../responsive-table/responsive-table.component";
|
||||
import { useMemo, useState } from "react";
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useLazyQuery } from "@apollo/client/react";
|
||||
import { useTreatmentsWithConfig } from "@splitsoftware/splitio-react";
|
||||
import { useTreatmentsWithConfig } from "../../feature-flags/splitio-react-replacement";
|
||||
import { Card, Form, Input, InputNumber, Select, Space, Switch } from "antd";
|
||||
import { useEffect, useRef } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { PageHeader } from "@ant-design/pro-layout";
|
||||
import { useMutation, useQuery } from "@apollo/client/react";
|
||||
import { useTreatmentsWithConfig } from "@splitsoftware/splitio-react";
|
||||
import { useTreatmentsWithConfig } from "../../feature-flags/splitio-react-replacement";
|
||||
import { Button, Form, Modal, Space } from "antd";
|
||||
import { useEffect, useState, useRef } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { DeleteFilled } from "@ant-design/icons";
|
||||
import { useApolloClient } from "@apollo/client/react";
|
||||
import { useTreatmentsWithConfig } from "@splitsoftware/splitio-react";
|
||||
import { useTreatmentsWithConfig } from "../../feature-flags/splitio-react-replacement";
|
||||
import { Button, Divider, Form, Input, InputNumber, Select, Space, Switch } from "antd";
|
||||
import { PageHeader } from "@ant-design/pro-layout";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
@@ -14,7 +14,7 @@ import {
|
||||
} from "../../graphql/notifications.queries.js";
|
||||
import { useMutation } from "@apollo/client/react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useTreatmentsWithConfig } from "@splitsoftware/splitio-react";
|
||||
import { FEATURE_FLAGS_CHANGED_EVENT, useTreatmentsWithConfig } from "../../feature-flags/splitio-react-replacement";
|
||||
import { INITIAL_NOTIFICATIONS, SocketContext } from "./useSocket.js";
|
||||
|
||||
const LIMIT = INITIAL_NOTIFICATIONS;
|
||||
@@ -280,6 +280,10 @@ const SocketProvider = ({ children, bodyshop, navigate, currentUser }) => {
|
||||
}
|
||||
};
|
||||
|
||||
const handleFeatureFlagsChanged = (payload) => {
|
||||
window.dispatchEvent(new CustomEvent(FEATURE_FLAGS_CHANGED_EVENT, { detail: payload }));
|
||||
};
|
||||
|
||||
const syncCurrentTokenToSocket = async () => {
|
||||
try {
|
||||
if (!auth.currentUser || !bodyshop?.id) return;
|
||||
@@ -574,6 +578,7 @@ const SocketProvider = ({ children, bodyshop, navigate, currentUser }) => {
|
||||
socketInstance.on("notification", handleNotification);
|
||||
socketInstance.on("sync-notification-read", handleSyncNotificationRead);
|
||||
socketInstance.on("sync-all-notifications-read", handleSyncAllNotificationsRead);
|
||||
socketInstance.on(FEATURE_FLAGS_CHANGED_EVENT, handleFeatureFlagsChanged);
|
||||
socketInstance.on("token-updated", handleTokenUpdated);
|
||||
|
||||
if (tokenSyncIntervalRef.current) {
|
||||
|
||||
71
client/src/feature-flags/README.md
Normal file
71
client/src/feature-flags/README.md
Normal file
@@ -0,0 +1,71 @@
|
||||
# Feature Flags
|
||||
|
||||
The app imports feature-flag hooks from `src/feature-flags/splitio-react-replacement.jsx`. That module keeps the old
|
||||
Split-shaped component and hook API intact while removing the runtime dependency on Split.
|
||||
|
||||
Code should import this local module directly. We no longer rely on a Vite alias for the old Split package.
|
||||
|
||||
## Current storage contract
|
||||
|
||||
The compatibility layer reads the active shop from Redux, then fetches DB-backed assignments from:
|
||||
|
||||
```text
|
||||
GET /feature-flags/bodyshops/:bodyshopId
|
||||
```
|
||||
|
||||
That endpoint verifies the Firebase user can access the bodyshop through Hasura permissions, then returns cached Redis
|
||||
data when present or refreshes from `feature_flags` + `bodyshop_feature_flags`.
|
||||
|
||||
On successful backend responses, the client stores the last-known flag payload in browser `localStorage` for the active
|
||||
bodyshop. If the backend cannot be reached later, the client uses that bodyshop-scoped browser cache for up to 24 hours.
|
||||
If there is no browser cache, unknown flags resolve to `"off"`.
|
||||
|
||||
Recommended backend payload shape:
|
||||
|
||||
```json
|
||||
{
|
||||
"flags": {
|
||||
"Enhanced_Payroll": {
|
||||
"treatment": "on",
|
||||
"config": null,
|
||||
"activeDate": null,
|
||||
"deactiveDate": null
|
||||
},
|
||||
"Demo_Feature": {
|
||||
"treatment": "on",
|
||||
"config": null,
|
||||
"activeDate": "2026-06-01T13:00:00-04:00",
|
||||
"deactiveDate": "2026-06-05T17:00:00-04:00"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Supported values:
|
||||
|
||||
- `true`, `"true"`, `1`, `"on"` -> treatment `"on"`
|
||||
- `false`, `"false"`, `0`, `"off"` -> treatment `"off"`
|
||||
- ISO-ish future date strings -> `"on"` until the date passes
|
||||
- `{ "treatment": "on" | "off" | "control" | "any-custom-treatment", "config": ... }`
|
||||
- Scheduled demo windows using `activeDate` and `deactiveDate`
|
||||
|
||||
Unknown flags default to `"off"`.
|
||||
|
||||
## Backend registry
|
||||
|
||||
Canonical feature flag definitions live in the Hasura-backed `feature_flags` table and are exposed to the admin panel
|
||||
through `GET /adm/feature-flags`.
|
||||
|
||||
Per-shop assignments live in `bodyshop_feature_flags`. The admin panel reads them through
|
||||
`GET /adm/bodyshops/:bodyshopId/feature-flags` and saves them through `POST /adm/updateshop`.
|
||||
|
||||
Hasura invalidates the Redis cache through `/feature-flags/cache/invalidate` when `bodyshop_feature_flags` or
|
||||
`feature_flags` changes. Assignment changes clear the affected shop cache for the current cache version; definition
|
||||
changes increment a global feature flag cache version so old per-shop cache entries become invisible and expire by TTL.
|
||||
|
||||
The backend also emits `feature-flags-changed` over the existing Socket.IO connection. `SocketProvider` bridges that
|
||||
socket message to a browser event, and `SplitFactoryProvider` refetches flags when the event is global or matches the
|
||||
active bodyshop. This keeps already-open tabs in sync with admin edits and Hasura-triggered invalidation.
|
||||
|
||||
For manual frontend testing, the global footer displays `Test Feature Flag Enabled` when `TEST_FLAG` resolves to
|
||||
the `on` treatment.
|
||||
411
client/src/feature-flags/splitio-react-replacement.jsx
Normal file
411
client/src/feature-flags/splitio-react-replacement.jsx
Normal file
@@ -0,0 +1,411 @@
|
||||
import axios from "axios";
|
||||
import { createContext, useCallback, useContext, useEffect, useMemo, useRef, useState } from "react";
|
||||
import { useSelector } from "react-redux";
|
||||
import { selectBodyshop } from "../redux/user/user.selectors";
|
||||
|
||||
const FeatureFlagContext = createContext({
|
||||
config: {},
|
||||
factory: null,
|
||||
flags: {},
|
||||
isReady: true,
|
||||
source: "local"
|
||||
});
|
||||
|
||||
const OFF_TREATMENT = Object.freeze({ treatment: "off", config: null });
|
||||
const LOCAL_STORAGE_PREFIX = "bodyshop-feature-flags";
|
||||
const LOCAL_STORAGE_MAX_AGE_MS = 24 * 60 * 60 * 1000;
|
||||
const FEATURE_FLAGS_REFRESH_DEBOUNCE_MS = 150;
|
||||
const MAX_SCHEDULE_REFRESH_DELAY_MS = 2_147_483_647;
|
||||
const hasOwn = (value, key) => Object.prototype.hasOwnProperty.call(value, key);
|
||||
const hasSchedule = (value) => value.activeDate != null || value.deactiveDate != null;
|
||||
|
||||
export const FEATURE_FLAGS_CHANGED_EVENT = "feature-flags-changed";
|
||||
|
||||
/**
|
||||
* Parses optional schedule timestamps into comparable epoch milliseconds.
|
||||
*/
|
||||
const parseDate = (value) => {
|
||||
if (value == null || value === "") return null;
|
||||
const timestamp = Date.parse(value);
|
||||
return Number.isNaN(timestamp) ? null : timestamp;
|
||||
};
|
||||
|
||||
/**
|
||||
* Determines whether a scheduled feature flag assignment is active at the current time.
|
||||
*/
|
||||
const isWithinSchedule = (value) => {
|
||||
if (!value || typeof value !== "object" || Array.isArray(value)) return true;
|
||||
|
||||
const now = Date.now();
|
||||
const startsAt = parseDate(value.activeDate);
|
||||
const endsAt = parseDate(value.deactiveDate);
|
||||
|
||||
if (startsAt != null && now < startsAt) return false;
|
||||
if (endsAt != null && now >= endsAt) return false;
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
/**
|
||||
* Normalizes backend config values into the object/string/null shape Split hooks expect.
|
||||
*/
|
||||
const normalizeConfig = (config) => {
|
||||
if (config == null || config === "") return null;
|
||||
if (typeof config === "string") {
|
||||
try {
|
||||
return JSON.parse(config);
|
||||
} catch {
|
||||
return config;
|
||||
}
|
||||
}
|
||||
return config;
|
||||
};
|
||||
|
||||
/**
|
||||
* Converts legacy boolean-ish values and custom treatment strings into a stable treatment value.
|
||||
*/
|
||||
const normalizeTreatment = (value) => {
|
||||
if (typeof value === "boolean") return value ? "on" : "off";
|
||||
if (typeof value === "number") return value > 0 ? "on" : "off";
|
||||
|
||||
if (typeof value === "string") {
|
||||
const normalized = value.trim();
|
||||
const lowered = normalized.toLowerCase();
|
||||
|
||||
if (lowered === "true") return "on";
|
||||
if (lowered === "false") return "off";
|
||||
if (lowered === "on" || lowered === "off" || lowered === "control") return lowered;
|
||||
|
||||
const dateValue = Date.parse(normalized);
|
||||
if (!Number.isNaN(dateValue)) return dateValue > Date.now() ? "on" : "off";
|
||||
|
||||
return normalized;
|
||||
}
|
||||
|
||||
return value ? "on" : "off";
|
||||
};
|
||||
|
||||
/**
|
||||
* Converts any supported backend flag value into a Split-compatible treatment/config pair.
|
||||
*/
|
||||
const normalizeFlagValue = (value) => {
|
||||
if (value == null) return OFF_TREATMENT;
|
||||
|
||||
if (typeof value === "object" && !Array.isArray(value)) {
|
||||
if (!isWithinSchedule(value)) return OFF_TREATMENT;
|
||||
|
||||
if (hasOwn(value, "treatment")) {
|
||||
return {
|
||||
treatment: normalizeTreatment(value.treatment),
|
||||
config: normalizeConfig(value.config)
|
||||
};
|
||||
}
|
||||
|
||||
if (hasOwn(value, "enabled")) {
|
||||
return {
|
||||
treatment: normalizeTreatment(value.enabled),
|
||||
config: normalizeConfig(value.config)
|
||||
};
|
||||
}
|
||||
|
||||
if (hasSchedule(value)) {
|
||||
return {
|
||||
treatment: "on",
|
||||
config: normalizeConfig(value.config)
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
treatment: normalizeTreatment(value),
|
||||
config: null
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Checks whether a socket/browser feature-flag change event applies to the active bodyshop.
|
||||
*/
|
||||
const isFeatureFlagChangeRelevant = (detail, bodyshopId) => {
|
||||
if (!detail || detail.scope === "global") return true;
|
||||
if (!bodyshopId) return false;
|
||||
return String(detail.bodyshopId) === String(bodyshopId);
|
||||
};
|
||||
|
||||
/**
|
||||
* Finds the next scheduled flag boundary that should force a local re-render.
|
||||
*/
|
||||
const getNextScheduleRefreshDelay = (flags = {}, now = Date.now()) => {
|
||||
const nextTimestamp = Object.values(flags).reduce((next, value) => {
|
||||
if (!value || typeof value !== "object" || Array.isArray(value)) return next;
|
||||
|
||||
const timestamps = [parseDate(value.activeDate), parseDate(value.deactiveDate)].filter(
|
||||
(timestamp) => timestamp != null && timestamp > now
|
||||
);
|
||||
if (!timestamps.length) return next;
|
||||
|
||||
const candidate = Math.min(...timestamps);
|
||||
return next == null ? candidate : Math.min(next, candidate);
|
||||
}, null);
|
||||
|
||||
if (nextTimestamp == null) return null;
|
||||
|
||||
return Math.min(Math.max(nextTimestamp - now + 50, 0), MAX_SCHEDULE_REFRESH_DELAY_MS);
|
||||
};
|
||||
|
||||
/**
|
||||
* Checks whether browser localStorage can be used in the current runtime.
|
||||
*/
|
||||
const isBrowserStorageAvailable = () => typeof window !== "undefined" && window.localStorage;
|
||||
|
||||
/**
|
||||
* Builds the browser cache key for one bodyshop's feature flags.
|
||||
*/
|
||||
const getLocalStorageKey = (bodyshopId) => `${LOCAL_STORAGE_PREFIX}:${bodyshopId}`;
|
||||
|
||||
/**
|
||||
* Reads a bodyshop-scoped last-known-good flag payload from browser storage.
|
||||
*/
|
||||
const readCachedFeatureFlags = (bodyshopId, now = Date.now()) => {
|
||||
if (!bodyshopId || !isBrowserStorageAvailable()) return null;
|
||||
|
||||
try {
|
||||
const rawValue = window.localStorage.getItem(getLocalStorageKey(bodyshopId));
|
||||
if (!rawValue) return null;
|
||||
|
||||
const parsed = JSON.parse(rawValue);
|
||||
if (!parsed?.flags || typeof parsed.flags !== "object" || Array.isArray(parsed.flags)) return null;
|
||||
const cachedAt = Date.parse(parsed.cachedAt);
|
||||
if (!parsed.cachedAt || Number.isNaN(cachedAt) || now - cachedAt > LOCAL_STORAGE_MAX_AGE_MS) return null;
|
||||
|
||||
return parsed.flags;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Persists a successful backend flag payload for short-term browser fallback.
|
||||
*/
|
||||
const writeCachedFeatureFlags = (bodyshopId, flags) => {
|
||||
if (!bodyshopId || !flags || !isBrowserStorageAvailable()) return;
|
||||
|
||||
try {
|
||||
window.localStorage.setItem(
|
||||
getLocalStorageKey(bodyshopId),
|
||||
JSON.stringify({
|
||||
cachedAt: new Date().toISOString(),
|
||||
flags
|
||||
})
|
||||
);
|
||||
} catch {
|
||||
// localStorage may be unavailable, full, or blocked. Runtime flags still work without the browser cache.
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Builds the local client object that mimics the Split client surface used by the app.
|
||||
*/
|
||||
const createFeatureFlagClient = ({ bodyshop, key, backendFlags }) => {
|
||||
const attributes = {};
|
||||
|
||||
const getTreatmentWithConfig = (name) => normalizeFlagValue(backendFlags?.[name]);
|
||||
|
||||
return {
|
||||
client: null,
|
||||
isReady: true,
|
||||
isReadyFromCache: true,
|
||||
key: key || bodyshop?.imexshopid || "anon",
|
||||
getTreatment: (name) => getTreatmentWithConfig(name).treatment,
|
||||
getTreatmentWithConfig,
|
||||
getTreatments: (names = []) =>
|
||||
names.reduce((acc, name) => {
|
||||
acc[name] = getTreatmentWithConfig(name).treatment;
|
||||
return acc;
|
||||
}, {}),
|
||||
getTreatmentsWithConfig: (names = []) =>
|
||||
names.reduce((acc, name) => {
|
||||
acc[name] = getTreatmentWithConfig(name);
|
||||
return acc;
|
||||
}, {}),
|
||||
setAttribute: (name, value) => {
|
||||
attributes[name] = value;
|
||||
return true;
|
||||
},
|
||||
setAttributes: (values = {}) => {
|
||||
Object.assign(attributes, values);
|
||||
return true;
|
||||
},
|
||||
getAttribute: (name) => attributes[name],
|
||||
getAttributes: () => ({ ...attributes }),
|
||||
ready: () => Promise.resolve(),
|
||||
on: () => {},
|
||||
off: () => {},
|
||||
destroy: () => {}
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Provides database-backed feature flags through a Split-shaped React context.
|
||||
*/
|
||||
export function SplitFactoryProvider({ children, config, factory }) {
|
||||
const bodyshop = useSelector(selectBodyshop);
|
||||
const [state, setState] = useState({ flags: {}, isReady: true, source: "local" });
|
||||
const loadIdRef = useRef(0);
|
||||
const refreshTimerRef = useRef(null);
|
||||
|
||||
const loadFeatureFlags = useCallback(async () => {
|
||||
const loadId = (loadIdRef.current += 1);
|
||||
|
||||
if (!bodyshop?.id) {
|
||||
setState({ flags: {}, isReady: true, source: "local" });
|
||||
return;
|
||||
}
|
||||
|
||||
setState((current) => ({ ...current, isReady: false }));
|
||||
|
||||
try {
|
||||
const { data } = await axios.get(`/feature-flags/bodyshops/${bodyshop.id}`);
|
||||
if (loadId !== loadIdRef.current) return;
|
||||
const flags = data.flags || {};
|
||||
writeCachedFeatureFlags(bodyshop.id, flags);
|
||||
setState({
|
||||
flags,
|
||||
isReady: true,
|
||||
source: data.source || "database"
|
||||
});
|
||||
} catch (error) {
|
||||
if (loadId !== loadIdRef.current) return;
|
||||
const cachedFlags = readCachedFeatureFlags(bodyshop.id);
|
||||
console.warn("Feature flags backend fetch failed; falling back to last-known browser cache.", error);
|
||||
setState({
|
||||
flags: cachedFlags || {},
|
||||
isReady: true,
|
||||
source: cachedFlags ? "browser-cache" : "local"
|
||||
});
|
||||
}
|
||||
}, [bodyshop?.id]);
|
||||
|
||||
useEffect(() => {
|
||||
loadFeatureFlags();
|
||||
|
||||
return () => {
|
||||
loadIdRef.current += 1;
|
||||
};
|
||||
}, [loadFeatureFlags]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!bodyshop?.id) return undefined;
|
||||
|
||||
const handleFeatureFlagsChanged = (event) => {
|
||||
if (!isFeatureFlagChangeRelevant(event.detail, bodyshop.id)) return;
|
||||
|
||||
if (refreshTimerRef.current) {
|
||||
clearTimeout(refreshTimerRef.current);
|
||||
}
|
||||
|
||||
refreshTimerRef.current = setTimeout(() => {
|
||||
refreshTimerRef.current = null;
|
||||
loadFeatureFlags();
|
||||
}, FEATURE_FLAGS_REFRESH_DEBOUNCE_MS);
|
||||
};
|
||||
|
||||
window.addEventListener(FEATURE_FLAGS_CHANGED_EVENT, handleFeatureFlagsChanged);
|
||||
|
||||
return () => {
|
||||
window.removeEventListener(FEATURE_FLAGS_CHANGED_EVENT, handleFeatureFlagsChanged);
|
||||
if (refreshTimerRef.current) {
|
||||
clearTimeout(refreshTimerRef.current);
|
||||
refreshTimerRef.current = null;
|
||||
}
|
||||
};
|
||||
}, [bodyshop?.id, loadFeatureFlags]);
|
||||
|
||||
useEffect(() => {
|
||||
const delay = getNextScheduleRefreshDelay(state.flags);
|
||||
if (delay == null) return undefined;
|
||||
|
||||
const timer = setTimeout(() => {
|
||||
setState((current) => ({ ...current, flags: { ...current.flags } }));
|
||||
}, delay);
|
||||
|
||||
return () => {
|
||||
clearTimeout(timer);
|
||||
};
|
||||
}, [state.flags]);
|
||||
|
||||
const value = useMemo(
|
||||
() => ({ config, factory, flags: state.flags, isReady: state.isReady, source: state.source }),
|
||||
[config, factory, state.flags, state.isReady, state.source]
|
||||
);
|
||||
return <FeatureFlagContext.Provider value={value}>{children}</FeatureFlagContext.Provider>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a Split-compatible client backed by the local feature flag context.
|
||||
*/
|
||||
export function useSplitClient(options = {}) {
|
||||
const bodyshop = useSelector(selectBodyshop);
|
||||
const context = useContext(FeatureFlagContext);
|
||||
|
||||
const client = useMemo(() => {
|
||||
const nextClient = createFeatureFlagClient({
|
||||
bodyshop,
|
||||
key: options.key,
|
||||
backendFlags: context.flags
|
||||
});
|
||||
nextClient.client = nextClient;
|
||||
nextClient.isReady = context.isReady;
|
||||
nextClient.isReadyFromCache = context.source === "redis" || context.source === "browser-cache";
|
||||
return nextClient;
|
||||
}, [bodyshop, options.key, context.flags, context.isReady, context.source]);
|
||||
|
||||
return client;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns treatment/config pairs for several feature flags.
|
||||
*/
|
||||
export function useTreatmentsWithConfig({ names = [] } = {}) {
|
||||
const client = useSplitClient();
|
||||
|
||||
return useMemo(
|
||||
() => ({
|
||||
treatments: client.getTreatmentsWithConfig(names),
|
||||
isReady: client.isReady,
|
||||
isReadyFromCache: client.isReadyFromCache,
|
||||
lastUpdate: Date.now()
|
||||
}),
|
||||
[client, names]
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns only the treatment string for one feature flag.
|
||||
*/
|
||||
export function useTreatment({ name } = {}) {
|
||||
const client = useSplitClient();
|
||||
return client.getTreatment(name);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the treatment/config pair for one feature flag.
|
||||
*/
|
||||
export function useTreatmentWithConfig({ name } = {}) {
|
||||
const client = useSplitClient();
|
||||
return client.getTreatmentWithConfig(name);
|
||||
}
|
||||
|
||||
export const FeatureFlagProvider = SplitFactoryProvider;
|
||||
export const useFeatureFlagClient = useSplitClient;
|
||||
export const SplitContext = FeatureFlagContext;
|
||||
export const useSplitContext = () => useContext(FeatureFlagContext);
|
||||
|
||||
export const __featureFlagTesting = {
|
||||
createFeatureFlagClient,
|
||||
getNextScheduleRefreshDelay,
|
||||
getLocalStorageKey,
|
||||
isFeatureFlagChangeRelevant,
|
||||
normalizeFlagValue,
|
||||
readCachedFeatureFlags,
|
||||
writeCachedFeatureFlags
|
||||
};
|
||||
166
client/src/feature-flags/splitio-react-replacement.test.jsx
Normal file
166
client/src/feature-flags/splitio-react-replacement.test.jsx
Normal file
@@ -0,0 +1,166 @@
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { __featureFlagTesting } from "./splitio-react-replacement";
|
||||
|
||||
const {
|
||||
createFeatureFlagClient,
|
||||
getNextScheduleRefreshDelay,
|
||||
getLocalStorageKey,
|
||||
isFeatureFlagChangeRelevant,
|
||||
normalizeFlagValue,
|
||||
readCachedFeatureFlags,
|
||||
writeCachedFeatureFlags
|
||||
} = __featureFlagTesting;
|
||||
|
||||
beforeEach(() => {
|
||||
window.localStorage.clear();
|
||||
vi.useRealTimers();
|
||||
});
|
||||
|
||||
describe("splitio-react-replacement feature flag normalization", () => {
|
||||
it("returns off for unknown or null values", () => {
|
||||
expect(normalizeFlagValue(null)).toEqual({ treatment: "off", config: null });
|
||||
expect(normalizeFlagValue(undefined)).toEqual({ treatment: "off", config: null });
|
||||
});
|
||||
|
||||
it("normalizes primitive values into Split-like treatments", () => {
|
||||
expect(normalizeFlagValue(true)).toEqual({ treatment: "on", config: null });
|
||||
expect(normalizeFlagValue(false)).toEqual({ treatment: "off", config: null });
|
||||
expect(normalizeFlagValue(1)).toEqual({ treatment: "on", config: null });
|
||||
expect(normalizeFlagValue(0)).toEqual({ treatment: "off", config: null });
|
||||
expect(normalizeFlagValue("true")).toEqual({ treatment: "on", config: null });
|
||||
expect(normalizeFlagValue("false")).toEqual({ treatment: "off", config: null });
|
||||
expect(normalizeFlagValue("variant-a")).toEqual({ treatment: "variant-a", config: null });
|
||||
});
|
||||
|
||||
it("preserves custom treatments and parses JSON config strings", () => {
|
||||
expect(
|
||||
normalizeFlagValue({
|
||||
treatment: "demo",
|
||||
config: "{\"limit\":25}"
|
||||
})
|
||||
).toEqual({
|
||||
treatment: "demo",
|
||||
config: { limit: 25 }
|
||||
});
|
||||
});
|
||||
|
||||
it("respects activeDate and deactiveDate windows", () => {
|
||||
vi.useFakeTimers();
|
||||
vi.setSystemTime(new Date("2026-05-19T15:00:00.000Z"));
|
||||
|
||||
expect(
|
||||
normalizeFlagValue({
|
||||
treatment: "on",
|
||||
activeDate: "2026-05-19T14:59:00.000Z",
|
||||
deactiveDate: "2026-05-19T15:01:00.000Z"
|
||||
})
|
||||
).toEqual({ treatment: "on", config: null });
|
||||
|
||||
expect(
|
||||
normalizeFlagValue({
|
||||
treatment: "on",
|
||||
activeDate: "2026-05-19T15:01:00.000Z"
|
||||
})
|
||||
).toEqual({ treatment: "off", config: null });
|
||||
|
||||
expect(
|
||||
normalizeFlagValue({
|
||||
treatment: "on",
|
||||
deactiveDate: "2026-05-19T15:00:00.000Z"
|
||||
})
|
||||
).toEqual({ treatment: "off", config: null });
|
||||
|
||||
vi.useRealTimers();
|
||||
});
|
||||
});
|
||||
|
||||
describe("splitio-react-replacement feature flag client", () => {
|
||||
it("uses backend flags", () => {
|
||||
const client = createFeatureFlagClient({
|
||||
bodyshop: {
|
||||
imexshopid: "APPLE"
|
||||
},
|
||||
backendFlags: {
|
||||
Enhanced_Payroll: { treatment: "on" }
|
||||
}
|
||||
});
|
||||
|
||||
expect(client.getTreatment("Enhanced_Payroll")).toBe("on");
|
||||
});
|
||||
|
||||
it("ignores old bodyshop feature JSON fallback values", () => {
|
||||
const client = createFeatureFlagClient({
|
||||
bodyshop: {
|
||||
imexshopid: "APPLE",
|
||||
features: {
|
||||
featureFlags: {
|
||||
Enhanced_Payroll: { treatment: "on" }
|
||||
}
|
||||
}
|
||||
},
|
||||
backendFlags: {}
|
||||
});
|
||||
|
||||
expect(client.getTreatment("Enhanced_Payroll")).toBe("off");
|
||||
});
|
||||
|
||||
it("returns off for flags that are not present in any source", () => {
|
||||
const client = createFeatureFlagClient({
|
||||
bodyshop: { imexshopid: "APPLE", features: {} },
|
||||
backendFlags: {}
|
||||
});
|
||||
|
||||
expect(client.getTreatment("Missing_Flag")).toBe("off");
|
||||
});
|
||||
|
||||
it("uses a bodyshop-scoped browser cache key", () => {
|
||||
expect(getLocalStorageKey("shop-1")).toBe("bodyshop-feature-flags:shop-1");
|
||||
});
|
||||
|
||||
it("stores and reads last-known backend flags from browser storage", () => {
|
||||
vi.useFakeTimers();
|
||||
vi.setSystemTime(new Date("2026-05-19T15:00:00.000Z"));
|
||||
|
||||
writeCachedFeatureFlags("shop-1", {
|
||||
Enhanced_Payroll: { treatment: "on", config: null }
|
||||
});
|
||||
|
||||
expect(readCachedFeatureFlags("shop-1")).toEqual({
|
||||
Enhanced_Payroll: { treatment: "on", config: null }
|
||||
});
|
||||
});
|
||||
|
||||
it("ignores expired browser cached flags", () => {
|
||||
vi.useFakeTimers();
|
||||
vi.setSystemTime(new Date("2026-05-19T15:00:00.000Z"));
|
||||
|
||||
writeCachedFeatureFlags("shop-1", {
|
||||
Enhanced_Payroll: { treatment: "on", config: null }
|
||||
});
|
||||
|
||||
expect(readCachedFeatureFlags("shop-1", Date.parse("2026-05-20T15:00:01.000Z"))).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe("splitio-react-replacement live refresh helpers", () => {
|
||||
it("matches global and bodyshop-scoped socket changes", () => {
|
||||
expect(isFeatureFlagChangeRelevant({ scope: "global" }, "shop-1")).toBe(true);
|
||||
expect(isFeatureFlagChangeRelevant({ bodyshopId: "shop-1", scope: "bodyshop" }, "shop-1")).toBe(true);
|
||||
expect(isFeatureFlagChangeRelevant({ bodyshopId: "shop-2", scope: "bodyshop" }, "shop-1")).toBe(false);
|
||||
});
|
||||
|
||||
it("finds the next active/deactive date boundary that needs a refresh", () => {
|
||||
const now = Date.parse("2026-05-19T15:00:00.000Z");
|
||||
|
||||
expect(
|
||||
getNextScheduleRefreshDelay(
|
||||
{
|
||||
Demo: { treatment: "on", activeDate: "2026-05-19T15:05:00.000Z" },
|
||||
Expiring: { treatment: "on", deactiveDate: "2026-05-19T15:02:00.000Z" },
|
||||
Expired: { treatment: "on", deactiveDate: "2026-05-19T14:59:00.000Z" }
|
||||
},
|
||||
now
|
||||
)
|
||||
).toBe(120050);
|
||||
});
|
||||
});
|
||||
@@ -6,7 +6,7 @@ import { createStructuredSelector } from "reselect";
|
||||
import queryString from "query-string";
|
||||
import { useQuery } from "@apollo/client/react";
|
||||
import { Button, Card, Col, Result, Row, Select, Space, Switch } from "antd";
|
||||
import { useTreatmentsWithConfig } from "@splitsoftware/splitio-react";
|
||||
import { useTreatmentsWithConfig } from "../../feature-flags/splitio-react-replacement";
|
||||
import { useSocket } from "../../contexts/SocketIO/useSocket.js";
|
||||
import { useNotification } from "../../contexts/Notifications/notificationContext.jsx";
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ import { useCallback, useEffect, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { connect } from "react-redux";
|
||||
import { useParams } from "react-router-dom";
|
||||
import { useTreatmentsWithConfig } from "@splitsoftware/splitio-react";
|
||||
import { useTreatmentsWithConfig } from "../../feature-flags/splitio-react-replacement";
|
||||
import { some } from "lodash";
|
||||
import axios from "axios";
|
||||
import AlertComponent from "../../components/alert/alert.component";
|
||||
|
||||
@@ -22,7 +22,7 @@ import { useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { connect } from "react-redux";
|
||||
// import { useNavigate } from 'react-router-dom';
|
||||
import { useTreatmentsWithConfig } from "@splitsoftware/splitio-react";
|
||||
import { useTreatmentsWithConfig } from "../../feature-flags/splitio-react-replacement";
|
||||
import Dinero from "dinero.js";
|
||||
import { Link } from "react-router-dom";
|
||||
import { createStructuredSelector } from "reselect";
|
||||
|
||||
@@ -2,7 +2,7 @@ import ProductionBoardKanbanContainer from "../../components/production-board-ka
|
||||
import { connect } from "react-redux";
|
||||
import { createStructuredSelector } from "reselect";
|
||||
import { selectBodyshop } from "../../redux/user/user.selectors";
|
||||
import { useTreatmentsWithConfig } from "@splitsoftware/splitio-react";
|
||||
import { useTreatmentsWithConfig } from "../../feature-flags/splitio-react-replacement";
|
||||
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
//currentUser: selectCurrentUser
|
||||
|
||||
@@ -3,7 +3,7 @@ import ProductionListTable from "../../components/production-list-table/producti
|
||||
import { connect } from "react-redux";
|
||||
import { createStructuredSelector } from "reselect";
|
||||
import { selectBodyshop } from "../../redux/user/user.selectors";
|
||||
import { useTreatmentsWithConfig } from "@splitsoftware/splitio-react";
|
||||
import { useTreatmentsWithConfig } from "../../feature-flags/splitio-react-replacement";
|
||||
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
bodyshop: selectBodyshop
|
||||
|
||||
@@ -13,7 +13,7 @@ import { GET_UNACCEPTED_PARTS_DISPATCH } from "../../graphql/parts-dispatch.quer
|
||||
import { selectTechnician } from "../../redux/tech/tech.selectors";
|
||||
import { selectBodyshop } from "../../redux/user/user.selectors";
|
||||
import { alphaSort } from "../../utils/sorters";
|
||||
import { useTreatmentsWithConfig } from "@splitsoftware/splitio-react";
|
||||
import { useTreatmentsWithConfig } from "../../feature-flags/splitio-react-replacement";
|
||||
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
//currentUser: selectCurrentUser
|
||||
|
||||
@@ -4,7 +4,7 @@ import JobsDocumentsContainer from "../../components/jobs-documents-gallery/jobs
|
||||
import LoadingSpinner from "../../components/loading-spinner/loading-spinner.component";
|
||||
import { QUERY_TEMPORARY_DOCS } from "../../graphql/documents.queries";
|
||||
|
||||
import { useTreatmentsWithConfig } from "@splitsoftware/splitio-react";
|
||||
import { useTreatmentsWithConfig } from "../../feature-flags/splitio-react-replacement";
|
||||
import { connect } from "react-redux";
|
||||
import { createStructuredSelector } from "reselect";
|
||||
import JobsDocumentsLocalGallery from "../../components/jobs-documents-local-gallery/jobs-documents-local-gallery.container";
|
||||
|
||||
@@ -18,7 +18,7 @@ import { setBreadcrumbs, setSelectedHeader } from "../../redux/application/appli
|
||||
import TimeTicketsCommit from "../../components/time-tickets-commit/time-tickets-commit.component";
|
||||
import FeatureWrapperComponent from "../../components/feature-wrapper/feature-wrapper.component";
|
||||
import InstanceRenderManager from "../../utils/instanceRenderMgr";
|
||||
import { useTreatmentsWithConfig } from "@splitsoftware/splitio-react";
|
||||
import { useTreatmentsWithConfig } from "../../feature-flags/splitio-react-replacement";
|
||||
import { selectBodyshop } from "../../redux/user/user.selectors";
|
||||
import UpsellComponent, { upsellEnum } from "../../components/upsell/upsell.component";
|
||||
|
||||
|
||||
Reference in New Issue
Block a user