From d3654ec16e20f1554bda59ed9b77c8cad0269145 Mon Sep 17 00:00:00 2001 From: Dave Richer Date: Thu, 11 Jan 2024 01:29:36 -0500 Subject: [PATCH] Progress Update. Signed-off-by: Dave Richer --- client/src/App/App.jsx | 44 ++-- .../chat-affix/chat-affix.container.jsx | 71 +++--- .../components/header/header.component.jsx | 40 +++- .../pages/manage/manage.page.component.jsx | 6 +- client/src/redux/store.js | 4 +- client/src/utils/betaHandler.js | 32 +++ client/src/utils/useKeyboardShortcut.jsx | 211 +++++++++--------- 7 files changed, 237 insertions(+), 171 deletions(-) create mode 100644 client/src/utils/betaHandler.js diff --git a/client/src/App/App.jsx b/client/src/App/App.jsx index faa8cc211..9243db625 100644 --- a/client/src/App/App.jsx +++ b/client/src/App/App.jsx @@ -1,7 +1,7 @@ import {useSplitClient} from "@splitsoftware/splitio-react"; import {Button, Result} from "antd"; import LogRocket from "logrocket"; -import React, {lazy, Suspense, useEffect} from "react"; +import React, {lazy, Suspense, useEffect, useState} from "react"; import {useTranslation} from "react-i18next"; import {connect} from "react-redux"; import {Route, Routes} from "react-router-dom"; @@ -19,6 +19,7 @@ import {checkUserSession} from "../redux/user/user.actions"; import {selectBodyshop, selectCurrentUser,} from "../redux/user/user.selectors"; import PrivateRoute from "../components/PrivateRoute"; import "./App.styles.scss"; +import handleBeta from "../utils/betaHandler"; const ResetPassword = lazy(() => import("../pages/reset-password/reset-password.component") @@ -40,14 +41,16 @@ const mapDispatchToProps = (dispatch) => ({ setOnline: (isOnline) => dispatch(setOnline(isOnline)), }); -export function App({ - bodyshop, - checkUserSession, - currentUser, - online, - setOnline, - }) { +export function App({bodyshop, checkUserSession, currentUser, online, setOnline}) { + const client = useSplitClient().client; + const [listenersAdded, setListenersAdded] = useState(false) + const {t} = useTranslation(); + + // Handle The Beta Switch. + useEffect(() => { + handleBeta(); + }, []) useEffect(() => { if (!navigator.onLine) { @@ -60,15 +63,28 @@ export function App({ //const b = Grid.useBreakpoint(); // console.log("Breakpoints:", b); - const {t} = useTranslation(); - - window.addEventListener("offline", function (e) { + const offlineListener = (e) => { setOnline(false); - }); + } - window.addEventListener("online", function (e) { + const onlineListener = (e) => { setOnline(true); - }); + } + + // Associate event listeners, memoize to prevent multiple listeners being added + useEffect(() => { + if (!listenersAdded) { + console.log('Added events for offline and online'); + window.addEventListener("offline", offlineListener); + window.addEventListener("online", onlineListener); + setListenersAdded(true); + } + + return () => { + window.removeEventListener("offline", offlineListener); + window.removeEventListener("online", onlineListener); + } + }, [listenersAdded]); useEffect(() => { if (currentUser.authorized && bodyshop) { diff --git a/client/src/components/chat-affix/chat-affix.container.jsx b/client/src/components/chat-affix/chat-affix.container.jsx index 55fab18bd..272028fe1 100644 --- a/client/src/components/chat-affix/chat-affix.container.jsx +++ b/client/src/components/chat-affix/chat-affix.container.jsx @@ -4,20 +4,11 @@ import { Button, notification, Space } from "antd"; import axios from "axios"; import React, { useEffect } from "react"; import { useTranslation } from "react-i18next"; -import { connect } from "react-redux"; -import { createStructuredSelector } from "reselect"; import { messaging, requestForToken } from "../../firebase/firebase.utils"; -import { selectChatVisible } from "../../redux/messaging/messaging.selectors"; -import { selectBodyshop } from "../../redux/user/user.selectors"; import FcmHandler from "../../utils/fcm-handler"; import ChatPopupComponent from "../chat-popup/chat-popup.component"; import "./chat-affix.styles.scss"; -const mapStateToProps = createStructuredSelector({ - bodyshop: selectBodyshop, - chatVisible: selectChatVisible, -}); - export function ChatAffixContainer({ bodyshop, chatVisible }) { const { t } = useTranslation(); const client = useApolloClient(); @@ -36,35 +27,34 @@ export function ChatAffixContainer({ bodyshop, chatVisible }) { console.log("FCM Topic Subscription", r.data); } catch (error) { console.log( - "Error attempting to subscribe to messaging topic: ", - error + "Error attempting to subscribe to messaging topic: ", + error ); notification.open({ type: "warning", message: t("general.errors.fcm"), btn: ( - - - - + + + + ), }); } @@ -81,16 +71,16 @@ export function ChatAffixContainer({ bodyshop, chatVisible }) { payload: (payload && payload.data && payload.data.data) || payload.data, }); } - let stopMessageListenr, channel; + let stopMessageListener, channel; try { - stopMessageListenr = onMessage(messaging, handleMessage); + stopMessageListener = onMessage(messaging, handleMessage); channel = new BroadcastChannel("imex-sw-messages"); channel.addEventListener("message", handleMessage); } catch (error) { console.log("Unable to set event listeners."); } return () => { - stopMessageListenr && stopMessageListenr(); + stopMessageListener && stopMessageListener(); channel && channel.removeEventListener("message", handleMessage); }; }, [client]); @@ -98,9 +88,10 @@ export function ChatAffixContainer({ bodyshop, chatVisible }) { if (!bodyshop || !bodyshop.messagingservicesid) return <>; return ( -
- {bodyshop && bodyshop.messagingservicesid ? : null} -
+
+ {bodyshop && bodyshop.messagingservicesid ? : null} +
); } -export default connect(mapStateToProps, null)(ChatAffixContainer); + +export default ChatAffixContainer; \ No newline at end of file diff --git a/client/src/components/header/header.component.jsx b/client/src/components/header/header.component.jsx index 7955e6970..21a719c2d 100644 --- a/client/src/components/header/header.component.jsx +++ b/client/src/components/header/header.component.jsx @@ -12,7 +12,7 @@ import Icon, { FileAddOutlined, FileFilled, HomeFilled, - ImportOutlined, + ImportOutlined, InfoCircleOutlined, LineChartOutlined, PaperClipOutlined, PhoneOutlined, @@ -25,8 +25,8 @@ import Icon, { UserOutlined, } from "@ant-design/icons"; import {useSplitTreatments} from "@splitsoftware/splitio-react"; -import {Layout, Menu} from "antd"; -import React from "react"; +import {Layout, Menu, Switch, Tooltip} from "antd"; +import React, {useEffect, useState} from "react"; import {useTranslation} from "react-i18next"; import {BsKanban} from "react-icons/bs"; import {FaCalendarAlt, FaCarCrash, FaCreditCard, FaFileInvoiceDollar,} from "react-icons/fa"; @@ -41,6 +41,7 @@ import {setModalContext} from "../../redux/modals/modals.actions"; import {signOutStart} from "../../redux/user/user.actions"; import {selectBodyshop, selectCurrentUser,} from "../../redux/user/user.selectors"; import {FiLogOut} from "react-icons/fi"; +import handleBeta, {checkBeta, setBeta} from "../../utils/betaHandler"; const mapStateToProps = createStructuredSelector({ currentUser: selectCurrentUser, @@ -70,9 +71,21 @@ function Header({handleMenuClick, currentUser, bodyshop, selectedHeader, signOut names: ["ImEXPay", "DmsAp", "Simple_Inventory"], splitKey: bodyshop && bodyshop.imexshopid, }); - + const [betaSwitch, setBetaSwitch] = useState(false); const {t} = useTranslation(); + useEffect(() => { + const isBeta = checkBeta(); + setBetaSwitch(isBeta ); + }, []); + + + const betaSwitchChange = (checked) => { + setBeta(checked); + setBetaSwitch(checked); + handleBeta(); + } + const accountingChildren = [ { key: 'bills', @@ -451,13 +464,28 @@ function Header({handleMenuClick, currentUser, bodyshop, selectedHeader, signOut })), } - ] + ]; + + + menuItems.push({ + key: 'beta-switch', + style: { marginLeft: 'auto' }, + label: ( + + + Try the new ImEX Online + + + ) + }); return ( { const widgetId = "IABVNO4scRKY11XBQkNr"; @@ -360,7 +362,7 @@ export function Manage({conflict, bodyshop}) { return ( <> - + diff --git a/client/src/redux/store.js b/client/src/redux/store.js index 19d33bb38..b83b80ffa 100644 --- a/client/src/redux/store.js +++ b/client/src/redux/store.js @@ -42,9 +42,7 @@ if (process.env.NODE_ENV === "development") { export const store = configureStore({ reducer: rootReducer, middleware: (getDefaultMiddleware) => getDefaultMiddleware({ - serializableCheck: { - ignoredActions: [FLUSH, REHYDRATE, PAUSE, PERSIST, PURGE, REGISTER], - }, + serializableCheck: false, }).concat(middlewares), // middleware: middlewares, devTools: process.env.NODE_ENV !== 'production', diff --git a/client/src/utils/betaHandler.js b/client/src/utils/betaHandler.js new file mode 100644 index 000000000..195796175 --- /dev/null +++ b/client/src/utils/betaHandler.js @@ -0,0 +1,32 @@ +export const BETA_KEY = 'betaSwitchImex'; + +export const checkBeta = () => document.cookie.split('; ').find(row => row.startsWith(BETA_KEY)).split('=')[1] === 'true'; + + +export const setBeta = (value) => { + const domain = window.location.hostname.split('.').slice(-2).join('.'); + document.cookie = `${BETA_KEY}=${value}; path=/; domain=.${domain}`; +} + +export const handleBeta = () => { + // If the current host name does not start with beta or test, then we don't need to do anything. + if (window.location.hostname.startsWith('localhost')) { + console.log('Not on beta or test, so no need to handle beta.'); + return; + } + + const isBeta = checkBeta(); + + const currentHostName = window.location.hostname; + + // Beta is enabled, but the current host name does start with beta. + if (isBeta && !currentHostName.startsWith('beta')) { + window.location.href = `${window.location.protocol}//beta.${currentHostName}${window.location.pathname}${window.location.search}${window.location.hash}`; + } + + // Beta is not enabled, but the current host name does start with beta. + else if (!isBeta && currentHostName.startsWith('beta')) { + window.location.href = `${window.location.protocol}//${currentHostName.replace('beta.', '')}${window.location.pathname}${window.location.search}${window.location.hash}`; + } +} +export default handleBeta; \ No newline at end of file diff --git a/client/src/utils/useKeyboardShortcut.jsx b/client/src/utils/useKeyboardShortcut.jsx index 1f31def21..32148b9ec 100644 --- a/client/src/utils/useKeyboardShortcut.jsx +++ b/client/src/utils/useKeyboardShortcut.jsx @@ -1,129 +1,128 @@ -import { useEffect, useCallback, useReducer } from "react"; +import {useCallback, useEffect, useReducer, useState} from "react"; //Based on https://www.fullstacklabs.co/blog/keyboard-shortcuts-with-react-hooks const blacklistedTargets = []; // ["INPUT", "TEXTAREA"]; export const useKeyboardSaveShortcut = (callback) => - useKeyboardShortcut(["Control", "S"], callback, { overrideSystem: true }); + useKeyboardShortcut(["Control", "S"], callback, {overrideSystem: true}); const keysReducer = (state, action) => { - switch (action.type) { - case "set-key-down": - const keydownState = { ...state, [action.key]: true }; - return keydownState; - case "set-key-up": - const keyUpState = { ...state, [action.key]: false }; - return keyUpState; - case "reset-keys": - const resetState = { ...action.data }; - return resetState; - default: - return state; - } + switch (action.type) { + case "set-key-down": + const keydownState = {...state, [action.key]: true}; + return keydownState; + case "set-key-up": + const keyUpState = {...state, [action.key]: false}; + return keyUpState; + case "reset-keys": + const resetState = {...action.data}; + return resetState; + default: + return state; + } }; const useKeyboardShortcut = (shortcutKeys, callback, options) => { - if (!Array.isArray(shortcutKeys)) - throw new Error( - "The first parameter to `useKeyboardShortcut` must be an ordered array of `KeyboardEvent.key` strings." + if (!Array.isArray(shortcutKeys)) + throw new Error( + "The first parameter to `useKeyboardShortcut` must be an ordered array of `KeyboardEvent.key` strings." + ); + + if (!shortcutKeys.length) + throw new Error( + "The first parameter to `useKeyboardShortcut` must contain atleast one `KeyboardEvent.key` string." + ); + + if (!callback || typeof callback !== "function") + throw new Error( + "The second parameter to `useKeyboardShortcut` must be a function that will be envoked when the keys are pressed." + ); + + const {overrideSystem} = options || {}; + const initalKeyMapping = shortcutKeys.reduce((currentKeys, key) => { + currentKeys[key.toLowerCase()] = false; + return currentKeys; + }, {}); + + const [keys, setKeys] = useReducer(keysReducer, initalKeyMapping); + const [listenersAdded, setListenersAdded] = useState(false); + + const keydownListener = useCallback( + (assignedKey) => (keydownEvent) => { + const loweredKey = assignedKey.toLowerCase(); + + if (keydownEvent.repeat) return; + if (blacklistedTargets.includes(keydownEvent.target.tagName)) return; + if (loweredKey !== keydownEvent.key.toLowerCase()) return; + if (keys[loweredKey] === undefined) return; + + if (overrideSystem) { + keydownEvent.preventDefault(); + disabledEventPropagation(keydownEvent); + } + + setKeys({type: "set-key-down", key: loweredKey}); + return false; + }, + [keys, overrideSystem] ); - if (!shortcutKeys.length) - throw new Error( - "The first parameter to `useKeyboardShortcut` must contain atleast one `KeyboardEvent.key` string." + const keyupListener = useCallback( + (assignedKey) => (keyupEvent) => { + const raisedKey = assignedKey.toLowerCase(); + + if (blacklistedTargets.includes(keyupEvent.target.tagName)) return; + if (keyupEvent.key.toLowerCase() !== raisedKey) return; + if (keys[raisedKey] === undefined) return; + + if (overrideSystem) { + keyupEvent.preventDefault(); + disabledEventPropagation(keyupEvent); + } + + setKeys({type: "set-key-up", key: raisedKey}); + return false; + }, + [keys, overrideSystem] ); - if (!callback || typeof callback !== "function") - throw new Error( - "The second parameter to `useKeyboardShortcut` must be a function that will be envoked when the keys are pressed." - ); + useEffect(() => { + if (!Object.values(keys).filter((value) => !value).length) { + callback(keys); + setKeys({type: "reset-keys", data: initalKeyMapping}); + } else { + setKeys({type: null}); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [callback, keys]); - const { overrideSystem } = options || {}; - const initalKeyMapping = shortcutKeys.reduce((currentKeys, key) => { - currentKeys[key.toLowerCase()] = false; - return currentKeys; - }, {}); + useEffect(() => { + if (!listenersAdded) { + console.log('Added events for keyup and keydown'); + shortcutKeys.forEach((k) => { + window.addEventListener("keydown", keydownListener(k)); + window.addEventListener("keyup", keyupListener(k)) + }); + } - const [keys, setKeys] = useReducer(keysReducer, initalKeyMapping); + setListenersAdded(true); - const keydownListener = useCallback( - (assignedKey) => (keydownEvent) => { - const loweredKey = assignedKey.toLowerCase(); + return () => { + shortcutKeys.forEach((k) => { + window.removeEventListener("keydown", keydownListener(k)); + window.removeEventListener("keyup", keyupListener(k)); + }); + } + }, [listenersAdded]); - if (keydownEvent.repeat) return; - if (blacklistedTargets.includes(keydownEvent.target.tagName)) return; - if (loweredKey !== keydownEvent.key.toLowerCase()) return; - if (keys[loweredKey] === undefined) return; - - if (overrideSystem) { - keydownEvent.preventDefault(); - disabledEventPropagation(keydownEvent); - } - - setKeys({ type: "set-key-down", key: loweredKey }); - return false; - }, - [keys, overrideSystem] - ); - - const keyupListener = useCallback( - (assignedKey) => (keyupEvent) => { - const raisedKey = assignedKey.toLowerCase(); - - if (blacklistedTargets.includes(keyupEvent.target.tagName)) return; - if (keyupEvent.key.toLowerCase() !== raisedKey) return; - if (keys[raisedKey] === undefined) return; - - if (overrideSystem) { - keyupEvent.preventDefault(); - disabledEventPropagation(keyupEvent); - } - - setKeys({ type: "set-key-up", key: raisedKey }); - return false; - }, - [keys, overrideSystem] - ); - - useEffect(() => { - if (!Object.values(keys).filter((value) => !value).length) { - callback(keys); - setKeys({ type: "reset-keys", data: initalKeyMapping }); - } else { - setKeys({ type: null }); - } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [callback, keys]); - - useEffect(() => { - shortcutKeys.forEach((k) => - window.addEventListener("keydown", keydownListener(k)) - ); - return () => - shortcutKeys.forEach((k) => - window.removeEventListener("keydown", keydownListener(k)) - ); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, []); - - useEffect(() => { - shortcutKeys.forEach((k) => - window.addEventListener("keyup", keyupListener(k)) - ); - return () => - shortcutKeys.forEach((k) => - window.removeEventListener("keyup", keyupListener(k)) - ); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, []); }; export default useKeyboardShortcut; function disabledEventPropagation(e) { - if (e) { - if (e.stopPropagation) { - e.stopPropagation(); - } else if (window.event) { - window.event.cancelBubble = true; + if (e) { + if (e.stopPropagation) { + e.stopPropagation(); + } else if (window.event) { + window.event.cancelBubble = true; + } } - } }