Merged in feature/IO-3499-React-19 (pull request #2821)

Feature/IO-3499 React 19
This commit is contained in:
Dave Richer
2026-01-14 16:58:13 +00:00
24 changed files with 448 additions and 382 deletions

View File

@@ -18,3 +18,4 @@ VITE_PUBLIC_POSTHOG_KEY=phc_xtLmBIu0rjWwExY73Oj5DTH1bGbwq1G1Y8jnlTceien
VITE_PUBLIC_POSTHOG_HOST=https://us.i.posthog.com VITE_PUBLIC_POSTHOG_HOST=https://us.i.posthog.com
VITE_APP_AMP_URL=https://vp8k908qy2.execute-api.ca-central-1.amazonaws.com VITE_APP_AMP_URL=https://vp8k908qy2.execute-api.ca-central-1.amazonaws.com
VITE_APP_AMP_KEY=6228a598e57cd66875cfd41604f1f891 VITE_APP_AMP_KEY=6228a598e57cd66875cfd41604f1f891
VITE_ENABLE_COMPILER_IN_DEV=1

View File

@@ -9,7 +9,7 @@
"version": "0.2.1", "version": "0.2.1",
"hasInstallScript": true, "hasInstallScript": true,
"dependencies": { "dependencies": {
"@amplitude/analytics-browser": "^2.33.1", "@amplitude/analytics-browser": "^2.33.2",
"@ant-design/pro-layout": "^7.22.6", "@ant-design/pro-layout": "^7.22.6",
"@apollo/client": "^4.0.13", "@apollo/client": "^4.0.13",
"@emotion/is-prop-valid": "^1.4.0", "@emotion/is-prop-valid": "^1.4.0",
@@ -51,7 +51,7 @@
"normalize-url": "^8.1.1", "normalize-url": "^8.1.1",
"object-hash": "^3.0.0", "object-hash": "^3.0.0",
"phone": "^3.1.69", "phone": "^3.1.69",
"posthog-js": "^1.319.2", "posthog-js": "^1.321.0",
"prop-types": "^15.8.1", "prop-types": "^15.8.1",
"query-string": "^9.3.1", "query-string": "^9.3.1",
"raf-schd": "^4.0.3", "raf-schd": "^4.0.3",
@@ -149,17 +149,17 @@
"license": "MIT" "license": "MIT"
}, },
"node_modules/@amplitude/analytics-browser": { "node_modules/@amplitude/analytics-browser": {
"version": "2.33.1", "version": "2.33.2",
"resolved": "https://registry.npmjs.org/@amplitude/analytics-browser/-/analytics-browser-2.33.1.tgz", "resolved": "https://registry.npmjs.org/@amplitude/analytics-browser/-/analytics-browser-2.33.2.tgz",
"integrity": "sha512-93wZjuAFJ7QdyptF82i1pezm5jKuBWITHI++XshDgpks1RstJvJ9n11Ak8MnE4L2BGQ93XDN2aVEHfmQkt0/Pw==", "integrity": "sha512-TOVa3oqHQqKJbceMix+fucUvaAe70Mq3eMK2lANz3GHrry/xrzuc/M8HpxdSwDbR1XG6BGKrd4vHREc945z56g==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@amplitude/analytics-core": "2.35.0", "@amplitude/analytics-core": "2.35.1",
"@amplitude/plugin-autocapture-browser": "1.18.3", "@amplitude/plugin-autocapture-browser": "1.18.4",
"@amplitude/plugin-network-capture-browser": "1.7.3", "@amplitude/plugin-network-capture-browser": "1.7.4",
"@amplitude/plugin-page-url-enrichment-browser": "0.5.9", "@amplitude/plugin-page-url-enrichment-browser": "0.5.10",
"@amplitude/plugin-page-view-tracking-browser": "2.6.6", "@amplitude/plugin-page-view-tracking-browser": "2.6.7",
"@amplitude/plugin-web-vitals-browser": "1.1.4", "@amplitude/plugin-web-vitals-browser": "1.1.5",
"tslib": "^2.4.1" "tslib": "^2.4.1"
} }
}, },
@@ -170,9 +170,9 @@
"license": "MIT" "license": "MIT"
}, },
"node_modules/@amplitude/analytics-core": { "node_modules/@amplitude/analytics-core": {
"version": "2.35.0", "version": "2.35.1",
"resolved": "https://registry.npmjs.org/@amplitude/analytics-core/-/analytics-core-2.35.0.tgz", "resolved": "https://registry.npmjs.org/@amplitude/analytics-core/-/analytics-core-2.35.1.tgz",
"integrity": "sha512-7RmHYELXCGu8yuO9D6lEXiqkMtiC5sePNhCWmwuP30dneDYHtH06gaYvAFH/YqOFuE6enwEEJfFYtcaPhyiqtA==", "integrity": "sha512-ZChD4oUtpbO6W5YhWZ0G9BbqVOx7DoX1+cPyAMFwFkglH6JrZCrKvUTrukhVpVB+wkLRRK1ZviN0PzP6mDaifw==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@amplitude/analytics-connector": "^1.6.4", "@amplitude/analytics-connector": "^1.6.4",
@@ -181,53 +181,52 @@
} }
}, },
"node_modules/@amplitude/plugin-autocapture-browser": { "node_modules/@amplitude/plugin-autocapture-browser": {
"version": "1.18.3", "version": "1.18.4",
"resolved": "https://registry.npmjs.org/@amplitude/plugin-autocapture-browser/-/plugin-autocapture-browser-1.18.3.tgz", "resolved": "https://registry.npmjs.org/@amplitude/plugin-autocapture-browser/-/plugin-autocapture-browser-1.18.4.tgz",
"integrity": "sha512-njYque5t1QCEEe5V8Ls4yVVklTM6V7OXxBk6pqznN/hj/Pc4X8Wjy898pZ2VtbnvpagBKKzGb5B6Syl8OXiicw==", "integrity": "sha512-D4BzLjTjT7+Q2TEA0US9THlKPDpukcyIkjknsa9jRhWyLhirnwKEnf5w3WhX+g2psfnq+zY0UjCboQ+WCDL0Zw==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@amplitude/analytics-core": "2.35.0", "@amplitude/analytics-core": "2.35.1",
"rxjs": "^7.8.1",
"tslib": "^2.4.1" "tslib": "^2.4.1"
} }
}, },
"node_modules/@amplitude/plugin-network-capture-browser": { "node_modules/@amplitude/plugin-network-capture-browser": {
"version": "1.7.3", "version": "1.7.4",
"resolved": "https://registry.npmjs.org/@amplitude/plugin-network-capture-browser/-/plugin-network-capture-browser-1.7.3.tgz", "resolved": "https://registry.npmjs.org/@amplitude/plugin-network-capture-browser/-/plugin-network-capture-browser-1.7.4.tgz",
"integrity": "sha512-zfWgAN7g6AigJAsgrGmlgVwydOHH6XvweBoxhU+qEvRydboiIVCDLSxuXczUsBG7kYVLWRdBK1DYoE5J7lqTGA==", "integrity": "sha512-en86lEWMNkQOPm64yYnBjOI3qyHxAmhKZF+zgxdxwHM4vOQ8M1ySxVitCyd0GJiLmdEHEWj0PWgPAVkkj7BjBQ==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@amplitude/analytics-core": "2.35.0", "@amplitude/analytics-core": "2.35.1",
"tslib": "^2.4.1" "tslib": "^2.4.1"
} }
}, },
"node_modules/@amplitude/plugin-page-url-enrichment-browser": { "node_modules/@amplitude/plugin-page-url-enrichment-browser": {
"version": "0.5.9", "version": "0.5.10",
"resolved": "https://registry.npmjs.org/@amplitude/plugin-page-url-enrichment-browser/-/plugin-page-url-enrichment-browser-0.5.9.tgz", "resolved": "https://registry.npmjs.org/@amplitude/plugin-page-url-enrichment-browser/-/plugin-page-url-enrichment-browser-0.5.10.tgz",
"integrity": "sha512-TqdELx4WrdRutCjHUFUzum/f/UjhbdTZw0UKkYFAj5gwAKDjaPEjL4waRvINOTaVLsne1A6ck4KEMfC8AKByFw==", "integrity": "sha512-lgp2uwz2UPXxJypYMgiQ5yHhoTIQ6QaZQu8yq//9sogkMkDt0ClybTYwRk3N1q/XVS1cR79vT68gtvzdLD62Lg==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@amplitude/analytics-core": "2.35.0", "@amplitude/analytics-core": "2.35.1",
"tslib": "^2.4.1" "tslib": "^2.4.1"
} }
}, },
"node_modules/@amplitude/plugin-page-view-tracking-browser": { "node_modules/@amplitude/plugin-page-view-tracking-browser": {
"version": "2.6.6", "version": "2.6.7",
"resolved": "https://registry.npmjs.org/@amplitude/plugin-page-view-tracking-browser/-/plugin-page-view-tracking-browser-2.6.6.tgz", "resolved": "https://registry.npmjs.org/@amplitude/plugin-page-view-tracking-browser/-/plugin-page-view-tracking-browser-2.6.7.tgz",
"integrity": "sha512-dBcJlrdKgPzSgS3exDRRrMLqhIaOjwlIy7o8sEMn1PpMawERlbumSSdtfII6L4L67HYUPo4PY4Kp4acqSzaLvQ==", "integrity": "sha512-cSiKOAJkgqI/h3+rjVXVvegrK2cma9XPxtWnBvShGbmVzh+ekIUrKktUFLsmxxFzkY94VVsVWiSGovQsKa8RuA==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@amplitude/analytics-core": "2.35.0", "@amplitude/analytics-core": "2.35.1",
"tslib": "^2.4.1" "tslib": "^2.4.1"
} }
}, },
"node_modules/@amplitude/plugin-web-vitals-browser": { "node_modules/@amplitude/plugin-web-vitals-browser": {
"version": "1.1.4", "version": "1.1.5",
"resolved": "https://registry.npmjs.org/@amplitude/plugin-web-vitals-browser/-/plugin-web-vitals-browser-1.1.4.tgz", "resolved": "https://registry.npmjs.org/@amplitude/plugin-web-vitals-browser/-/plugin-web-vitals-browser-1.1.5.tgz",
"integrity": "sha512-XQXI9OjTNSz2yi0lXw2VYMensDzzSkMCfvXNniTb1LgnHwBcQ1JWPcTqHLPFrvvNckeIdOT78vjs7yA+c1FyzA==", "integrity": "sha512-6tcaSi5nM5pd6/bcMl90+LSR4cCsqFLP2SG9RUy+bHQN/DCh+Nzq1X+5a1St+MqX8Qr6s4q6YHbkIUcEMHo+Zg==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@amplitude/analytics-core": "2.35.0", "@amplitude/analytics-core": "2.35.1",
"tslib": "^2.4.1", "tslib": "^2.4.1",
"web-vitals": "5.1.0" "web-vitals": "5.1.0"
} }
@@ -4566,9 +4565,9 @@
} }
}, },
"node_modules/@posthog/types": { "node_modules/@posthog/types": {
"version": "1.319.2", "version": "1.321.0",
"resolved": "https://registry.npmjs.org/@posthog/types/-/types-1.319.2.tgz", "resolved": "https://registry.npmjs.org/@posthog/types/-/types-1.321.0.tgz",
"integrity": "sha512-mGyQx5T4mpX+r4hyFKXJ41sck7WkWSiPgq7NTDGPbFPNW9F2mtD0R+myDhXxHrQUxAEa9ZIgrIvysTY37UYagA==", "integrity": "sha512-dNxsez/AqV3dt/UO6h5aJ+qBj7Tj0a17hqc9zE1XvvlXxpVFuk0EFsSlxtrBNumWWxh29jINw0x0YitrozNqIQ==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/@protobufjs/aspromise": { "node_modules/@protobufjs/aspromise": {
@@ -7325,6 +7324,12 @@
"integrity": "sha512-D1XC7WK8K+zZEveUPY+cf4+kgauk8N4eHr/XIHXGlGYkHLud6hK9lYfZk1ry1TNh798cZUCgb6MqGEG8DkJt6Q==", "integrity": "sha512-D1XC7WK8K+zZEveUPY+cf4+kgauk8N4eHr/XIHXGlGYkHLud6hK9lYfZk1ry1TNh798cZUCgb6MqGEG8DkJt6Q==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/@types/zen-observable": {
"version": "0.8.3",
"resolved": "https://registry.npmjs.org/@types/zen-observable/-/zen-observable-0.8.3.tgz",
"integrity": "sha512-fbF6oTd4sGGy0xjHPKAt+eS2CrxJ3+6gQ3FGcBoIJR2TLAyCkCyI8JqZNy+FeON0AhVgNJoUumVoZQjBFUqHkw==",
"license": "MIT"
},
"node_modules/@umijs/route-utils": { "node_modules/@umijs/route-utils": {
"version": "4.0.1", "version": "4.0.1",
"resolved": "https://registry.npmjs.org/@umijs/route-utils/-/route-utils-4.0.1.tgz", "resolved": "https://registry.npmjs.org/@umijs/route-utils/-/route-utils-4.0.1.tgz",
@@ -14694,9 +14699,9 @@
"license": "MIT" "license": "MIT"
}, },
"node_modules/posthog-js": { "node_modules/posthog-js": {
"version": "1.319.2", "version": "1.321.0",
"resolved": "https://registry.npmjs.org/posthog-js/-/posthog-js-1.319.2.tgz", "resolved": "https://registry.npmjs.org/posthog-js/-/posthog-js-1.321.0.tgz",
"integrity": "sha512-mYFoRPSYZ34Ywdz3Ph4ME/md5H60NoKc8I/DTEr31YEGIC6dYKOOWBRFO/MLMvnAny5C7VEir8YE5dQ9484vPw==", "integrity": "sha512-IFdm/iBoFHltHwdZ/qjtni4RAtFCU6NEt6QTNOzBcuAk5srAFQBb7o+8MxryGON7EXLKCbAA6hueksHFB/WY/A==",
"license": "SEE LICENSE IN LICENSE", "license": "SEE LICENSE IN LICENSE",
"dependencies": { "dependencies": {
"@opentelemetry/api": "^1.9.0", "@opentelemetry/api": "^1.9.0",
@@ -14705,11 +14710,11 @@
"@opentelemetry/resources": "^2.2.0", "@opentelemetry/resources": "^2.2.0",
"@opentelemetry/sdk-logs": "^0.208.0", "@opentelemetry/sdk-logs": "^0.208.0",
"@posthog/core": "1.9.1", "@posthog/core": "1.9.1",
"@posthog/types": "1.319.2", "@posthog/types": "1.321.0",
"core-js": "^3.38.1", "core-js": "^3.38.1",
"dompurify": "^3.3.1", "dompurify": "^3.3.1",
"fflate": "^0.4.8", "fflate": "^0.4.8",
"preact": "^10.19.3", "preact": "^10.28.0",
"query-selector-shadow-dom": "^1.0.1", "query-selector-shadow-dom": "^1.0.1",
"web-vitals": "^4.2.4" "web-vitals": "^4.2.4"
} }
@@ -19198,11 +19203,12 @@
"license": "MIT" "license": "MIT"
}, },
"node_modules/zen-observable-ts": { "node_modules/zen-observable-ts": {
"version": "1.2.5", "version": "1.1.0",
"resolved": "https://registry.npmjs.org/zen-observable-ts/-/zen-observable-ts-1.2.5.tgz", "resolved": "https://registry.npmjs.org/zen-observable-ts/-/zen-observable-ts-1.1.0.tgz",
"integrity": "sha512-QZWQekv6iB72Naeake9hS1KxHlotfRpe+WGNbNx5/ta+R3DNjVO2bswf63gXlWDcs+EMd7XY8HfVQyP1X6T4Zg==", "integrity": "sha512-1h4zlLSqI2cRLPJUHJFL8bCWHhkpuXkF+dbGkRaWjgDIG26DmzyshUMrdV/rL3UnR+mhaX4fRq8LPouq0MYYIA==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@types/zen-observable": "0.8.3",
"zen-observable": "0.8.15" "zen-observable": "0.8.15"
} }
}, },

View File

@@ -8,7 +8,7 @@
"private": true, "private": true,
"proxy": "http://localhost:4000", "proxy": "http://localhost:4000",
"dependencies": { "dependencies": {
"@amplitude/analytics-browser": "^2.33.1", "@amplitude/analytics-browser": "^2.33.2",
"@ant-design/pro-layout": "^7.22.6", "@ant-design/pro-layout": "^7.22.6",
"@apollo/client": "^4.0.13", "@apollo/client": "^4.0.13",
"@emotion/is-prop-valid": "^1.4.0", "@emotion/is-prop-valid": "^1.4.0",
@@ -50,7 +50,7 @@
"normalize-url": "^8.1.1", "normalize-url": "^8.1.1",
"object-hash": "^3.0.0", "object-hash": "^3.0.0",
"phone": "^3.1.69", "phone": "^3.1.69",
"posthog-js": "^1.319.2", "posthog-js": "^1.321.0",
"prop-types": "^15.8.1", "prop-types": "^15.8.1",
"query-string": "^9.3.1", "query-string": "^9.3.1",
"raf-schd": "^4.0.3", "raf-schd": "^4.0.3",

View File

@@ -1,9 +1,8 @@
import { Select } from "antd"; import { Select } from "antd";
import { forwardRef } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import InstanceRenderMgr from "../../utils/instanceRenderMgr"; import InstanceRenderMgr from "../../utils/instanceRenderMgr";
const BillLineSearchSelect = ({ options, disabled, allowRemoved, ...restProps }, ref) => { const BillLineSearchSelect = ({ options, disabled, allowRemoved, ref, ...restProps }) => {
const { t } = useTranslation(); const { t } = useTranslation();
return ( return (
@@ -68,4 +67,4 @@ function generateLineName(item) {
</div> </div>
); );
} }
export default forwardRef(BillLineSearchSelect); export default BillLineSearchSelect;

View File

@@ -46,6 +46,8 @@ const CardPaymentModalComponent = ({
const [form] = Form.useForm(); const [form] = Form.useForm();
const [paymentLink, setPaymentLink] = useState(); const [paymentLink, setPaymentLink] = useState();
const isMountedRef = useRef(true);
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [insertPaymentResponse] = useMutation(INSERT_PAYMENT_RESPONSE); const [insertPaymentResponse] = useMutation(INSERT_PAYMENT_RESPONSE);
const { t } = useTranslation(); const { t } = useTranslation();
@@ -69,6 +71,16 @@ const CardPaymentModalComponent = ({
[called, loadRoAndOwnerByJobPks, refetch] [called, loadRoAndOwnerByJobPks, refetch]
); );
useEffect(() => {
return () => {
isMountedRef.current = false;
};
}, []);
const setLoadingSafe = useCallback((value) => {
if (isMountedRef.current) setLoading(value);
}, []);
// Watch form payments so we can query jobs when all jobids are filled (without side effects during render) // Watch form payments so we can query jobs when all jobids are filled (without side effects during render)
const payments = Form.useWatch(["payments"], form); const payments = Form.useWatch(["payments"], form);
@@ -106,24 +118,49 @@ const CardPaymentModalComponent = ({
const SetIntellipayCallbackFunctions = () => { const SetIntellipayCallbackFunctions = () => {
console.log("*** Set IntelliPay callback functions."); console.log("*** Set IntelliPay callback functions.");
const isLikelyUserCancel = (response) => {
const reason = String(response?.declinereason ?? "").toLowerCase();
// Heuristics: adjust if IntelliPay gives you a known cancel code/message
return (
reason.includes("cancel") ||
reason.includes("canceled") ||
reason.includes("closed") ||
// many gateways won't have a paymentid if user cancels before submitting
!response?.paymentid
);
};
window.intellipay.runOnClose(() => { window.intellipay.runOnClose(() => {
//window.intellipay.initialize(); // This is the path for Cancel / X
try {
// If IntelliPay uses this flag, clear it so initialize() won't reopen unexpectedly
window.intellipay.isAutoOpen = false;
} catch {
// ignore
}
// Optional: if IntelliPay needs re-init after close, uncomment:
// try { window.intellipay.initialize?.(); } catch {}
setLoadingSafe(false);
}); });
window.intellipay.runOnApproval(() => { window.intellipay.runOnApproval(() => {
// 2024-04-25: Nothing is going to happen here anymore. We'll completely rely on the callback. // keep your existing behavior
// Add a slight delay to allow the refetch to properly get the data.
setTimeout(() => { setTimeout(() => {
if (actions?.refetch) actions.refetch(); if (actions?.refetch) actions.refetch();
setLoading(false); setLoadingSafe(false);
toggleModalVisible(); toggleModalVisible();
}, 750); }, 750);
}); });
window.intellipay.runOnNonApproval(async (response) => { window.intellipay.runOnNonApproval(async (response) => {
// Mutate unsuccessful payment try {
// If cancel is reported as "non-approval", don't record it as a failed payment
if (isLikelyUserCancel(response)) return;
const { payments } = form.getFieldsValue(); const { payments } = form.getFieldsValue();
await insertPaymentResponse({ await insertPaymentResponse({
variables: { variables: {
paymentResponse: payments.map((payment) => ({ paymentResponse: payments.map((payment) => ({
@@ -131,7 +168,7 @@ const CardPaymentModalComponent = ({
bodyshopid: bodyshop.id, bodyshopid: bodyshop.id,
jobid: payment.jobid, jobid: payment.jobid,
declinereason: response.declinereason, declinereason: response.declinereason,
ext_paymentid: response.paymentid.toString(), ext_paymentid: response.paymentid?.toString?.() ?? null,
successful: false, successful: false,
response response
})) }))
@@ -145,6 +182,10 @@ const CardPaymentModalComponent = ({
type: "failedpayment" type: "failedpayment"
}) })
); );
} finally {
// IMPORTANT: always clear loading, even on errors
setLoadingSafe(false);
}
}); });
}; };
@@ -154,7 +195,7 @@ const CardPaymentModalComponent = ({
try { try {
await form.validateFields(); await form.validateFields();
} catch { } catch {
setLoading(false); setLoadingSafe(false);
return; return;
} }
@@ -185,7 +226,7 @@ const CardPaymentModalComponent = ({
document.documentElement.appendChild(node); document.documentElement.appendChild(node);
pollForIntelliPay(() => { pollForIntelliPay(() => {
SetIntellipayCallbackFunctions(); SetIntellipayCallbackFunctions();
// eslint-disable-next-line react-compiler/react-compiler
window.intellipay.isAutoOpen = true; window.intellipay.isAutoOpen = true;
window.intellipay.initialize(); window.intellipay.initialize();
}); });
@@ -194,7 +235,7 @@ const CardPaymentModalComponent = ({
notification.error({ notification.error({
title: t("job_payments.notifications.error.openingip") title: t("job_payments.notifications.error.openingip")
}); });
setLoading(false); setLoadingSafe(false);
} }
}; };
@@ -204,7 +245,7 @@ const CardPaymentModalComponent = ({
try { try {
await form.validateFields(); await form.validateFields();
} catch { } catch {
setLoading(false); setLoadingSafe(false);
return; return;
} }
@@ -227,12 +268,12 @@ const CardPaymentModalComponent = ({
await navigator.clipboard.writeText(response.data.shorUrl); await navigator.clipboard.writeText(response.data.shorUrl);
message.success(t("general.actions.copied")); message.success(t("general.actions.copied"));
} }
setLoading(false); setLoadingSafe(false);
} catch { } catch {
notification.error({ notification.error({
title: t("job_payments.notifications.error.openingip") title: t("job_payments.notifications.error.openingip")
}); });
setLoading(false); setLoadingSafe(false);
} }
}; };

View File

@@ -1,10 +1,10 @@
import { forwardRef, useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { Select } from "antd"; import { Select } from "antd";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
const { Option } = Select; const { Option } = Select;
const ContractStatusComponent = forwardRef(({ value, onChange }, ref) => { const ContractStatusComponent = ({ value, onChange, ref }) => {
const [option, setOption] = useState(value); const [option, setOption] = useState(value);
const { t } = useTranslation(); const { t } = useTranslation();
@@ -21,7 +21,7 @@ const ContractStatusComponent = forwardRef(({ value, onChange }, ref) => {
<Option value="contracts.status.returned">{t("contracts.status.out")}</Option> <Option value="contracts.status.returned">{t("contracts.status.out")}</Option>
</Select> </Select>
); );
}); };
ContractStatusComponent.displayName = "ContractStatusComponent"; ContractStatusComponent.displayName = "ContractStatusComponent";

View File

@@ -1,8 +1,7 @@
import { Slider } from "antd"; import { Slider } from "antd";
import { forwardRef } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
const CourtesyCarFuelComponent = (props, ref) => { const CourtesyCarFuelComponent = ({ ref, ...props }) => {
const { t } = useTranslation(); const { t } = useTranslation();
const marks = { const marks = {
@@ -63,4 +62,4 @@ const CourtesyCarFuelComponent = (props, ref) => {
/> />
); );
}; };
export default forwardRef(CourtesyCarFuelComponent); export default CourtesyCarFuelComponent;

View File

@@ -1,10 +1,10 @@
import { Select } from "antd"; import { Select } from "antd";
import { forwardRef, useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
const { Option } = Select; const { Option } = Select;
const CourtesyCarReadinessComponent = ({ value, onChange }, ref) => { const CourtesyCarReadinessComponent = ({ value, onChange, ref }) => {
const [option, setOption] = useState(value); const [option, setOption] = useState(value);
const { t } = useTranslation(); const { t } = useTranslation();
@@ -29,4 +29,4 @@ const CourtesyCarReadinessComponent = ({ value, onChange }, ref) => {
</Select> </Select>
); );
}; };
export default forwardRef(CourtesyCarReadinessComponent); export default CourtesyCarReadinessComponent;

View File

@@ -1,10 +1,10 @@
import { forwardRef, useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { Select } from "antd"; import { Select } from "antd";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
const { Option } = Select; const { Option } = Select;
const CourtesyCarStatusComponent = ({ value, onChange }, ref) => { const CourtesyCarStatusComponent = ({ value, onChange, ref }) => {
const [option, setOption] = useState(value); const [option, setOption] = useState(value);
const { t } = useTranslation(); const { t } = useTranslation();
@@ -32,4 +32,4 @@ const CourtesyCarStatusComponent = ({ value, onChange }, ref) => {
</Select> </Select>
); );
}; };
export default forwardRef(CourtesyCarStatusComponent); export default CourtesyCarStatusComponent;

View File

@@ -1,17 +1,17 @@
import { useQuery } from "@apollo/client/react"; import { useQuery } from "@apollo/client/react";
import { Select } from "antd"; import { Select } from "antd";
import { forwardRef } from "react";
import { QUERY_TEAMS } from "../../graphql/employee_teams.queries"; import { QUERY_TEAMS } from "../../graphql/employee_teams.queries";
import AlertComponent from "../alert/alert.component"; import AlertComponent from "../alert/alert.component";
//To be used as a form element only. //To be used as a form element only.
const EmployeeTeamSearchSelect = ({ ...props }) => { const EmployeeTeamSearchSelect = ({ ref, ...props }) => {
const { loading, error, data } = useQuery(QUERY_TEAMS); const { loading, error, data } = useQuery(QUERY_TEAMS);
if (error) return <AlertComponent title={JSON.stringify(error)} type="error" />; if (error) return <AlertComponent title={JSON.stringify(error)} type="error" />;
return ( return (
<Select <Select
ref={ref}
showSearch showSearch
allowClear allowClear
loading={loading} loading={loading}
@@ -30,4 +30,4 @@ const EmployeeTeamSearchSelect = ({ ...props }) => {
/> />
); );
}; };
export default forwardRef(EmployeeTeamSearchSelect); export default EmployeeTeamSearchSelect;

View File

@@ -1,5 +1,5 @@
import { InputNumber, Popover } from "antd"; import { InputNumber, Popover } from "antd";
import { forwardRef, useEffect, useRef, useState } from "react"; import { useEffect, useRef, useState } from "react";
const FormInputNUmberCalculator = ({ value: formValue, onChange: formOnChange, ...restProps }) => { const FormInputNUmberCalculator = ({ value: formValue, onChange: formOnChange, ...restProps }) => {
const [value, setValue] = useState(formValue); const [value, setValue] = useState(formValue);
@@ -105,4 +105,4 @@ const FormInputNUmberCalculator = ({ value: formValue, onChange: formOnChange, .
); );
}; };
export default forwardRef(FormInputNUmberCalculator); export default FormInputNUmberCalculator;

View File

@@ -1,5 +1,4 @@
import { InputNumber } from "antd"; import { InputNumber } from "antd";
import { forwardRef } from "react";
// const locale = "en-us"; // const locale = "en-us";
// const currencyFormatter = (value) => { // const currencyFormatter = (value) => {
@@ -41,7 +40,7 @@ import { forwardRef } from "react";
// } // }
// }; // };
function FormItemCurrency(props, ref) { function FormItemCurrency({ ref, ...props }) {
return ( return (
<InputNumber <InputNumber
{...props} {...props}
@@ -54,4 +53,4 @@ function FormItemCurrency(props, ref) {
); );
} }
export default forwardRef(FormItemCurrency); export default FormItemCurrency;

View File

@@ -1,9 +1,7 @@
import { MailFilled } from "@ant-design/icons"; import { MailFilled } from "@ant-design/icons";
import { Button, Input, Space } from "antd"; import { Button, Input, Space } from "antd";
import { forwardRef } from "react";
function FormItemEmail(props, ref) { function FormItemEmail({ defaultValue, value, ref, ...restProps }) {
const { defaultValue, value, ...restProps } = props;
const emailValue = defaultValue || value; const emailValue = defaultValue || value;
return ( return (
@@ -18,4 +16,4 @@ function FormItemEmail(props, ref) {
); );
} }
export default forwardRef(FormItemEmail); export default FormItemEmail;

View File

@@ -1,4 +1,3 @@
import { forwardRef } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
const LaborTypeFormItem = ({ value }) => { const LaborTypeFormItem = ({ value }) => {
@@ -8,4 +7,4 @@ const LaborTypeFormItem = ({ value }) => {
return <div>{t(`joblines.fields.lbr_types.${value}`)}</div>; return <div>{t(`joblines.fields.lbr_types.${value}`)}</div>;
}; };
export default forwardRef(LaborTypeFormItem); export default LaborTypeFormItem;

View File

@@ -1,4 +1,3 @@
import { forwardRef } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
const PartTypeFormItem = ({ value }) => { const PartTypeFormItem = ({ value }) => {
@@ -10,4 +9,4 @@ const PartTypeFormItem = ({ value }) => {
<div style={{ wordWrap: "break-word", overflowWrap: "break-word" }}>{t(`joblines.fields.part_types.${value}`)}</div> <div style={{ wordWrap: "break-word", overflowWrap: "break-word" }}>{t(`joblines.fields.part_types.${value}`)}</div>
); );
}; };
export default forwardRef(PartTypeFormItem); export default PartTypeFormItem;

View File

@@ -1,14 +1,13 @@
import { Input } from "antd"; import { Input } from "antd";
import i18n from "i18next"; import i18n from "i18next";
import parsePhoneNumber from "libphonenumber-js"; import parsePhoneNumber from "libphonenumber-js";
import { forwardRef } from "react";
import "./phone-form-item.styles.scss"; import "./phone-form-item.styles.scss";
function FormItemPhone(props, ref) { function FormItemPhone({ ref, ...props }) {
return <Input ref={ref} {...props} />; return <Input ref={ref} {...props} />;
} }
export default forwardRef(FormItemPhone); export default FormItemPhone;
export const PhoneItemFormatterValidation = () => ({ export const PhoneItemFormatterValidation = () => ({
async validator(rule, value) { async validator(rule, value) {

View File

@@ -2,7 +2,7 @@ import { LoadingOutlined } from "@ant-design/icons";
import { useLazyQuery } from "@apollo/client/react"; import { useLazyQuery } from "@apollo/client/react";
import { Select, Space, Spin, Tag } from "antd"; import { Select, Space, Spin, Tag } from "antd";
import _ from "lodash"; import _ from "lodash";
import { forwardRef, useEffect, useMemo, useState } from "react"; import { useEffect, useMemo, useState } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { SEARCH_JOBS_BY_ID_FOR_AUTOCOMPLETE, SEARCH_JOBS_FOR_AUTOCOMPLETE } from "../../graphql/jobs.queries"; import { SEARCH_JOBS_BY_ID_FOR_AUTOCOMPLETE, SEARCH_JOBS_FOR_AUTOCOMPLETE } from "../../graphql/jobs.queries";
import AlertComponent from "../alert/alert.component"; import AlertComponent from "../alert/alert.component";
@@ -10,10 +10,15 @@ import { OwnerNameDisplayFunction } from "../owner-name-display/owner-name-displ
const { Option } = Select; const { Option } = Select;
const JobSearchSelect = ( const JobSearchSelect = ({
{ disabled, convertedOnly = false, notInvoiced = false, notExported = true, clm_no = false, ...restProps }, disabled,
ref convertedOnly = false,
) => { notInvoiced = false,
notExported = true,
clm_no = false,
ref,
...restProps
}) => {
const { t } = useTranslation(); const { t } = useTranslation();
const [theOptions, setTheOptions] = useState([]); const [theOptions, setTheOptions] = useState([]);
@@ -107,4 +112,4 @@ const JobSearchSelect = (
); );
}; };
export default forwardRef(JobSearchSelect); export default JobSearchSelect;

View File

@@ -5,18 +5,15 @@ import { useTranslation } from "react-i18next";
import { useNavigate } from "react-router-dom"; import { useNavigate } from "react-router-dom";
import "./notification-center.styles.scss"; import "./notification-center.styles.scss";
import day from "../../utils/day.js"; import day from "../../utils/day.js";
import { forwardRef, useEffect, useRef } from "react"; import { useEffect, useRef } from "react";
import { DateTimeFormat } from "../../utils/DateFormatter.jsx"; import { DateTimeFormat } from "../../utils/DateFormatter.jsx";
const { Text, Title } = Typography; const { Text, Title } = Typography;
/** /**
* Notification Center Component * Notification Center Component
* @type {React.ForwardRefExoticComponent<React.PropsWithoutRef<{readonly visible?: *, readonly onClose?: *, readonly notifications?: *, readonly loading?: *, readonly showUnreadOnly?: *, readonly toggleUnreadOnly?: *, readonly markAllRead?: *, readonly loadMore?: *, readonly onNotificationClick?: *, readonly unreadCount?: *}> & React.RefAttributes<unknown>>}
*/ */
const NotificationCenterComponent = forwardRef( const NotificationCenterComponent = ({
(
{
visible, visible,
notifications, notifications,
loading, loading,
@@ -26,10 +23,9 @@ const NotificationCenterComponent = forwardRef(
loadMore, loadMore,
onNotificationClick, onNotificationClick,
unreadCount, unreadCount,
isEmployee isEmployee,
},
ref ref
) => { }) => {
const { t } = useTranslation(); const { t } = useTranslation();
const navigate = useNavigate(); const navigate = useNavigate();
const virtuosoRef = useRef(null); const virtuosoRef = useRef(null);
@@ -127,8 +123,7 @@ const NotificationCenterComponent = forwardRef(
)} )}
</div> </div>
); );
} };
);
NotificationCenterComponent.displayName = "NotificationCenterComponent"; NotificationCenterComponent.displayName = "NotificationCenterComponent";

View File

@@ -2,14 +2,14 @@ import { LoadingOutlined } from "@ant-design/icons";
import { useLazyQuery } from "@apollo/client/react"; import { useLazyQuery } from "@apollo/client/react";
import { Empty, Select } from "antd"; import { Empty, Select } from "antd";
import _ from "lodash"; import _ from "lodash";
import { forwardRef, useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { SEARCH_OWNERS_BY_ID_FOR_AUTOCOMPLETE, SEARCH_OWNERS_FOR_AUTOCOMPLETE } from "../../graphql/owners.queries"; import { SEARCH_OWNERS_BY_ID_FOR_AUTOCOMPLETE, SEARCH_OWNERS_FOR_AUTOCOMPLETE } from "../../graphql/owners.queries";
import AlertComponent from "../alert/alert.component"; import AlertComponent from "../alert/alert.component";
import { OwnerNameDisplayFunction } from "../owner-name-display/owner-name-display.component"; import { OwnerNameDisplayFunction } from "../owner-name-display/owner-name-display.component";
const { Option } = Select; const { Option } = Select;
const OwnerSearchSelect = ({ value, onChange, onBlur, disabled }, ref) => { const OwnerSearchSelect = ({ value, onChange, onBlur, disabled, ref }) => {
const [callSearch, { loading, error, data }] = useLazyQuery(SEARCH_OWNERS_FOR_AUTOCOMPLETE); const [callSearch, { loading, error, data }] = useLazyQuery(SEARCH_OWNERS_FOR_AUTOCOMPLETE);
const [callIdSearch, { loading: idLoading, error: idError, data: idData }] = useLazyQuery( const [callIdSearch, { loading: idLoading, error: idError, data: idData }] = useLazyQuery(
@@ -85,4 +85,4 @@ const OwnerSearchSelect = ({ value, onChange, onBlur, disabled }, ref) => {
</div> </div>
); );
}; };
export default forwardRef(OwnerSearchSelect); export default OwnerSearchSelect;

View File

@@ -1,10 +1,8 @@
import { forwardRef } from "react"; const ListComponent = ({ style, children, ref, ...props }) => (
const ListComponent = forwardRef(({ style, children, ...props }, ref) => (
<div ref={ref} {...props} style={{ ...style }}> <div ref={ref} {...props} style={{ ...style }}>
{children} {children}
</div> </div>
)); );
ListComponent.displayName = "ListComponent"; ListComponent.displayName = "ListComponent";

View File

@@ -1,7 +1,7 @@
import { Virtuoso } from "react-virtuoso"; import { Virtuoso } from "react-virtuoso";
import { Badge, Button, Spin } from "antd"; import { Badge, Button, Spin } from "antd";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { forwardRef, useMemo, useRef } from "react"; import { useMemo, useRef } from "react";
import day from "../../utils/day.js"; import day from "../../utils/day.js";
import "./task-center.styles.scss"; import "./task-center.styles.scss";
import { import {
@@ -12,8 +12,18 @@ import {
QuestionCircleOutlined QuestionCircleOutlined
} from "@ant-design/icons"; } from "@ant-design/icons";
const TaskCenterComponent = forwardRef( const TaskCenterComponent = ({
({ visible, tasks, loading, error, onTaskClick, onLoadMore, hasMore, createNewTask, incompleteTaskCount }, ref) => { visible,
tasks,
loading,
error,
onTaskClick,
onLoadMore,
hasMore,
createNewTask,
incompleteTaskCount,
ref
}) => {
const { t } = useTranslation(); const { t } = useTranslation();
const virtuosoRef = useRef(null); const virtuosoRef = useRef(null);
@@ -149,8 +159,7 @@ const TaskCenterComponent = forwardRef(
)} )}
</div> </div>
); );
} };
);
TaskCenterComponent.displayName = "TaskCenterComponent"; TaskCenterComponent.displayName = "TaskCenterComponent";
export default TaskCenterComponent; export default TaskCenterComponent;

View File

@@ -2,7 +2,7 @@ import { LoadingOutlined } from "@ant-design/icons";
import { useLazyQuery } from "@apollo/client/react"; import { useLazyQuery } from "@apollo/client/react";
import { Empty, Select } from "antd"; import { Empty, Select } from "antd";
import _ from "lodash"; import _ from "lodash";
import { forwardRef, useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { import {
SEARCH_VEHICLES_BY_ID_FOR_AUTOCOMPLETE, SEARCH_VEHICLES_BY_ID_FOR_AUTOCOMPLETE,
SEARCH_VEHICLES_FOR_AUTOCOMPLETE SEARCH_VEHICLES_FOR_AUTOCOMPLETE
@@ -11,7 +11,7 @@ import AlertComponent from "../alert/alert.component";
const { Option } = Select; const { Option } = Select;
const VehicleSearchSelect = ({ value, onChange, onBlur, disabled }, ref) => { const VehicleSearchSelect = ({ value, onChange, onBlur, disabled, ref }) => {
const [callSearch, { loading, error, data }] = useLazyQuery(SEARCH_VEHICLES_FOR_AUTOCOMPLETE); const [callSearch, { loading, error, data }] = useLazyQuery(SEARCH_VEHICLES_FOR_AUTOCOMPLETE);
const [callIdSearch, { loading: idLoading, error: idError, data: idData }] = useLazyQuery( const [callIdSearch, { loading: idLoading, error: idError, data: idData }] = useLazyQuery(
@@ -87,4 +87,4 @@ const VehicleSearchSelect = ({ value, onChange, onBlur, disabled }, ref) => {
</div> </div>
); );
}; };
export default forwardRef(VehicleSearchSelect); export default VehicleSearchSelect;

View File

@@ -1,13 +1,13 @@
import { HeartOutlined } from "@ant-design/icons"; import { HeartOutlined } from "@ant-design/icons";
import { Select, Space, Tag } from "antd"; import { Select, Space, Tag } from "antd";
import { forwardRef, useEffect, useState } from "react"; import { useEffect, useState } from "react";
import PhoneNumberFormatter from "../../utils/PhoneFormatter"; import PhoneNumberFormatter from "../../utils/PhoneFormatter";
const { Option } = Select; const { Option } = Select;
// To be used as a form element only. // To be used as a form element only.
const VendorSearchSelect = ({ value, onChange, options, onSelect, disabled, preferredMake, showPhone }, ref) => { const VendorSearchSelect = ({ value, onChange, options, onSelect, disabled, preferredMake, showPhone, ref }) => {
const [option, setOption] = useState(value); const [option, setOption] = useState(value);
useEffect(() => { useEffect(() => {
@@ -131,4 +131,4 @@ const VendorSearchSelect = ({ value, onChange, options, onSelect, disabled, pref
</Select> </Select>
); );
}; };
export default forwardRef(VendorSearchSelect); export default VendorSearchSelect;

View File

@@ -37,7 +37,23 @@ const getFormattedTimestamp = () =>
export const logger = createLogger("info", { allowClearScreen: false }); export const logger = createLogger("info", { allowClearScreen: false });
export default defineConfig({ // Read HTTPS certs once (used by both server + preview)
const httpsCerts = {
key: await fsPromises.readFile("../certs/key.pem"),
cert: await fsPromises.readFile("../certs/cert.pem")
};
export default defineConfig(({ command, mode }) => {
// Only enable React Compiler on build in production/test (keeps dev as fast as possible)
const isBuild = command === "build";
const isTestBuild =
mode === "test" || process.env.VITE_APP_IS_TEST === "true" || process.env.VITE_APP_IS_TEST === "1";
const enableReactCompiler =
process.env.VITE_ENABLE_COMPILER_IN_DEV || (isBuild && (mode === "production" || isTestBuild));
console.log(enableReactCompiler ? "React Compiler enabled" : "React Compiler disabled");
return {
base: "/", base: "/",
plugins: [ plugins: [
ViteEjsPlugin((viteConfig) => ({ env: viteConfig.env })), ViteEjsPlugin((viteConfig) => ({ env: viteConfig.env })),
@@ -101,18 +117,26 @@ export default defineConfig({
} }
}), }),
react({ react(
enableReactCompiler
? {
babel: { babel: {
plugins: [ plugins: [
['babel-plugin-react-compiler', { [
"babel-plugin-react-compiler",
{
// Exclude third-party drag-and-drop library from compilation // Exclude third-party drag-and-drop library from compilation
sources: (filename) => { sources: (filename) => {
return !filename.includes('trello-board/dnd'); return !filename.includes("trello-board/dnd");
} }
}] }
]
] ]
} }
}), }
: undefined
),
eslint(), eslint(),
// Sentry only for production builds (no dev overhead) // Sentry only for production builds (no dev overhead)
@@ -165,20 +189,14 @@ export default defineConfig({
} }
} }
}, },
https: { https: httpsCerts
key: await fsPromises.readFile("../certs/key.pem"),
cert: await fsPromises.readFile("../certs/cert.pem")
}
}, },
preview: { preview: {
port: 6000, port: 6000,
host: true, host: true,
open: true, open: true,
https: { https: httpsCerts,
key: await fsPromises.readFile("../certs/key.pem"),
cert: await fsPromises.readFile("../certs/cert.pem")
},
proxy: { proxy: {
"/ws": { "/ws": {
target: "ws://localhost:4000", target: "ws://localhost:4000",
@@ -272,4 +290,5 @@ export default defineConfig({
scss: { quietDeps: true } scss: { quietDeps: true }
} }
} }
};
}); });