IO-3020 IO-3036 Update ESLint. Add LockWrapper for Header. Add Blur Wrapper.

This commit is contained in:
Patrick Fic
2024-11-29 14:38:56 -08:00
parent 4433f0f57f
commit 801cd724ac
7 changed files with 393 additions and 212 deletions

View File

@@ -3,10 +3,19 @@ import pluginJs from "@eslint/js";
import pluginReact from "eslint-plugin-react"; import pluginReact from "eslint-plugin-react";
/** @type {import('eslint').Linter.Config[]} */ /** @type {import('eslint').Linter.Config[]} */
export default [ export default [
{ files: ["**/*.{js,mjs,cjs,jsx}"] }, {
files: ["**/*.{js,mjs,cjs,jsx}"]
},
{ languageOptions: { globals: globals.browser } }, { languageOptions: { globals: globals.browser } },
pluginJs.configs.recommended, 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"]
]; ];

View File

@@ -173,3 +173,9 @@
.muted-button:hover { .muted-button:hover {
color: darkgrey; color: darkgrey;
} }
.blur-feature {
filter: blur(5px);
user-select: none;
pointer-events: none;
}

View File

@@ -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();
}

View File

@@ -11,11 +11,22 @@ const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop bodyshop: selectBodyshop
}); });
function FeatureWrapper({ bodyshop, featureName, noauth, children, ...restProps }) { function FeatureWrapper({ bodyshop, featureName, noauth, blurContent = false, children, ...restProps }) {
const { t } = useTranslation(); const { t } = useTranslation();
if (HasFeatureAccess({ featureName, bodyshop })) return children; if (HasFeatureAccess({ featureName, bodyshop })) return children;
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 ( return (
noauth || ( noauth || (
<AlertComponent <AlertComponent
@@ -30,6 +41,7 @@ function FeatureWrapper({ bodyshop, featureName, noauth, children, ...restProps
) )
); );
} }
}
export function HasFeatureAccess({ featureName, bodyshop }) { export function HasFeatureAccess({ featureName, bodyshop }) {
return bodyshop?.features?.allAccess || dayjs(bodyshop?.features[featureName]).isAfter(dayjs()); return bodyshop?.features?.allAccess || dayjs(bodyshop?.features[featureName]).isAfter(dayjs());

View File

@@ -26,7 +26,7 @@ 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, Space } from "antd";
import React from "react"; import React from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { BsKanban } from "react-icons/bs"; 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 { selectBodyshop, selectCurrentUser } from "../../redux/user/user.selectors";
import InstanceRenderManager from "../../utils/instanceRenderMgr"; import InstanceRenderManager from "../../utils/instanceRenderMgr";
import { HasFeatureAccess } from "../feature-wrapper/feature-wrapper.component"; import { HasFeatureAccess } from "../feature-wrapper/feature-wrapper.component";
import LockWrapper from "../lock-wrapper/locker-wrapper.component";
const mapStateToProps = createStructuredSelector({ const mapStateToProps = createStructuredSelector({
currentUser: selectCurrentUser, currentUser: selectCurrentUser,
@@ -128,25 +129,32 @@ function Header({
const accountingChildren = []; const accountingChildren = [];
if (
InstanceRenderManager({
imex: HasFeatureAccess({ featureName: "bills", bodyshop }),
rome: "USE_IMEX"
})
) {
accountingChildren.push( accountingChildren.push(
{ {
key: "bills", key: "bills",
id: "header-accounting-bills", id: "header-accounting-bills",
icon: <Icon component={FaFileInvoiceDollar} />, icon: <Icon component={FaFileInvoiceDollar} />,
label: <Link to="/manage/bills">{t("menus.header.bills")}</Link> label: (
<Link to="/manage/bills">
<LockWrapper featureName="bills" bodyshop={bodyshop}>
{t("menus.header.bills")}
</LockWrapper>
</Link>
)
}, },
{ {
key: "enterbills", key: "enterbills",
id: "header-accounting-enterbills", id: "header-accounting-enterbills",
icon: <Icon component={GiPayMoney} />, icon: <Icon component={GiPayMoney} />,
label: t("menus.header.enterbills"), label: (
<Space>
<LockWrapper featureName="bills" bodyshop={bodyshop}>
{t(t("menus.header.enterbills"))}
</LockWrapper>
</Space>
),
onClick: () => { onClick: () => {
HasFeatureAccess({ featureName: "bills", bodyshop }) &&
setBillEnterContext({ setBillEnterContext({
actions: {}, actions: {},
context: {} context: {}
@@ -154,7 +162,6 @@ function Header({
} }
} }
); );
}
if (Simple_Inventory.treatment === "on") { if (Simple_Inventory.treatment === "on") {
accountingChildren.push( accountingChildren.push(
@@ -169,12 +176,7 @@ function Header({
} }
); );
} }
if (
InstanceRenderManager({
imex: HasFeatureAccess({ featureName: "payments", bodyshop }),
rome: "USE_IMEX"
})
) {
accountingChildren.push( accountingChildren.push(
{ {
type: "divider" type: "divider"
@@ -183,14 +185,25 @@ function Header({
key: "allpayments", key: "allpayments",
id: "header-accounting-allpayments", id: "header-accounting-allpayments",
icon: <BankFilled />, icon: <BankFilled />,
label: <Link to="/manage/payments">{t("menus.header.allpayments")}</Link> label: (
<Link to="/manage/payments">
<LockWrapper featureName="payments" bodyshop={bodyshop}>
{t("menus.header.allpayments")}
</LockWrapper>
</Link>
)
}, },
{ {
key: "enterpayments", key: "enterpayments",
id: "header-accounting-enterpayments", id: "header-accounting-enterpayments",
icon: <Icon component={FaCreditCard} />, icon: <Icon component={FaCreditCard} />,
label: t("menus.header.enterpayment"), label: (
<LockWrapper featureName="payments" bodyshop={bodyshop}>
{t("menus.header.enterpayment")}
</LockWrapper>
),
onClick: () => { onClick: () => {
HasFeatureAccess({ featureName: "payments", bodyshop }) &&
setPaymentContext({ setPaymentContext({
actions: {}, actions: {},
context: null context: null
@@ -198,7 +211,6 @@ function Header({
} }
} }
); );
}
if (ImEXPay.treatment === "on") { if (ImEXPay.treatment === "on") {
accountingChildren.push({ accountingChildren.push({
@@ -215,12 +227,6 @@ function Header({
}); });
} }
if (
InstanceRenderManager({
imex: HasFeatureAccess({ featureName: "timetickets", bodyshop }),
rome: "USE_IMEX"
})
) {
accountingChildren.push( accountingChildren.push(
{ {
type: "divider" type: "divider"
@@ -229,7 +235,13 @@ function Header({
key: "timetickets", key: "timetickets",
id: "header-accounting-timetickets", id: "header-accounting-timetickets",
icon: <FieldTimeOutlined />, icon: <FieldTimeOutlined />,
label: <Link to="/manage/timetickets">{t("menus.header.timetickets")}</Link> label: (
<Link to="/manage/timetickets">
<LockWrapper featureName="timetickets" bodyshop={bodyshop}>
{t("menus.header.timetickets")}
</LockWrapper>
</Link>
)
} }
); );
@@ -245,9 +257,14 @@ function Header({
{ {
key: "entertimetickets", key: "entertimetickets",
icon: <Icon component={GiPlayerTime} />, icon: <Icon component={GiPlayerTime} />,
label: t("menus.header.entertimeticket"), label: (
<LockWrapper featureName="timetickets" bodyshop={bodyshop}>
{t("menus.header.entertimeticket")}
</LockWrapper>
),
id: "header-accounting-entertimetickets", id: "header-accounting-entertimetickets",
onClick: () => { onClick: () => {
HasFeatureAccess({ featureName: "timetickets", bodyshop }) &&
setTimeTicketContext({ setTimeTicketContext({
actions: {}, actions: {},
context: { context: {
@@ -262,13 +279,18 @@ function Header({
type: "divider" type: "divider"
} }
); );
}
const accountingExportChildren = [ const accountingExportChildren = [
{ {
key: "receivables", key: "receivables",
id: "header-accounting-receivables", id: "header-accounting-receivables",
label: <Link to="/manage/accounting/receivables">{t("menus.header.accounting-receivables")}</Link> label: (
<Link to="/manage/accounting/receivables">
<LockWrapper featureName="export" bodyshop={bodyshop}>
{t("menus.header.accounting-receivables")}
</LockWrapper>
</Link>
)
} }
]; ];
@@ -276,7 +298,13 @@ function Header({
accountingExportChildren.push({ accountingExportChildren.push({
key: "payables", key: "payables",
id: "header-accounting-payables", id: "header-accounting-payables",
label: <Link to="/manage/accounting/payables">{t("menus.header.accounting-payables")}</Link> label: (
<Link to="/manage/accounting/payables">
<LockWrapper featureName="export" bodyshop={bodyshop}>
{t("menus.header.accounting-payables")}
</LockWrapper>
</Link>
)
}); });
} }
@@ -284,7 +312,13 @@ function Header({
accountingExportChildren.push({ accountingExportChildren.push({
key: "payments", key: "payments",
id: "header-accounting-payments", id: "header-accounting-payments",
label: <Link to="/manage/accounting/payments">{t("menus.header.accounting-payments")}</Link> label: (
<Link to="/manage/accounting/payments">
<LockWrapper featureName="export" bodyshop={bodyshop}>
{t("menus.header.accounting-payments")}
</LockWrapper>
</Link>
)
}); });
} }
@@ -295,24 +329,27 @@ function Header({
{ {
key: "exportlogs", key: "exportlogs",
id: "header-accounting-exportlogs", id: "header-accounting-exportlogs",
label: <Link to="/manage/accounting/exportlogs">{t("menus.header.export-logs")}</Link> label: (
<Link to="/manage/accounting/exportlogs">
<LockWrapper featureName="export" bodyshop={bodyshop}>
{t("menus.header.export-logs")}
</LockWrapper>
</Link>
)
} }
); );
if (
InstanceRenderManager({
imex: HasFeatureAccess({ featureName: "export", bodyshop }),
rome: "USE_IMEX"
})
) {
accountingChildren.push({ accountingChildren.push({
key: "accountingexport", key: "accountingexport",
id: "header-accounting-export", id: "header-accounting-export",
icon: <ExportOutlined />, icon: <ExportOutlined />,
label: t("menus.header.export"), label: (
<LockWrapper featureName="export" bodyshop={bodyshop}>
{t("menus.header.export")}
</LockWrapper>
),
children: accountingExportChildren children: accountingExportChildren
}); });
}
const menuItems = [ const menuItems = [
{ {
@@ -381,25 +418,20 @@ function Header({
icon: <ScheduleOutlined />, icon: <ScheduleOutlined />,
label: <Link to="/manage/production/list">{t("menus.header.productionlist")}</Link> label: <Link to="/manage/production/list">{t("menus.header.productionlist")}</Link>
}, },
...(InstanceRenderManager({
imex: HasFeatureAccess({ featureName: "visualboard", bodyshop }),
rome: "USE_IMEX"
})
? [
{ {
key: "productionboard", key: "productionboard",
id: "header-production-board", id: "header-production-board",
icon: <Icon component={BsKanban} />, icon: <Icon component={BsKanban} />,
label: <Link to="/manage/production/board">{t("menus.header.productionboard")}</Link> label: (
} <Link to="/manage/production/board">
] <LockWrapper featureName="visualboard" bodyshop={bodyshop}>
: []), {t("menus.header.productionboard")}
</LockWrapper>
</Link>
)
},
...(InstanceRenderManager({
imex: HasFeatureAccess({ featureName: "scoreboard", bodyshop }),
rome: "USE_IMEX"
})
? [
{ {
type: "divider" type: "divider"
}, },
@@ -407,11 +439,15 @@ function Header({
key: "scoreboard", key: "scoreboard",
id: "header-scoreboard", id: "header-scoreboard",
icon: <LineChartOutlined />, icon: <LineChartOutlined />,
label: <Link to="/manage/scoreboard">{t("menus.header.scoreboard")}</Link> label: (
<Link to="/manage/scoreboard">
<LockWrapper featureName="scoreboard" bodyshop={bodyshop}>
{t("menus.header.scoreboard")}
</LockWrapper>
</Link>
)
} }
] ]
: [])
]
}, },
{ {
key: "customers", key: "customers",
@@ -433,39 +469,54 @@ function Header({
} }
] ]
}, },
...(InstanceRenderManager({
imex: HasFeatureAccess({ featureName: "courtesycars", bodyshop }),
rome: "USE_IMEX"
})
? [
{ {
key: "ccs", key: "ccs",
id: "header-css", id: "header-css",
icon: <CarFilled />, icon: <CarFilled />,
label: t("menus.header.courtesycars"), label: (
<LockWrapper featureName="courtesycars" bodyshop={bodyshop}>
{t("menus.header.courtesycars")}
</LockWrapper>
),
children: [ children: [
{ {
key: "courtesycarsall", key: "courtesycarsall",
id: "header-courtesycars-all", id: "header-courtesycars-all",
icon: <CarFilled />, icon: <CarFilled />,
label: <Link to="/manage/courtesycars">{t("menus.header.courtesycars-all")}</Link> label: (
<Link to="/manage/courtesycars">
<LockWrapper featureName="courtesycars" bodyshop={bodyshop}>
{t("menus.header.courtesycars-all")}
</LockWrapper>
</Link>
)
}, },
{ {
key: "contracts", key: "contracts",
id: "header-contracts", id: "header-contracts",
icon: <FileFilled />, icon: <FileFilled />,
label: <Link to="/manage/courtesycars/contracts">{t("menus.header.courtesycars-contracts")}</Link> label: (
<Link to="/manage/courtesycars/contracts">
<LockWrapper featureName="courtesycars" bodyshop={bodyshop}>
{t("menus.header.courtesycars-contracts")}
</LockWrapper>
</Link>
)
}, },
{ {
key: "newcontract", key: "newcontract",
id: "header-newcontract", id: "header-newcontract",
icon: <FileAddFilled />, icon: <FileAddFilled />,
label: <Link to="/manage/courtesycars/contracts/new">{t("menus.header.courtesycars-newcontract")}</Link> label: (
<Link to="/manage/courtesycars/contracts/new">
<LockWrapper featureName="courtesycars" bodyshop={bodyshop}>
{t("menus.header.courtesycars-newcontract")}
</LockWrapper>
</Link>
)
} }
] ]
} },
]
: []),
...(accountingChildren.length > 0 ...(accountingChildren.length > 0
? [ ? [
@@ -484,19 +535,20 @@ function Header({
icon: <PhoneOutlined />, icon: <PhoneOutlined />,
label: <Link to="/manage/phonebook">{t("menus.header.phonebook")}</Link> label: <Link to="/manage/phonebook">{t("menus.header.phonebook")}</Link>
}, },
...(InstanceRenderManager({
imex: HasFeatureAccess({ featureName: "media", bodyshop }),
rome: "USE_IMEX"
})
? [
{ {
key: "temporarydocs", key: "temporarydocs",
id: "header-temporarydocs", id: "header-temporarydocs",
icon: <PaperClipOutlined />, icon: <PaperClipOutlined />,
label: <Link to="/manage/temporarydocs">{t("menus.header.temporarydocs")}</Link> label: (
} <Link to="/manage/temporarydocs">
] <LockWrapper featureName="media" bodyshop={bodyshop}>
: []), {t("menus.header.temporarydocs")}
</LockWrapper>
</Link>
)
},
{ {
key: "tasks", key: "tasks",
id: "tasks", id: "tasks",
@@ -562,20 +614,20 @@ function Header({
icon: <Icon component={IoBusinessOutline} />, icon: <Icon component={IoBusinessOutline} />,
label: <Link to="/manage/shop/vendors">{t("menus.header.shop_vendors")}</Link> label: <Link to="/manage/shop/vendors">{t("menus.header.shop_vendors")}</Link>
}, },
...(InstanceRenderManager({
imex: HasFeatureAccess({ featureName: "csi", bodyshop }),
rome: "USE_IMEX"
})
? [
{ {
key: "shop-csi", key: "shop-csi",
id: "header-shop-csi", id: "header-shop-csi",
icon: <Icon component={RiSurveyLine} />, icon: <Icon component={RiSurveyLine} />,
label: <Link to="/manage/shop/csi">{t("menus.header.shop_csi")}</Link> label: (
<Link to="/manage/shop/csi">
<LockWrapper featureName="export" bodyshop={bodyshop}>
{t("menus.header.shop_csi")}
</LockWrapper>
</Link>
)
} }
] ]
: [])
]
}, },
{ {
key: "user", key: "user",
@@ -630,7 +682,13 @@ function Header({
key: "shiftclock", key: "shiftclock",
id: "header-shiftclock", id: "header-shiftclock",
icon: <Icon component={GiPlayerTime} />, icon: <Icon component={GiPlayerTime} />,
label: <Link to="/manage/shiftclock">{t("menus.header.shiftclock")}</Link> label: (
<Link to="/manage/shiftclock">
<LockWrapper featureName="export" bodyshop={bodyshop}>
{t("menus.header.shiftclock")}
</LockWrapper>
</Link>
)
} }
] ]
: []), : []),

View File

@@ -6,6 +6,7 @@ import InstanceRenderManager from "../../utils/instanceRenderMgr";
import AlertComponent from "../alert/alert.component"; import AlertComponent from "../alert/alert.component";
import LoadingSkeleton from "../loading-skeleton/loading-skeleton.component"; import LoadingSkeleton from "../loading-skeleton/loading-skeleton.component";
import "./job-bills-total.styles.scss"; import "./job-bills-total.styles.scss";
import BlurWrapperComponent from "../feature-wrapper/blur-wrapper.component";
export default function JobBillsTotalComponent({ export default function JobBillsTotalComponent({
loading, loading,
@@ -18,7 +19,7 @@ export default function JobBillsTotalComponent({
const { t } = useTranslation(); const { t } = useTranslation();
if (loading) return <LoadingSkeleton />; if (loading) return <LoadingSkeleton />;
if (!!!jobTotals) { if (!jobTotals) {
if (showWarning && warningCallback && typeof warningCallback === "function") { if (showWarning && warningCallback && typeof warningCallback === "function") {
warningCallback({ key: "bills", warning: t("jobs.errors.nofinancial") }); warningCallback({ key: "bills", warning: t("jobs.errors.nofinancial") });
} }
@@ -97,7 +98,7 @@ export default function JobBillsTotalComponent({
.add( .add(
InstanceRenderManager({ InstanceRenderManager({
imex: Dinero(), imex: Dinero(),
rome: Dinero(totals.additional.additionalCosts), rome: Dinero(totals.additional.additionalCosts)
}) })
); // Additional costs were captured for Rome, but not imex. ); // Additional costs were captured for Rome, but not imex.

View File

@@ -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 (
<Space>
{!HasFeatureAccess({ featureName: featureName, bodyshop }) && <LockOutlined style={{ color: "tomato" }} />}
{renderedChildren}
</Space>
);
};
export default connect(mapStateToProps, null)(LockWrapper);