feature/IO-3499-React-19 -Checkpoint

This commit is contained in:
Dave
2026-01-29 12:31:04 -05:00
parent 9f573fc5b4
commit a9280a83ba
5 changed files with 464 additions and 477 deletions

View File

@@ -11,7 +11,7 @@
"dependencies": {
"@amplitude/analytics-browser": "^2.34.0",
"@ant-design/pro-layout": "^7.22.6",
"@apollo/client": "^4.1.2",
"@apollo/client": "^4.1.3",
"@emotion/is-prop-valid": "^1.4.0",
"@fingerprintjs/fingerprintjs": "^5.0.1",
"@firebase/analytics": "^0.10.19",
@@ -51,7 +51,7 @@
"normalize-url": "^8.1.1",
"object-hash": "^3.0.0",
"phone": "^3.1.70",
"posthog-js": "^1.335.5",
"posthog-js": "^1.336.1",
"prop-types": "^15.8.1",
"query-string": "^9.3.1",
"raf-schd": "^4.0.3",
@@ -540,9 +540,9 @@
}
},
"node_modules/@apollo/client": {
"version": "4.1.2",
"resolved": "https://registry.npmjs.org/@apollo/client/-/client-4.1.2.tgz",
"integrity": "sha512-MxlWuO94Y6TRf6+d4KfG5bCUXg5NP4s7zPKRA0PDNNa18K86zcbpHUgWKdx6wMT/5KVMeC5rsZkDqZLr/R0mFw==",
"version": "4.1.3",
"resolved": "https://registry.npmjs.org/@apollo/client/-/client-4.1.3.tgz",
"integrity": "sha512-2D0eN9R0IHj9qp1RwjM1/brKqcBGldlDfY0YiP5ecCj9FtVrhOtXqMj98SZ1CA0YGDY5X+dxx32Ljh7J0VHTfA==",
"license": "MIT",
"workspaces": [
"dist",
@@ -4771,18 +4771,18 @@
}
},
"node_modules/@posthog/core": {
"version": "1.14.1",
"resolved": "https://registry.npmjs.org/@posthog/core/-/core-1.14.1.tgz",
"integrity": "sha512-DtmJ1y1IDauX8yAZtIotRAYDRkgCCMLk5S9vFFRX7vufhWblQuRUOgn9WYSJrocJlZKm1aEjDzGQ0uyL7HcdLw==",
"version": "1.15.0",
"resolved": "https://registry.npmjs.org/@posthog/core/-/core-1.15.0.tgz",
"integrity": "sha512-n2/Yy0+qc8xhmlcOFiYqTcGHBZuuaQjVolfFXk7yTCynzdMe8Fx1zYvPPUrbdQK5tWwXyilkzybpqhK6I7aV4Q==",
"license": "MIT",
"dependencies": {
"cross-spawn": "^7.0.6"
}
},
"node_modules/@posthog/types": {
"version": "1.335.5",
"resolved": "https://registry.npmjs.org/@posthog/types/-/types-1.335.5.tgz",
"integrity": "sha512-QYj5c8wSaXGvV4ugEN65GHD0sIXRveGiZxV4tqpyoP7YIAvAwwA0do0yNfTrEjDXucCQn25pMbCqO25hJrMi5w==",
"version": "1.336.1",
"resolved": "https://registry.npmjs.org/@posthog/types/-/types-1.336.1.tgz",
"integrity": "sha512-KSGst/a/HK7GhfLSbwAy35HtU3KjDqjLtq3+PoDlGfbz9SbO0owjc6jo6hAHnMz67QTSvrn/r0xgimDO4NQ+rA==",
"license": "MIT"
},
"node_modules/@protobufjs/aspromise": {
@@ -14974,9 +14974,9 @@
"license": "MIT"
},
"node_modules/posthog-js": {
"version": "1.335.5",
"resolved": "https://registry.npmjs.org/posthog-js/-/posthog-js-1.335.5.tgz",
"integrity": "sha512-1zCEdn7bc1mQ/jpd62YY8U1CyNiftIBE6uKqE2L+mjZ5aJyB2rtUAXefaTbaR/3A98tItjSej4aIa8FBN+O1fw==",
"version": "1.336.1",
"resolved": "https://registry.npmjs.org/posthog-js/-/posthog-js-1.336.1.tgz",
"integrity": "sha512-YphbVhXnImmZoALvf2oh129Cxu6IRQ9P9sWhuyY+dGe7jqt1jBp6Dg7QEK39stB4rzxmT/N3OLFcWZM7ZYQzCg==",
"license": "SEE LICENSE IN LICENSE",
"dependencies": {
"@opentelemetry/api": "^1.9.0",
@@ -14984,8 +14984,8 @@
"@opentelemetry/exporter-logs-otlp-http": "^0.208.0",
"@opentelemetry/resources": "^2.2.0",
"@opentelemetry/sdk-logs": "^0.208.0",
"@posthog/core": "1.14.1",
"@posthog/types": "1.335.5",
"@posthog/core": "1.15.0",
"@posthog/types": "1.336.1",
"core-js": "^3.38.1",
"dompurify": "^3.3.1",
"fflate": "^0.4.8",

View File

@@ -10,7 +10,7 @@
"dependencies": {
"@amplitude/analytics-browser": "^2.34.0",
"@ant-design/pro-layout": "^7.22.6",
"@apollo/client": "^4.1.2",
"@apollo/client": "^4.1.3",
"@emotion/is-prop-valid": "^1.4.0",
"@fingerprintjs/fingerprintjs": "^5.0.1",
"@firebase/analytics": "^0.10.19",
@@ -50,7 +50,7 @@
"normalize-url": "^8.1.1",
"object-hash": "^3.0.0",
"phone": "^3.1.70",
"posthog-js": "^1.335.5",
"posthog-js": "^1.336.1",
"prop-types": "^15.8.1",
"query-string": "^9.3.1",
"raf-schd": "^4.0.3",

View File

@@ -1,10 +1,11 @@
import { DownCircleFilled } from "@ant-design/icons";
import { useApolloClient, useMutation, useQuery } from "@apollo/client/react";
import { useTreatmentsWithConfig } from "@splitsoftware/splitio-react";
import { Button, Card, Dropdown, Form, Input, Modal, Popconfirm, Popover, Select, Space } from "antd";
import { Button, Card, Dropdown, Form, Input, Modal, Popover, Select, Space } from "antd";
import axios from "axios";
import parsePhoneNumber from "libphonenumber-js";
import { useCallback, useMemo, useRef, useState } from "react";
import { HasRbacAccess } from "../../components/rbac-wrapper/rbac-wrapper.component";
import { useTranslation } from "react-i18next";
import { connect } from "react-redux";
import { Link, useNavigate } from "react-router-dom";
@@ -20,7 +21,7 @@ import { selectJobReadOnly } from "../../redux/application/application.selectors
import { setEmailOptions } from "../../redux/email/email.actions";
import { openChatByPhone, setMessage } from "../../redux/messaging/messaging.actions";
import { setModalContext } from "../../redux/modals/modals.actions";
import { selectBodyshop, selectCurrentUser } from "../../redux/user/user.selectors";
import { selectAuthLevel, selectBodyshop, selectCurrentUser } from "../../redux/user/user.selectors";
import AuditTrailMapping from "../../utils/AuditTrailMappings";
import { DateTimeFormatter } from "../../utils/DateFormatter";
import { TemplateList } from "../../utils/TemplateConstants";
@@ -28,7 +29,6 @@ import dayjs from "../../utils/day";
import { HasFeatureAccess } from "../feature-wrapper/feature-wrapper.component";
import FormDateTimePickerComponent from "../form-date-time-picker/form-date-time-picker.component";
import LockerWrapperComponent from "../lock-wrapper/lock-wrapper.component";
import RbacWrapper from "../rbac-wrapper/rbac-wrapper.component";
import ShareToTeamsButton from "../share-to-teams/share-to-teams.component.jsx";
import AddToProduction from "./jobs-detail-header-actions.addtoproduction.util";
import DuplicateJob from "./jobs-detail-header-actions.duplicate.util";
@@ -39,7 +39,8 @@ const EMPTY_ARRAY = Object.freeze([]);
const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop,
jobRO: selectJobReadOnly,
currentUser: selectCurrentUser
currentUser: selectCurrentUser,
authLevel: selectAuthLevel
});
const mapDispatchToProps = (dispatch) => ({
@@ -117,7 +118,8 @@ export function JobsDetailHeaderActions({
openChatByPhone,
setMessage,
setTimeTicketTaskContext,
setTaskUpsertContext
setTaskUpsertContext,
authLevel
}) {
const { t } = useTranslation();
const client = useApolloClient();
@@ -129,10 +131,6 @@ export function JobsDetailHeaderActions({
const jobId = job?.id;
const watcherVars = useMemo(() => ({ jobid: jobId }), [jobId]);
// Option A: coordinated Dropdown + Popconfirm open state so the menu doesn't unmount before Popconfirm renders.
const [confirmKey, setConfirmKey] = useState(null);
const confirmKeyRef = useRef(null);
const [isCancelScheduleModalVisible, setIsCancelScheduleModalVisible] = useState(false);
const [insertAppointment] = useMutation(INSERT_MANUAL_APPT);
const [deleteJob] = useMutation(DELETE_JOB);
@@ -150,6 +148,8 @@ export function JobsDetailHeaderActions({
const devEmails = ["imex.dev", "rome.dev"];
const prodEmails = ["imex.prod", "rome.prod", "imex.test", "rome.test"];
const canVoidJob = useMemo(() => HasRbacAccess({ authLevel, bodyshop, action: "jobs:void" }), [authLevel, bodyshop]);
const hasValidEmail = (emails) => emails.some((email) => userEmail.endsWith(email));
const canSubmitForTesting = (isDevEnv && hasValidEmail(devEmails)) || (isProdEnv && hasValidEmail(prodEmails));
@@ -179,83 +179,69 @@ export function JobsDetailHeaderActions({
const jobInPreProduction = preProductionStatuses.includes(jobStatus);
const jobInPostProduction = postProductionStatuses.includes(jobStatus);
const openConfirm = useCallback((key) => {
confirmKeyRef.current = key;
setConfirmKey(key);
setDropdownOpen(true);
}, []);
const makeConfirmId = () =>
globalThis.crypto?.randomUUID?.() ?? `${Date.now()}-${Math.random().toString(16).slice(2)}`;
const closeConfirm = useCallback(() => {
confirmKeyRef.current = null;
setConfirmKey(null);
}, []);
const [modal, modalContextHolder] = Modal.useModal();
const handleDropdownOpenChange = useCallback(
(nextOpen, info) => {
if (!nextOpen && info?.source === "menu" && confirmKeyRef.current) return;
setDropdownOpen(nextOpen);
if (!nextOpen) closeConfirm();
},
[closeConfirm]
);
const confirmInstancesRef = useRef(new Map());
const renderPopconfirmMenuLabel = ({
key,
text,
const closeConfirmById = (id) => {
const inst = confirmInstancesRef.current.get(id);
if (inst) inst.destroy(); // hard close
confirmInstancesRef.current.delete(id);
};
const openConfirmFromMenu = ({
variant = "confirm", // "confirm" | "info" | "warning"
title,
content,
okText,
cancelText,
showCancel = true,
closeDropdownOnConfirm = true,
onConfirm
}) => (
<Popconfirm
title={title}
okText={okText}
cancelText={cancelText}
showCancel={showCancel}
open={confirmKey === key}
onOpenChange={(nextOpen) => {
if (nextOpen) openConfirm(key);
else closeConfirm();
}}
onConfirm={(e) => {
e?.stopPropagation?.();
closeConfirm();
onOk,
onCancel
}) => {
// close the dropdown immediately; confirm dialog is separate
setDropdownOpen(false);
// Critical: for informational popconfirms, keep the dropdown open so the Popconfirm can cleanly close.
if (closeDropdownOnConfirm) {
setDropdownOpen(false);
const id = makeConfirmId();
const openFn = variant === "info" ? modal.info : variant === "warning" ? modal.warning : modal.confirm;
const inst = openFn({
title,
content,
okText,
cancelText,
centered: true,
maskClosable: false,
onCancel: () => {
closeConfirmById(id);
onCancel?.();
},
onOk: async () => {
try {
await onOk?.();
} finally {
closeConfirmById(id);
}
},
...(showCancel ? {} : { okCancel: false })
});
onConfirm?.(e);
}}
onCancel={(e) => {
e?.stopPropagation?.();
closeConfirm();
// Keep dropdown open on cancel so the user can continue using the menu.
}}
getPopupContainer={() => document.body}
>
<div
style={{ width: "100%" }}
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
openConfirm(key);
}}
>
{text}
</div>
</Popconfirm>
);
confirmInstancesRef.current.set(id, inst);
return id;
};
const handleDropdownOpenChange = useCallback((nextOpen) => {
setDropdownOpen(nextOpen);
}, []);
// Function to show modal
const showCancelScheduleModal = () => {
setIsCancelScheduleModalVisible(true);
};
// Function to handle Cancel
const handleCancelScheduleModalCancel = () => {
setIsCancelScheduleModalVisible(false);
};
@@ -476,6 +462,11 @@ export function JobsDetailHeaderActions({
};
const handleVoidJob = async () => {
if (!canVoidJob) {
notification.error({ title: t("general.messages.rbacunauth") });
return;
}
//delete the job.
const result = await voidJob({
variables: {
@@ -964,26 +955,26 @@ export function JobsDetailHeaderActions({
{
key: "duplicate",
id: "job-actions-duplicate",
label: renderPopconfirmMenuLabel({
key: "confirm-duplicate",
text: t("menus.jobsactions.duplicate"),
title: t("jobs.labels.duplicateconfirm"),
okText: t("general.labels.yes"),
cancelText: t("general.labels.no"),
onConfirm: handleDuplicate
})
label: t("menus.jobsactions.duplicate"),
onClick: () =>
openConfirmFromMenu({
title: t("jobs.labels.duplicateconfirm"),
okText: t("general.labels.yes"),
cancelText: t("general.labels.no"),
onOk: handleDuplicate
})
},
{
key: "duplicatenolines",
id: "job-actions-duplicatenolines",
label: renderPopconfirmMenuLabel({
key: "confirm-duplicate-nolines",
text: t("menus.jobsactions.duplicatenolines"),
title: t("jobs.labels.duplicateconfirm"),
okText: t("general.labels.yes"),
cancelText: t("general.labels.no"),
onConfirm: handleDuplicateConfirm
})
label: t("menus.jobsactions.duplicatenolines"),
onClick: () =>
openConfirmFromMenu({
title: t("jobs.labels.duplicateconfirm"),
okText: t("general.labels.yes"),
cancelText: t("general.labels.no"),
onOk: handleDuplicateConfirm
})
}
]
},
@@ -1156,26 +1147,25 @@ export function JobsDetailHeaderActions({
menuItems.push({
key: "deletejob",
id: "job-actions-deletejob",
label:
jobWatchersCount === 0
? renderPopconfirmMenuLabel({
key: "confirm-deletejob",
text: t("menus.jobsactions.deletejob"),
title: t("jobs.labels.deleteconfirm"),
okText: t("general.labels.yes"),
cancelText: t("general.labels.no"),
onConfirm: handleDeleteJob
})
: renderPopconfirmMenuLabel({
key: "confirm-deletejob-watchers",
text: t("menus.jobsactions.deletejob"),
title: t("jobs.labels.deletewatchers"),
showCancel: false,
closeDropdownOnConfirm: false, // <-- FIX: keep dropdown mounted so Popconfirm can close cleanly
onConfirm: () => {
// informational confirm only
}
})
label: t("menus.jobsactions.deletejob"),
onClick: () => {
if (jobWatchersCount === 0) {
openConfirmFromMenu({
title: t("jobs.labels.deleteconfirm"),
okText: t("general.labels.yes"),
cancelText: t("general.labels.no"),
onOk: handleDeleteJob
});
} else {
// informational "OK only"
openConfirmFromMenu({
variant: "info",
title: t("jobs.labels.deletewatchers"),
okText: t("general.actions.ok") ?? "OK",
showCancel: false
});
}
}
});
}
@@ -1188,22 +1178,18 @@ export function JobsDetailHeaderActions({
label: t("appointments.labels.manualevent")
});
if (!jobRO && job.converted) {
if (!jobRO && job.converted && canVoidJob) {
menuItems.push({
key: "voidjob",
id: "job-actions-voidjob",
label: (
<RbacWrapper action="jobs:void" noauth>
{renderPopconfirmMenuLabel({
key: "confirm-voidjob",
text: t("menus.jobsactions.void"),
title: t("jobs.labels.voidjob"),
okText: t("general.labels.yes"),
cancelText: t("general.labels.no"),
onConfirm: handleVoidJob
})}
</RbacWrapper>
)
label: t("menus.jobsactions.void"),
onClick: () =>
openConfirmFromMenu({
title: t("jobs.labels.voidjob"),
okText: t("general.labels.yes"),
cancelText: t("general.labels.no"),
onOk: handleVoidJob
})
});
}
@@ -1235,6 +1221,7 @@ export function JobsDetailHeaderActions({
return (
<>
{modalContextHolder}
<Modal
title={t("menus.jobsactions.cancelallappointments")}
open={isCancelScheduleModalVisible}

654
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -18,23 +18,23 @@
"job-totals-fixtures:local": "docker exec node-app /usr/bin/node /app/download-job-totals-fixtures.js"
},
"dependencies": {
"@aws-sdk/client-cloudwatch-logs": "^3.975.0",
"@aws-sdk/client-elasticache": "^3.975.0",
"@aws-sdk/client-s3": "^3.975.0",
"@aws-sdk/client-secrets-manager": "^3.975.0",
"@aws-sdk/client-ses": "^3.975.0",
"@aws-sdk/credential-provider-node": "^3.972.1",
"@aws-sdk/lib-storage": "^3.975.0",
"@aws-sdk/s3-request-presigner": "^3.975.0",
"@aws-sdk/client-cloudwatch-logs": "^3.978.0",
"@aws-sdk/client-elasticache": "^3.978.0",
"@aws-sdk/client-s3": "^3.978.0",
"@aws-sdk/client-secrets-manager": "^3.978.0",
"@aws-sdk/client-ses": "^3.978.0",
"@aws-sdk/credential-provider-node": "^3.972.2",
"@aws-sdk/lib-storage": "^3.978.0",
"@aws-sdk/s3-request-presigner": "^3.978.0",
"@opensearch-project/opensearch": "^2.13.0",
"@socket.io/admin-ui": "^0.5.1",
"@socket.io/redis-adapter": "^8.3.0",
"archiver": "^7.0.1",
"aws4": "^1.13.2",
"axios": "^1.13.3",
"axios": "^1.13.4",
"axios-curlirize": "^2.0.0",
"better-queue": "^3.8.12",
"bullmq": "^5.67.1",
"bullmq": "^5.67.2",
"chart.js": "^4.5.1",
"cloudinary": "^2.9.0",
"compression": "^1.8.1",
@@ -65,7 +65,7 @@
"recursive-diff": "^1.0.9",
"rimraf": "^6.1.2",
"skia-canvas": "^3.0.8",
"soap": "^1.6.3",
"soap": "^1.6.4",
"socket.io": "^4.8.3",
"socket.io-adapter": "^2.5.6",
"ssh2-sftp-client": "^11.0.0",
@@ -82,7 +82,7 @@
"@eslint/js": "^9.39.2",
"eslint": "^9.39.2",
"eslint-plugin-react": "^7.37.5",
"globals": "^17.1.0",
"globals": "^17.2.0",
"mock-require": "^3.0.3",
"p-limit": "^3.1.0",
"prettier": "^3.8.1",