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,24 +11,36 @@ 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;
return ( if (blurContent) {
noauth || ( const childrenWithBlurProps = React.Children.map(children, (child) => {
<AlertComponent // Checking isValidElement is the safe way and avoids a
message={t("general.messages.nofeatureaccess", { // typescript error too.
app: InstanceRenderManager({ if (React.isValidElement(child)) {
imex: "$t(titles.imexonline)", return React.cloneElement(child, { blur: true });
rome: "$t(titles.romeonline)" }
}) return child;
})} });
type="warning" return childrenWithBlurProps;
/> } else {
) return (
); noauth || (
<AlertComponent
message={t("general.messages.nofeatureaccess", {
app: InstanceRenderManager({
imex: "$t(titles.imexonline)",
rome: "$t(titles.romeonline)"
})
})}
type="warning"
/>
)
);
}
} }
export function HasFeatureAccess({ featureName, bodyshop }) { export function HasFeatureAccess({ featureName, bodyshop }) {

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,33 +129,39 @@ function Header({
const accountingChildren = []; const accountingChildren = [];
if ( accountingChildren.push(
InstanceRenderManager({ {
imex: HasFeatureAccess({ featureName: "bills", bodyshop }), key: "bills",
rome: "USE_IMEX" id: "header-accounting-bills",
}) icon: <Icon component={FaFileInvoiceDollar} />,
) { label: (
accountingChildren.push( <Link to="/manage/bills">
{ <LockWrapper featureName="bills" bodyshop={bodyshop}>
key: "bills", {t("menus.header.bills")}
id: "header-accounting-bills", </LockWrapper>
icon: <Icon component={FaFileInvoiceDollar} />, </Link>
label: <Link to="/manage/bills">{t("menus.header.bills")}</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: (
onClick: () => { <Space>
<LockWrapper featureName="bills" bodyshop={bodyshop}>
{t(t("menus.header.enterbills"))}
</LockWrapper>
</Space>
),
onClick: () => {
HasFeatureAccess({ featureName: "bills", bodyshop }) &&
setBillEnterContext({ setBillEnterContext({
actions: {}, actions: {},
context: {} context: {}
}); });
}
} }
); }
} );
if (Simple_Inventory.treatment === "on") { if (Simple_Inventory.treatment === "on") {
accountingChildren.push( accountingChildren.push(
@@ -169,36 +176,41 @@ function Header({
} }
); );
} }
if (
InstanceRenderManager({ accountingChildren.push(
imex: HasFeatureAccess({ featureName: "payments", bodyshop }), {
rome: "USE_IMEX" type: "divider"
}) },
) { {
accountingChildren.push( key: "allpayments",
{ id: "header-accounting-allpayments",
type: "divider" icon: <BankFilled />,
}, label: (
{ <Link to="/manage/payments">
key: "allpayments", <LockWrapper featureName="payments" bodyshop={bodyshop}>
id: "header-accounting-allpayments", {t("menus.header.allpayments")}
icon: <BankFilled />, </LockWrapper>
label: <Link to="/manage/payments">{t("menus.header.allpayments")}</Link> </Link>
}, )
{ },
key: "enterpayments", {
id: "header-accounting-enterpayments", key: "enterpayments",
icon: <Icon component={FaCreditCard} />, id: "header-accounting-enterpayments",
label: t("menus.header.enterpayment"), icon: <Icon component={FaCreditCard} />,
onClick: () => { label: (
<LockWrapper featureName="payments" bodyshop={bodyshop}>
{t("menus.header.enterpayment")}
</LockWrapper>
),
onClick: () => {
HasFeatureAccess({ featureName: "payments", bodyshop }) &&
setPaymentContext({ setPaymentContext({
actions: {}, actions: {},
context: null context: null
}); });
}
} }
); }
} );
if (ImEXPay.treatment === "on") { if (ImEXPay.treatment === "on") {
accountingChildren.push({ accountingChildren.push({
@@ -215,39 +227,44 @@ function Header({
}); });
} }
if ( accountingChildren.push(
InstanceRenderManager({ {
imex: HasFeatureAccess({ featureName: "timetickets", bodyshop }), type: "divider"
rome: "USE_IMEX" },
}) {
) { key: "timetickets",
accountingChildren.push( id: "header-accounting-timetickets",
{ icon: <FieldTimeOutlined />,
type: "divider" label: (
}, <Link to="/manage/timetickets">
{ <LockWrapper featureName="timetickets" bodyshop={bodyshop}>
key: "timetickets", {t("menus.header.timetickets")}
id: "header-accounting-timetickets", </LockWrapper>
icon: <FieldTimeOutlined />, </Link>
label: <Link to="/manage/timetickets">{t("menus.header.timetickets")}</Link> )
}
);
if (bodyshop?.md_tasks_presets?.use_approvals) {
accountingChildren.push({
key: "ttapprovals",
id: "header-accounting-ttapprovals",
icon: <FieldTimeOutlined />,
label: <Link to="/manage/ttapprovals">{t("menus.header.ttapprovals")}</Link>
});
} }
accountingChildren.push( );
{
key: "entertimetickets", if (bodyshop?.md_tasks_presets?.use_approvals) {
icon: <Icon component={GiPlayerTime} />, accountingChildren.push({
label: t("menus.header.entertimeticket"), key: "ttapprovals",
id: "header-accounting-entertimetickets", id: "header-accounting-ttapprovals",
onClick: () => { icon: <FieldTimeOutlined />,
label: <Link to="/manage/ttapprovals">{t("menus.header.ttapprovals")}</Link>
});
}
accountingChildren.push(
{
key: "entertimetickets",
icon: <Icon component={GiPlayerTime} />,
label: (
<LockWrapper featureName="timetickets" bodyshop={bodyshop}>
{t("menus.header.entertimeticket")}
</LockWrapper>
),
id: "header-accounting-entertimetickets",
onClick: () => {
HasFeatureAccess({ featureName: "timetickets", bodyshop }) &&
setTimeTicketContext({ setTimeTicketContext({
actions: {}, actions: {},
context: { context: {
@@ -256,19 +273,24 @@ function Header({
: currentUser.email : currentUser.email
} }
}); });
}
},
{
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 ( accountingChildren.push({
InstanceRenderManager({ key: "accountingexport",
imex: HasFeatureAccess({ featureName: "export", bodyshop }), id: "header-accounting-export",
rome: "USE_IMEX" icon: <ExportOutlined />,
}) label: (
) { <LockWrapper featureName="export" bodyshop={bodyshop}>
accountingChildren.push({ {t("menus.header.export")}
key: "accountingexport", </LockWrapper>
id: "header-accounting-export", ),
icon: <ExportOutlined />, children: accountingExportChildren
label: t("menus.header.export"), });
children: accountingExportChildren
});
}
const menuItems = [ const menuItems = [
{ {
@@ -381,36 +418,35 @@ 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",
id: "header-production-board",
icon: <Icon component={BsKanban} />,
label: <Link to="/manage/production/board">{t("menus.header.productionboard")}</Link>
}
]
: []),
...(InstanceRenderManager({ {
imex: HasFeatureAccess({ featureName: "scoreboard", bodyshop }), key: "productionboard",
rome: "USE_IMEX" id: "header-production-board",
}) icon: <Icon component={BsKanban} />,
? [ label: (
{ <Link to="/manage/production/board">
type: "divider" <LockWrapper featureName="visualboard" bodyshop={bodyshop}>
}, {t("menus.header.productionboard")}
{ </LockWrapper>
key: "scoreboard", </Link>
id: "header-scoreboard", )
icon: <LineChartOutlined />, },
label: <Link to="/manage/scoreboard">{t("menus.header.scoreboard")}</Link>
} {
] type: "divider"
: []) },
{
key: "scoreboard",
id: "header-scoreboard",
icon: <LineChartOutlined />,
label: (
<Link to="/manage/scoreboard">
<LockWrapper featureName="scoreboard" bodyshop={bodyshop}>
{t("menus.header.scoreboard")}
</LockWrapper>
</Link>
)
}
] ]
}, },
{ {
@@ -433,39 +469,54 @@ function Header({
} }
] ]
}, },
...(InstanceRenderManager({ {
imex: HasFeatureAccess({ featureName: "courtesycars", bodyshop }), key: "ccs",
rome: "USE_IMEX" id: "header-css",
}) icon: <CarFilled />,
? [ label: (
{ <LockWrapper featureName="courtesycars" bodyshop={bodyshop}>
key: "ccs", {t("menus.header.courtesycars")}
id: "header-css", </LockWrapper>
icon: <CarFilled />, ),
label: t("menus.header.courtesycars"), children: [
children: [ {
{ key: "courtesycarsall",
key: "courtesycarsall", id: "header-courtesycars-all",
id: "header-courtesycars-all", icon: <CarFilled />,
icon: <CarFilled />, label: (
label: <Link to="/manage/courtesycars">{t("menus.header.courtesycars-all")}</Link> <Link to="/manage/courtesycars">
}, <LockWrapper featureName="courtesycars" bodyshop={bodyshop}>
{ {t("menus.header.courtesycars-all")}
key: "contracts", </LockWrapper>
id: "header-contracts", </Link>
icon: <FileFilled />, )
label: <Link to="/manage/courtesycars/contracts">{t("menus.header.courtesycars-contracts")}</Link> },
}, {
{ key: "contracts",
key: "newcontract", id: "header-contracts",
id: "header-newcontract", icon: <FileFilled />,
icon: <FileAddFilled />, label: (
label: <Link to="/manage/courtesycars/contracts/new">{t("menus.header.courtesycars-newcontract")}</Link> <Link to="/manage/courtesycars/contracts">
} <LockWrapper featureName="courtesycars" bodyshop={bodyshop}>
] {t("menus.header.courtesycars-contracts")}
} </LockWrapper>
] </Link>
: []), )
},
{
key: "newcontract",
id: "header-newcontract",
icon: <FileAddFilled />,
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",
}) id: "header-temporarydocs",
? [ icon: <PaperClipOutlined />,
{ label: (
key: "temporarydocs", <Link to="/manage/temporarydocs">
id: "header-temporarydocs", <LockWrapper featureName="media" bodyshop={bodyshop}>
icon: <PaperClipOutlined />, {t("menus.header.temporarydocs")}
label: <Link to="/manage/temporarydocs">{t("menus.header.temporarydocs")}</Link> </LockWrapper>
} </Link>
] )
: []), },
{ {
key: "tasks", key: "tasks",
id: "tasks", id: "tasks",
@@ -562,19 +614,19 @@ 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",
}) id: "header-shop-csi",
? [ icon: <Icon component={RiSurveyLine} />,
{ label: (
key: "shop-csi", <Link to="/manage/shop/csi">
id: "header-shop-csi", <LockWrapper featureName="export" bodyshop={bodyshop}>
icon: <Icon component={RiSurveyLine} />, {t("menus.header.shop_csi")}
label: <Link to="/manage/shop/csi">{t("menus.header.shop_csi")}</Link> </LockWrapper>
} </Link>
] )
: []) }
] ]
}, },
{ {
@@ -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);