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

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

View File

@@ -46,6 +46,8 @@ const CardPaymentModalComponent = ({
const [form] = Form.useForm();
const [paymentLink, setPaymentLink] = useState();
const isMountedRef = useRef(true);
const [loading, setLoading] = useState(false);
const [insertPaymentResponse] = useMutation(INSERT_PAYMENT_RESPONSE);
const { t } = useTranslation();
@@ -69,6 +71,16 @@ const CardPaymentModalComponent = ({
[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)
const payments = Form.useWatch(["payments"], form);
@@ -106,45 +118,74 @@ const CardPaymentModalComponent = ({
const SetIntellipayCallbackFunctions = () => {
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.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(() => {
// 2024-04-25: Nothing is going to happen here anymore. We'll completely rely on the callback.
// Add a slight delay to allow the refetch to properly get the data.
// keep your existing behavior
setTimeout(() => {
if (actions?.refetch) actions.refetch();
setLoading(false);
setLoadingSafe(false);
toggleModalVisible();
}, 750);
});
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();
await insertPaymentResponse({
variables: {
paymentResponse: payments.map((payment) => ({
amount: payment.amount,
bodyshopid: bodyshop.id,
const { payments } = form.getFieldsValue();
await insertPaymentResponse({
variables: {
paymentResponse: payments.map((payment) => ({
amount: payment.amount,
bodyshopid: bodyshop.id,
jobid: payment.jobid,
declinereason: response.declinereason,
ext_paymentid: response.paymentid?.toString?.() ?? null,
successful: false,
response
}))
}
});
payments.forEach((payment) =>
insertAuditTrail({
jobid: payment.jobid,
declinereason: response.declinereason,
ext_paymentid: response.paymentid.toString(),
successful: false,
response
}))
}
});
payments.forEach((payment) =>
insertAuditTrail({
jobid: payment.jobid,
operation: AuditTrailMapping.failedpayment(),
type: "failedpayment"
})
);
operation: AuditTrailMapping.failedpayment(),
type: "failedpayment"
})
);
} finally {
// IMPORTANT: always clear loading, even on errors
setLoadingSafe(false);
}
});
};
@@ -154,7 +195,7 @@ const CardPaymentModalComponent = ({
try {
await form.validateFields();
} catch {
setLoading(false);
setLoadingSafe(false);
return;
}
@@ -185,7 +226,7 @@ const CardPaymentModalComponent = ({
document.documentElement.appendChild(node);
pollForIntelliPay(() => {
SetIntellipayCallbackFunctions();
// eslint-disable-next-line react-compiler/react-compiler
window.intellipay.isAutoOpen = true;
window.intellipay.initialize();
});
@@ -194,7 +235,7 @@ const CardPaymentModalComponent = ({
notification.error({
title: t("job_payments.notifications.error.openingip")
});
setLoading(false);
setLoadingSafe(false);
}
};
@@ -204,7 +245,7 @@ const CardPaymentModalComponent = ({
try {
await form.validateFields();
} catch {
setLoading(false);
setLoadingSafe(false);
return;
}
@@ -227,12 +268,12 @@ const CardPaymentModalComponent = ({
await navigator.clipboard.writeText(response.data.shorUrl);
message.success(t("general.actions.copied"));
}
setLoading(false);
setLoadingSafe(false);
} catch {
notification.error({
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 { useTranslation } from "react-i18next";
const { Option } = Select;
const ContractStatusComponent = forwardRef(({ value, onChange }, ref) => {
const ContractStatusComponent = ({ value, onChange, ref }) => {
const [option, setOption] = useState(value);
const { t } = useTranslation();
@@ -21,7 +21,7 @@ const ContractStatusComponent = forwardRef(({ value, onChange }, ref) => {
<Option value="contracts.status.returned">{t("contracts.status.out")}</Option>
</Select>
);
});
};
ContractStatusComponent.displayName = "ContractStatusComponent";

View File

@@ -1,8 +1,7 @@
import { Slider } from "antd";
import { forwardRef } from "react";
import { useTranslation } from "react-i18next";
const CourtesyCarFuelComponent = (props, ref) => {
const CourtesyCarFuelComponent = ({ ref, ...props }) => {
const { t } = useTranslation();
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 { forwardRef, useEffect, useState } from "react";
import { useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
const { Option } = Select;
const CourtesyCarReadinessComponent = ({ value, onChange }, ref) => {
const CourtesyCarReadinessComponent = ({ value, onChange, ref }) => {
const [option, setOption] = useState(value);
const { t } = useTranslation();
@@ -29,4 +29,4 @@ const CourtesyCarReadinessComponent = ({ value, onChange }, ref) => {
</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 { useTranslation } from "react-i18next";
const { Option } = Select;
const CourtesyCarStatusComponent = ({ value, onChange }, ref) => {
const CourtesyCarStatusComponent = ({ value, onChange, ref }) => {
const [option, setOption] = useState(value);
const { t } = useTranslation();
@@ -32,4 +32,4 @@ const CourtesyCarStatusComponent = ({ value, onChange }, ref) => {
</Select>
);
};
export default forwardRef(CourtesyCarStatusComponent);
export default CourtesyCarStatusComponent;

View File

@@ -1,17 +1,17 @@
import { useQuery } from "@apollo/client/react";
import { Select } from "antd";
import { forwardRef } from "react";
import { QUERY_TEAMS } from "../../graphql/employee_teams.queries";
import AlertComponent from "../alert/alert.component";
//To be used as a form element only.
const EmployeeTeamSearchSelect = ({ ...props }) => {
const EmployeeTeamSearchSelect = ({ ref, ...props }) => {
const { loading, error, data } = useQuery(QUERY_TEAMS);
if (error) return <AlertComponent title={JSON.stringify(error)} type="error" />;
return (
<Select
ref={ref}
showSearch
allowClear
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 { forwardRef, useEffect, useRef, useState } from "react";
import { useEffect, useRef, useState } from "react";
const FormInputNUmberCalculator = ({ value: formValue, onChange: formOnChange, ...restProps }) => {
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 { forwardRef } from "react";
// const locale = "en-us";
// const currencyFormatter = (value) => {
@@ -41,7 +40,7 @@ import { forwardRef } from "react";
// }
// };
function FormItemCurrency(props, ref) {
function FormItemCurrency({ ref, ...props }) {
return (
<InputNumber
{...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 { Button, Input, Space } from "antd";
import { forwardRef } from "react";
function FormItemEmail(props, ref) {
const { defaultValue, value, ...restProps } = props;
function FormItemEmail({ defaultValue, value, ref, ...restProps }) {
const emailValue = defaultValue || value;
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";
const LaborTypeFormItem = ({ value }) => {
@@ -8,4 +7,4 @@ const LaborTypeFormItem = ({ value }) => {
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";
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>
);
};
export default forwardRef(PartTypeFormItem);
export default PartTypeFormItem;

View File

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

View File

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

View File

@@ -5,31 +5,27 @@ import { useTranslation } from "react-i18next";
import { useNavigate } from "react-router-dom";
import "./notification-center.styles.scss";
import day from "../../utils/day.js";
import { forwardRef, useEffect, useRef } from "react";
import { useEffect, useRef } from "react";
import { DateTimeFormat } from "../../utils/DateFormatter.jsx";
const { Text, Title } = Typography;
/**
* 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(
(
{
visible,
notifications,
loading,
showUnreadOnly,
toggleUnreadOnly,
markAllRead,
loadMore,
onNotificationClick,
unreadCount,
isEmployee
},
ref
) => {
const NotificationCenterComponent = ({
visible,
notifications,
loading,
showUnreadOnly,
toggleUnreadOnly,
markAllRead,
loadMore,
onNotificationClick,
unreadCount,
isEmployee,
ref
}) => {
const { t } = useTranslation();
const navigate = useNavigate();
const virtuosoRef = useRef(null);
@@ -127,8 +123,7 @@ const NotificationCenterComponent = forwardRef(
)}
</div>
);
}
);
};
NotificationCenterComponent.displayName = "NotificationCenterComponent";

View File

@@ -2,14 +2,14 @@ import { LoadingOutlined } from "@ant-design/icons";
import { useLazyQuery } from "@apollo/client/react";
import { Empty, Select } from "antd";
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 AlertComponent from "../alert/alert.component";
import { OwnerNameDisplayFunction } from "../owner-name-display/owner-name-display.component";
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 [callIdSearch, { loading: idLoading, error: idError, data: idData }] = useLazyQuery(
@@ -85,4 +85,4 @@ const OwnerSearchSelect = ({ value, onChange, onBlur, disabled }, ref) => {
</div>
);
};
export default forwardRef(OwnerSearchSelect);
export default OwnerSearchSelect;

View File

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

View File

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

View File

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

View File

@@ -1,13 +1,13 @@
import { HeartOutlined } from "@ant-design/icons";
import { Select, Space, Tag } from "antd";
import { forwardRef, useEffect, useState } from "react";
import { useEffect, useState } from "react";
import PhoneNumberFormatter from "../../utils/PhoneFormatter";
const { Option } = Select;
// 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);
useEffect(() => {
@@ -131,4 +131,4 @@ const VendorSearchSelect = ({ value, onChange, options, onSelect, disabled, pref
</Select>
);
};
export default forwardRef(VendorSearchSelect);
export default VendorSearchSelect;