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": { "dependencies": {
"@amplitude/analytics-browser": "^2.34.0", "@amplitude/analytics-browser": "^2.34.0",
"@ant-design/pro-layout": "^7.22.6", "@ant-design/pro-layout": "^7.22.6",
"@apollo/client": "^4.1.2", "@apollo/client": "^4.1.3",
"@emotion/is-prop-valid": "^1.4.0", "@emotion/is-prop-valid": "^1.4.0",
"@fingerprintjs/fingerprintjs": "^5.0.1", "@fingerprintjs/fingerprintjs": "^5.0.1",
"@firebase/analytics": "^0.10.19", "@firebase/analytics": "^0.10.19",
@@ -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.70", "phone": "^3.1.70",
"posthog-js": "^1.335.5", "posthog-js": "^1.336.1",
"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",
@@ -540,9 +540,9 @@
} }
}, },
"node_modules/@apollo/client": { "node_modules/@apollo/client": {
"version": "4.1.2", "version": "4.1.3",
"resolved": "https://registry.npmjs.org/@apollo/client/-/client-4.1.2.tgz", "resolved": "https://registry.npmjs.org/@apollo/client/-/client-4.1.3.tgz",
"integrity": "sha512-MxlWuO94Y6TRf6+d4KfG5bCUXg5NP4s7zPKRA0PDNNa18K86zcbpHUgWKdx6wMT/5KVMeC5rsZkDqZLr/R0mFw==", "integrity": "sha512-2D0eN9R0IHj9qp1RwjM1/brKqcBGldlDfY0YiP5ecCj9FtVrhOtXqMj98SZ1CA0YGDY5X+dxx32Ljh7J0VHTfA==",
"license": "MIT", "license": "MIT",
"workspaces": [ "workspaces": [
"dist", "dist",
@@ -4771,18 +4771,18 @@
} }
}, },
"node_modules/@posthog/core": { "node_modules/@posthog/core": {
"version": "1.14.1", "version": "1.15.0",
"resolved": "https://registry.npmjs.org/@posthog/core/-/core-1.14.1.tgz", "resolved": "https://registry.npmjs.org/@posthog/core/-/core-1.15.0.tgz",
"integrity": "sha512-DtmJ1y1IDauX8yAZtIotRAYDRkgCCMLk5S9vFFRX7vufhWblQuRUOgn9WYSJrocJlZKm1aEjDzGQ0uyL7HcdLw==", "integrity": "sha512-n2/Yy0+qc8xhmlcOFiYqTcGHBZuuaQjVolfFXk7yTCynzdMe8Fx1zYvPPUrbdQK5tWwXyilkzybpqhK6I7aV4Q==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"cross-spawn": "^7.0.6" "cross-spawn": "^7.0.6"
} }
}, },
"node_modules/@posthog/types": { "node_modules/@posthog/types": {
"version": "1.335.5", "version": "1.336.1",
"resolved": "https://registry.npmjs.org/@posthog/types/-/types-1.335.5.tgz", "resolved": "https://registry.npmjs.org/@posthog/types/-/types-1.336.1.tgz",
"integrity": "sha512-QYj5c8wSaXGvV4ugEN65GHD0sIXRveGiZxV4tqpyoP7YIAvAwwA0do0yNfTrEjDXucCQn25pMbCqO25hJrMi5w==", "integrity": "sha512-KSGst/a/HK7GhfLSbwAy35HtU3KjDqjLtq3+PoDlGfbz9SbO0owjc6jo6hAHnMz67QTSvrn/r0xgimDO4NQ+rA==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/@protobufjs/aspromise": { "node_modules/@protobufjs/aspromise": {
@@ -14974,9 +14974,9 @@
"license": "MIT" "license": "MIT"
}, },
"node_modules/posthog-js": { "node_modules/posthog-js": {
"version": "1.335.5", "version": "1.336.1",
"resolved": "https://registry.npmjs.org/posthog-js/-/posthog-js-1.335.5.tgz", "resolved": "https://registry.npmjs.org/posthog-js/-/posthog-js-1.336.1.tgz",
"integrity": "sha512-1zCEdn7bc1mQ/jpd62YY8U1CyNiftIBE6uKqE2L+mjZ5aJyB2rtUAXefaTbaR/3A98tItjSej4aIa8FBN+O1fw==", "integrity": "sha512-YphbVhXnImmZoALvf2oh129Cxu6IRQ9P9sWhuyY+dGe7jqt1jBp6Dg7QEK39stB4rzxmT/N3OLFcWZM7ZYQzCg==",
"license": "SEE LICENSE IN LICENSE", "license": "SEE LICENSE IN LICENSE",
"dependencies": { "dependencies": {
"@opentelemetry/api": "^1.9.0", "@opentelemetry/api": "^1.9.0",
@@ -14984,8 +14984,8 @@
"@opentelemetry/exporter-logs-otlp-http": "^0.208.0", "@opentelemetry/exporter-logs-otlp-http": "^0.208.0",
"@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.14.1", "@posthog/core": "1.15.0",
"@posthog/types": "1.335.5", "@posthog/types": "1.336.1",
"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",

View File

@@ -10,7 +10,7 @@
"dependencies": { "dependencies": {
"@amplitude/analytics-browser": "^2.34.0", "@amplitude/analytics-browser": "^2.34.0",
"@ant-design/pro-layout": "^7.22.6", "@ant-design/pro-layout": "^7.22.6",
"@apollo/client": "^4.1.2", "@apollo/client": "^4.1.3",
"@emotion/is-prop-valid": "^1.4.0", "@emotion/is-prop-valid": "^1.4.0",
"@fingerprintjs/fingerprintjs": "^5.0.1", "@fingerprintjs/fingerprintjs": "^5.0.1",
"@firebase/analytics": "^0.10.19", "@firebase/analytics": "^0.10.19",
@@ -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.70", "phone": "^3.1.70",
"posthog-js": "^1.335.5", "posthog-js": "^1.336.1",
"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,10 +1,11 @@
import { DownCircleFilled } from "@ant-design/icons"; import { DownCircleFilled } from "@ant-design/icons";
import { useApolloClient, useMutation, useQuery } from "@apollo/client/react"; import { useApolloClient, useMutation, useQuery } from "@apollo/client/react";
import { useTreatmentsWithConfig } from "@splitsoftware/splitio-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 axios from "axios";
import parsePhoneNumber from "libphonenumber-js"; import parsePhoneNumber from "libphonenumber-js";
import { useCallback, useMemo, useRef, useState } from "react"; import { useCallback, useMemo, useRef, useState } from "react";
import { HasRbacAccess } from "../../components/rbac-wrapper/rbac-wrapper.component";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { connect } from "react-redux"; import { connect } from "react-redux";
import { Link, useNavigate } from "react-router-dom"; 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 { setEmailOptions } from "../../redux/email/email.actions";
import { openChatByPhone, setMessage } from "../../redux/messaging/messaging.actions"; import { openChatByPhone, setMessage } from "../../redux/messaging/messaging.actions";
import { setModalContext } from "../../redux/modals/modals.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 AuditTrailMapping from "../../utils/AuditTrailMappings";
import { DateTimeFormatter } from "../../utils/DateFormatter"; import { DateTimeFormatter } from "../../utils/DateFormatter";
import { TemplateList } from "../../utils/TemplateConstants"; import { TemplateList } from "../../utils/TemplateConstants";
@@ -28,7 +29,6 @@ import dayjs from "../../utils/day";
import { HasFeatureAccess } from "../feature-wrapper/feature-wrapper.component"; import { HasFeatureAccess } from "../feature-wrapper/feature-wrapper.component";
import FormDateTimePickerComponent from "../form-date-time-picker/form-date-time-picker.component"; import FormDateTimePickerComponent from "../form-date-time-picker/form-date-time-picker.component";
import LockerWrapperComponent from "../lock-wrapper/lock-wrapper.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 ShareToTeamsButton from "../share-to-teams/share-to-teams.component.jsx";
import AddToProduction from "./jobs-detail-header-actions.addtoproduction.util"; import AddToProduction from "./jobs-detail-header-actions.addtoproduction.util";
import DuplicateJob from "./jobs-detail-header-actions.duplicate.util"; import DuplicateJob from "./jobs-detail-header-actions.duplicate.util";
@@ -39,7 +39,8 @@ const EMPTY_ARRAY = Object.freeze([]);
const mapStateToProps = createStructuredSelector({ const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop, bodyshop: selectBodyshop,
jobRO: selectJobReadOnly, jobRO: selectJobReadOnly,
currentUser: selectCurrentUser currentUser: selectCurrentUser,
authLevel: selectAuthLevel
}); });
const mapDispatchToProps = (dispatch) => ({ const mapDispatchToProps = (dispatch) => ({
@@ -117,7 +118,8 @@ export function JobsDetailHeaderActions({
openChatByPhone, openChatByPhone,
setMessage, setMessage,
setTimeTicketTaskContext, setTimeTicketTaskContext,
setTaskUpsertContext setTaskUpsertContext,
authLevel
}) { }) {
const { t } = useTranslation(); const { t } = useTranslation();
const client = useApolloClient(); const client = useApolloClient();
@@ -129,10 +131,6 @@ export function JobsDetailHeaderActions({
const jobId = job?.id; const jobId = job?.id;
const watcherVars = useMemo(() => ({ jobid: jobId }), [jobId]); 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 [isCancelScheduleModalVisible, setIsCancelScheduleModalVisible] = useState(false);
const [insertAppointment] = useMutation(INSERT_MANUAL_APPT); const [insertAppointment] = useMutation(INSERT_MANUAL_APPT);
const [deleteJob] = useMutation(DELETE_JOB); const [deleteJob] = useMutation(DELETE_JOB);
@@ -150,6 +148,8 @@ export function JobsDetailHeaderActions({
const devEmails = ["imex.dev", "rome.dev"]; const devEmails = ["imex.dev", "rome.dev"];
const prodEmails = ["imex.prod", "rome.prod", "imex.test", "rome.test"]; 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 hasValidEmail = (emails) => emails.some((email) => userEmail.endsWith(email));
const canSubmitForTesting = (isDevEnv && hasValidEmail(devEmails)) || (isProdEnv && hasValidEmail(prodEmails)); const canSubmitForTesting = (isDevEnv && hasValidEmail(devEmails)) || (isProdEnv && hasValidEmail(prodEmails));
@@ -179,83 +179,69 @@ export function JobsDetailHeaderActions({
const jobInPreProduction = preProductionStatuses.includes(jobStatus); const jobInPreProduction = preProductionStatuses.includes(jobStatus);
const jobInPostProduction = postProductionStatuses.includes(jobStatus); const jobInPostProduction = postProductionStatuses.includes(jobStatus);
const openConfirm = useCallback((key) => { const makeConfirmId = () =>
confirmKeyRef.current = key; globalThis.crypto?.randomUUID?.() ?? `${Date.now()}-${Math.random().toString(16).slice(2)}`;
setConfirmKey(key);
setDropdownOpen(true);
}, []);
const closeConfirm = useCallback(() => { const [modal, modalContextHolder] = Modal.useModal();
confirmKeyRef.current = null;
setConfirmKey(null);
}, []);
const handleDropdownOpenChange = useCallback( const confirmInstancesRef = useRef(new Map());
(nextOpen, info) => {
if (!nextOpen && info?.source === "menu" && confirmKeyRef.current) return;
setDropdownOpen(nextOpen);
if (!nextOpen) closeConfirm();
},
[closeConfirm]
);
const renderPopconfirmMenuLabel = ({ const closeConfirmById = (id) => {
key, const inst = confirmInstancesRef.current.get(id);
text, if (inst) inst.destroy(); // hard close
confirmInstancesRef.current.delete(id);
};
const openConfirmFromMenu = ({
variant = "confirm", // "confirm" | "info" | "warning"
title, title,
content,
okText, okText,
cancelText, cancelText,
showCancel = true, showCancel = true,
closeDropdownOnConfirm = true, onOk,
onConfirm onCancel
}) => ( }) => {
<Popconfirm // close the dropdown immediately; confirm dialog is separate
title={title} setDropdownOpen(false);
okText={okText}
cancelText={cancelText}
showCancel={showCancel}
open={confirmKey === key}
onOpenChange={(nextOpen) => {
if (nextOpen) openConfirm(key);
else closeConfirm();
}}
onConfirm={(e) => {
e?.stopPropagation?.();
closeConfirm();
// Critical: for informational popconfirms, keep the dropdown open so the Popconfirm can cleanly close. const id = makeConfirmId();
if (closeDropdownOnConfirm) {
setDropdownOpen(false); 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); confirmInstancesRef.current.set(id, inst);
}} return id;
onCancel={(e) => { };
e?.stopPropagation?.();
closeConfirm(); const handleDropdownOpenChange = useCallback((nextOpen) => {
// Keep dropdown open on cancel so the user can continue using the menu. setDropdownOpen(nextOpen);
}} }, []);
getPopupContainer={() => document.body}
>
<div
style={{ width: "100%" }}
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
openConfirm(key);
}}
>
{text}
</div>
</Popconfirm>
);
// Function to show modal
const showCancelScheduleModal = () => { const showCancelScheduleModal = () => {
setIsCancelScheduleModalVisible(true); setIsCancelScheduleModalVisible(true);
}; };
// Function to handle Cancel
const handleCancelScheduleModalCancel = () => { const handleCancelScheduleModalCancel = () => {
setIsCancelScheduleModalVisible(false); setIsCancelScheduleModalVisible(false);
}; };
@@ -476,6 +462,11 @@ export function JobsDetailHeaderActions({
}; };
const handleVoidJob = async () => { const handleVoidJob = async () => {
if (!canVoidJob) {
notification.error({ title: t("general.messages.rbacunauth") });
return;
}
//delete the job. //delete the job.
const result = await voidJob({ const result = await voidJob({
variables: { variables: {
@@ -964,26 +955,26 @@ export function JobsDetailHeaderActions({
{ {
key: "duplicate", key: "duplicate",
id: "job-actions-duplicate", id: "job-actions-duplicate",
label: renderPopconfirmMenuLabel({ label: t("menus.jobsactions.duplicate"),
key: "confirm-duplicate", onClick: () =>
text: t("menus.jobsactions.duplicate"), openConfirmFromMenu({
title: t("jobs.labels.duplicateconfirm"), title: t("jobs.labels.duplicateconfirm"),
okText: t("general.labels.yes"), okText: t("general.labels.yes"),
cancelText: t("general.labels.no"), cancelText: t("general.labels.no"),
onConfirm: handleDuplicate onOk: handleDuplicate
}) })
}, },
{ {
key: "duplicatenolines", key: "duplicatenolines",
id: "job-actions-duplicatenolines", id: "job-actions-duplicatenolines",
label: renderPopconfirmMenuLabel({ label: t("menus.jobsactions.duplicatenolines"),
key: "confirm-duplicate-nolines", onClick: () =>
text: t("menus.jobsactions.duplicatenolines"), openConfirmFromMenu({
title: t("jobs.labels.duplicateconfirm"), title: t("jobs.labels.duplicateconfirm"),
okText: t("general.labels.yes"), okText: t("general.labels.yes"),
cancelText: t("general.labels.no"), cancelText: t("general.labels.no"),
onConfirm: handleDuplicateConfirm onOk: handleDuplicateConfirm
}) })
} }
] ]
}, },
@@ -1156,26 +1147,25 @@ export function JobsDetailHeaderActions({
menuItems.push({ menuItems.push({
key: "deletejob", key: "deletejob",
id: "job-actions-deletejob", id: "job-actions-deletejob",
label: label: t("menus.jobsactions.deletejob"),
jobWatchersCount === 0 onClick: () => {
? renderPopconfirmMenuLabel({ if (jobWatchersCount === 0) {
key: "confirm-deletejob", openConfirmFromMenu({
text: t("menus.jobsactions.deletejob"), title: t("jobs.labels.deleteconfirm"),
title: t("jobs.labels.deleteconfirm"), okText: t("general.labels.yes"),
okText: t("general.labels.yes"), cancelText: t("general.labels.no"),
cancelText: t("general.labels.no"), onOk: handleDeleteJob
onConfirm: handleDeleteJob });
}) } else {
: renderPopconfirmMenuLabel({ // informational "OK only"
key: "confirm-deletejob-watchers", openConfirmFromMenu({
text: t("menus.jobsactions.deletejob"), variant: "info",
title: t("jobs.labels.deletewatchers"), title: t("jobs.labels.deletewatchers"),
showCancel: false, okText: t("general.actions.ok") ?? "OK",
closeDropdownOnConfirm: false, // <-- FIX: keep dropdown mounted so Popconfirm can close cleanly showCancel: false
onConfirm: () => { });
// informational confirm only }
} }
})
}); });
} }
@@ -1188,22 +1178,18 @@ export function JobsDetailHeaderActions({
label: t("appointments.labels.manualevent") label: t("appointments.labels.manualevent")
}); });
if (!jobRO && job.converted) { if (!jobRO && job.converted && canVoidJob) {
menuItems.push({ menuItems.push({
key: "voidjob", key: "voidjob",
id: "job-actions-voidjob", id: "job-actions-voidjob",
label: ( label: t("menus.jobsactions.void"),
<RbacWrapper action="jobs:void" noauth> onClick: () =>
{renderPopconfirmMenuLabel({ openConfirmFromMenu({
key: "confirm-voidjob", title: t("jobs.labels.voidjob"),
text: t("menus.jobsactions.void"), okText: t("general.labels.yes"),
title: t("jobs.labels.voidjob"), cancelText: t("general.labels.no"),
okText: t("general.labels.yes"), onOk: handleVoidJob
cancelText: t("general.labels.no"), })
onConfirm: handleVoidJob
})}
</RbacWrapper>
)
}); });
} }
@@ -1235,6 +1221,7 @@ export function JobsDetailHeaderActions({
return ( return (
<> <>
{modalContextHolder}
<Modal <Modal
title={t("menus.jobsactions.cancelallappointments")} title={t("menus.jobsactions.cancelallappointments")}
open={isCancelScheduleModalVisible} 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" "job-totals-fixtures:local": "docker exec node-app /usr/bin/node /app/download-job-totals-fixtures.js"
}, },
"dependencies": { "dependencies": {
"@aws-sdk/client-cloudwatch-logs": "^3.975.0", "@aws-sdk/client-cloudwatch-logs": "^3.978.0",
"@aws-sdk/client-elasticache": "^3.975.0", "@aws-sdk/client-elasticache": "^3.978.0",
"@aws-sdk/client-s3": "^3.975.0", "@aws-sdk/client-s3": "^3.978.0",
"@aws-sdk/client-secrets-manager": "^3.975.0", "@aws-sdk/client-secrets-manager": "^3.978.0",
"@aws-sdk/client-ses": "^3.975.0", "@aws-sdk/client-ses": "^3.978.0",
"@aws-sdk/credential-provider-node": "^3.972.1", "@aws-sdk/credential-provider-node": "^3.972.2",
"@aws-sdk/lib-storage": "^3.975.0", "@aws-sdk/lib-storage": "^3.978.0",
"@aws-sdk/s3-request-presigner": "^3.975.0", "@aws-sdk/s3-request-presigner": "^3.978.0",
"@opensearch-project/opensearch": "^2.13.0", "@opensearch-project/opensearch": "^2.13.0",
"@socket.io/admin-ui": "^0.5.1", "@socket.io/admin-ui": "^0.5.1",
"@socket.io/redis-adapter": "^8.3.0", "@socket.io/redis-adapter": "^8.3.0",
"archiver": "^7.0.1", "archiver": "^7.0.1",
"aws4": "^1.13.2", "aws4": "^1.13.2",
"axios": "^1.13.3", "axios": "^1.13.4",
"axios-curlirize": "^2.0.0", "axios-curlirize": "^2.0.0",
"better-queue": "^3.8.12", "better-queue": "^3.8.12",
"bullmq": "^5.67.1", "bullmq": "^5.67.2",
"chart.js": "^4.5.1", "chart.js": "^4.5.1",
"cloudinary": "^2.9.0", "cloudinary": "^2.9.0",
"compression": "^1.8.1", "compression": "^1.8.1",
@@ -65,7 +65,7 @@
"recursive-diff": "^1.0.9", "recursive-diff": "^1.0.9",
"rimraf": "^6.1.2", "rimraf": "^6.1.2",
"skia-canvas": "^3.0.8", "skia-canvas": "^3.0.8",
"soap": "^1.6.3", "soap": "^1.6.4",
"socket.io": "^4.8.3", "socket.io": "^4.8.3",
"socket.io-adapter": "^2.5.6", "socket.io-adapter": "^2.5.6",
"ssh2-sftp-client": "^11.0.0", "ssh2-sftp-client": "^11.0.0",
@@ -82,7 +82,7 @@
"@eslint/js": "^9.39.2", "@eslint/js": "^9.39.2",
"eslint": "^9.39.2", "eslint": "^9.39.2",
"eslint-plugin-react": "^7.37.5", "eslint-plugin-react": "^7.37.5",
"globals": "^17.1.0", "globals": "^17.2.0",
"mock-require": "^3.0.3", "mock-require": "^3.0.3",
"p-limit": "^3.1.0", "p-limit": "^3.1.0",
"prettier": "^3.8.1", "prettier": "^3.8.1",