From 801cd724ac60847c6c69896bf1a7676ee7685d83 Mon Sep 17 00:00:00 2001 From: Patrick Fic Date: Fri, 29 Nov 2024 14:38:56 -0800 Subject: [PATCH] IO-3020 IO-3036 Update ESLint. Add LockWrapper for Header. Add Blur Wrapper. --- client/eslint.config.js | 15 +- client/src/App/App.styles.scss | 6 + .../blur-wrapper.component.jsx | 57 +++ .../feature-wrapper.component.jsx | 40 +- .../components/header/header.component.jsx | 444 ++++++++++-------- .../job-bills-total.component.jsx | 5 +- .../lock-wrapper/locker-wrapper.component.jsx | 38 ++ 7 files changed, 393 insertions(+), 212 deletions(-) create mode 100644 client/src/components/feature-wrapper/blur-wrapper.component.jsx create mode 100644 client/src/components/lock-wrapper/locker-wrapper.component.jsx diff --git a/client/eslint.config.js b/client/eslint.config.js index b1f7dd1c2..0ced5d379 100644 --- a/client/eslint.config.js +++ b/client/eslint.config.js @@ -3,10 +3,19 @@ import pluginJs from "@eslint/js"; import pluginReact from "eslint-plugin-react"; /** @type {import('eslint').Linter.Config[]} */ + export default [ - { files: ["**/*.{js,mjs,cjs,jsx}"] }, + { + files: ["**/*.{js,mjs,cjs,jsx}"] + }, { languageOptions: { globals: globals.browser } }, pluginJs.configs.recommended, - pluginReact.configs.flat.recommended, // This is not a plugin object, but a shareable config object - pluginReact.configs.flat["jsx-runtime"] // Add this if you are using React 17+ + { + ...pluginReact.configs.flat.recommended, + rules: { + ...pluginReact.configs.flat.recommended.rules, + "react/prop-types": 0 + } + }, + pluginReact.configs.flat["jsx-runtime"] ]; diff --git a/client/src/App/App.styles.scss b/client/src/App/App.styles.scss index 827d0dfc7..8d44de002 100644 --- a/client/src/App/App.styles.scss +++ b/client/src/App/App.styles.scss @@ -173,3 +173,9 @@ .muted-button:hover { color: darkgrey; } + +.blur-feature { + filter: blur(5px); + user-select: none; + pointer-events: none; +} diff --git a/client/src/components/feature-wrapper/blur-wrapper.component.jsx b/client/src/components/feature-wrapper/blur-wrapper.component.jsx new file mode 100644 index 000000000..53d3ae0fb --- /dev/null +++ b/client/src/components/feature-wrapper/blur-wrapper.component.jsx @@ -0,0 +1,57 @@ +import Dinero from "dinero.js"; +import React from "react"; +import { connect } from "react-redux"; +import { createStructuredSelector } from "reselect"; +import { selectBodyshop } from "../../redux/user/user.selectors"; +import { HasFeatureAccess } from "./feature-wrapper.component"; + +const mapStateToProps = createStructuredSelector({ + bodyshop: selectBodyshop +}); +const blurringProps = { + filter: "blur(6px)" +}; + +export function BlurWrapper({ + bodyshop, + featureName, + styleProp = "style", + valueProp = "value", + overrideValue = true, + overrideValueFunction, + children +}) { + if (!HasFeatureAccess({ featureName, bodyshop })) { + const childrenWithBlurProps = React.Children.map(children, (child) => { + if (React.isValidElement(child)) { + //Clone the child, and spread in our props to overwrite it. + let newValueProp; + if (!overrideValue) { + newValueProp = child.props[valueProp]; + } else { + if (typeof overrideValueFunction === "function") { + newValueProp = overrideValueFunction(); + } else if (overrideValueFunction === "RandomDinero") { + newValueProp = RandomDinero(); + } else { + newValueProp = "This is some random text. Nothing interesting here."; + } + } + + return React.cloneElement(child, { + [valueProp]: newValueProp, + [styleProp]: { ...child.props[styleProp], ...blurringProps } + }); + } + return child; + }); + + return childrenWithBlurProps; + } + return children; +} +export default connect(mapStateToProps, null)(BlurWrapper); + +function RandomDinero() { + return Dinero({ amount: Math.round(Math.exp(Math.random() * 100, 2)) }).toFormat(); +} diff --git a/client/src/components/feature-wrapper/feature-wrapper.component.jsx b/client/src/components/feature-wrapper/feature-wrapper.component.jsx index 8f2336369..2efc7430f 100644 --- a/client/src/components/feature-wrapper/feature-wrapper.component.jsx +++ b/client/src/components/feature-wrapper/feature-wrapper.component.jsx @@ -11,24 +11,36 @@ const mapStateToProps = createStructuredSelector({ bodyshop: selectBodyshop }); -function FeatureWrapper({ bodyshop, featureName, noauth, children, ...restProps }) { +function FeatureWrapper({ bodyshop, featureName, noauth, blurContent = false, children, ...restProps }) { const { t } = useTranslation(); if (HasFeatureAccess({ featureName, bodyshop })) return children; - return ( - noauth || ( - - ) - ); + if (blurContent) { + const childrenWithBlurProps = React.Children.map(children, (child) => { + // Checking isValidElement is the safe way and avoids a + // typescript error too. + if (React.isValidElement(child)) { + return React.cloneElement(child, { blur: true }); + } + return child; + }); + return childrenWithBlurProps; + } else { + return ( + noauth || ( + + ) + ); + } } export function HasFeatureAccess({ featureName, bodyshop }) { diff --git a/client/src/components/header/header.component.jsx b/client/src/components/header/header.component.jsx index 6457de46d..3d814d211 100644 --- a/client/src/components/header/header.component.jsx +++ b/client/src/components/header/header.component.jsx @@ -26,7 +26,7 @@ import Icon, { UserOutlined } from "@ant-design/icons"; import { useSplitTreatments } from "@splitsoftware/splitio-react"; -import { Layout, Menu } from "antd"; +import { Layout, Menu, Space } from "antd"; import React from "react"; import { useTranslation } from "react-i18next"; import { BsKanban } from "react-icons/bs"; @@ -44,6 +44,7 @@ import { signOutStart } from "../../redux/user/user.actions"; import { selectBodyshop, selectCurrentUser } from "../../redux/user/user.selectors"; import InstanceRenderManager from "../../utils/instanceRenderMgr"; import { HasFeatureAccess } from "../feature-wrapper/feature-wrapper.component"; +import LockWrapper from "../lock-wrapper/locker-wrapper.component"; const mapStateToProps = createStructuredSelector({ currentUser: selectCurrentUser, @@ -128,33 +129,39 @@ function Header({ const accountingChildren = []; - if ( - InstanceRenderManager({ - imex: HasFeatureAccess({ featureName: "bills", bodyshop }), - rome: "USE_IMEX" - }) - ) { - accountingChildren.push( - { - key: "bills", - id: "header-accounting-bills", - icon: , - label: {t("menus.header.bills")} - }, - { - key: "enterbills", - id: "header-accounting-enterbills", - icon: , - label: t("menus.header.enterbills"), - onClick: () => { + accountingChildren.push( + { + key: "bills", + id: "header-accounting-bills", + icon: , + label: ( + + + {t("menus.header.bills")} + + + ) + }, + { + key: "enterbills", + id: "header-accounting-enterbills", + icon: , + label: ( + + + {t(t("menus.header.enterbills"))} + + + ), + onClick: () => { + HasFeatureAccess({ featureName: "bills", bodyshop }) && setBillEnterContext({ actions: {}, context: {} }); - } } - ); - } + } + ); if (Simple_Inventory.treatment === "on") { accountingChildren.push( @@ -169,36 +176,41 @@ function Header({ } ); } - if ( - InstanceRenderManager({ - imex: HasFeatureAccess({ featureName: "payments", bodyshop }), - rome: "USE_IMEX" - }) - ) { - accountingChildren.push( - { - type: "divider" - }, - { - key: "allpayments", - id: "header-accounting-allpayments", - icon: , - label: {t("menus.header.allpayments")} - }, - { - key: "enterpayments", - id: "header-accounting-enterpayments", - icon: , - label: t("menus.header.enterpayment"), - onClick: () => { + + accountingChildren.push( + { + type: "divider" + }, + { + key: "allpayments", + id: "header-accounting-allpayments", + icon: , + label: ( + + + {t("menus.header.allpayments")} + + + ) + }, + { + key: "enterpayments", + id: "header-accounting-enterpayments", + icon: , + label: ( + + {t("menus.header.enterpayment")} + + ), + onClick: () => { + HasFeatureAccess({ featureName: "payments", bodyshop }) && setPaymentContext({ actions: {}, context: null }); - } } - ); - } + } + ); if (ImEXPay.treatment === "on") { accountingChildren.push({ @@ -215,39 +227,44 @@ function Header({ }); } - if ( - InstanceRenderManager({ - imex: HasFeatureAccess({ featureName: "timetickets", bodyshop }), - rome: "USE_IMEX" - }) - ) { - accountingChildren.push( - { - type: "divider" - }, - { - key: "timetickets", - id: "header-accounting-timetickets", - icon: , - label: {t("menus.header.timetickets")} - } - ); - - if (bodyshop?.md_tasks_presets?.use_approvals) { - accountingChildren.push({ - key: "ttapprovals", - id: "header-accounting-ttapprovals", - icon: , - label: {t("menus.header.ttapprovals")} - }); + accountingChildren.push( + { + type: "divider" + }, + { + key: "timetickets", + id: "header-accounting-timetickets", + icon: , + label: ( + + + {t("menus.header.timetickets")} + + + ) } - accountingChildren.push( - { - key: "entertimetickets", - icon: , - label: t("menus.header.entertimeticket"), - id: "header-accounting-entertimetickets", - onClick: () => { + ); + + if (bodyshop?.md_tasks_presets?.use_approvals) { + accountingChildren.push({ + key: "ttapprovals", + id: "header-accounting-ttapprovals", + icon: , + label: {t("menus.header.ttapprovals")} + }); + } + accountingChildren.push( + { + key: "entertimetickets", + icon: , + label: ( + + {t("menus.header.entertimeticket")} + + ), + id: "header-accounting-entertimetickets", + onClick: () => { + HasFeatureAccess({ featureName: "timetickets", bodyshop }) && setTimeTicketContext({ actions: {}, context: { @@ -256,19 +273,24 @@ function Header({ : currentUser.email } }); - } - }, - { - type: "divider" } - ); - } + }, + { + type: "divider" + } + ); const accountingExportChildren = [ { key: "receivables", id: "header-accounting-receivables", - label: {t("menus.header.accounting-receivables")} + label: ( + + + {t("menus.header.accounting-receivables")} + + + ) } ]; @@ -276,7 +298,13 @@ function Header({ accountingExportChildren.push({ key: "payables", id: "header-accounting-payables", - label: {t("menus.header.accounting-payables")} + label: ( + + + {t("menus.header.accounting-payables")} + + + ) }); } @@ -284,7 +312,13 @@ function Header({ accountingExportChildren.push({ key: "payments", id: "header-accounting-payments", - label: {t("menus.header.accounting-payments")} + label: ( + + + {t("menus.header.accounting-payments")} + + + ) }); } @@ -295,24 +329,27 @@ function Header({ { key: "exportlogs", id: "header-accounting-exportlogs", - label: {t("menus.header.export-logs")} + label: ( + + + {t("menus.header.export-logs")} + + + ) } ); - if ( - InstanceRenderManager({ - imex: HasFeatureAccess({ featureName: "export", bodyshop }), - rome: "USE_IMEX" - }) - ) { - accountingChildren.push({ - key: "accountingexport", - id: "header-accounting-export", - icon: , - label: t("menus.header.export"), - children: accountingExportChildren - }); - } + accountingChildren.push({ + key: "accountingexport", + id: "header-accounting-export", + icon: , + label: ( + + {t("menus.header.export")} + + ), + children: accountingExportChildren + }); const menuItems = [ { @@ -381,36 +418,35 @@ function Header({ icon: , label: {t("menus.header.productionlist")} }, - ...(InstanceRenderManager({ - imex: HasFeatureAccess({ featureName: "visualboard", bodyshop }), - rome: "USE_IMEX" - }) - ? [ - { - key: "productionboard", - id: "header-production-board", - icon: , - label: {t("menus.header.productionboard")} - } - ] - : []), - ...(InstanceRenderManager({ - imex: HasFeatureAccess({ featureName: "scoreboard", bodyshop }), - rome: "USE_IMEX" - }) - ? [ - { - type: "divider" - }, - { - key: "scoreboard", - id: "header-scoreboard", - icon: , - label: {t("menus.header.scoreboard")} - } - ] - : []) + { + key: "productionboard", + id: "header-production-board", + icon: , + label: ( + + + {t("menus.header.productionboard")} + + + ) + }, + + { + type: "divider" + }, + { + key: "scoreboard", + id: "header-scoreboard", + icon: , + label: ( + + + {t("menus.header.scoreboard")} + + + ) + } ] }, { @@ -433,39 +469,54 @@ function Header({ } ] }, - ...(InstanceRenderManager({ - imex: HasFeatureAccess({ featureName: "courtesycars", bodyshop }), - rome: "USE_IMEX" - }) - ? [ - { - key: "ccs", - id: "header-css", - icon: , - label: t("menus.header.courtesycars"), - children: [ - { - key: "courtesycarsall", - id: "header-courtesycars-all", - icon: , - label: {t("menus.header.courtesycars-all")} - }, - { - key: "contracts", - id: "header-contracts", - icon: , - label: {t("menus.header.courtesycars-contracts")} - }, - { - key: "newcontract", - id: "header-newcontract", - icon: , - label: {t("menus.header.courtesycars-newcontract")} - } - ] - } - ] - : []), + { + key: "ccs", + id: "header-css", + icon: , + label: ( + + {t("menus.header.courtesycars")} + + ), + children: [ + { + key: "courtesycarsall", + id: "header-courtesycars-all", + icon: , + label: ( + + + {t("menus.header.courtesycars-all")} + + + ) + }, + { + key: "contracts", + id: "header-contracts", + icon: , + label: ( + + + {t("menus.header.courtesycars-contracts")} + + + ) + }, + { + key: "newcontract", + id: "header-newcontract", + icon: , + label: ( + + + {t("menus.header.courtesycars-newcontract")} + + + ) + } + ] + }, ...(accountingChildren.length > 0 ? [ @@ -484,19 +535,20 @@ function Header({ icon: , label: {t("menus.header.phonebook")} }, - ...(InstanceRenderManager({ - imex: HasFeatureAccess({ featureName: "media", bodyshop }), - rome: "USE_IMEX" - }) - ? [ - { - key: "temporarydocs", - id: "header-temporarydocs", - icon: , - label: {t("menus.header.temporarydocs")} - } - ] - : []), + + { + key: "temporarydocs", + id: "header-temporarydocs", + icon: , + label: ( + + + {t("menus.header.temporarydocs")} + + + ) + }, + { key: "tasks", id: "tasks", @@ -562,19 +614,19 @@ function Header({ icon: , label: {t("menus.header.shop_vendors")} }, - ...(InstanceRenderManager({ - imex: HasFeatureAccess({ featureName: "csi", bodyshop }), - rome: "USE_IMEX" - }) - ? [ - { - key: "shop-csi", - id: "header-shop-csi", - icon: , - label: {t("menus.header.shop_csi")} - } - ] - : []) + + { + key: "shop-csi", + id: "header-shop-csi", + icon: , + label: ( + + + {t("menus.header.shop_csi")} + + + ) + } ] }, { @@ -630,7 +682,13 @@ function Header({ key: "shiftclock", id: "header-shiftclock", icon: , - label: {t("menus.header.shiftclock")} + label: ( + + + {t("menus.header.shiftclock")} + + + ) } ] : []), diff --git a/client/src/components/job-bills-total/job-bills-total.component.jsx b/client/src/components/job-bills-total/job-bills-total.component.jsx index ed123fa9c..140326283 100644 --- a/client/src/components/job-bills-total/job-bills-total.component.jsx +++ b/client/src/components/job-bills-total/job-bills-total.component.jsx @@ -6,6 +6,7 @@ import InstanceRenderManager from "../../utils/instanceRenderMgr"; import AlertComponent from "../alert/alert.component"; import LoadingSkeleton from "../loading-skeleton/loading-skeleton.component"; import "./job-bills-total.styles.scss"; +import BlurWrapperComponent from "../feature-wrapper/blur-wrapper.component"; export default function JobBillsTotalComponent({ loading, @@ -18,7 +19,7 @@ export default function JobBillsTotalComponent({ const { t } = useTranslation(); if (loading) return ; - if (!!!jobTotals) { + if (!jobTotals) { if (showWarning && warningCallback && typeof warningCallback === "function") { warningCallback({ key: "bills", warning: t("jobs.errors.nofinancial") }); } @@ -97,7 +98,7 @@ export default function JobBillsTotalComponent({ .add( InstanceRenderManager({ imex: Dinero(), - rome: Dinero(totals.additional.additionalCosts), + rome: Dinero(totals.additional.additionalCosts) }) ); // Additional costs were captured for Rome, but not imex. diff --git a/client/src/components/lock-wrapper/locker-wrapper.component.jsx b/client/src/components/lock-wrapper/locker-wrapper.component.jsx new file mode 100644 index 000000000..8b8711089 --- /dev/null +++ b/client/src/components/lock-wrapper/locker-wrapper.component.jsx @@ -0,0 +1,38 @@ +import { LockOutlined } from "@ant-design/icons"; +import { Space } from "antd"; +import React from "react"; +import { connect } from "react-redux"; +import { createStructuredSelector } from "reselect"; +import { selectRecentItems, selectSelectedHeader } from "../../redux/application/application.selectors"; +import { selectBodyshop, selectCurrentUser } from "../../redux/user/user.selectors"; +import { HasFeatureAccess } from "../feature-wrapper/feature-wrapper.component"; + +const mapStateToProps = createStructuredSelector({ + currentUser: selectCurrentUser, + recentItems: selectRecentItems, + selectedHeader: selectSelectedHeader, + bodyshop: selectBodyshop +}); + +const LockWrapper = ({ featureName, bodyshop, children, disabled = true }) => { + let renderedChildren = children; + + if (disabled) { + renderedChildren = React.Children.map(children, (child) => { + if (React.isValidElement(child)) { + return React.cloneElement(child, { + disabled: true + }); + } + return child; + }); + } + + return ( + + {!HasFeatureAccess({ featureName: featureName, bodyshop }) && } + {renderedChildren} + + ); +}; +export default connect(mapStateToProps, null)(LockWrapper);