Progress Update.

Signed-off-by: Dave Richer <dave@imexsystems.ca>
This commit is contained in:
Dave Richer
2024-01-11 01:29:36 -05:00
parent ab299619dd
commit d3654ec16e
7 changed files with 237 additions and 171 deletions

View File

@@ -1,7 +1,7 @@
import {useSplitClient} from "@splitsoftware/splitio-react"; import {useSplitClient} from "@splitsoftware/splitio-react";
import {Button, Result} from "antd"; import {Button, Result} from "antd";
import LogRocket from "logrocket"; 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 {useTranslation} from "react-i18next";
import {connect} from "react-redux"; import {connect} from "react-redux";
import {Route, Routes} from "react-router-dom"; 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 {selectBodyshop, selectCurrentUser,} from "../redux/user/user.selectors";
import PrivateRoute from "../components/PrivateRoute"; import PrivateRoute from "../components/PrivateRoute";
import "./App.styles.scss"; import "./App.styles.scss";
import handleBeta from "../utils/betaHandler";
const ResetPassword = lazy(() => const ResetPassword = lazy(() =>
import("../pages/reset-password/reset-password.component") import("../pages/reset-password/reset-password.component")
@@ -40,14 +41,16 @@ const mapDispatchToProps = (dispatch) => ({
setOnline: (isOnline) => dispatch(setOnline(isOnline)), setOnline: (isOnline) => dispatch(setOnline(isOnline)),
}); });
export function App({ export function App({bodyshop, checkUserSession, currentUser, online, setOnline}) {
bodyshop,
checkUserSession,
currentUser,
online,
setOnline,
}) {
const client = useSplitClient().client; const client = useSplitClient().client;
const [listenersAdded, setListenersAdded] = useState(false)
const {t} = useTranslation();
// Handle The Beta Switch.
useEffect(() => {
handleBeta();
}, [])
useEffect(() => { useEffect(() => {
if (!navigator.onLine) { if (!navigator.onLine) {
@@ -60,15 +63,28 @@ export function App({
//const b = Grid.useBreakpoint(); //const b = Grid.useBreakpoint();
// console.log("Breakpoints:", b); // console.log("Breakpoints:", b);
const {t} = useTranslation(); const offlineListener = (e) => {
window.addEventListener("offline", function (e) {
setOnline(false); setOnline(false);
}); }
window.addEventListener("online", function (e) { const onlineListener = (e) => {
setOnline(true); 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(() => { useEffect(() => {
if (currentUser.authorized && bodyshop) { if (currentUser.authorized && bodyshop) {

View File

@@ -4,20 +4,11 @@ import { Button, notification, Space } from "antd";
import axios from "axios"; import axios from "axios";
import React, { useEffect } from "react"; import React, { useEffect } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
import { messaging, requestForToken } from "../../firebase/firebase.utils"; 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 FcmHandler from "../../utils/fcm-handler";
import ChatPopupComponent from "../chat-popup/chat-popup.component"; import ChatPopupComponent from "../chat-popup/chat-popup.component";
import "./chat-affix.styles.scss"; import "./chat-affix.styles.scss";
const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop,
chatVisible: selectChatVisible,
});
export function ChatAffixContainer({ bodyshop, chatVisible }) { export function ChatAffixContainer({ bodyshop, chatVisible }) {
const { t } = useTranslation(); const { t } = useTranslation();
const client = useApolloClient(); const client = useApolloClient();
@@ -36,35 +27,34 @@ export function ChatAffixContainer({ bodyshop, chatVisible }) {
console.log("FCM Topic Subscription", r.data); console.log("FCM Topic Subscription", r.data);
} catch (error) { } catch (error) {
console.log( console.log(
"Error attempting to subscribe to messaging topic: ", "Error attempting to subscribe to messaging topic: ",
error error
); );
notification.open({ notification.open({
type: "warning", type: "warning",
message: t("general.errors.fcm"), message: t("general.errors.fcm"),
btn: ( btn: (
<Space> <Space>
<Button <Button
onClick={async () => { onClick={async () => {
await requestForToken(); await requestForToken();
SubscribeToTopic();
SubscribeToTopic(); }}
}} >
> {t("general.actions.tryagain")}
{t("general.actions.tryagain")} </Button>
</Button> <Button
<Button onClick={() => {
onClick={() => { const win = window.open(
const win = window.open( "https://help.imex.online/en/article/enabling-notifications-o978xi/",
"https://help.imex.online/en/article/enabling-notifications-o978xi/", "_blank"
"_blank" );
); win.focus();
win.focus(); }}
}} >
> {t("general.labels.help")}
{t("general.labels.help")} </Button>
</Button> </Space>
</Space>
), ),
}); });
} }
@@ -81,16 +71,16 @@ export function ChatAffixContainer({ bodyshop, chatVisible }) {
payload: (payload && payload.data && payload.data.data) || payload.data, payload: (payload && payload.data && payload.data.data) || payload.data,
}); });
} }
let stopMessageListenr, channel; let stopMessageListener, channel;
try { try {
stopMessageListenr = onMessage(messaging, handleMessage); stopMessageListener = onMessage(messaging, handleMessage);
channel = new BroadcastChannel("imex-sw-messages"); channel = new BroadcastChannel("imex-sw-messages");
channel.addEventListener("message", handleMessage); channel.addEventListener("message", handleMessage);
} catch (error) { } catch (error) {
console.log("Unable to set event listeners."); console.log("Unable to set event listeners.");
} }
return () => { return () => {
stopMessageListenr && stopMessageListenr(); stopMessageListener && stopMessageListener();
channel && channel.removeEventListener("message", handleMessage); channel && channel.removeEventListener("message", handleMessage);
}; };
}, [client]); }, [client]);
@@ -98,9 +88,10 @@ export function ChatAffixContainer({ bodyshop, chatVisible }) {
if (!bodyshop || !bodyshop.messagingservicesid) return <></>; if (!bodyshop || !bodyshop.messagingservicesid) return <></>;
return ( return (
<div className={`chat-affix ${chatVisible ? "chat-affix-open" : ""}`}> <div className={`chat-affix ${chatVisible ? "chat-affix-open" : ""}`}>
{bodyshop && bodyshop.messagingservicesid ? <ChatPopupComponent /> : null} {bodyshop && bodyshop.messagingservicesid ? <ChatPopupComponent /> : null}
</div> </div>
); );
} }
export default connect(mapStateToProps, null)(ChatAffixContainer);
export default ChatAffixContainer;

View File

@@ -12,7 +12,7 @@ import Icon, {
FileAddOutlined, FileAddOutlined,
FileFilled, FileFilled,
HomeFilled, HomeFilled,
ImportOutlined, ImportOutlined, InfoCircleOutlined,
LineChartOutlined, LineChartOutlined,
PaperClipOutlined, PaperClipOutlined,
PhoneOutlined, PhoneOutlined,
@@ -25,8 +25,8 @@ import Icon, {
UserOutlined, UserOutlined,
} from "@ant-design/icons"; } from "@ant-design/icons";
import {useSplitTreatments} from "@splitsoftware/splitio-react"; import {useSplitTreatments} from "@splitsoftware/splitio-react";
import {Layout, Menu} from "antd"; import {Layout, Menu, Switch, Tooltip} from "antd";
import React from "react"; import React, {useEffect, useState} from "react";
import {useTranslation} from "react-i18next"; import {useTranslation} from "react-i18next";
import {BsKanban} from "react-icons/bs"; import {BsKanban} from "react-icons/bs";
import {FaCalendarAlt, FaCarCrash, FaCreditCard, FaFileInvoiceDollar,} from "react-icons/fa"; 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 {signOutStart} from "../../redux/user/user.actions";
import {selectBodyshop, selectCurrentUser,} from "../../redux/user/user.selectors"; import {selectBodyshop, selectCurrentUser,} from "../../redux/user/user.selectors";
import {FiLogOut} from "react-icons/fi"; import {FiLogOut} from "react-icons/fi";
import handleBeta, {checkBeta, setBeta} from "../../utils/betaHandler";
const mapStateToProps = createStructuredSelector({ const mapStateToProps = createStructuredSelector({
currentUser: selectCurrentUser, currentUser: selectCurrentUser,
@@ -70,9 +71,21 @@ function Header({handleMenuClick, currentUser, bodyshop, selectedHeader, signOut
names: ["ImEXPay", "DmsAp", "Simple_Inventory"], names: ["ImEXPay", "DmsAp", "Simple_Inventory"],
splitKey: bodyshop && bodyshop.imexshopid, splitKey: bodyshop && bodyshop.imexshopid,
}); });
const [betaSwitch, setBetaSwitch] = useState(false);
const {t} = useTranslation(); const {t} = useTranslation();
useEffect(() => {
const isBeta = checkBeta();
setBetaSwitch(isBeta );
}, []);
const betaSwitchChange = (checked) => {
setBeta(checked);
setBetaSwitch(checked);
handleBeta();
}
const accountingChildren = [ const accountingChildren = [
{ {
key: 'bills', key: 'bills',
@@ -451,13 +464,28 @@ function Header({handleMenuClick, currentUser, bodyshop, selectedHeader, signOut
})), })),
} }
] ];
menuItems.push({
key: 'beta-switch',
style: { marginLeft: 'auto' },
label: (
<Tooltip title="A faster more modern ImEX Online is ready for you to try! You can switch back at any time.">
<InfoCircleOutlined />
<span style={{marginRight: 8}}>Try the new ImEX Online</span>
<Switch
checked={betaSwitch}
onChange={betaSwitchChange}
/>
</Tooltip>
)
});
return ( return (
<Layout.Header> <Layout.Header>
<Menu <Menu
mode="horizontal" mode="horizontal"
//theme="light"
theme={"dark"} theme={"dark"}
selectedKeys={[selectedHeader]} selectedKeys={[selectedHeader]}
onClick={handleMenuClick} onClick={handleMenuClick}

View File

@@ -1,6 +1,6 @@
import {FloatButton, Layout} from "antd"; import {FloatButton, Layout} from "antd";
import preval from "preval.macro"; import preval from "preval.macro";
import React, {lazy, Suspense, useEffect} from "react"; import React, {lazy, Suspense, useEffect, useState} from "react";
import {useTranslation} from "react-i18next"; import {useTranslation} from "react-i18next";
import {connect} from "react-redux"; import {connect} from "react-redux";
import {Link, Route, Routes} from "react-router-dom"; import {Link, Route, Routes} from "react-router-dom";
@@ -177,6 +177,8 @@ const mapStateToProps = createStructuredSelector({
export function Manage({conflict, bodyshop}) { export function Manage({conflict, bodyshop}) {
const {t} = useTranslation(); const {t} = useTranslation();
const [chatVisible, setChatVisible] = useState(false);
useEffect(() => { useEffect(() => {
const widgetId = "IABVNO4scRKY11XBQkNr"; const widgetId = "IABVNO4scRKY11XBQkNr";
@@ -360,7 +362,7 @@ export function Manage({conflict, bodyshop}) {
return ( return (
<> <>
<ChatAffixContainer/> <ChatAffixContainer bodyshop={bodyshop} chatVisible={chatVisible} />
<Layout className="layout-container"> <Layout className="layout-container">
<UpdateAlert/> <UpdateAlert/>
<HeaderContainer/> <HeaderContainer/>

View File

@@ -42,9 +42,7 @@ if (process.env.NODE_ENV === "development") {
export const store = configureStore({ export const store = configureStore({
reducer: rootReducer, reducer: rootReducer,
middleware: (getDefaultMiddleware) => getDefaultMiddleware({ middleware: (getDefaultMiddleware) => getDefaultMiddleware({
serializableCheck: { serializableCheck: false,
ignoredActions: [FLUSH, REHYDRATE, PAUSE, PERSIST, PURGE, REGISTER],
},
}).concat(middlewares), }).concat(middlewares),
// middleware: middlewares, // middleware: middlewares,
devTools: process.env.NODE_ENV !== 'production', devTools: process.env.NODE_ENV !== 'production',

View File

@@ -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;

View File

@@ -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 //Based on https://www.fullstacklabs.co/blog/keyboard-shortcuts-with-react-hooks
const blacklistedTargets = []; // ["INPUT", "TEXTAREA"]; const blacklistedTargets = []; // ["INPUT", "TEXTAREA"];
export const useKeyboardSaveShortcut = (callback) => export const useKeyboardSaveShortcut = (callback) =>
useKeyboardShortcut(["Control", "S"], callback, { overrideSystem: true }); useKeyboardShortcut(["Control", "S"], callback, {overrideSystem: true});
const keysReducer = (state, action) => { const keysReducer = (state, action) => {
switch (action.type) { switch (action.type) {
case "set-key-down": case "set-key-down":
const keydownState = { ...state, [action.key]: true }; const keydownState = {...state, [action.key]: true};
return keydownState; return keydownState;
case "set-key-up": case "set-key-up":
const keyUpState = { ...state, [action.key]: false }; const keyUpState = {...state, [action.key]: false};
return keyUpState; return keyUpState;
case "reset-keys": case "reset-keys":
const resetState = { ...action.data }; const resetState = {...action.data};
return resetState; return resetState;
default: default:
return state; return state;
} }
}; };
const useKeyboardShortcut = (shortcutKeys, callback, options) => { const useKeyboardShortcut = (shortcutKeys, callback, options) => {
if (!Array.isArray(shortcutKeys)) if (!Array.isArray(shortcutKeys))
throw new Error( throw new Error(
"The first parameter to `useKeyboardShortcut` must be an ordered array of `KeyboardEvent.key` strings." "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) const keyupListener = useCallback(
throw new Error( (assignedKey) => (keyupEvent) => {
"The first parameter to `useKeyboardShortcut` must contain atleast one `KeyboardEvent.key` string." 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") useEffect(() => {
throw new Error( if (!Object.values(keys).filter((value) => !value).length) {
"The second parameter to `useKeyboardShortcut` must be a function that will be envoked when the keys are pressed." 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 || {}; useEffect(() => {
const initalKeyMapping = shortcutKeys.reduce((currentKeys, key) => { if (!listenersAdded) {
currentKeys[key.toLowerCase()] = false; console.log('Added events for keyup and keydown');
return currentKeys; shortcutKeys.forEach((k) => {
}, {}); window.addEventListener("keydown", keydownListener(k));
window.addEventListener("keyup", keyupListener(k))
});
}
const [keys, setKeys] = useReducer(keysReducer, initalKeyMapping); setListenersAdded(true);
const keydownListener = useCallback( return () => {
(assignedKey) => (keydownEvent) => { shortcutKeys.forEach((k) => {
const loweredKey = assignedKey.toLowerCase(); 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; export default useKeyboardShortcut;
function disabledEventPropagation(e) { function disabledEventPropagation(e) {
if (e) { if (e) {
if (e.stopPropagation) { if (e.stopPropagation) {
e.stopPropagation(); e.stopPropagation();
} else if (window.event) { } else if (window.event) {
window.event.cancelBubble = true; window.event.cancelBubble = true;
}
} }
}
} }