diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 000000000..c810a1224 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,83 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## Overview + +ePAD JS is a React-based DICOM medical imaging viewer and annotation platform. It enables radiologists to view medical images from PACS systems, create/manage annotations (AIMs), and generate clinical reports. + +## Commands + +```bash +npm start # Development server +npm run build # Production build +npm test # Run tests (Jest + jsdom) +``` + +No lint script is defined. Tests use `react-app-rewired test --env=jsdom`. + +To run a single test file: +```bash +npm test -- --testPathPattern= +``` + +## Architecture + +### Tech Stack +- **React 16** (class components predominate, some hooks) +- **Redux + redux-thunk** for state management +- **React Router v4** for routing +- **Cornerstone.js** for DICOM image rendering +- **Keycloak** (v4.0.0-beta.2) for OAuth/OIDC authentication +- **Axios** for HTTP, with auth interceptors in `src/services/httpService.js` + +### Routing (`src/App.js`) +All routes except `/login`/`/logout` are wrapped in `ProtectedRoute`: +- `/display` — DICOM viewer (main imaging workflow) +- `/list/:pid` — Patient/study search (SearchView) +- `/flex/:pid` — Flexible layout view +- `/anotate` — Annotation creation +- `/progress` — Upload progress tracking +- `/management` — Admin panel +- `/annotationSearch` — Cross-project annotation search + +### Redux Store (`src/reducers.js`) +Three combined reducers: +- **`annotationsListReducer`** — Core viewer state: `openSeries[]`, `aimsList{}`, selected project/patients/studies, `projectMap{}`, AIM templates, UI toggles (`showAnnotations`, `showLabels`, etc.) +- **`searchViewReducer`** — Study list state, user-selected studies, Cornerstone instances +- **`managementReducer`** — Admin/management UI state + +### Service Layer (`src/services/`) +19 service modules wrapping the backend API. Key ones: +- `httpService.js` — Axios instance with auth header injection; reads `apiUrl`/`wadoUrl` from `sessionStorage` +- `annotationServices.js` — AIM CRUD +- `seriesServices.js` / `studyServices.js` — DICOM data fetching +- `authService.js` — Keycloak integration +- `projectServices.js` — Project management + +### Key Component Modules (`src/components/`) +- **`display/`** — DICOM viewer; `displayView.jsx` is the largest file (~116KB). Contains Cornerstone tool integration (Arrow, Circle, Line, Bidirectional, Freehand, Probe, segmentation) +- **`searchView/`** — Patient/study search with reporting (ADLA, RECIST, Waterfall), filters, and a drag-drop study grid +- **`annotationsList/`** — Annotation management with Redux reducer/actions; includes `annotationDock/` +- **`sideBar/`** — Left navigation: patient/project tree, worklist integration +- **`ToolMenu/`** — Drawing/measurement tool palette including SmartBrush for segmentation +- **`management/`** — Project/user/permission administration +- **`aimEditor/`** — AIM annotation editor with tag editing (`tagEditor/`) +- **`MediaExport/`** — GIF and PowerPoint export +- **`common/`** — Shared UI: `protectedRoute.jsx`, modals, `SelectModalMenu.jsx` + +### Session & Persistence +- **`sessionStorage`**: API credentials (`apiUrl`, `wadoUrl`, `username`, tokens), operating mode (`lite` vs standard) +- **`localStorage`**: Tree data, PHI visibility preferences, filters + +### Build Configuration +- `react-app-rewired` overrides CRA config via `config-overrides.js` — adds Node.js polyfills (`NodePolyfillPlugin`) for crypto/Buffer, TypeScript resolution, and `fs: false` fallback +- `jsconfig.json` sets `baseUrl: "src"` so imports can be relative to `src/` (e.g., `import X from 'components/...'`) +- Babel: `@babel/preset-env`, `@babel/react`, Emotion plugin + +### Deployment +Docker multi-stage build: Node 11 Alpine for build → Nginx for serving. Nginx proxies: +- `/api` → `epad_lite:8080` (backend) +- `/pacs` → `epad_dicomweb:8090` (DICOM server) +- `/keycloak` → Keycloak auth server +- WADO images cached for 60 minutes diff --git a/package-lock.json b/package-lock.json index c5d0b2107..3dac4f8e6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13,7 +13,7 @@ "@dnd-kit/sortable": "^10.0.0-next-202410244445", "@dnd-kit/utilities": "^3.2.2", "aimapi": "^0.1.26", - "axios": "^0.21.4", + "axios": "^1.13.6", "bootstrap": "^5.1.3", "cornerstone-core": "^2.6.1", "cornerstone-math": "^0.1.8", @@ -2892,7 +2892,6 @@ "version": "4.4.0", "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz", "integrity": "sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA==", - "dev": true, "license": "MIT", "dependencies": { "eslint-visitor-keys": "^3.3.0" @@ -2908,7 +2907,6 @@ "version": "3.4.3", "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", - "dev": true, "license": "Apache-2.0", "engines": { "node": "^12.22.0 || ^14.17.0 || >=16.0.0" @@ -2921,7 +2919,6 @@ "version": "4.11.0", "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.11.0.tgz", "integrity": "sha512-G/M/tIiMrTAxEWRfLfQJMmGNX28IxBg4PBz8XqQhqUHLFI6TL2htpIB1iQCj144V5ee/JaKyT9/WZ0MGZWfA7A==", - "dev": true, "license": "MIT", "engines": { "node": "^12.0.0 || ^14.0.0 || >=16.0.0" @@ -2931,7 +2928,6 @@ "version": "2.1.4", "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.4.tgz", "integrity": "sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ==", - "dev": true, "license": "MIT", "dependencies": { "ajv": "^6.12.4", @@ -2955,7 +2951,6 @@ "version": "4.3.6", "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.6.tgz", "integrity": "sha512-O/09Bd4Z1fBrU4VzkhFqVgpPzaGbw6Sm9FEkBT1A/YBXQFGuuSxa1dN2nxgxS34JmKXqYx8CZAwEVoJFImUXIg==", - "dev": true, "license": "MIT", "dependencies": { "ms": "2.1.2" @@ -2973,7 +2968,6 @@ "version": "13.24.0", "resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz", "integrity": "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==", - "dev": true, "license": "MIT", "dependencies": { "type-fest": "^0.20.2" @@ -2989,14 +2983,12 @@ "version": "2.1.2", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", - "dev": true, "license": "MIT" }, "node_modules/@eslint/eslintrc/node_modules/type-fest": { "version": "0.20.2", "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", - "dev": true, "license": "(MIT OR CC0-1.0)", "engines": { "node": ">=10" @@ -3009,7 +3001,6 @@ "version": "8.57.0", "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.57.0.tgz", "integrity": "sha512-Ys+3g2TaW7gADOJzPt83SJtCDhMjndcDMFVQ/Tj9iA1BfJzFKD9mAUXT3OenpuPHbI6P/myECxRJrofUsDx/5g==", - "dev": true, "license": "MIT", "engines": { "node": "^12.22.0 || ^14.17.0 || >=16.0.0" @@ -3020,7 +3011,6 @@ "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.14.tgz", "integrity": "sha512-3T8LkOmg45BV5FICb15QQMsyUSWrQ8AygVfC7ZG32zOalnqrilm018ZVCw0eapXux8FtA33q8PSRSstjee3jSg==", "deprecated": "Use @eslint/config-array instead", - "dev": true, "license": "Apache-2.0", "dependencies": { "@humanwhocodes/object-schema": "^2.0.2", @@ -3035,7 +3025,6 @@ "version": "4.3.6", "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.6.tgz", "integrity": "sha512-O/09Bd4Z1fBrU4VzkhFqVgpPzaGbw6Sm9FEkBT1A/YBXQFGuuSxa1dN2nxgxS34JmKXqYx8CZAwEVoJFImUXIg==", - "dev": true, "license": "MIT", "dependencies": { "ms": "2.1.2" @@ -3053,14 +3042,12 @@ "version": "2.1.2", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", - "dev": true, "license": "MIT" }, "node_modules/@humanwhocodes/module-importer": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", - "dev": true, "license": "Apache-2.0", "engines": { "node": ">=12.22" @@ -3075,7 +3062,6 @@ "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-2.0.3.tgz", "integrity": "sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA==", "deprecated": "Use @eslint/object-schema instead", - "dev": true, "license": "BSD-3-Clause" }, "node_modules/@isaacs/cliui": { @@ -3226,9 +3212,9 @@ } }, "node_modules/@istanbuljs/load-nyc-config/node_modules/js-yaml": { - "version": "3.14.1", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz", - "integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==", + "version": "3.14.2", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.2.tgz", + "integrity": "sha512-PMSmkqxr106Xa156c2M265Z+FTrPl+oxd/rgOQy2tijQeK5TxQ43psO1ZCwhVOSdnn+RzkzlRz/eY4BgJBYVpg==", "dev": true, "license": "MIT", "dependencies": { @@ -3967,7 +3953,6 @@ "version": "0.3.6", "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.6.tgz", "integrity": "sha512-1ZJTZebgqllO79ue2bm3rIGud/bOe0pP5BjSRCRxxYkEZS8STV7zN84UBbiYu7jy+eCKSnVIUgoWWE/tt+shMQ==", - "dev": true, "license": "MIT", "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", @@ -4029,7 +4014,6 @@ "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", - "dev": true, "license": "MIT", "dependencies": { "@nodelib/fs.stat": "2.0.5", @@ -4043,7 +4027,6 @@ "version": "2.0.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", - "dev": true, "license": "MIT", "engines": { "node": ">= 8" @@ -4053,7 +4036,6 @@ "version": "1.2.8", "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", - "dev": true, "license": "MIT", "dependencies": { "@nodelib/fs.scandir": "2.1.5", @@ -4124,9 +4106,9 @@ } }, "node_modules/@pmmmwh/react-refresh-webpack-plugin/node_modules/ajv": { - "version": "8.17.1", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", - "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.18.0.tgz", + "integrity": "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==", "dev": true, "license": "MIT", "dependencies": { @@ -4640,16 +4622,6 @@ "node": ">= 6" } }, - "node_modules/@trysound/sax": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/@trysound/sax/-/sax-0.2.0.tgz", - "integrity": "sha512-L7z9BgrNEcYyUYtF+HaEfiS5ebkh9jXqbszz7pC0hRBPaatV0XjSD3+eHrpqFemQfgwiFF0QPIarnIihIDn7OA==", - "dev": true, - "license": "ISC", - "engines": { - "node": ">=10.13.0" - } - }, "node_modules/@types/babel__core": { "version": "7.20.5", "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", @@ -4754,7 +4726,6 @@ "version": "8.56.11", "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-8.56.11.tgz", "integrity": "sha512-sVBpJMf7UPo/wGecYOpk2aQya2VUGeHhe38WG7/mN5FufNSubf5VT9Uh9Uyp8/eLJpu1/tuhJ/qTo4mhSB4V4Q==", - "dev": true, "license": "MIT", "dependencies": { "@types/estree": "*", @@ -4765,7 +4736,6 @@ "version": "3.7.7", "resolved": "https://registry.npmjs.org/@types/eslint-scope/-/eslint-scope-3.7.7.tgz", "integrity": "sha512-MzMFlSLBqNF2gcHWO0G1vP/YQyfvrxZ0bF+u7mzUdZ1/xK4A4sru+nraZz5i3iEIk1l1uyicaDVTB4QbbEkAYg==", - "dev": true, "license": "MIT", "dependencies": { "@types/eslint": "*", @@ -4776,7 +4746,6 @@ "version": "1.0.8", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", - "dev": true, "license": "MIT" }, "node_modules/@types/express": { @@ -4923,7 +4892,6 @@ "version": "22.2.0", "resolved": "https://registry.npmjs.org/@types/node/-/node-22.2.0.tgz", "integrity": "sha512-bm6EG6/pCpkxDf/0gDNDdtDILMOHgaQBVOJGdwsqClnxA3xL6jtMv76rLBc006RVMWbmaf0xbmom4Z/5o2nRkQ==", - "dev": true, "license": "MIT", "dependencies": { "undici-types": "~6.13.0" @@ -5517,14 +5485,12 @@ "version": "1.2.0", "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.2.0.tgz", "integrity": "sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==", - "dev": true, "license": "ISC" }, "node_modules/@webassemblyjs/ast": { "version": "1.14.1", "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.14.1.tgz", "integrity": "sha512-nuBEDgQfm1ccRp/8bCQrx1frohyufl4JlbMMZ4P1wpeOfDhF6FQkxZJ1b/e+PLwr6X1Nhw6OLme5usuBWYBvuQ==", - "dev": true, "license": "MIT", "dependencies": { "@webassemblyjs/helper-numbers": "1.13.2", @@ -5535,28 +5501,24 @@ "version": "1.13.2", "resolved": "https://registry.npmjs.org/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.13.2.tgz", "integrity": "sha512-6oXyTOzbKxGH4steLbLNOu71Oj+C8Lg34n6CqRvqfS2O71BxY6ByfMDRhBytzknj9yGUPVJ1qIKhRlAwO1AovA==", - "dev": true, "license": "MIT" }, "node_modules/@webassemblyjs/helper-api-error": { "version": "1.13.2", "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-api-error/-/helper-api-error-1.13.2.tgz", "integrity": "sha512-U56GMYxy4ZQCbDZd6JuvvNV/WFildOjsaWD3Tzzvmw/mas3cXzRJPMjP83JqEsgSbyrmaGjBfDtV7KDXV9UzFQ==", - "dev": true, "license": "MIT" }, "node_modules/@webassemblyjs/helper-buffer": { "version": "1.14.1", "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-buffer/-/helper-buffer-1.14.1.tgz", "integrity": "sha512-jyH7wtcHiKssDtFPRB+iQdxlDf96m0E39yb0k5uJVhFGleZFoNw1c4aeIcVUPPbXUVJ94wwnMOAqUHyzoEPVMA==", - "dev": true, "license": "MIT" }, "node_modules/@webassemblyjs/helper-numbers": { "version": "1.13.2", "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-numbers/-/helper-numbers-1.13.2.tgz", "integrity": "sha512-FE8aCmS5Q6eQYcV3gI35O4J789wlQA+7JrqTTpJqn5emA4U2hvwJmvFRC0HODS+3Ye6WioDklgd6scJ3+PLnEA==", - "dev": true, "license": "MIT", "dependencies": { "@webassemblyjs/floating-point-hex-parser": "1.13.2", @@ -5568,14 +5530,12 @@ "version": "1.13.2", "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.13.2.tgz", "integrity": "sha512-3QbLKy93F0EAIXLh0ogEVR6rOubA9AoZ+WRYhNbFyuB70j3dRdwH9g+qXhLAO0kiYGlg3TxDV+I4rQTr/YNXkA==", - "dev": true, "license": "MIT" }, "node_modules/@webassemblyjs/helper-wasm-section": { "version": "1.14.1", "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.14.1.tgz", "integrity": "sha512-ds5mXEqTJ6oxRoqjhWDU83OgzAYjwsCV8Lo/N+oRsNDmx/ZDpqalmrtgOMkHwxsG0iI//3BwWAErYRHtgn0dZw==", - "dev": true, "license": "MIT", "dependencies": { "@webassemblyjs/ast": "1.14.1", @@ -5588,7 +5548,6 @@ "version": "1.13.2", "resolved": "https://registry.npmjs.org/@webassemblyjs/ieee754/-/ieee754-1.13.2.tgz", "integrity": "sha512-4LtOzh58S/5lX4ITKxnAK2USuNEvpdVV9AlgGQb8rJDHaLeHciwG4zlGr0j/SNWlr7x3vO1lDEsuePvtcDNCkw==", - "dev": true, "license": "MIT", "dependencies": { "@xtuc/ieee754": "^1.2.0" @@ -5598,7 +5557,6 @@ "version": "1.13.2", "resolved": "https://registry.npmjs.org/@webassemblyjs/leb128/-/leb128-1.13.2.tgz", "integrity": "sha512-Lde1oNoIdzVzdkNEAWZ1dZ5orIbff80YPdHx20mrHwHrVNNTjNr8E3xz9BdpcGqRQbAEa+fkrCb+fRFTl/6sQw==", - "dev": true, "license": "Apache-2.0", "dependencies": { "@xtuc/long": "4.2.2" @@ -5608,14 +5566,12 @@ "version": "1.13.2", "resolved": "https://registry.npmjs.org/@webassemblyjs/utf8/-/utf8-1.13.2.tgz", "integrity": "sha512-3NQWGjKTASY1xV5m7Hr0iPeXD9+RDobLll3T9d2AO+g3my8xy5peVyjSag4I50mR1bBSN/Ct12lo+R9tJk0NZQ==", - "dev": true, "license": "MIT" }, "node_modules/@webassemblyjs/wasm-edit": { "version": "1.14.1", "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-edit/-/wasm-edit-1.14.1.tgz", "integrity": "sha512-RNJUIQH/J8iA/1NzlE4N7KtyZNHi3w7at7hDjvRNm5rcUXa00z1vRz3glZoULfJ5mpvYhLybmVcwcjGrC1pRrQ==", - "dev": true, "license": "MIT", "dependencies": { "@webassemblyjs/ast": "1.14.1", @@ -5632,7 +5588,6 @@ "version": "1.14.1", "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-gen/-/wasm-gen-1.14.1.tgz", "integrity": "sha512-AmomSIjP8ZbfGQhumkNvgC33AY7qtMCXnN6bL2u2Js4gVCg8fp735aEiMSBbDR7UQIj90n4wKAFUSEd0QN2Ukg==", - "dev": true, "license": "MIT", "dependencies": { "@webassemblyjs/ast": "1.14.1", @@ -5646,7 +5601,6 @@ "version": "1.14.1", "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-opt/-/wasm-opt-1.14.1.tgz", "integrity": "sha512-PTcKLUNvBqnY2U6E5bdOQcSM+oVP/PmrDY9NzowJjislEjwP/C4an2303MCVS2Mg9d3AJpIGdUFIQQWbPds0Sw==", - "dev": true, "license": "MIT", "dependencies": { "@webassemblyjs/ast": "1.14.1", @@ -5659,7 +5613,6 @@ "version": "1.14.1", "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-parser/-/wasm-parser-1.14.1.tgz", "integrity": "sha512-JLBl+KZ0R5qB7mCnud/yyX08jWFw5MsoalJ1pQ4EdFlgj9VdXKGuENGsiCIjegI1W7p91rUlcB/LB5yRJKNTcQ==", - "dev": true, "license": "MIT", "dependencies": { "@webassemblyjs/ast": "1.14.1", @@ -5674,7 +5627,6 @@ "version": "1.14.1", "resolved": "https://registry.npmjs.org/@webassemblyjs/wast-printer/-/wast-printer-1.14.1.tgz", "integrity": "sha512-kPSSXE6De1XOR820C90RIo2ogvZG+c3KiHzqUoO/F34Y2shGzesfqv7o57xrxovZJH/MetF5UjroJ/R/3isoiw==", - "dev": true, "license": "MIT", "dependencies": { "@webassemblyjs/ast": "1.14.1", @@ -5685,14 +5637,12 @@ "version": "1.2.0", "resolved": "https://registry.npmjs.org/@xtuc/ieee754/-/ieee754-1.2.0.tgz", "integrity": "sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA==", - "dev": true, "license": "BSD-3-Clause" }, "node_modules/@xtuc/long": { "version": "4.2.2", "resolved": "https://registry.npmjs.org/@xtuc/long/-/long-4.2.2.tgz", "integrity": "sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==", - "dev": true, "license": "Apache-2.0" }, "node_modules/abab": { @@ -5733,7 +5683,6 @@ "version": "8.15.0", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", - "dev": true, "license": "MIT", "bin": { "acorn": "bin/acorn" @@ -5770,7 +5719,6 @@ "version": "1.0.4", "resolved": "https://registry.npmjs.org/acorn-import-phases/-/acorn-import-phases-1.0.4.tgz", "integrity": "sha512-wKmbr/DDiIXzEOiWrTTUcDm24kQ2vGfZQvM2fwg2vXqR5uW6aapr7ObPtj1th32b9u90/Pf4AItvdTh42fBmVQ==", - "dev": true, "license": "MIT", "engines": { "node": ">=10.13.0" @@ -5783,7 +5731,6 @@ "version": "5.3.2", "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", - "dev": true, "license": "MIT", "peerDependencies": { "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" @@ -5894,9 +5841,9 @@ } }, "node_modules/ajv": { - "version": "6.12.6", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", - "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "version": "6.14.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.14.0.tgz", + "integrity": "sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw==", "license": "MIT", "dependencies": { "fast-deep-equal": "^3.1.1", @@ -5913,7 +5860,6 @@ "version": "2.1.1", "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-2.1.1.tgz", "integrity": "sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==", - "dev": true, "license": "MIT", "dependencies": { "ajv": "^8.0.0" @@ -5928,10 +5874,9 @@ } }, "node_modules/ajv-formats/node_modules/ajv": { - "version": "8.17.1", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", - "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", - "dev": true, + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.18.0.tgz", + "integrity": "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==", "license": "MIT", "dependencies": { "fast-deep-equal": "^3.1.3", @@ -5948,7 +5893,6 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", - "dev": true, "license": "MIT" }, "node_modules/ajv-keywords": { @@ -6067,7 +6011,6 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", - "dev": true, "license": "Python-2.0" }, "node_modules/aria-query": { @@ -6296,9 +6239,9 @@ } }, "node_modules/asn1.js/node_modules/bn.js": { - "version": "4.12.0", - "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.0.tgz", - "integrity": "sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA==", + "version": "4.12.3", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.3.tgz", + "integrity": "sha512-fGTi3gxV/23FTYdAoUtLYp6qySe2KE3teyZitipKNRuVYcBkoP/bB3guXN/XVKUe9mxCHXnc9C4ocyz8OmgN0g==", "license": "MIT" }, "node_modules/assert": { @@ -6342,7 +6285,6 @@ "version": "0.4.0", "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", - "dev": true, "license": "MIT" }, "node_modules/at-least-node": { @@ -6438,12 +6380,30 @@ } }, "node_modules/axios": { - "version": "0.21.4", - "resolved": "https://registry.npmjs.org/axios/-/axios-0.21.4.tgz", - "integrity": "sha512-ut5vewkiu8jjGBdqpM44XxjuCjq9LAKeHVmoVfHVzy8eHgxxq8SbAVQNovDA8mVi05kP0Ea/n/UzcSHcTJQfNg==", + "version": "1.13.6", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.13.6.tgz", + "integrity": "sha512-ChTCHMouEe2kn713WHbQGcuYrr6fXTBiu460OTwWrWob16g1bXn4vtz07Ope7ewMozJAnEquLk5lWQWtBig9DQ==", "license": "MIT", "dependencies": { - "follow-redirects": "^1.14.0" + "follow-redirects": "^1.15.11", + "form-data": "^4.0.5", + "proxy-from-env": "^1.1.0" + } + }, + "node_modules/axios/node_modules/form-data": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz", + "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==", + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.2", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" } }, "node_modules/axobject-query": { @@ -7162,7 +7122,6 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", - "dev": true, "license": "MIT" }, "node_modules/base64-js": { @@ -7248,46 +7207,36 @@ "license": "MIT" }, "node_modules/bn.js": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-5.2.1.tgz", - "integrity": "sha512-eXRvHzWyYPBuB4NBy0cmYQjGitUrtqwbvlzP3G6VFnNRbsZQIxQ10PbKKHt8gZ/HW/D/747aDl+QkDqg3KQLMQ==", + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-5.2.3.tgz", + "integrity": "sha512-EAcmnPkxpntVL+DS7bO1zhcZNvCkxqtkd0ZY53h06GNQ3DEkkGZ/gKgmDv6DdZQGj9BgfSPKtJJ7Dp1GPP8f7w==", "license": "MIT" }, "node_modules/body-parser": { - "version": "1.20.2", - "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.2.tgz", - "integrity": "sha512-ml9pReCu3M61kGlqoTm2umSXTlRTuGTx0bfYj+uIUKKYycG5NtSbeetV3faSU6R7ajOPw0g/J1PvK4qNy7s5bA==", + "version": "1.20.4", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.4.tgz", + "integrity": "sha512-ZTgYYLMOXY9qKU/57FAo8F+HA2dGX7bqGc71txDRC1rS4frdFI5R7NhluHxH6M0YItAP0sHB4uqAOcYKxO6uGA==", "dev": true, "license": "MIT", "dependencies": { - "bytes": "3.1.2", + "bytes": "~3.1.2", "content-type": "~1.0.5", "debug": "2.6.9", "depd": "2.0.0", - "destroy": "1.2.0", - "http-errors": "2.0.0", - "iconv-lite": "0.4.24", - "on-finished": "2.4.1", - "qs": "6.11.0", - "raw-body": "2.5.2", + "destroy": "~1.2.0", + "http-errors": "~2.0.1", + "iconv-lite": "~0.4.24", + "on-finished": "~2.4.1", + "qs": "~6.14.0", + "raw-body": "~2.5.3", "type-is": "~1.6.18", - "unpipe": "1.0.0" + "unpipe": "~1.0.0" }, "engines": { "node": ">= 0.8", "npm": "1.2.8000 || >= 1.4.16" } }, - "node_modules/body-parser/node_modules/bytes": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", - "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, "node_modules/body-parser/node_modules/iconv-lite": { "version": "0.4.24", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", @@ -7302,13 +7251,13 @@ } }, "node_modules/body-parser/node_modules/qs": { - "version": "6.11.0", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.11.0.tgz", - "integrity": "sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==", + "version": "6.14.2", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.2.tgz", + "integrity": "sha512-V/yCWTTF7VJ9hIh18Ugr2zhJMP01MY7c5kh4J870L7imm6/DIzBsNLTXzMwUA3yZ5b/KBqLx8Kp3uRvd7xSe3Q==", "dev": true, "license": "BSD-3-Clause", "dependencies": { - "side-channel": "^1.0.4" + "side-channel": "^1.1.0" }, "engines": { "node": ">=0.6" @@ -7358,7 +7307,6 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", - "dev": true, "license": "MIT", "dependencies": { "balanced-match": "^1.0.0" @@ -7428,34 +7376,37 @@ } }, "node_modules/browserify-rsa": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/browserify-rsa/-/browserify-rsa-4.1.0.tgz", - "integrity": "sha512-AdEER0Hkspgno2aR97SAf6vi0y0k8NuOpGnVH3O99rcA5Q6sh8QxcngtHuJ6uXwnfAXNM4Gn1Gb7/MV1+Ymbog==", + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/browserify-rsa/-/browserify-rsa-4.1.1.tgz", + "integrity": "sha512-YBjSAiTqM04ZVei6sXighu679a3SqWORA3qZTEqZImnlkDIFtKc6pNutpjyZ8RJTjQtuYfeetkxM11GwoYXMIQ==", "license": "MIT", "dependencies": { - "bn.js": "^5.0.0", - "randombytes": "^2.0.1" + "bn.js": "^5.2.1", + "randombytes": "^2.1.0", + "safe-buffer": "^5.2.1" + }, + "engines": { + "node": ">= 0.10" } }, "node_modules/browserify-sign": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/browserify-sign/-/browserify-sign-4.2.3.tgz", - "integrity": "sha512-JWCZW6SKhfhjJxO8Tyiiy+XYB7cqd2S5/+WeYHsKdNKFlCBhKbblba1A/HN/90YwtxKc8tCErjffZl++UNmGiw==", + "version": "4.2.5", + "resolved": "https://registry.npmjs.org/browserify-sign/-/browserify-sign-4.2.5.tgz", + "integrity": "sha512-C2AUdAJg6rlM2W5QMp2Q4KGQMVBwR1lIimTsUnutJ8bMpW5B52pGpR2gEnNBNwijumDo5FojQ0L9JrXA8m4YEw==", "license": "ISC", "dependencies": { - "bn.js": "^5.2.1", - "browserify-rsa": "^4.1.0", + "bn.js": "^5.2.2", + "browserify-rsa": "^4.1.1", "create-hash": "^1.2.0", "create-hmac": "^1.1.7", - "elliptic": "^6.5.5", - "hash-base": "~3.0", + "elliptic": "^6.6.1", "inherits": "^2.0.4", - "parse-asn1": "^5.1.7", + "parse-asn1": "^5.1.9", "readable-stream": "^2.3.8", "safe-buffer": "^5.2.1" }, "engines": { - "node": ">= 0.12" + "node": ">= 0.10" } }, "node_modules/browserify-sign/node_modules/isarray": { @@ -7596,7 +7547,6 @@ "version": "1.1.2", "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", - "dev": true, "license": "MIT" }, "node_modules/buffer-xor": { @@ -7625,9 +7575,9 @@ "license": "MIT" }, "node_modules/bytes": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.0.0.tgz", - "integrity": "sha512-pMhOfFDPiv9t5jjIXkHosWmkSyQbvsgEVNkz0ERHbuLh2T/7j4Mqqpz523Fe8MVY89KC6Sh/QfS2sM+SjgFDcw==", + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", "dev": true, "license": "MIT", "engines": { @@ -7921,7 +7871,6 @@ "version": "1.0.4", "resolved": "https://registry.npmjs.org/chrome-trace-event/-/chrome-trace-event-1.0.4.tgz", "integrity": "sha512-rNjApaLzuwaOTjCiT8lSDdGN1APCiqkChLMJxJPWLunPAt5fy8xgU9/jNOchV84wfIxrA0lRQB7oCT8jrn/wrQ==", - "dev": true, "license": "MIT", "engines": { "node": ">=6.0" @@ -7951,6 +7900,16 @@ "node": ">=10" } }, + "node_modules/chromedriver/node_modules/axios": { + "version": "0.21.4", + "resolved": "https://registry.npmjs.org/axios/-/axios-0.21.4.tgz", + "integrity": "sha512-ut5vewkiu8jjGBdqpM44XxjuCjq9LAKeHVmoVfHVzy8eHgxxq8SbAVQNovDA8mVi05kP0Ea/n/UzcSHcTJQfNg==", + "dev": true, + "license": "MIT", + "dependencies": { + "follow-redirects": "^1.14.0" + } + }, "node_modules/ci-info": { "version": "3.9.0", "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.9.0.tgz", @@ -8192,7 +8151,6 @@ "version": "1.0.8", "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", - "dev": true, "license": "MIT", "dependencies": { "delayed-stream": "~1.0.0" @@ -8242,30 +8200,33 @@ } }, "node_modules/compression": { - "version": "1.7.4", - "resolved": "https://registry.npmjs.org/compression/-/compression-1.7.4.tgz", - "integrity": "sha512-jaSIDzP9pZVS4ZfQ+TzvtiWhdpFhE2RDHz8QJkpX9SIpLq88VueF5jJw6t+6CUQcAoA6t+x89MLrWAqpfDE8iQ==", + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/compression/-/compression-1.8.1.tgz", + "integrity": "sha512-9mAqGPHLakhCLeNyxPkK4xVo746zQ/czLH1Ky+vkitMnWfWZps8r0qXuwhwizagCRttsL4lfG4pIOvaWLpAP0w==", "dev": true, "license": "MIT", "dependencies": { - "accepts": "~1.3.5", - "bytes": "3.0.0", - "compressible": "~2.0.16", + "bytes": "3.1.2", + "compressible": "~2.0.18", "debug": "2.6.9", - "on-headers": "~1.0.2", - "safe-buffer": "5.1.2", + "negotiator": "~0.6.4", + "on-headers": "~1.1.0", + "safe-buffer": "5.2.1", "vary": "~1.1.2" }, "engines": { "node": ">= 0.8.0" } }, - "node_modules/compression/node_modules/safe-buffer": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "node_modules/compression/node_modules/negotiator": { + "version": "0.6.4", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.4.tgz", + "integrity": "sha512-myRT3DiWPHqho5PrJaIRyaMv2kgYf0mUVgBNOYMuCH5Ki1yEiQaf/ZJuQ62nvpc44wL5WDbTX7yGJi1Neevw8w==", "dev": true, - "license": "MIT" + "license": "MIT", + "engines": { + "node": ">= 0.6" + } }, "node_modules/confusing-browser-globals": { "version": "1.0.11", @@ -8325,9 +8286,9 @@ "license": "MIT" }, "node_modules/cookie": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.6.0.tgz", - "integrity": "sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==", + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", "dev": true, "license": "MIT", "engines": { @@ -8458,9 +8419,9 @@ } }, "node_modules/create-ecdh/node_modules/bn.js": { - "version": "4.12.0", - "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.0.tgz", - "integrity": "sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA==", + "version": "4.12.3", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.3.tgz", + "integrity": "sha512-fGTi3gxV/23FTYdAoUtLYp6qySe2KE3teyZitipKNRuVYcBkoP/bB3guXN/XVKUe9mxCHXnc9C4ocyz8OmgN0g==", "license": "MIT" }, "node_modules/create-hash": { @@ -8494,7 +8455,6 @@ "version": "7.0.5", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.5.tgz", "integrity": "sha512-ZVJrKKYunU38/76t0RMOulHOnUcbU9GbpWKAOZ0mhjr7CX6FVrH+4FrAapSOekrgFQ3f/8gwMEuIft0aKq6Hug==", - "dev": true, "license": "MIT", "dependencies": { "path-key": "^3.1.0", @@ -8506,25 +8466,29 @@ } }, "node_modules/crypto-browserify": { - "version": "3.12.0", - "resolved": "https://registry.npmjs.org/crypto-browserify/-/crypto-browserify-3.12.0.tgz", - "integrity": "sha512-fz4spIh+znjO2VjL+IdhEpRJ3YN6sMzITSBijk6FK2UvTqruSQW+/cCZTSNsMiZNvUeq0CqurF+dAbyiGOY6Wg==", + "version": "3.12.1", + "resolved": "https://registry.npmjs.org/crypto-browserify/-/crypto-browserify-3.12.1.tgz", + "integrity": "sha512-r4ESw/IlusD17lgQi1O20Fa3qNnsckR126TdUuBgAu7GBYSIPvdNyONd3Zrxh0xCwA4+6w/TDArBPsMvhur+KQ==", "license": "MIT", "dependencies": { - "browserify-cipher": "^1.0.0", - "browserify-sign": "^4.0.0", - "create-ecdh": "^4.0.0", - "create-hash": "^1.1.0", - "create-hmac": "^1.1.0", - "diffie-hellman": "^5.0.0", - "inherits": "^2.0.1", - "pbkdf2": "^3.0.3", - "public-encrypt": "^4.0.0", - "randombytes": "^2.0.0", - "randomfill": "^1.0.3" + "browserify-cipher": "^1.0.1", + "browserify-sign": "^4.2.3", + "create-ecdh": "^4.0.4", + "create-hash": "^1.2.0", + "create-hmac": "^1.1.7", + "diffie-hellman": "^5.0.3", + "hash-base": "~3.0.4", + "inherits": "^2.0.4", + "pbkdf2": "^3.1.2", + "public-encrypt": "^4.0.3", + "randombytes": "^2.1.0", + "randomfill": "^1.0.4" }, "engines": { - "node": "*" + "node": ">= 0.10" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, "node_modules/crypto-random-string": { @@ -8699,9 +8663,9 @@ } }, "node_modules/css-minimizer-webpack-plugin/node_modules/ajv": { - "version": "8.17.1", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", - "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.18.0.tgz", + "integrity": "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==", "dev": true, "license": "MIT", "dependencies": { @@ -9312,7 +9276,6 @@ "version": "0.1.4", "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", - "dev": true, "license": "MIT" }, "node_modules/deepmerge": { @@ -9419,7 +9382,6 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", - "dev": true, "license": "MIT", "engines": { "node": ">=0.4.0" @@ -9535,9 +9497,9 @@ } }, "node_modules/diffie-hellman/node_modules/bn.js": { - "version": "4.12.0", - "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.0.tgz", - "integrity": "sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA==", + "version": "4.12.3", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.3.tgz", + "integrity": "sha512-fGTi3gxV/23FTYdAoUtLYp6qySe2KE3teyZitipKNRuVYcBkoP/bB3guXN/XVKUe9mxCHXnc9C4ocyz8OmgN0g==", "license": "MIT" }, "node_modules/dir-glob": { @@ -9577,7 +9539,6 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==", - "dev": true, "license": "Apache-2.0", "dependencies": { "esutils": "^2.0.2" @@ -9815,9 +9776,9 @@ } }, "node_modules/elliptic/node_modules/bn.js": { - "version": "4.12.0", - "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.0.tgz", - "integrity": "sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA==", + "version": "4.12.3", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.3.tgz", + "integrity": "sha512-fGTi3gxV/23FTYdAoUtLYp6qySe2KE3teyZitipKNRuVYcBkoP/bB3guXN/XVKUe9mxCHXnc9C4ocyz8OmgN0g==", "license": "MIT" }, "node_modules/emittery": { @@ -9850,9 +9811,9 @@ } }, "node_modules/encodeurl": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", - "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", "dev": true, "license": "MIT", "engines": { @@ -9873,7 +9834,6 @@ "version": "5.18.3", "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.18.3.tgz", "integrity": "sha512-d4lC8xfavMeBjzGr2vECC3fsGXziXZQyJxD868h2M/mBI3PwAuODxAkLkq5HYuvrPYcUtiLzsTo8U3PgX3Ocww==", - "dev": true, "license": "MIT", "dependencies": { "graceful-fs": "^4.2.4", @@ -10052,7 +10012,6 @@ "version": "1.5.4", "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.5.4.tgz", "integrity": "sha512-MVNK56NiMrOwitFB7cqDwq0CQutbw+0BvLshJSse0MUNU+y1FC3bUS/AQg7oUng+/wKrrki7JfmwtVHkVfPLlw==", - "dev": true, "license": "MIT" }, "node_modules/es-object-atoms": { @@ -10068,15 +10027,15 @@ } }, "node_modules/es-set-tostringtag": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.0.3.tgz", - "integrity": "sha512-3T8uNMC3OQTHkFUsFq8r/BwAXLHvU/9O9mE0fBc/MY5iq/8H7ncvO947LmYA6ldWw9Uh8Yhf25zu6n7nML5QWQ==", - "dev": true, + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", "license": "MIT", "dependencies": { - "get-intrinsic": "^1.2.4", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", "has-tostringtag": "^1.0.2", - "hasown": "^2.0.1" + "hasown": "^2.0.2" }, "engines": { "node": ">= 0.4" @@ -10171,7 +10130,6 @@ "version": "8.57.0", "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.57.0.tgz", "integrity": "sha512-dZ6+mexnaTIbSBZWgou51U6OmzIhYM2VcNdtiTtI7qPNZm35Akpr0f6vtw3w1Kmn5PYo+tZVfh13WrhpS6oLqQ==", - "dev": true, "license": "MIT", "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", @@ -10605,9 +10563,9 @@ } }, "node_modules/eslint-webpack-plugin/node_modules/ajv": { - "version": "8.17.1", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", - "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.18.0.tgz", + "integrity": "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==", "dev": true, "license": "MIT", "dependencies": { @@ -10696,7 +10654,6 @@ "version": "5.0.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -10706,7 +10663,6 @@ "version": "4.3.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, "license": "MIT", "dependencies": { "color-convert": "^2.0.1" @@ -10722,7 +10678,6 @@ "version": "4.1.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, "license": "MIT", "dependencies": { "ansi-styles": "^4.1.0", @@ -10739,7 +10694,6 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, "license": "MIT", "dependencies": { "color-name": "~1.1.4" @@ -10752,14 +10706,12 @@ "version": "1.1.4", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true, "license": "MIT" }, "node_modules/eslint/node_modules/debug": { "version": "4.3.6", "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.6.tgz", "integrity": "sha512-O/09Bd4Z1fBrU4VzkhFqVgpPzaGbw6Sm9FEkBT1A/YBXQFGuuSxa1dN2nxgxS34JmKXqYx8CZAwEVoJFImUXIg==", - "dev": true, "license": "MIT", "dependencies": { "ms": "2.1.2" @@ -10777,7 +10729,6 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", - "dev": true, "license": "MIT", "engines": { "node": ">=10" @@ -10790,7 +10741,6 @@ "version": "7.2.2", "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.2.tgz", "integrity": "sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==", - "dev": true, "license": "BSD-2-Clause", "dependencies": { "esrecurse": "^4.3.0", @@ -10807,7 +10757,6 @@ "version": "3.4.3", "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", - "dev": true, "license": "Apache-2.0", "engines": { "node": "^12.22.0 || ^14.17.0 || >=16.0.0" @@ -10820,7 +10769,6 @@ "version": "5.3.0", "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", - "dev": true, "license": "BSD-2-Clause", "engines": { "node": ">=4.0" @@ -10830,7 +10778,6 @@ "version": "13.24.0", "resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz", "integrity": "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==", - "dev": true, "license": "MIT", "dependencies": { "type-fest": "^0.20.2" @@ -10846,14 +10793,12 @@ "version": "2.1.2", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", - "dev": true, "license": "MIT" }, "node_modules/eslint/node_modules/strip-ansi": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, "license": "MIT", "dependencies": { "ansi-regex": "^5.0.1" @@ -10866,7 +10811,6 @@ "version": "7.2.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, "license": "MIT", "dependencies": { "has-flag": "^4.0.0" @@ -10879,7 +10823,6 @@ "version": "0.20.2", "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", - "dev": true, "license": "(MIT OR CC0-1.0)", "engines": { "node": ">=10" @@ -10892,7 +10835,6 @@ "version": "9.6.1", "resolved": "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz", "integrity": "sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==", - "dev": true, "license": "BSD-2-Clause", "dependencies": { "acorn": "^8.9.0", @@ -10910,7 +10852,6 @@ "version": "3.4.3", "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", - "dev": true, "license": "Apache-2.0", "engines": { "node": "^12.22.0 || ^14.17.0 || >=16.0.0" @@ -10937,7 +10878,6 @@ "version": "1.6.0", "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.6.0.tgz", "integrity": "sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==", - "dev": true, "license": "BSD-3-Clause", "dependencies": { "estraverse": "^5.1.0" @@ -10950,7 +10890,6 @@ "version": "5.3.0", "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", - "dev": true, "license": "BSD-2-Clause", "engines": { "node": ">=4.0" @@ -10997,7 +10936,6 @@ "version": "2.0.3", "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", - "dev": true, "license": "BSD-2-Clause", "engines": { "node": ">=0.10.0" @@ -11117,63 +11055,67 @@ } }, "node_modules/express": { - "version": "4.19.2", - "resolved": "https://registry.npmjs.org/express/-/express-4.19.2.tgz", - "integrity": "sha512-5T6nhjsT+EOMzuck8JjBHARTHfMht0POzlA60WV2pMD3gyXw2LZnZ+ueGdNxG+0calOJcWKbpFcuzLZ91YWq9Q==", + "version": "4.22.1", + "resolved": "https://registry.npmjs.org/express/-/express-4.22.1.tgz", + "integrity": "sha512-F2X8g9P1X7uCPZMA3MVf9wcTqlyNp7IhH5qPCI0izhaOIYXaW9L535tGA3qmjRzpH+bZczqq7hVKxTR4NWnu+g==", "dev": true, "license": "MIT", "dependencies": { "accepts": "~1.3.8", "array-flatten": "1.1.1", - "body-parser": "1.20.2", - "content-disposition": "0.5.4", + "body-parser": "~1.20.3", + "content-disposition": "~0.5.4", "content-type": "~1.0.4", - "cookie": "0.6.0", - "cookie-signature": "1.0.6", + "cookie": "~0.7.1", + "cookie-signature": "~1.0.6", "debug": "2.6.9", "depd": "2.0.0", - "encodeurl": "~1.0.2", + "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "etag": "~1.8.1", - "finalhandler": "1.2.0", - "fresh": "0.5.2", - "http-errors": "2.0.0", - "merge-descriptors": "1.0.1", + "finalhandler": "~1.3.1", + "fresh": "~0.5.2", + "http-errors": "~2.0.0", + "merge-descriptors": "1.0.3", "methods": "~1.1.2", - "on-finished": "2.4.1", + "on-finished": "~2.4.1", "parseurl": "~1.3.3", - "path-to-regexp": "0.1.7", + "path-to-regexp": "~0.1.12", "proxy-addr": "~2.0.7", - "qs": "6.11.0", + "qs": "~6.14.0", "range-parser": "~1.2.1", "safe-buffer": "5.2.1", - "send": "0.18.0", - "serve-static": "1.15.0", + "send": "~0.19.0", + "serve-static": "~1.16.2", "setprototypeof": "1.2.0", - "statuses": "2.0.1", + "statuses": "~2.0.1", "type-is": "~1.6.18", "utils-merge": "1.0.1", "vary": "~1.1.2" }, "engines": { "node": ">= 0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, "node_modules/express/node_modules/path-to-regexp": { - "version": "0.1.7", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz", - "integrity": "sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ==", + "version": "0.1.12", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.12.tgz", + "integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==", "dev": true, "license": "MIT" }, "node_modules/express/node_modules/qs": { - "version": "6.11.0", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.11.0.tgz", - "integrity": "sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==", + "version": "6.14.2", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.2.tgz", + "integrity": "sha512-V/yCWTTF7VJ9hIh18Ugr2zhJMP01MY7c5kh4J870L7imm6/DIzBsNLTXzMwUA3yZ5b/KBqLx8Kp3uRvd7xSe3Q==", "dev": true, "license": "BSD-3-Clause", "dependencies": { - "side-channel": "^1.0.4" + "side-channel": "^1.1.0" }, "engines": { "node": ">=0.6" @@ -11274,7 +11216,6 @@ "version": "2.0.6", "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", - "dev": true, "license": "MIT" }, "node_modules/fast-memoize": { @@ -11287,14 +11228,12 @@ "version": "3.0.1", "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.0.1.tgz", "integrity": "sha512-MWipKbbYiYI0UC7cl8m/i/IWTqfC8YXsqjzybjddLsFjStroQzsHXkc73JutMvBiXmOvapk+axIl79ig5t55Bw==", - "dev": true, "license": "MIT" }, "node_modules/fastq": { "version": "1.17.1", "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.17.1.tgz", "integrity": "sha512-sRVD3lWVIXWg6By68ZN7vho9a1pQcN/WBFaAAsDDFzlJjvoGx0P8z7V1t72grFJfJhu3YPZBuu25f7Kaw2jN1w==", - "dev": true, "license": "ISC", "dependencies": { "reusify": "^1.0.4" @@ -11337,7 +11276,6 @@ "version": "6.0.1", "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", "integrity": "sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==", - "dev": true, "license": "MIT", "dependencies": { "flat-cache": "^3.0.4" @@ -11397,9 +11335,9 @@ } }, "node_modules/filelist/node_modules/minimatch": { - "version": "5.1.6", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", - "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", + "version": "5.1.9", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.9.tgz", + "integrity": "sha512-7o1wEA2RyMP7Iu7GNba9vc0RWWGACJOCZBJX2GJWip0ikV+wcOsgVuY9uE8CPiyQhkGFSlhuSkZPavN7u1c2Fw==", "dev": true, "license": "ISC", "dependencies": { @@ -11442,18 +11380,18 @@ } }, "node_modules/finalhandler": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.2.0.tgz", - "integrity": "sha512-5uXcUVftlQMFnWC9qu/svkWv3GTd2PfUhK/3PLkYNAe7FbqJMt3515HaxE6eRL74GdsriiwujiawdaB1BpEISg==", + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.2.tgz", + "integrity": "sha512-aA4RyPcd3badbdABGDuTXCMTtOneUCAYH/gxoYRTZlIJdF0YPWuGqiAsIrhNnnqdXGswYk6dGujem4w80UJFhg==", "dev": true, "license": "MIT", "dependencies": { "debug": "2.6.9", - "encodeurl": "~1.0.2", + "encodeurl": "~2.0.0", "escape-html": "~1.0.3", - "on-finished": "2.4.1", + "on-finished": "~2.4.1", "parseurl": "~1.3.3", - "statuses": "2.0.1", + "statuses": "~2.0.2", "unpipe": "~1.0.0" }, "engines": { @@ -11488,7 +11426,6 @@ "version": "5.0.0", "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", - "dev": true, "license": "MIT", "dependencies": { "locate-path": "^6.0.0", @@ -11505,7 +11442,6 @@ "version": "3.2.0", "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.2.0.tgz", "integrity": "sha512-CYcENa+FtcUKLmhhqyctpclsq7QF38pKjZHsGNiSQF5r4FtoKDWabFDl3hzaEQMvT1LHEysw5twgLvpYYb4vbw==", - "dev": true, "license": "MIT", "dependencies": { "flatted": "^3.2.9", @@ -11517,16 +11453,15 @@ } }, "node_modules/flatted": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.1.tgz", - "integrity": "sha512-X8cqMLLie7KsNUDSdzeN8FYK9rEt4Dt67OsG/DNGnYTSDBG4uFAJFBnUeiV+zCVAvwFy56IjM9sH51jVaEhNxw==", - "dev": true, + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.2.tgz", + "integrity": "sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==", "license": "ISC" }, "node_modules/follow-redirects": { - "version": "1.15.6", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.6.tgz", - "integrity": "sha512-wWN62YITEaOpSK584EZXJafH1AGpO8RVgElfkuXbTOrPX4fIfOyEpW/CsiNd8JdYrAoOvafRTOEnvsO++qCqFA==", + "version": "1.15.11", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz", + "integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==", "funding": [ { "type": "individual", @@ -11779,15 +11714,17 @@ } }, "node_modules/form-data": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-3.0.1.tgz", - "integrity": "sha512-RHkBKtLWUVwd7SqRIvCZMEvAMoGUp0XU+seQiZejj0COz3RI3hWP4sCv3gZWWLjJTd7rGwcsF5eKZGii0r/hbg==", + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-3.0.4.tgz", + "integrity": "sha512-f0cRzm6dkyVYV3nPoooP8XlccPQukegwhAnpoLcXy+X+A8KfpGOoXwDr9FLZd3wzgLaBGQBE3lY93Zm/i1JvIQ==", "dev": true, "license": "MIT", "dependencies": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", - "mime-types": "^2.1.12" + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.2", + "mime-types": "^2.1.35" }, "engines": { "node": ">= 6" @@ -11872,7 +11809,6 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", - "dev": true, "license": "ISC" }, "node_modules/fsevents": { @@ -12093,7 +12029,6 @@ "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", "deprecated": "Glob versions prior to v9 are no longer supported", - "dev": true, "license": "ISC", "dependencies": { "fs.realpath": "^1.0.0", @@ -12114,7 +12049,6 @@ "version": "6.0.2", "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", - "dev": true, "license": "ISC", "dependencies": { "is-glob": "^4.0.3" @@ -12127,7 +12061,6 @@ "version": "0.4.1", "resolved": "https://registry.npmjs.org/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz", "integrity": "sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==", - "dev": true, "license": "BSD-2-Clause" }, "node_modules/global-modules": { @@ -12261,14 +12194,12 @@ "version": "4.2.11", "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", - "dev": true, "license": "ISC" }, "node_modules/graphemer": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", - "dev": true, "license": "MIT" }, "node_modules/gzip-size": { @@ -12337,7 +12268,6 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -12688,20 +12618,24 @@ "license": "MIT" }, "node_modules/http-errors": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", - "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", + "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", "dev": true, "license": "MIT", "dependencies": { - "depd": "2.0.0", - "inherits": "2.0.4", - "setprototypeof": "1.2.0", - "statuses": "2.0.1", - "toidentifier": "1.0.1" + "depd": "~2.0.0", + "inherits": "~2.0.4", + "setprototypeof": "~1.2.0", + "statuses": "~2.0.2", + "toidentifier": "~1.0.1" }, "engines": { "node": ">= 0.8" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, "node_modules/http-parser-js": { @@ -12767,9 +12701,9 @@ "license": "MIT" }, "node_modules/http-proxy-middleware": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/http-proxy-middleware/-/http-proxy-middleware-2.0.6.tgz", - "integrity": "sha512-ya/UeJ6HVBYxrgYotAZo1KvPWlgB48kUJLDePFeneHsVujFaW5WNj2NgWCAE//B1Dl02BIfYlpNgBy8Kf8Rjmw==", + "version": "2.0.9", + "resolved": "https://registry.npmjs.org/http-proxy-middleware/-/http-proxy-middleware-2.0.9.tgz", + "integrity": "sha512-c1IyJYLYppU574+YI7R4QyX2ystMtVXZwIdzazUIPIJsHuWNd+mho2j+bKoHftndicGj9yh+xjd+l0yj7VeT1Q==", "dev": true, "license": "MIT", "dependencies": { @@ -12936,7 +12870,6 @@ "version": "5.3.2", "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", - "dev": true, "license": "MIT", "engines": { "node": ">= 4" @@ -13023,7 +12956,6 @@ "version": "0.1.4", "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", - "dev": true, "license": "MIT", "engines": { "node": ">=0.8.19" @@ -13044,7 +12976,6 @@ "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", - "dev": true, "license": "ISC", "dependencies": { "once": "^1.3.0", @@ -13297,7 +13228,6 @@ "version": "2.1.1", "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", - "dev": true, "license": "MIT", "engines": { "node": ">=0.10.0" @@ -13355,7 +13285,6 @@ "version": "4.0.3", "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", - "dev": true, "license": "MIT", "dependencies": { "is-extglob": "^2.1.1" @@ -13463,7 +13392,6 @@ "version": "3.0.3", "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz", "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==", - "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -13710,7 +13638,6 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", - "dev": true, "license": "ISC" }, "node_modules/istanbul-lib-coverage": { @@ -15935,7 +15862,6 @@ "version": "27.5.1", "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-27.5.1.tgz", "integrity": "sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg==", - "dev": true, "license": "MIT", "dependencies": { "@types/node": "*", @@ -15950,7 +15876,6 @@ "version": "8.1.1", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", - "dev": true, "license": "MIT", "dependencies": { "has-flag": "^4.0.0" @@ -15997,10 +15922,9 @@ "license": "MIT" }, "node_modules/js-yaml": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", - "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", - "dev": true, + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", + "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", "license": "MIT", "dependencies": { "argparse": "^2.0.1" @@ -16072,7 +15996,6 @@ "version": "3.0.1", "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", - "dev": true, "license": "MIT" }, "node_modules/json-parse-even-better-errors": { @@ -16098,7 +16021,6 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", - "dev": true, "license": "MIT" }, "node_modules/json5": { @@ -16127,21 +16049,21 @@ } }, "node_modules/jsonpath": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/jsonpath/-/jsonpath-1.1.1.tgz", - "integrity": "sha512-l6Cg7jRpixfbgoWgkrl77dgEj8RPvND0wMH6TwQmi9Qs4TFfS9u5cUFnbeKTwj5ga5Y3BTGGNI28k117LJ009w==", + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/jsonpath/-/jsonpath-1.3.0.tgz", + "integrity": "sha512-0kjkYHJBkAy50Z5QzArZ7udmvxrJzkpKYW27fiF//BrMY7TQibYLl+FYIXN2BiYmwMIVzSfD8aDRj6IzgBX2/w==", "dev": true, "license": "MIT", "dependencies": { - "esprima": "1.2.2", - "static-eval": "2.0.2", - "underscore": "1.12.1" + "esprima": "1.2.5", + "static-eval": "2.1.1", + "underscore": "1.13.6" } }, "node_modules/jsonpath/node_modules/esprima": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/esprima/-/esprima-1.2.2.tgz", - "integrity": "sha512-+JpPZam9w5DuJ3Q67SqsMGtiHKENSMRVoxvArfJZK01/BfLEObtZ6orJa/MtoGNR/rfMgp5837T41PAmTwAv/A==", + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-1.2.5.tgz", + "integrity": "sha512-S9VbPDU0adFErpDai3qDkjq8+G05ONtKzcyNrPKg/ZKa+tf879nX2KexNU95b31UoTJjRLInNBHHHjFPoCd7lQ==", "dev": true, "bin": { "esparse": "bin/esparse.js", @@ -16241,7 +16163,6 @@ "version": "4.5.4", "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", - "dev": true, "license": "MIT", "dependencies": { "json-buffer": "3.0.1" @@ -16322,7 +16243,6 @@ "version": "0.4.1", "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", - "dev": true, "license": "MIT", "dependencies": { "prelude-ls": "^1.2.1", @@ -16361,7 +16281,6 @@ "version": "4.3.0", "resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-4.3.0.tgz", "integrity": "sha512-3R/1M+yS3j5ou80Me59j7F9IMs4PXs3VqRrm0TU3AbKPxlmpoY1TNscJV/oGJXo8qCatFGTfDbY6W6ipGOYXfg==", - "dev": true, "license": "MIT", "engines": { "node": ">=6.11.5" @@ -16385,7 +16304,6 @@ "version": "6.0.0", "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", - "dev": true, "license": "MIT", "dependencies": { "p-locate": "^5.0.0" @@ -16398,15 +16316,15 @@ } }, "node_modules/lodash": { - "version": "4.17.21", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", - "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", + "version": "4.17.23", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.23.tgz", + "integrity": "sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w==", "license": "MIT" }, "node_modules/lodash-es": { - "version": "4.17.21", - "resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.17.21.tgz", - "integrity": "sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==", + "version": "4.17.23", + "resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.17.23.tgz", + "integrity": "sha512-kVI48u3PZr38HdYz98UmfPnXl2DXrpdctLrFLCd3kOx1xUkOmpFPx7gCWWM5MPkL/fD8zb+Ph0QzjGFs4+hHWg==", "license": "MIT" }, "node_modules/lodash.clonedeep": { @@ -16432,7 +16350,6 @@ "version": "4.6.2", "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", - "dev": true, "license": "MIT" }, "node_modules/lodash.sortby": { @@ -16626,11 +16543,14 @@ "license": "MIT" }, "node_modules/merge-descriptors": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", - "integrity": "sha512-cCi6g3/Zr1iqQi6ySbseM1Xvooa98N0w31jzUYrXPX2xqObmFGHJ0tQ5u74H3mVh7wLouTseZyYIq39g8cNp1w==", + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz", + "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==", "dev": true, - "license": "MIT" + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } }, "node_modules/merge-source-map": { "version": "1.1.0", @@ -16645,7 +16565,6 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", - "dev": true, "license": "MIT" }, "node_modules/merge2": { @@ -16669,9 +16588,9 @@ } }, "node_modules/micromatch": { - "version": "4.0.7", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.7.tgz", - "integrity": "sha512-LPP/3KorzCwBxfeUuZmaR6bG2kdeHSbe0P2tY3FLRU4vYrjYz5hI4QZwV0njUx3jeuKe67YukQ1LSPZBKDqO/Q==", + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", "dev": true, "license": "MIT", "dependencies": { @@ -16696,9 +16615,9 @@ } }, "node_modules/miller-rabin/node_modules/bn.js": { - "version": "4.12.0", - "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.0.tgz", - "integrity": "sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA==", + "version": "4.12.3", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.3.tgz", + "integrity": "sha512-fGTi3gxV/23FTYdAoUtLYp6qySe2KE3teyZitipKNRuVYcBkoP/bB3guXN/XVKUe9mxCHXnc9C4ocyz8OmgN0g==", "license": "MIT" }, "node_modules/mime": { @@ -16718,7 +16637,6 @@ "version": "1.52.0", "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.6" @@ -16728,7 +16646,6 @@ "version": "2.1.35", "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", - "dev": true, "license": "MIT", "dependencies": { "mime-db": "1.52.0" @@ -16779,9 +16696,9 @@ } }, "node_modules/mini-css-extract-plugin/node_modules/ajv": { - "version": "8.17.1", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", - "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.18.0.tgz", + "integrity": "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==", "dev": true, "license": "MIT", "dependencies": { @@ -16848,10 +16765,9 @@ "license": "MIT" }, "node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dev": true, + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", "license": "ISC", "dependencies": { "brace-expansion": "^1.1.7" @@ -16967,9 +16883,9 @@ } }, "node_modules/nanoid": { - "version": "3.3.7", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz", - "integrity": "sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==", + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", "dev": true, "funding": [ { @@ -16989,7 +16905,6 @@ "version": "1.4.0", "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", - "dev": true, "license": "MIT" }, "node_modules/natural-compare-lite": { @@ -17023,7 +16938,6 @@ "version": "2.6.2", "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==", - "dev": true, "license": "MIT" }, "node_modules/no-case": { @@ -17045,9 +16959,9 @@ "license": "0BSD" }, "node_modules/node-forge": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-1.3.1.tgz", - "integrity": "sha512-dPEtOeMvF9VMcYV/1Wb8CPoVAXtp6MKMlcbAt4ddqmGqUJ6fQZFXkNZNkNlfevtNkGtaSoXf/vNNNSvgrdXwtA==", + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-1.4.0.tgz", + "integrity": "sha512-LarFH0+6VfriEhqMMcLX2F7SwSXeWwnEAJEsYm5QKWchiVYVvJyV9v7UDvUv+w5HO23ZpQTXDv/GxdDdMyOuoQ==", "dev": true, "license": "(BSD-3-Clause OR GPL-2.0)", "engines": { @@ -17191,9 +17105,9 @@ } }, "node_modules/object-inspect": { - "version": "1.13.2", - "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.2.tgz", - "integrity": "sha512-IRZSRuzJiynemAXPYtPe5BoI/RESNYR7TYm50MC5Mqbd3Jmw5y790sErYw3V6SryFJD64b74qQQs9wn5Bg/k3g==", + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", "license": "MIT", "engines": { "node": ">= 0.4" @@ -17355,9 +17269,9 @@ } }, "node_modules/on-headers": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.0.2.tgz", - "integrity": "sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.1.0.tgz", + "integrity": "sha512-737ZY3yNnXy37FHkQxPzt4UZ2UWPWiCZWLvFZ4fu5cueciegX0zGPnrlY6bwRg4FdQOe9YU8MkmJwGhoMybl8A==", "dev": true, "license": "MIT", "engines": { @@ -17368,7 +17282,6 @@ "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", - "dev": true, "license": "ISC", "dependencies": { "wrappy": "1" @@ -17412,7 +17325,6 @@ "version": "0.9.4", "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", - "dev": true, "license": "MIT", "dependencies": { "deep-is": "^0.1.3", @@ -17454,7 +17366,6 @@ "version": "3.1.0", "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", - "dev": true, "license": "MIT", "dependencies": { "yocto-queue": "^0.1.0" @@ -17470,7 +17381,6 @@ "version": "5.0.0", "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", - "dev": true, "license": "MIT", "dependencies": { "p-limit": "^3.0.2" @@ -17566,16 +17476,15 @@ } }, "node_modules/parse-asn1": { - "version": "5.1.7", - "resolved": "https://registry.npmjs.org/parse-asn1/-/parse-asn1-5.1.7.tgz", - "integrity": "sha512-CTM5kuWR3sx9IFamcl5ErfPl6ea/N8IYwiJ+vpeB2g+1iknv7zBl5uPwbMbRVznRVbrNY6lGuDoE5b30grmbqg==", + "version": "5.1.9", + "resolved": "https://registry.npmjs.org/parse-asn1/-/parse-asn1-5.1.9.tgz", + "integrity": "sha512-fIYNuZ/HastSb80baGOuPRo1O9cf4baWw5WsAp7dBuUzeTD/BoaG8sVTdlPFksBE2lF21dN+A1AnrpIjSWqHHg==", "license": "ISC", "dependencies": { "asn1.js": "^4.10.1", "browserify-aes": "^1.2.0", "evp_bytestokey": "^1.0.3", - "hash-base": "~3.0", - "pbkdf2": "^3.1.2", + "pbkdf2": "^3.1.5", "safe-buffer": "^5.2.1" }, "engines": { @@ -17645,7 +17554,6 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", - "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -17655,7 +17563,6 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", - "dev": true, "license": "MIT", "engines": { "node": ">=0.10.0" @@ -17665,7 +17572,6 @@ "version": "3.1.1", "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", - "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -17712,9 +17618,9 @@ } }, "node_modules/path-to-regexp": { - "version": "1.8.0", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-1.8.0.tgz", - "integrity": "sha512-n43JRhlUKUAlibEJhPeir1ncUID16QnEjNpwzNdO3Lm4ywrBpBZ5oLD0I6br9evr1Y9JTqwRtAh7JLoOzAQdVA==", + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-1.9.0.tgz", + "integrity": "sha512-xIp7/apCFJuUHdDLWe8O1HIkb0kQrOMb/0u6FXQjemHn/ii5LrIzU6bdECnsiTF/GjZkMEKg1xdiZwNqDYlZ6g==", "license": "MIT", "dependencies": { "isarray": "0.0.1" @@ -19285,19 +19191,29 @@ "dev": true, "license": "CC0-1.0" }, + "node_modules/postcss-svgo/node_modules/sax": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/sax/-/sax-1.6.0.tgz", + "integrity": "sha512-6R3J5M4AcbtLUdZmRv2SygeVaM7IhrLXu9BmnOGmmACak8fiUtOsYNWUS4uK7upbmHIBbLBeFeI//477BKLBzA==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=11.0.0" + } + }, "node_modules/postcss-svgo/node_modules/svgo": { - "version": "2.8.0", - "resolved": "https://registry.npmjs.org/svgo/-/svgo-2.8.0.tgz", - "integrity": "sha512-+N/Q9kV1+F+UeWYoSiULYo4xYSDQlTgb+ayMobAXPwMnLvop7oxKMo9OzIrX5x3eS4L4f2UHhc9axXwY8DpChg==", + "version": "2.8.2", + "resolved": "https://registry.npmjs.org/svgo/-/svgo-2.8.2.tgz", + "integrity": "sha512-TyzE4NVGLUFy+H/Uy4N6c3G0HEeprsVfge6Lmq+0FdQQ/zqoVYB62IsBZORsiL+o96s6ff/V6/3UQo/C0cgCAA==", "dev": true, "license": "MIT", "dependencies": { - "@trysound/sax": "0.2.0", "commander": "^7.2.0", "css-select": "^4.1.3", "css-tree": "^1.1.3", "csso": "^4.2.0", "picocolors": "^1.0.0", + "sax": "^1.5.0", "stable": "^0.1.8" }, "bin": { @@ -19361,7 +19277,6 @@ "version": "1.2.1", "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.8.0" @@ -19527,7 +19442,6 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", - "dev": true, "license": "MIT" }, "node_modules/psl": { @@ -19552,9 +19466,9 @@ } }, "node_modules/public-encrypt/node_modules/bn.js": { - "version": "4.12.0", - "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.0.tgz", - "integrity": "sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA==", + "version": "4.12.3", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.3.tgz", + "integrity": "sha512-fGTi3gxV/23FTYdAoUtLYp6qySe2KE3teyZitipKNRuVYcBkoP/bB3guXN/XVKUe9mxCHXnc9C4ocyz8OmgN0g==", "license": "MIT" }, "node_modules/pump": { @@ -19590,12 +19504,12 @@ } }, "node_modules/qs": { - "version": "6.13.0", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz", - "integrity": "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==", + "version": "6.15.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.0.tgz", + "integrity": "sha512-mAZTtNCeetKMH+pSjrb76NAM8V9a05I9aBZOHztWy/UqcJdQYNsf59vrRKWnojAT9Y+GbIvoTBC++CPHqpDBhQ==", "license": "BSD-3-Clause", "dependencies": { - "side-channel": "^1.0.6" + "side-channel": "^1.1.0" }, "engines": { "node": ">=0.6" @@ -19632,7 +19546,6 @@ "version": "1.2.3", "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", - "dev": true, "funding": [ { "type": "github", @@ -19708,31 +19621,21 @@ } }, "node_modules/raw-body": { - "version": "2.5.2", - "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.2.tgz", - "integrity": "sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==", + "version": "2.5.3", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.3.tgz", + "integrity": "sha512-s4VSOf6yN0rvbRZGxs8Om5CWj6seneMwK3oDb4lWDH0UPhWcxwOWw5+qk24bxq87szX1ydrwylIOp2uG1ojUpA==", "dev": true, "license": "MIT", "dependencies": { - "bytes": "3.1.2", - "http-errors": "2.0.0", - "iconv-lite": "0.4.24", - "unpipe": "1.0.0" + "bytes": "~3.1.2", + "http-errors": "~2.0.1", + "iconv-lite": "~0.4.24", + "unpipe": "~1.0.0" }, "engines": { "node": ">= 0.8" } }, - "node_modules/raw-body/node_modules/bytes": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", - "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, "node_modules/raw-body/node_modules/iconv-lite": { "version": "0.4.24", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", @@ -20907,7 +20810,6 @@ "version": "2.0.2", "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", - "dev": true, "license": "MIT", "engines": { "node": ">=0.10.0" @@ -21085,7 +20987,6 @@ "version": "1.0.4", "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==", - "dev": true, "license": "MIT", "engines": { "iojs": ">=1.0.0", @@ -21097,7 +20998,6 @@ "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", "deprecated": "Rimraf versions prior to v4 are no longer supported", - "dev": true, "license": "ISC", "dependencies": { "glob": "^7.1.3" @@ -21180,9 +21080,9 @@ "license": "MIT" }, "node_modules/rollup": { - "version": "2.79.1", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-2.79.1.tgz", - "integrity": "sha512-uKxbd0IhMZOhjAiD5oAFp7BqvkA4Dv47qpOCtaNvng4HBwdbWtdOh8f5nZNuk2rp51PMGk3bzfWu5oayNEuYnw==", + "version": "2.80.0", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-2.80.0.tgz", + "integrity": "sha512-cIFJOD1DESzpjOBl763Kp1AH7UE/0fcdHe6rZXUdQ9c50uvgigvW97u3IcSeBwOkgqL/PXPBktBCh0KEu5L8XQ==", "dev": true, "license": "MIT", "bin": { @@ -21254,7 +21154,6 @@ "version": "1.2.0", "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", - "dev": true, "funding": [ { "type": "github", @@ -21508,25 +21407,25 @@ } }, "node_modules/send": { - "version": "0.18.0", - "resolved": "https://registry.npmjs.org/send/-/send-0.18.0.tgz", - "integrity": "sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg==", + "version": "0.19.2", + "resolved": "https://registry.npmjs.org/send/-/send-0.19.2.tgz", + "integrity": "sha512-VMbMxbDeehAxpOtWJXlcUS5E8iXh6QmN+BkRX1GARS3wRaXEEgzCcB10gTQazO42tpNIya8xIyNx8fll1OFPrg==", "dev": true, "license": "MIT", "dependencies": { "debug": "2.6.9", "depd": "2.0.0", "destroy": "1.2.0", - "encodeurl": "~1.0.2", + "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "etag": "~1.8.1", - "fresh": "0.5.2", - "http-errors": "2.0.0", + "fresh": "~0.5.2", + "http-errors": "~2.0.1", "mime": "1.6.0", "ms": "2.1.3", - "on-finished": "2.4.1", + "on-finished": "~2.4.1", "range-parser": "~1.2.1", - "statuses": "2.0.1" + "statuses": "~2.0.2" }, "engines": { "node": ">= 0.8.0" @@ -21619,16 +21518,16 @@ } }, "node_modules/serve-static": { - "version": "1.15.0", - "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.15.0.tgz", - "integrity": "sha512-XGuRDNjXUijsUL0vl6nSD7cwURuzEgglbOaFuZM9g3kwDXOWVTck0jLzjPzGD+TazWbboZYu52/9/XPdUgne9g==", + "version": "1.16.3", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.3.tgz", + "integrity": "sha512-x0RTqQel6g5SY7Lg6ZreMmsOzncHFU7nhnRWkKgWuMTu5NN0DR5oruckMqRvacAN9d5w6ARnRBXl9xhDCgfMeA==", "dev": true, "license": "MIT", "dependencies": { - "encodeurl": "~1.0.2", + "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "parseurl": "~1.3.3", - "send": "0.18.0" + "send": "~0.19.1" }, "engines": { "node": ">= 0.8.0" @@ -21704,7 +21603,6 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", - "dev": true, "license": "MIT", "dependencies": { "shebang-regex": "^3.0.0" @@ -21717,7 +21615,6 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", - "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -21734,15 +21631,69 @@ } }, "node_modules/side-channel": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.6.tgz", - "integrity": "sha512-fDW/EZ6Q9RiO8eFG8Hj+7u/oW+XrPTIChwCOM2+th2A6OblDtYYIpve9m+KvI9Z4C9qSEXlaGR6bTEYHReuglA==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", + "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", "license": "MIT", "dependencies": { - "call-bind": "^1.0.7", "es-errors": "^1.3.0", - "get-intrinsic": "^1.2.4", - "object-inspect": "^1.13.1" + "object-inspect": "^1.13.3", + "side-channel-list": "^1.0.0", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", + "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" }, "engines": { "node": ">= 0.4" @@ -21864,7 +21815,6 @@ "version": "0.5.21", "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", - "dev": true, "license": "MIT", "dependencies": { "buffer-from": "^1.0.0", @@ -22030,96 +21980,19 @@ "license": "MIT" }, "node_modules/static-eval": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/static-eval/-/static-eval-2.0.2.tgz", - "integrity": "sha512-N/D219Hcr2bPjLxPiV+TQE++Tsmrady7TqAJugLy7Xk1EumfDWS/f5dtBbkRCGE7wKKXuYockQoj8Rm2/pVKyg==", - "dev": true, - "license": "MIT", - "dependencies": { - "escodegen": "^1.8.1" - } - }, - "node_modules/static-eval/node_modules/escodegen": { - "version": "1.14.3", - "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-1.14.3.tgz", - "integrity": "sha512-qFcX0XJkdg+PB3xjZZG/wKSuT1PnQWx57+TVSjIMmILd2yC/6ByYElPwJnslDsuWuSAp4AwJGumarAAmJch5Kw==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "esprima": "^4.0.1", - "estraverse": "^4.2.0", - "esutils": "^2.0.2", - "optionator": "^0.8.1" - }, - "bin": { - "escodegen": "bin/escodegen.js", - "esgenerate": "bin/esgenerate.js" - }, - "engines": { - "node": ">=4.0" - }, - "optionalDependencies": { - "source-map": "~0.6.1" - } - }, - "node_modules/static-eval/node_modules/levn": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/levn/-/levn-0.3.0.tgz", - "integrity": "sha512-0OO4y2iOHix2W6ujICbKIaEQXvFQHue65vUG3pb5EUomzPI90z9hsA1VsO/dbIIpC53J8gxM9Q4Oho0jrCM/yA==", - "dev": true, - "license": "MIT", - "dependencies": { - "prelude-ls": "~1.1.2", - "type-check": "~0.3.2" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/static-eval/node_modules/optionator": { - "version": "0.8.3", - "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.8.3.tgz", - "integrity": "sha512-+IW9pACdk3XWmmTXG8m3upGUJst5XRGzxMRjXzAuJ1XnIFNvfhjjIuYkDvysnPQ7qzqVzLt78BCruntqRhWQbA==", - "dev": true, - "license": "MIT", - "dependencies": { - "deep-is": "~0.1.3", - "fast-levenshtein": "~2.0.6", - "levn": "~0.3.0", - "prelude-ls": "~1.1.2", - "type-check": "~0.3.2", - "word-wrap": "~1.2.3" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/static-eval/node_modules/prelude-ls": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.1.2.tgz", - "integrity": "sha512-ESF23V4SKG6lVSGZgYNpbsiaAkdab6ZgOxe52p7+Kid3W3u3bxR4Vfd/o21dmN7jSt0IwgZ4v5MUd26FEtXE9w==", - "dev": true, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/static-eval/node_modules/type-check": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.3.2.tgz", - "integrity": "sha512-ZCmOJdvOWDBYJlzAoFkC+Q0+bUyEOS1ltgp1MGU03fqHG+dbi9tBFU2Rd9QKiDZFAYrhPh2JUf7rZRIuHRKtOg==", + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/static-eval/-/static-eval-2.1.1.tgz", + "integrity": "sha512-MgWpQ/ZjGieSVB3eOJVs4OA2LT/q1vx98KPCTTQPzq/aLr0YUXTsgryTXr4SLfR0ZfUUCiedM9n/ABeDIyy4mA==", "dev": true, "license": "MIT", "dependencies": { - "prelude-ls": "~1.1.2" - }, - "engines": { - "node": ">= 0.8.0" + "escodegen": "^2.1.0" } }, "node_modules/statuses": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", - "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", + "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", "dev": true, "license": "MIT", "engines": { @@ -22520,7 +22393,6 @@ "version": "3.1.1", "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", - "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -22683,9 +22555,10 @@ } }, "node_modules/sucrase/node_modules/glob": { - "version": "10.4.5", - "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz", - "integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==", + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz", + "integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", "dev": true, "license": "ISC", "dependencies": { @@ -22704,13 +22577,13 @@ } }, "node_modules/sucrase/node_modules/minimatch": { - "version": "9.0.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", - "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "version": "9.0.9", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz", + "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==", "dev": true, "license": "ISC", "dependencies": { - "brace-expansion": "^2.0.1" + "brace-expansion": "^2.0.2" }, "engines": { "node": ">=16 || 14 >=14.17" @@ -22889,9 +22762,9 @@ } }, "node_modules/svgo/node_modules/js-yaml": { - "version": "3.14.1", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz", - "integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==", + "version": "3.14.2", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.2.tgz", + "integrity": "sha512-PMSmkqxr106Xa156c2M265Z+FTrPl+oxd/rgOQy2tijQeK5TxQ43psO1ZCwhVOSdnn+RzkzlRz/eY4BgJBYVpg==", "dev": true, "license": "MIT", "dependencies": { @@ -22987,7 +22860,6 @@ "version": "2.3.0", "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.0.tgz", "integrity": "sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg==", - "dev": true, "license": "MIT", "engines": { "node": ">=6" @@ -23121,7 +22993,6 @@ "version": "5.31.6", "resolved": "https://registry.npmjs.org/terser/-/terser-5.31.6.tgz", "integrity": "sha512-PQ4DAriWzKj+qgehQ7LK5bQqCFNMmlhjR2PFFLuqGCpuCAauxemVBWwWOxo3UIwWQx8+Pr61Df++r76wDmkQBg==", - "dev": true, "license": "BSD-2-Clause", "dependencies": { "@jridgewell/source-map": "^0.3.3", @@ -23137,16 +23008,14 @@ } }, "node_modules/terser-webpack-plugin": { - "version": "5.3.14", - "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.3.14.tgz", - "integrity": "sha512-vkZjpUjb6OMS7dhV+tILUW6BhpDR7P2L/aQSAv+Uwk+m8KATX9EccViHTJR2qDtACKPIYndLGCyl3FMo+r2LMw==", - "dev": true, + "version": "5.4.0", + "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.4.0.tgz", + "integrity": "sha512-Bn5vxm48flOIfkdl5CaD2+1CiUVbonWQ3KQPyP7/EuIl9Gbzq/gQFOzaMFUEgVjB1396tcK0SG8XcNJ/2kDH8g==", "license": "MIT", "dependencies": { "@jridgewell/trace-mapping": "^0.3.25", "jest-worker": "^27.4.5", "schema-utils": "^4.3.0", - "serialize-javascript": "^6.0.2", "terser": "^5.31.1" }, "engines": { @@ -23172,10 +23041,9 @@ } }, "node_modules/terser-webpack-plugin/node_modules/ajv": { - "version": "8.17.1", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", - "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", - "dev": true, + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.18.0.tgz", + "integrity": "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==", "license": "MIT", "dependencies": { "fast-deep-equal": "^3.1.3", @@ -23192,7 +23060,6 @@ "version": "5.1.0", "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-5.1.0.tgz", "integrity": "sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw==", - "dev": true, "license": "MIT", "dependencies": { "fast-deep-equal": "^3.1.3" @@ -23205,14 +23072,12 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", - "dev": true, "license": "MIT" }, "node_modules/terser-webpack-plugin/node_modules/schema-utils": { "version": "4.3.3", "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.3.3.tgz", "integrity": "sha512-eflK8wEtyOE6+hsaRVPxvUKYCpRgzLqDTb8krvAsRIwOGlHoSgYLgBXoubGgLd2fT41/OUYdb48v4k4WWHQurA==", - "dev": true, "license": "MIT", "dependencies": { "@types/json-schema": "^7.0.9", @@ -23232,7 +23097,6 @@ "version": "2.20.3", "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", - "dev": true, "license": "MIT" }, "node_modules/test-exclude": { @@ -23254,7 +23118,6 @@ "version": "0.2.0", "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==", - "dev": true, "license": "MIT" }, "node_modules/thenify": { @@ -23319,9 +23182,9 @@ "license": "MIT" }, "node_modules/tmp": { - "version": "0.2.3", - "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.3.tgz", - "integrity": "sha512-nZD7m9iCPC5g0pYmcaxogYKggSfLsdxl8of3Q/oIbqCqLLIO9IAF0GWjX1z9NZRHPiXv8Wex4yDCaZsgEw0Y8w==", + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.5.tgz", + "integrity": "sha512-voyz6MApa1rQGUxT3E+BK7/ROe8itEx7vD8/HEvt4xwXucvQ5G5oeEiHkmHZJuBO21RpOf+YYm9MOivj709jow==", "dev": true, "license": "MIT", "engines": { @@ -23509,7 +23372,6 @@ "version": "0.4.0", "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", - "dev": true, "license": "MIT", "dependencies": { "prelude-ls": "^1.2.1" @@ -23640,6 +23502,21 @@ "is-typedarray": "^1.0.0" } }, + "node_modules/typescript": { + "version": "4.9.5", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.5.tgz", + "integrity": "sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==", + "dev": true, + "license": "Apache-2.0", + "peer": true, + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=4.2.0" + } + }, "node_modules/unbox-primitive": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.0.2.tgz", @@ -23672,9 +23549,9 @@ } }, "node_modules/underscore": { - "version": "1.12.1", - "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.12.1.tgz", - "integrity": "sha512-hEQt0+ZLDVUMhebKxL4x1BTtDY7bavVofhZ9KZ4aI26X9SRaE+Y3m83XUL1UP2jn8ynjndwCCpEHdUG+9pP1Tw==", + "version": "1.13.6", + "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.13.6.tgz", + "integrity": "sha512-+A5Sja4HP1M08MaXya7p5LvjuM7K6q/2EaC0+iovj/wOcMsTzMvDFbasi/oSapiwOlt252IqsKqPjCl7huKS0A==", "dev": true, "license": "MIT" }, @@ -23682,7 +23559,6 @@ "version": "6.13.0", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.13.0.tgz", "integrity": "sha512-xtFJHudx8S2DSoujjMd1WeWvn7KKWFRESZTMeL1RptAYERu29D6jphMjjY+vn96jvN3kVPDNxU/E13VTaXj6jg==", - "dev": true, "license": "MIT" }, "node_modules/unicode-canonical-property-names-ecmascript": { @@ -24032,7 +23908,6 @@ "version": "2.4.4", "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.4.tgz", "integrity": "sha512-c5EGNOiyxxV5qmTtAB7rbiXxi1ooX1pQKMLX/MIabJjRA0SJBQOjKF+KSVfHkr9U1cADPon0mRiVe/riyaiDUA==", - "dev": true, "license": "MIT", "dependencies": { "glob-to-regexp": "^0.4.1", @@ -24066,7 +23941,6 @@ "version": "5.102.1", "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.102.1.tgz", "integrity": "sha512-7h/weGm9d/ywQ6qzJ+Xy+r9n/3qgp/thalBbpOi5i223dPXKi04IBtqPN9nTd+jBc7QKfvDbaBnFipYp4sJAUQ==", - "dev": true, "license": "MIT", "dependencies": { "@types/eslint-scope": "^3.7.7", @@ -24136,9 +24010,9 @@ } }, "node_modules/webpack-dev-middleware/node_modules/ajv": { - "version": "8.17.1", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", - "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.18.0.tgz", + "integrity": "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==", "dev": true, "license": "MIT", "dependencies": { @@ -24253,9 +24127,9 @@ } }, "node_modules/webpack-dev-server/node_modules/ajv": { - "version": "8.17.1", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", - "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.18.0.tgz", + "integrity": "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==", "dev": true, "license": "MIT", "dependencies": { @@ -24366,17 +24240,15 @@ "version": "3.3.3", "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-3.3.3.tgz", "integrity": "sha512-yd1RBzSGanHkitROoPFd6qsrxt+oFhg/129YzheDGqeustzX0vTZJZsSsQjVQC4yzBQ56K55XU8gaNCtIzOnTg==", - "dev": true, "license": "MIT", "engines": { "node": ">=10.13.0" } }, "node_modules/webpack/node_modules/ajv": { - "version": "8.17.1", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", - "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", - "dev": true, + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.18.0.tgz", + "integrity": "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==", "license": "MIT", "dependencies": { "fast-deep-equal": "^3.1.3", @@ -24393,7 +24265,6 @@ "version": "5.1.0", "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-5.1.0.tgz", "integrity": "sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw==", - "dev": true, "license": "MIT", "dependencies": { "fast-deep-equal": "^3.1.3" @@ -24406,14 +24277,12 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", - "dev": true, "license": "MIT" }, "node_modules/webpack/node_modules/schema-utils": { "version": "4.3.3", "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.3.3.tgz", "integrity": "sha512-eflK8wEtyOE6+hsaRVPxvUKYCpRgzLqDTb8krvAsRIwOGlHoSgYLgBXoubGgLd2fT41/OUYdb48v4k4WWHQurA==", - "dev": true, "license": "MIT", "dependencies": { "@types/json-schema": "^7.0.9", @@ -24516,7 +24385,6 @@ "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", - "dev": true, "license": "ISC", "dependencies": { "isexe": "^2.0.0" @@ -24623,7 +24491,6 @@ "version": "1.2.5", "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", - "dev": true, "license": "MIT", "engines": { "node": ">=0.10.0" @@ -24718,9 +24585,9 @@ } }, "node_modules/workbox-build/node_modules/ajv": { - "version": "8.17.1", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", - "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.18.0.tgz", + "integrity": "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==", "dev": true, "license": "MIT", "dependencies": { @@ -25128,7 +24995,6 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", - "dev": true, "license": "ISC" }, "node_modules/write-file-atomic": { @@ -25272,7 +25138,6 @@ "version": "0.1.0", "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", - "dev": true, "license": "MIT", "engines": { "node": ">=10" diff --git a/package.json b/package.json index 7297f6601..920679cd6 100644 --- a/package.json +++ b/package.json @@ -8,7 +8,7 @@ "@dnd-kit/sortable": "^10.0.0-next-202410244445", "@dnd-kit/utilities": "^3.2.2", "aimapi": "^0.1.26", - "axios": "^0.21.4", + "axios": "^1.13.6", "bootstrap": "^5.1.3", "cornerstone-core": "^2.6.1", "cornerstone-math": "^0.1.8", @@ -108,4 +108,4 @@ "image-size": "1.2.1", "brace-expansion": "2.0.1" } -} \ No newline at end of file +} diff --git a/src/Utils/aid.js b/src/Utils/aid.js index ca2b86b45..064e6d5a6 100644 --- a/src/Utils/aid.js +++ b/src/Utils/aid.js @@ -436,9 +436,8 @@ export const persistColorInDeleteAim = (oldList, newList, colorList) => { }; export const isSupportedModality = (serie) => { - // To be on the safe side do not filter if (!serie.examType) return true; - return DISP_MODALITIES.includes(serie.examType); + return DISP_MODALITIES.includes(serie.examType.toUpperCase()); }; export const getAllowedTermsOfTemplateComponent = (template, componentLabel) => { diff --git a/src/components/ToolMenu/ToolMenu.css b/src/components/ToolMenu/ToolMenu.css index 25ff075c1..4494a856b 100644 --- a/src/components/ToolMenu/ToolMenu.css +++ b/src/components/ToolMenu/ToolMenu.css @@ -105,4 +105,45 @@ .input-range__track { height: 0.5rem; +} + +/* Mammogram toolbar group — sits at the trailing end of the toolbar */ +.mammo-toolbar-group { + display: inline-flex; + align-items: center; + background-color: #2a4a6b; + border-radius: 4px; + padding: 2px 6px; + margin-left: 10px; + border-left: 2px solid #4a7ab5; +} + +.mammo-toolbar-btn { + cursor: pointer; + display: inline-block; + min-width: 40px; + outline: none; + text-align: center; + margin-left: 4px; + color: white; + font-size: 11px; + background: none; + border: none; + padding: 2px 4px; + transition: all 0.2s ease-in-out; +} + +.mammo-toolbar-btn:first-child { + margin-left: 0; +} + +.mammo-toolbar-btn:hover:not(:disabled) { + color: #aadcff; + transform: scale(1.1); +} + +.mammo-toolbar-btn:disabled, +.mammo-toolbar-btn--disabled { + opacity: 0.4; + cursor: not-allowed; } \ No newline at end of file diff --git a/src/components/ToolMenu/ToolMenu.jsx b/src/components/ToolMenu/ToolMenu.jsx index ab55a4b1c..3f551684a 100644 --- a/src/components/ToolMenu/ToolMenu.jsx +++ b/src/components/ToolMenu/ToolMenu.jsx @@ -43,7 +43,7 @@ import { BsArrowUpLeft } from "react-icons/bs"; import { FiSun, FiSunset, FiZoomIn, FiRotateCw } from "react-icons/fi"; import { IoMdEgg } from "react-icons/io"; import { MdLoop, MdPanTool, MdMyLocation, MdOutlineKeyboardCommandKey } from "react-icons/md"; -import { TbReplace } from "react-icons/tb"; +import { TbReplace, TbReorder, TbContrast2 } from "react-icons/tb"; import { TiDeleteOutline, TiPencil, @@ -253,6 +253,8 @@ class ToolMenu extends Component { this.managementTools = [ { name: "Save order", icon: , tool: "order", teaching: true }, { name: "Hot Keys", icon: , tool: "keys", teaching: true }, + { name: "Reorder", icon: , tool: "reorder", teaching: true }, + { name: "Save State", icon: , tool: "saveState", teaching: true }, ] this.segmentationTools = [ @@ -477,7 +479,9 @@ class ToolMenu extends Component { MetaData: true, fuse: true, order: true, - keys: true + keys: true, + reorder: true, + saveState: true, }; if (!notActiveTools[tool]) sessionStorage.setItem("activeTool", tool); @@ -577,6 +581,12 @@ class ToolMenu extends Component { } else if (tool === 'keys') { this.showHotkeyInfo(); return; + } else if (tool === 'reorder') { + if (this.props.onReorder) this.props.onReorder(); + return; + } else if (tool === 'saveState') { + if (this.props.onSaveState) this.props.onSaveState(); + return; } else if (tool === 'next') { this.props.openNextWLStudy(worklistID, studyUID); this.closeAllActions(); @@ -1065,6 +1075,7 @@ class ToolMenu extends Component { {this.state.showFuse && } {this.state.showMetaData && ()} {this.state.keys && ( this.setState({keys: null})} list={this.state.keys} />)} + {this.props.children} ); } diff --git a/src/components/annotationSearch/AnnotationTable.jsx b/src/components/annotationSearch/AnnotationTable.jsx index 354e667e3..4d80130e6 100644 --- a/src/components/annotationSearch/AnnotationTable.jsx +++ b/src/components/annotationSearch/AnnotationTable.jsx @@ -13,15 +13,18 @@ import { clearCarets, convertDateFormat } from "../../Utils/aid.js"; import { changeActivePort, jumpToAim, - addToGrid, - getSingleSerie, startLoading, loadCompleted, annotationsLoadingError, updateSearchTableIndex, setSeriesData, - storeAimSelection + storeAimSelection, + setMammogramSeries, + clearMammogramSeries, + setPageOrderSeries, + clearPageOrderSeries, } from "../annotationsList/action"; +import { openSeriesInDisplay, isMammogramStudy } from "../common/openSeriesHelper"; import { formatDate } from "../flexView/helperMethods"; import { getSeries, getSignificantSeries } from "../../services/seriesServices"; import SelectSerieModal from "../annotationsList/selectSerieModal"; @@ -392,13 +395,17 @@ function AnnotationTable(props) { seriesData[projectID][patientID][studyUID] && seriesData[projectID][patientID][studyUID].list; if (!dataExists && seriesCallSent !== studyUID) { - const { data: series } = await getSeries( + let { data: series } = await getSeries( projectID, patientID, - studyUID, + studyUID, force, "getSeriesData, AnnotationTable" ); + if (!series || series.length === 0) { + // Test server has 2 DBs behind forceDicomweb=false vs true; retry with opposite. + ({ data: series } = await getSeries(projectID, patientID, studyUID, !force, "getSeriesData, AnnotationTable [retry]")); + } seriesCallSent = studyUID; props.dispatch(setSeriesData(projectID, patientID, studyUID, series)); props.dispatch(loadCompleted()); @@ -532,61 +539,108 @@ function AnnotationTable(props) { // CHECK const displaySeries = async (selected) => { const { subjectID: patientID, studyUID, aimID, projectID, template } = selected; + console.log(selected); let isTeachingFile = teachingFileTempCode === template; - let seriesArr = []; + let seriesArr = []; let existingData = getExistingData(selected); - + try { + props.dispatch(clearPageOrderSeries()); + props.dispatch(clearMammogramSeries()); + seriesArr = await getSeriesData(selected); + console.log('seriesArr before filter'); + console.log(seriesArr); + const filtered = Array.isArray(seriesArr) ? seriesArr.filter(isSupportedModality) : []; + console.log('filtered -->', filtered); + + // Decision tree: significant series take priority over mammogram fallback. + const significant = filtered.filter(s => s.significanceOrder != null); + const hasPageOrder = significant.length > 0 && significant.some(s => s.pageOrder != null); + + console.log(significant, hasPageOrder); + if (significant.length > 0 && hasPageOrder) { + // Case 1: pageOrder navigation — load page 1, enable Next/Prev by pageOrder. + props.dispatch(setPageOrderSeries(significant)); + let toDisplay = significant + .filter(s => s.pageOrder === 1) + .sort((a, b) => (a.significanceOrder || 0) - (b.significanceOrder || 0)); + if (toDisplay.length === 0) toDisplay = significant.slice(0, maxPort); + setSelected(toDisplay); + openSeriesInDisplay({ + dispatch: props.dispatch, + navigate: props.switchToDisplay, + openSeries: props.openSeries, + series: toDisplay, + aimID, + existingData: filtered, + }); + return; + } + + if (isMammogramStudy(filtered)) { + // Case 3 (no significant) or Case 6 (significant, no pageOrder, MG). + props.dispatch(clearPageOrderSeries()); + props.dispatch(setMammogramSeries(filtered, studyUID)); + const toDisplay = significant.length > 0 + ? significant.sort((a, b) => (a.significanceOrder || 0) - (b.significanceOrder || 0)) // Case 6 + : filtered.slice(0, maxPort); // Case 3 + setSelected(toDisplay); + openSeriesInDisplay({ + dispatch: props.dispatch, + navigate: props.switchToDisplay, + openSeries: props.openSeries, + series: toDisplay, + aimID, + existingData: filtered, + }); + return; + } + + if (significant.length > 0) { + // Case 2: significant series, no pageOrder, non-mammogram — load directly, no Next/Prev. + props.dispatch(clearPageOrderSeries()); + const toDisplay = significant.sort((a, b) => (a.significanceOrder || 0) - (b.significanceOrder || 0)); + setSelected(toDisplay); + openSeriesInDisplay({ + dispatch: props.dispatch, + navigate: props.switchToDisplay, + openSeries: props.openSeries, + series: toDisplay, + aimID, + existingData: getExistingData(selected), + }); + return; + } + if (isTeachingFile) { - seriesArr = await getSignificantSeriesData(selected); - if (seriesArr.length > 0){ - seriesArr = seriesArr.map( el => ({...el, patientID, studyUID, projectID, template }));} - else if (existingData && existingData.length <= maxPort) { + seriesArr = await getSignificantSeriesData(selected); + if (seriesArr.length > 0) { + seriesArr = seriesArr.map(el => ({ ...el, patientID, studyUID, projectID, template })); + } else if (existingData && existingData.length <= maxPort) { seriesArr = existingData; } else if (existingData && existingData.length > maxPort) { - seriesArr = existingData.slice(0,maxPort); - // setSelected(seriesArr); - // setShowSelectSeriesModal(true); + seriesArr = existingData.slice(0, maxPort); } else { seriesArr = await getSeriesData(selected, true); - seriesArr = seriesArr.slice(0,maxPort); + seriesArr = seriesArr.slice(0, maxPort); } - } else - seriesArr = await getSeriesData(selected); - + } } catch (err) { setShowSpinner(false); } setSelected(seriesArr); - if (props.openSeries.length === maxPort) { - setShowSelectSeriesModal(true); - return; - } - //get extraction of the series (extract unopen series) - if (seriesArr && seriesArr.length > 0) seriesArr = excludeOpenSeries(seriesArr); - - // filter the series according to displayable modalities seriesArr = Array.isArray(seriesArr) ? seriesArr.filter(isSupportedModality) : []; - //check if there is enough room - if (seriesArr.length + props.openSeries.length > maxPort) { - //if there is not bring the modal - setShowSelectSeriesModal(true); - // TODO show toast - } else { - //if there is enough room - //add serie to the grid - const promiseArr = []; - - existingData = getExistingData(selected); - for (let i = 0; i < seriesArr.length; i++) { - props.dispatch(addToGrid(seriesArr[i], aimID)); - promiseArr.push(props.dispatch(getSingleSerie(seriesArr[i], aimID, null, existingData))); - } - //getsingleSerie - Promise.all(promiseArr).then(() => { props.switchToDisplay(); }).catch((err) => console.error(err)); - } + openSeriesInDisplay({ + dispatch: props.dispatch, + navigate: props.switchToDisplay, + openSeries: props.openSeries, + series: seriesArr, + aimID, + existingData: getExistingData(selected), + onGridFull: () => setShowSelectSeriesModal(true), + }); }; diff --git a/src/components/annotationsList/action.js b/src/components/annotationsList/action.js index df042e6da..389944e48 100644 --- a/src/components/annotationsList/action.js +++ b/src/components/annotationsList/action.js @@ -56,6 +56,12 @@ import { TOGGLE_ALL_CALCULATIONS, SET_LAST_LOCATION, SHOW_PHI, + SET_MAMMOGRAM_SERIES, + SET_MAMMOGRAM_PAGE, + CLEAR_MAMMOGRAM_SERIES, + SET_PAGE_ORDER_SERIES, + SET_PAGE_ORDER, + CLEAR_PAGE_ORDER_SERIES, colors, commonLabels, } from "./types"; @@ -384,7 +390,7 @@ export const selectAnnotation = ( // opens a new port to display series // adds series details to the array export const addToGrid = (serie, annotation, port, worklistID) => { - let { patientID, studyUID, seriesUID, projectID, patientName, examType, modality, comment, seriesDescription, numberOfAnnotations, numberOfImages, seriesNo, template, significanceOrder, multiFrameIndex } = serie; + let { patientID, studyUID, seriesUID, projectID, patientName, examType, modality, comment, seriesDescription, numberOfAnnotations, numberOfImages, seriesNo, template, significanceOrder, multiFrameIndex, displayState } = serie; const modFmComment = comment ? comment.split('/')[0].trim() : ''; examType = examType ? examType.toUpperCase() : modality ? modality.toUpperCase() : modFmComment.toUpperCase(); @@ -406,7 +412,8 @@ export const addToGrid = (serie, annotation, port, worklistID) => { seriesNo, template, significanceOrder, - worklistID + worklistID, + displayState, // imageIndex: 0 }; if (multiFrameIndex) reference.multiFrameIndex = multiFrameIndex; @@ -1202,6 +1209,34 @@ export const aimDelete = (aimRefs) => { return { type: AIM_DELETE, payload: aimRefs }; }; +export const setMammogramSeries = (allSeries, studyUID) => ({ + type: SET_MAMMOGRAM_SERIES, + payload: { allSeries, studyUID }, +}); + +export const setMammogramPage = (pageIndex) => ({ + type: SET_MAMMOGRAM_PAGE, + payload: pageIndex, +}); + +export const clearMammogramSeries = () => ({ + type: CLEAR_MAMMOGRAM_SERIES, +}); + +export const setPageOrderSeries = (allSeries) => ({ + type: SET_PAGE_ORDER_SERIES, + payload: allSeries, +}); + +export const setPageOrder = (pageOrder) => ({ + type: SET_PAGE_ORDER, + payload: pageOrder, +}); + +export const clearPageOrderSeries = () => ({ + type: CLEAR_PAGE_ORDER_SERIES, +}); + export const otherAimsUpdated = (seriesList, aimRefs) => { return { type: AIM_SAVE, payload: { seriesList, aimRefs } }; diff --git a/src/components/annotationsList/annotationDock/newAnnotationsLink.jsx b/src/components/annotationsList/annotationDock/newAnnotationsLink.jsx index 5df38288b..41812b0fb 100644 --- a/src/components/annotationsList/annotationDock/newAnnotationsLink.jsx +++ b/src/components/annotationsList/annotationDock/newAnnotationsLink.jsx @@ -1,12 +1,12 @@ import React, { useEffect, useState } from "react"; import { connect } from "react-redux"; import { - getSingleSerie, clearSelection, changeActivePort, addToGrid, jumpToAim, } from "../action"; +import { openSeriesInDisplay } from "../../common/openSeriesHelper"; import "../annotationsList.css"; import cornerstone from "cornerstone-core"; @@ -90,40 +90,33 @@ const annotationsLink = (props) => { } const displayAnnotations = (e, selected) => { - const { openSeriesAddition, activePort } = props; - const maxPort = parseInt(sessionStorage.getItem("maxPort")); - - let isGridFull = openSeries.length === maxPort; const { isOpen, index } = checkIfSerieOpen(selected.seriesUID, selected.imgIDs); if (isOpen) { + // Fusion-aware jump: skip jump when the active layer is the fused CT companion. const imageUID = Object.keys(selected.imgIDs); const imgIDArr = imageUID[0].split("/frames/"); props.dispatch(changeActivePort(index)); - // if ct do not jump to aim - const {seriesUID} = getFusedSerieInfoAndAnnotations({...props, activePort: index}); + const { seriesUID } = getFusedSerieInfoAndAnnotations({ ...props, activePort: index }); const notFusionCT = seriesUID === props.openSeries[index].seriesUID; if (notFusionCT) { - // No need to change props.dispatch(jumpToAim(selected.seriesUID, selected.aimID, index)); - // change the arguments to handle the multiframe handleJumpToAim(selected.aimID, index, imgIDArr[0], imgIDArr[1]); } else { console.log('Cannot jump on a fused image that is not the active layer'); } props.dispatch(clearSelection()); } else { - if (isGridFull) { - props.dispatch(addToGrid(selected, selected.aimID, props.activePort)); - } else { - props.dispatch(addToGrid(selected, selected.aimID)); - } - const list = getExistingSeriesData(selected); - props - .dispatch(getSingleSerie(selected, selected.aimID, null, list)) - .then(() => {}) - .catch((err) => console.error(err)); - props.dispatch(clearSelection()); + openSeriesInDisplay({ + dispatch: props.dispatch, + navigate: () => {}, // already in display view + openSeries: props.openSeries, + series: selected, + aimID: selected.aimID, + existingData: getExistingSeriesData(selected), + // When grid is full, replace the active viewport instead of blocking. + onGridFull: () => props.dispatch(addToGrid(selected, selected.aimID, props.activePort)), + }); } }; diff --git a/src/components/annotationsList/containers/listItem.jsx b/src/components/annotationsList/containers/listItem.jsx index a832b3575..4ef09d202 100644 --- a/src/components/annotationsList/containers/listItem.jsx +++ b/src/components/annotationsList/containers/listItem.jsx @@ -7,13 +7,13 @@ import { toggleAllAnnotations, changeActivePort, showAnnotationWindow, - getSingleSerie, alertViewPortFull, addToGrid, updatePatient, jumpToAim, showAnnotationDock } from "../action"; +import { openSeriesInDisplay } from "../../common/openSeriesHelper"; const maxPort = parseInt(sessionStorage.getItem("maxPort")); @@ -62,134 +62,56 @@ class ListItem extends React.Component { }; openSerie = async e => { - const { patientID, studyUID, seriesUID } = this.props.serie; - const openSeries = Object.values(this.props.openSeries); - let serieCheck = this.checkIfSerieOpen(this.props.serie); - //check if there is enough space in the grid - let isGridFull = openSeries.length === maxPort; - //check if the serie is already open - if (serieCheck.isOpen) { - this.props.dispatch(changeActivePort(serieCheck.index)); - } - if (!serieCheck.isOpen) { - if (isGridFull) { - this.props.dispatch(alertViewPortFull()); - } else { - this.props.dispatch(addToGrid(this.props.serie)); - this.props - .dispatch(getSingleSerie(this.props.serie)) - .then(() => {}) - .catch(err => console.log(err)); - // -----> Delete after v1.0 <----- - // this.props.dispatch( - // updatePatient("serie", true, patientID, studyUID, seriesUID) - // ); - } - } + openSeriesInDisplay({ + dispatch: this.props.dispatch, + navigate: () => {}, // already in display view + openSeries: Object.values(this.props.openSeries), + series: this.props.serie, + }); }; handleAnnotationClick = async e => { const { seriesUID, studyUID, patientID } = this.props.serie; const { seriesid, aimid } = e.target.dataset; - const activeSeriesUID = this.props.openSeries[this.props.activePort] - .seriesUID; + const activeSeriesUID = this.props.openSeries[this.props.activePort].seriesUID; + if (activeSeriesUID === seriesid) { - // this.checkIfSerieOpen(seriesid).index; this.props.dispatch(jumpToAim(seriesid, aimid, this.props.activePort)); } else { - //if doesn't match check if the serie exists in the open series - const isOpen = this.checkIfSerieOpen(seriesid).isOpen; - const index = this.checkIfSerieOpen(seriesid).index; - if (isOpen) { - // if it exists in the openSeries update activeport - this.props.dispatch(changeActivePort(index)); - //update the status of the clicked annotation - this.props.dispatch(jumpToAim(seriesid, aimid, index)); - } else { - //else get single serie dispatch action - if (this.props.openSeries.length === maxPort) { - this.props.dispatch(alertViewPortFull()); - } else { - // let { patientID, studyUID, seriesUID, projectID } = serie; - this.props.dispatch(addToGrid(this.props.serie, aimid)); - this.props - .dispatch(getSingleSerie(this.props.serie, aimid)) - .then(() => { - // this.props.dispatch(showAnnotationDock()); - - this.props.dispatch( - updateAnnotationDisplay( - patientID, - studyUID, - seriesUID, - aimid, - true - ) - ); - }) - .catch(err => console.log(err)); - } - } + openSeriesInDisplay({ + dispatch: this.props.dispatch, + navigate: () => {}, // already in display view + openSeries: this.props.openSeries, + series: this.props.serie, + aimID: aimid, + })?.then(() => { + this.props.dispatch( + updateAnnotationDisplay(patientID, studyUID, seriesUID, aimid, true) + ); + }); } this.props.dispatch(showAnnotationWindow()); }; handleToggleSerie = async (checked, e, id) => { - //select de select all anotations - const { patientID, studyUID, seriesUID } = this.props.serie; - // const { seriesid } = e.target.dataset; - // const activeSeriesUID = this.props.openSeries[this.props.activePort] - // .seriesUID; - //check if user toggle on or off and change the state accordingly + const { seriesUID } = this.props.serie; await this.setState({ displayAnnotations: checked }); - // if checked true - const isOpen = this.checkIfSerieOpen(seriesUID).isOpen; - const index = this.checkIfSerieOpen(seriesUID).index; + const { isOpen, index } = this.checkIfSerieOpen(seriesUID); if (checked) { - //check if the serie is already open - //if open if (isOpen) { - //update the active port this.props.dispatch(changeActivePort(index)); - // change the annotations as displayed in patient and aimlist - // -----> Delete after v1.0 <----- - // this.props.dispatch( - // updatePatient("serie", checked, patientID, studyUID, seriesUID) - // ); this.props.dispatch(toggleAllAnnotations(seriesUID, checked)); - //else - if not open } else { - //check if the grid is full - if (this.props.openSeries.length === maxPort) { - //if full bring modal - this.props.dispatch(alertViewPortFull()); - //else - not full - } else { - //addtogrid - this.props.dispatch(addToGrid(this.props.serie)); - //getsingleserie - this.props - .dispatch(getSingleSerie(this.props.serie)) - .then(() => {}) - .catch(err => console.log(err)); - //update patient?? with serie - // -----> Delete after v1.0 <----- - // this.props.dispatch( - // updatePatient("serie", checked, patientID, studyUID, seriesUID) - // ); - } + openSeriesInDisplay({ + dispatch: this.props.dispatch, + navigate: () => {}, // already in display view + openSeries: this.props.openSeries, + series: this.props.serie, + }); } - // if checked false } else { - //update patients and aimlist just annotations to be false - // -----> Delete after v1.0 <----- - // this.props.dispatch( - // updatePatient("serie", checked, patientID, studyUID, seriesUID) - // ); this.props.dispatch(toggleAllAnnotations(seriesUID, checked)); - if (isOpen) { - this.props.dispatch(changeActivePort(index)); - } + if (isOpen) this.props.dispatch(changeActivePort(index)); } }; diff --git a/src/components/annotationsList/reducer.js b/src/components/annotationsList/reducer.js index 4fcf7d895..553088efc 100644 --- a/src/components/annotationsList/reducer.js +++ b/src/components/annotationsList/reducer.js @@ -57,6 +57,12 @@ import { TOGGLE_ALL_CALCULATIONS, SET_LAST_LOCATION, SHOW_PHI, + SET_MAMMOGRAM_SERIES, + SET_MAMMOGRAM_PAGE, + CLEAR_MAMMOGRAM_SERIES, + SET_PAGE_ORDER_SERIES, + SET_PAGE_ORDER, + CLEAR_PAGE_ORDER_SERIES, colors, commonLabels, } from "./types"; @@ -112,6 +118,11 @@ const initialState = { showAnnotations: mode === 'teaching' ? false : true, lastLocation: '', showingPHI: isPHIVisible || false, + mammogramSeries: [], + mammogramStudyUID: null, + mammogramPageIndex: 0, + pageOrderSeries: [], + currentPageOrder: 1, }; @@ -1031,6 +1042,23 @@ const asyncReducer = (state = initialState, action) => { } return { ...state, openSeriesAddition: newOpenSeriesAddition, otherSeriesAimsList: deepOther }; } + case SET_MAMMOGRAM_SERIES: + return { + ...state, + mammogramSeries: action.payload.allSeries, + mammogramStudyUID: action.payload.studyUID, + mammogramPageIndex: 0, + }; + case SET_MAMMOGRAM_PAGE: + return { ...state, mammogramPageIndex: action.payload }; + case CLEAR_MAMMOGRAM_SERIES: + return { ...state, mammogramSeries: [], mammogramStudyUID: null, mammogramPageIndex: 0 }; + case SET_PAGE_ORDER_SERIES: + return { ...state, pageOrderSeries: action.payload, currentPageOrder: 1 }; + case SET_PAGE_ORDER: + return { ...state, currentPageOrder: action.payload }; + case CLEAR_PAGE_ORDER_SERIES: + return { ...state, pageOrderSeries: [], currentPageOrder: 1 }; default: return state; } diff --git a/src/components/annotationsList/selectSerieModal.jsx b/src/components/annotationsList/selectSerieModal.jsx index 7c9e5db7c..0b56a1d72 100644 --- a/src/components/annotationsList/selectSerieModal.jsx +++ b/src/components/annotationsList/selectSerieModal.jsx @@ -9,11 +9,10 @@ import Button from "react-bootstrap/Button"; import { clearGrid, getWholeData, - getSingleSerie, clearSelection, - addToGrid, setSeriesData, } from "./action"; +import { openSeriesInDisplay } from "../common/openSeriesHelper"; import SelectionItem from "./containers/selectionItem"; import { FaRegCheckSquare } from "react-icons/fa"; import { getSeries, setSignificantSeries } from "../../services/seriesServices"; @@ -66,12 +65,15 @@ class selectSerieModal extends React.Component { selectionType = "aim"; } this.setState({ selectionType }); + + // For mammogram studies, allow up to 8 significant series. this.setPreSelecteds(); const limit = this.updateLimit(); this.setState({ limit }); // teaching file save related const { isTeachingFile } = this.props; + if (isTeachingFile) this.maxPort = 8; if (isTeachingFile) { const element = document.getElementById("questionaire"); const speciality = document.getElementById("speciality"); @@ -217,28 +219,36 @@ class selectSerieModal extends React.Component { displaySelection = async (aimID) => { let studies = Object.values(this.props.seriesPassed); let series = []; - // TODO: what is the logic here? - studies.forEach((arr) => { - series = series.concat(arr); - }); + studies.forEach((arr) => { series = series.concat(arr); }); const seriesArr = await this.saveSignificantSeries(series); - //concatanete all arrays to getther - // for (let key of Object.keys(selectedToDisplay)) { - for (let el of seriesArr) { - let serie = this.findSerieFromSeries(el.seriesUID, series); - const existingData = this.getExistingSeriesData(serie); - if (aimID) this.props.dispatch(addToGrid(serie, aimID)); - else this.props.dispatch(addToGrid(serie, serie.aimID, null, this.props.worklistID )); - if (this.state.selectionType === "aim") { - this.props.dispatch(getSingleSerie(serie, serie.aimID, this.wadoUrl, existingData)); - } else { - if (aimID) - this.props.dispatch(getSingleSerie(serie, aimID, this.wadoUrl, existingData)); - else this.props.dispatch(getSingleSerie(serie, null, this.wadoUrl, existingData)); - } - } - this.props.history.push("/display"); - this.handleCancel(true); + + // Only open the first page of significant series (up to maxPort). + // Additional pages are navigated via PREV/NEXT in the display view. + const viewportLimit = parseInt(sessionStorage.getItem('maxPort')) || 4; + const pageOneArr = seriesArr + .filter(el => el.pageOrder == null || el.pageOrder === 1) + .slice(0, viewportLimit); + + // Resolve each series and embed the effective aimID so the helper can use it. + const resolvedSeries = pageOneArr.map(el => { + const serie = this.findSerieFromSeries(el.seriesUID, series); + return { + ...serie, + aimID: this.state.selectionType === "aim" ? serie.aimID : (aimID || null), + }; + }); + + openSeriesInDisplay({ + dispatch: this.props.dispatch, + navigate: () => { + this.props.history.push("/display"); + this.handleCancel(true); + }, + openSeries: this.props.openSeries, + series: resolvedSeries, + worklistID: this.props.worklistID, + existingData: serie => this.getExistingSeriesData(serie), + }); }; groupUnderPatient = (objArr) => { @@ -278,6 +288,11 @@ class selectSerieModal extends React.Component { let selectedCount = 0; let series = Object.values(seriesPassed); let count = 0; + + // If any series carry pageOrder, only pre-select page 1 series. + const allSeries = series.flat(); + const hasPageOrder = allSeries.some(s => s.pageOrder != null); + for (let i = 0; i < series.length; i++) { for (let k = 0; k < series[i].length; k++) { if (openSeries.length + selectedCount >= this.maxPort) { @@ -286,17 +301,10 @@ class selectSerieModal extends React.Component { }); return; } - // if (!this.isSerieOpen(series[i][k].seriesUID)) { - // selectedToDisplay[series[i][k].seriesUID] = series[i][k].significanceOrder - // ? true - // : false; - // selectedCount++; - // } - if ( - series[i][k].significanceOrder && - !this.isSerieOpen(series[i][k].seriesUID) - ) { - selectedToDisplay[series[i][k].seriesUID] = true; + const s = series[i][k]; + const isPageOneOrLegacy = !hasPageOrder || s.pageOrder === 1; + if (s.significanceOrder && !this.isSerieOpen(s.seriesUID) && isPageOneOrLegacy) { + selectedToDisplay[s.seriesUID] = true; selectedCount++; } } @@ -359,7 +367,9 @@ class selectSerieModal extends React.Component { let seriesNo = series[i][k].seriesNo || ""; let desc = series[i][k].seriesDescription; let description = desc ? desc : !desc && series[i][k].significanceOrder ? `Sig Series ${series[i][k].significanceOrder}` : "Unnamed Series"; - desc = `${seriesNo} - ${description}`; + const modality = series[i][k].examType || series[i][k].modality; + const modalityLabel = modality ? ` [${modality.toUpperCase()}]` : ""; + desc = `${seriesNo} - ${description}${modalityLabel}`; if (series[i][k].significanceOrder) { desc = desc + " (S)"; isSignificant = true; diff --git a/src/components/annotationsList/types.js b/src/components/annotationsList/types.js index 3517cdbea..9a5c89f72 100644 --- a/src/components/annotationsList/types.js +++ b/src/components/annotationsList/types.js @@ -69,6 +69,12 @@ export const STORE_AIM_SELECTION_ALL = 'epadjs/annotationList/STORE_AIM_SELECTIO export const TOGGLE_ALL_CALCULATIONS = 'epadjs/annotationList/TOGGLE_ALL_CALCULATIONS'; export const SET_LAST_LOCATION = 'epadjs/annotationList/SET_LAST_LOCATION'; export const SHOW_PHI = 'epadjs/annotationList/SHOW_PHI'; +export const SET_MAMMOGRAM_SERIES = 'epadjs/annotationList/SET_MAMMOGRAM_SERIES'; +export const SET_MAMMOGRAM_PAGE = 'epadjs/annotationList/SET_MAMMOGRAM_PAGE'; +export const CLEAR_MAMMOGRAM_SERIES = 'epadjs/annotationList/CLEAR_MAMMOGRAM_SERIES'; +export const SET_PAGE_ORDER_SERIES = 'epadjs/annotationList/SET_PAGE_ORDER_SERIES'; +export const SET_PAGE_ORDER = 'epadjs/annotationList/SET_PAGE_ORDER'; +export const CLEAR_PAGE_ORDER_SERIES = 'epadjs/annotationList/CLEAR_PAGE_ORDER_SERIES'; export const commonLabels = { button: { background: "#6c757d", color: "#ECECEC", border: "#acacac solid 1px" }, diff --git a/src/components/common/openSeriesHelper.js b/src/components/common/openSeriesHelper.js new file mode 100644 index 000000000..693b22b32 --- /dev/null +++ b/src/components/common/openSeriesHelper.js @@ -0,0 +1,111 @@ +import { + addToGrid, + getSingleSerie, + changeActivePort, + clearSelection, + jumpToAim, + alertViewPortFull, +} from '../annotationsList/action'; + +/** + * Returns true when the app is in lite mode AND every series in the filtered + * array is a mammogram (MG). Pass only the isSupportedModality-filtered list. + */ +export const isMammogramStudy = (seriesArray) => { + console.log(" ------ Checking mammogram ------", seriesArray); + if (sessionStorage.getItem('mode') !== 'teaching') return false; + console.log('after mode check') + if (!Array.isArray(seriesArray) || seriesArray.length === 0) return false; + console.log('after Array check') + const mGVerified = seriesArray.every(s => s.examType?.toUpperCase() === 'MG'); + console.log(' ---> mGVerified', mGVerified) + return mGVerified; +}; + +/** + * Finds a series by UID in the open series list. + * @returns {{ isOpen: boolean, index: number }} + */ +export const findInOpenSeries = (seriesUID, openSeries) => { + const index = openSeries.findIndex(s => s && s.seriesUID === seriesUID); + return { isOpen: index !== -1, index }; +}; + +/** + * Opens one or more DICOM series in the display view. + * + * Handles the dispatch+navigate sequence shared across all entry points: + * already-open detection, viewport capacity check, addToGrid/getSingleSerie + * dispatches, and navigation. Component-specific concerns — fetching series + * from the API, filtering by modality, managing modal state — remain in the + * calling component. + * + * @param {Object} options + * @param {Function} options.dispatch Redux dispatch + * @param {Function} options.navigate Navigate to /display; pass () => {} when already there + * @param {Array} options.openSeries Currently open series from Redux state + * @param {Array|Object} options.series Series object(s) to open; each may carry aimID/aimUID + * @param {string} [options.aimID] Aim ID — overrides per-series aimID/aimUID when provided + * @param {string} [options.worklistID] Passed through to addToGrid + * @param {Array|Function} [options.existingData] Cached image data for the study; pass a + * function (serie) => data for per-serie caching + * @param {Function} [options.onGridFull] Called with the pending series array when there + * is no room; defaults to alertViewPortFull + * @returns {Promise|undefined} + */ +export const openSeriesInDisplay = ({ + dispatch, + navigate, + openSeries, + series, + aimID = null, + worklistID = null, + existingData = null, + onGridFull = null, +}) => { + const maxPort = parseInt(sessionStorage.getItem('maxPort')); + const seriesArr = Array.isArray(series) ? series : [series]; + const toOpen = []; + + // Activate already-open series and, when an aim is provided, jump to it. + for (const serie of seriesArr) { + const { isOpen, index } = findInOpenSeries(serie.seriesUID, openSeries); + if (isOpen) { + dispatch(changeActivePort(index)); + const resolvedAimID = aimID || serie.aimID || serie.aimUID || null; + if (resolvedAimID) dispatch(jumpToAim(serie.seriesUID, resolvedAimID, index)); + } else { + toOpen.push(serie); + } + } + + // All series already open — just navigate. + if (toOpen.length === 0) { + dispatch(clearSelection()); + navigate(); + return Promise.resolve(); + } + + // Not enough free viewports. + if (toOpen.length + openSeries.length > maxPort) { + if (onGridFull) onGridFull(toOpen); + else dispatch(alertViewPortFull()); + return; + } + + const resolveExistingData = + typeof existingData === 'function' ? existingData : () => existingData; + + const promiseArr = toOpen.map(serie => { + const resolvedAimID = aimID || serie.aimID || serie.aimUID || null; + dispatch(addToGrid(serie, resolvedAimID, null, worklistID)); + return dispatch(getSingleSerie(serie, resolvedAimID, null, resolveExistingData(serie))); + }); + + return Promise.all(promiseArr) + .then(() => { + dispatch(clearSelection()); + navigate(); + }) + .catch(err => console.error(err)); +}; diff --git a/src/components/display/SeriesDropDown.jsx b/src/components/display/SeriesDropDown.jsx index 18855060a..e8087de8c 100644 --- a/src/components/display/SeriesDropDown.jsx +++ b/src/components/display/SeriesDropDown.jsx @@ -32,6 +32,13 @@ const SeriesDropDown = (props) => { const makeKey = (projectID, patientID, studyUID) => `${projectID}|${patientID}|${studyUID}`; + const getSeriesWithFallback = (projectID, patientID, studyUID, label) => + getSeries(projectID, patientID, studyUID, false, label).then(res => + res.data && res.data.length > 0 + ? res + : getSeries(projectID, patientID, studyUID, true, label) + ); + const checkMultiframe = () => { const { openSeries, activePort, openSeriesAddition } = props; // if the currrent series is multiframe @@ -121,7 +128,7 @@ const SeriesDropDown = (props) => { try { if (checkMultiframe() && studyExist && checkAllSameSeries(data[projectID][patientID][studyUID].list) && !data[projectID][patientID][studyUID].mfMerged) { if (!studyInGrid && !seriesCallSentRef.current.has(key)) { - getSeries(projectID, patientID, studyUID, false, 'seriesdropdown, checkMultiframe').then(res => { + getSeriesWithFallback(projectID, patientID, studyUID, 'seriesdropdown, checkMultiframe').then(res => { const newList = mergeLists(data[projectID][patientID][studyUID], res.data); props.dispatch(setSeriesData(projectID, patientID, studyUID, newList, true, true)); setLoading(false); @@ -143,7 +150,7 @@ const SeriesDropDown = (props) => { } else { if (!studyInGrid && !seriesCallSentRef.current.has(key)) { setLoading(true); - getSeries(projectID, patientID, studyUID, false, 'series dropdown, 2').then(res => { + getSeriesWithFallback(projectID, patientID, studyUID, 'series dropdown, 2').then(res => { props.dispatch(setSeriesData(projectID, patientID, studyUID, res.data, true, 'here')); setLoading(false); seriesCallSentRef.current.add(key); diff --git a/src/components/display/SeriesOrderModal.css b/src/components/display/SeriesOrderModal.css new file mode 100644 index 000000000..07fbdb3e5 --- /dev/null +++ b/src/components/display/SeriesOrderModal.css @@ -0,0 +1,248 @@ +/* ── Bootstrap Modal overrides ─────────────────────────────── */ +.series-order-modal .modal-content, +.som-warn-modal .modal-content { + background-color: #1a2035; + border: 1px solid #2e3a50; + color: #d0d8e8; +} + +.series-order-modal .modal-header, +.som-warn-modal .modal-header { + background-color: #1a2035; + border-bottom: 1px solid #2e3a50; + color: #d0d8e8; + display: flex; + align-items: center; + justify-content: space-between; +} + +.series-order-modal .modal-title, +.som-warn-modal .modal-title { + color: #d0d8e8; + font-size: 1rem; +} + +.series-order-modal .modal-header .btn-close, +.som-warn-modal .modal-header .btn-close { + filter: invert(1) grayscale(1) brightness(1.5); + opacity: 0.7; +} + +.series-order-modal .modal-header .btn-close:hover, +.som-warn-modal .modal-header .btn-close:hover { + opacity: 1; +} + +.series-order-modal .modal-body, +.som-warn-modal .modal-body { + background-color: #1a2035; + color: #d0d8e8; +} + +.series-order-modal .modal-footer, +.som-warn-modal .modal-footer { + background-color: #1a2035; + border-top: 1px solid #2e3a50; +} + +/* ── Loading ──────────────────────────────────────────────── */ +.som-loading { + text-align: center; + padding: 2rem; + color: #aaa; +} + +/* ── Grid + actions row ───────────────────────────────────── */ +.som-grid-row { + display: flex; + align-items: flex-start; + gap: 16px; + margin-bottom: 12px; +} + +.som-grid { + display: grid; + grid-template-columns: 1fr 1fr; + grid-template-rows: 1fr 1fr; + gap: 8px; + flex: 0 0 auto; + width: 360px; +} + +.som-slot { + height: 72px; + border-radius: 4px; + display: flex; + align-items: center; + justify-content: center; + border: 2px dashed #3a4a5e; + transition: border-color 0.15s; +} + +.som-slot--empty { + background: #151d2e; +} + +.som-slot--filled { + border-style: solid; + border-color: #4a7ab5; + background: #1e2a3a; +} + +.som-slot--droptarget:hover { + border-color: #aadcff; +} + +.som-slot-number { + font-size: 1.4rem; + color: #3a4a5e; + font-weight: bold; +} + +/* ── Series chip ──────────────────────────────────────────── */ +.som-chip { + display: inline-block; + background: #2d4a6e; + color: #cce0ff; + border-radius: 3px; + padding: 4px 8px; + font-size: 0.78rem; + cursor: grab; + max-width: 160px; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + user-select: none; +} + +.som-chip:active { + cursor: grabbing; +} + +.som-chip--list { + margin: 4px; + max-width: 100%; +} + +/* ── Grid action buttons ──────────────────────────────────── */ +.som-grid-actions { + display: flex; + flex-direction: column; + gap: 8px; + padding-top: 4px; +} + +/* ── Page navigation ──────────────────────────────────────── */ +.som-pagination { + display: flex; + align-items: center; + gap: 4px; + margin-bottom: 12px; +} + +.som-page-nav, +.som-page-num { + background: #222c3e; + border: 1px solid #3a4a5e; + color: #aab8cc; + border-radius: 3px; + padding: 2px 8px; + cursor: pointer; + font-size: 0.85rem; + transition: background 0.15s; +} + +.som-page-nav:disabled { + opacity: 0.3; + cursor: not-allowed; +} + +.som-page-nav:not(:disabled):hover, +.som-page-num:hover { + background: #4a7ab5; + color: #fff; +} + +.som-page-num--active { + background: #4a7ab5; + color: #fff; + border-color: #4a7ab5; +} + +/* ── Separator between grid area and unordered panel ─────── */ +.som-divider { + border: none; + border-top: 1px solid #2e3a50; + margin: 12px 0; +} + +/* ── Unordered list ───────────────────────────────────────── */ +.som-unordered { + border: 1px solid #2e3a50; + border-radius: 4px; + min-height: 80px; + padding: 8px; + background: #151d2e; +} + +.som-unordered-label { + font-size: 0.85rem; + color: #5a6a80; + margin-bottom: 8px; +} + +.som-unordered-empty { + font-size: 0.85rem; + color: #4a5a6e; + padding: 8px 0; +} + +/* ── Buttons ──────────────────────────────────────────────── */ +.som-btn { + padding: 6px 16px; + border-radius: 3px; + border: none; + cursor: pointer; + font-size: 0.85rem; + transition: opacity 0.15s, background 0.15s; +} + +.som-btn:disabled { + opacity: 0.4; + cursor: not-allowed; +} + +.som-btn--primary { + background: #4a7ab5; + color: #fff; +} + +.som-btn--primary:hover:not(:disabled) { + background: #5a8ac5; +} + +.som-btn--secondary { + background: #1e2a3a; + color: #aab8cc; + border: 1px solid #3a4a5e; +} + +.som-btn--secondary:hover:not(:disabled) { + background: #283a52; +} + +/* ── Warning modal body text ──────────────────────────────── */ +.som-warn-modal .modal-body p { + color: #c0cce0; + font-size: 0.9rem; +} + +.som-warn-check { + display: flex; + align-items: center; + font-size: 0.85rem; + color: #aab8cc; + cursor: pointer; + margin-top: 8px; + gap: 6px; +} diff --git a/src/components/display/SeriesOrderModal.jsx b/src/components/display/SeriesOrderModal.jsx new file mode 100644 index 000000000..7db429000 --- /dev/null +++ b/src/components/display/SeriesOrderModal.jsx @@ -0,0 +1,341 @@ +import React, { useState, useEffect, useRef } from 'react'; +import Modal from 'react-bootstrap/Modal'; +import { toast } from 'react-toastify'; +import { getSeries } from '../../services/seriesServices'; +import { setSignificantSeries } from '../../services/seriesServices'; +import './SeriesOrderModal.css'; + +const SLOTS = 4; +const WARN_KEY = 'noSeriesStateWarn'; + +const hasDisplayState = (serie) => + serie.displayState && + Object.values(serie.displayState).some(v => v !== null && v !== '' && v !== undefined); + +export default function SeriesOrderModal({ show, onClose, onSaved, projectID, subjectUID, studyUID }) { + const [pages, setPages] = useState([Array(SLOTS).fill(null)]); + const [unordered, setUnordered] = useState([]); + const [currentPage, setCurrentPage] = useState(0); + const [loading, setLoading] = useState(false); + const [dragSource, setDragSource] = useState(null); + const [showStateWarn, setShowStateWarn] = useState(false); + const [dontShowAgain, setDontShowAgain] = useState(false); + const [showEmptyWarn, setShowEmptyWarn] = useState(false); + const [emptyPages, setEmptyPages] = useState([]); + const [pendingSavePages, setPendingSavePages] = useState(null); + const stateWarnShown = useRef(false); + const initialWithState = useRef(new Set()); + + useEffect(() => { + if (show) { + stateWarnShown.current = false; + initialWithState.current = new Set(); + setCurrentPage(0); + setShowStateWarn(false); + setShowEmptyWarn(false); + setPendingSavePages(null); + loadData(); + } + }, [show]); + + const loadData = async () => { + setLoading(true); + try { + let { data: seriesArr } = await getSeries(projectID, subjectUID, studyUID, false); + if (!seriesArr || seriesArr.length === 0) { + ({ data: seriesArr } = await getSeries(projectID, subjectUID, studyUID, true)); + } + + const ordered = seriesArr.filter(s => s.significanceOrder != null); + const unorderedArr = seriesArr.filter(s => s.significanceOrder == null); + + // Track which initially-ordered series have a displayState + ordered.forEach(s => { if (hasDisplayState(s)) initialWithState.current.add(s.seriesUID); }); + + // Build page grid. Series whose significanceOrder overflows a page (e.g. legacy MG + // data with significanceOrder 1–8 and no pageOrder) wrap to the next page automatically. + const pageMap = {}; + ordered.forEach(s => { + const basePage = Math.max((s.pageOrder != null ? s.pageOrder : 1) - 1, 0); + const slotIdx = (s.significanceOrder || 1) - 1; + const pageIdx = basePage + Math.floor(slotIdx / SLOTS); + const effectiveSlot = slotIdx % SLOTS; + if (!pageMap[pageIdx]) pageMap[pageIdx] = Array(SLOTS).fill(null); + pageMap[pageIdx][effectiveSlot] = s; + }); + + const maxPage = ordered.length > 0 ? Math.max(...Object.keys(pageMap).map(Number)) : 0; + const pagesArr = []; + for (let i = 0; i <= maxPage; i++) { + pagesArr.push(pageMap[i] || Array(SLOTS).fill(null)); + } + + setPages(pagesArr); + setUnordered(unorderedArr); + } catch (err) { + toast.error('Could not load series data'); + } finally { + setLoading(false); + } + }; + + // --- Drag helpers --- + + const handleDragStart = (source) => setDragSource(source); + + const handleDropOnSlot = (targetSlot) => { + if (!dragSource) return; + const newPages = pages.map(p => [...p]); + const newUnordered = [...unordered]; + let draggedSerie; + + if (dragSource.type === 'slot') { + draggedSerie = newPages[dragSource.page][dragSource.slot]; + newPages[dragSource.page][dragSource.slot] = null; + } else { + draggedSerie = newUnordered.splice(dragSource.index, 1)[0]; + } + + const displaced = newPages[currentPage][targetSlot]; + newPages[currentPage][targetSlot] = draggedSerie; + + if (displaced) { + if (dragSource.type === 'slot') { + newPages[dragSource.page][dragSource.slot] = displaced; + } else { + newUnordered.push(displaced); + maybeShowStateWarn(displaced); + } + } + + setPages(newPages); + setUnordered(newUnordered); + setDragSource(null); + }; + + const handleDropOnList = () => { + if (!dragSource || dragSource.type === 'list') return; + const newPages = pages.map(p => [...p]); + const draggedSerie = newPages[dragSource.page][dragSource.slot]; + if (!draggedSerie) return; + + newPages[dragSource.page][dragSource.slot] = null; + maybeShowStateWarn(draggedSerie); + setPages(newPages); + setUnordered(prev => [...prev, draggedSerie]); + setDragSource(null); + }; + + const maybeShowStateWarn = (serie) => { + if (stateWarnShown.current) return; + if (localStorage.getItem(WARN_KEY) === 'true') return; + if (initialWithState.current.has(serie.seriesUID)) { + setShowStateWarn(true); + stateWarnShown.current = true; + } + }; + + // --- Grid actions --- + + const handleClearGrid = () => { + const newPages = pages.map(p => [...p]); + const cleared = newPages[currentPage].filter(Boolean); + newPages[currentPage] = Array(SLOTS).fill(null); + cleared.forEach(maybeShowStateWarn); + setPages(newPages); + setUnordered(prev => [...prev, ...cleared]); + }; + + const handleAddPage = () => { + setPages(prev => [...prev, Array(SLOTS).fill(null)]); + setCurrentPage(pages.length); + }; + + // --- Save --- + + const handleSave = () => { + // Find empty pages sandwiched between non-empty pages + const nonEmptyIndices = pages.reduce((acc, p, i) => { if (p.some(Boolean)) acc.push(i); return acc; }, []); + if (nonEmptyIndices.length > 1) { + const first = nonEmptyIndices[0]; + const last = nonEmptyIndices[nonEmptyIndices.length - 1]; + const emptyBetween = []; + for (let i = first + 1; i < last; i++) { + if (!pages[i].some(Boolean)) emptyBetween.push(i + 1); + } + if (emptyBetween.length > 0) { + setEmptyPages(emptyBetween); + setPendingSavePages(pages); + setShowEmptyWarn(true); + return; + } + } + doSave(pages); + }; + + const doSave = (pagesArr) => { + const nonEmptyPages = pagesArr.filter(p => p.some(Boolean)); + const payload = []; + nonEmptyPages.forEach((page, pageIdx) => { + page.forEach((serie, slotIdx) => { + if (!serie) return; + const record = { seriesUID: serie.seriesUID, significanceOrder: slotIdx + 1, pageOrder: pageIdx + 1 }; + if (hasDisplayState(serie)) record.displayState = serie.displayState; + payload.push(record); + }); + }); + setSignificantSeries(projectID, subjectUID, studyUID, payload, true) + .then(() => { + toast.success('Series order saved!'); + if (onSaved) onSaved(); + onClose(); + }) + .catch(() => toast.error('Could not save series order')); + }; + + // --- Render helpers --- + + const seriesLabel = (serie) => + `${serie.seriesNo != null ? serie.seriesNo : '–'} · ${serie.seriesDescription || serie.examType || ''}`; + + const currentSlots = pages[currentPage] || Array(SLOTS).fill(null); + + return ( + <> + + + Series Display Order + + + {loading ? ( +
Loading series…
+ ) : ( + <> + {/* Grid + action buttons */} +
+
+ {currentSlots.map((serie, slotIdx) => ( +
e.preventDefault()} + onDrop={() => handleDropOnSlot(slotIdx)} + > + {serie ? ( + handleDragStart({ type: 'slot', page: currentPage, slot: slotIdx, serie })} + > + {seriesLabel(serie)} + + ) : ( + {slotIdx + 1} + )} +
+ ))} +
+
+ + +
+
+ + {/* Page navigation */} +
+ + {pages.map((_, i) => ( + + ))} + +
+ +
+ + {/* Unordered list */} +
e.preventDefault()} + onDrop={handleDropOnList} + > +
── Unordered Series ──
+ {unordered.length === 0 &&
All series are assigned to pages.
} + {unordered.map((serie, idx) => ( + handleDragStart({ type: 'list', index: idx, serie })} + > + {seriesLabel(serie)} + + ))} +
+ + )} +
+ + + + +
+ + {/* Display state loss warning */} + {showStateWarn && ( + setShowStateWarn(false)} size="sm" className="som-warn-modal"> + + ⚠ Saved State Will Be Lost + + +

Moving a series out of the order permanently removes its saved display state (window/level, zoom, invert). This cannot be undone.

+ +
+ + + +
+ )} + + {/* Empty page warning */} + {showEmptyWarn && ( + setShowEmptyWarn(false)} size="sm" className="som-warn-modal"> + + ⚠ Page {emptyPages[0]} is Empty + + +

+ Page {emptyPages[0]} has no series assigned. If you continue, pages will be renumbered: + Page {emptyPages[0] + 1} will become Page {emptyPages[0]}. +

+
+ + + + +
+ )} + + ); +} diff --git a/src/components/display/displayView.jsx b/src/components/display/displayView.jsx index 4ab4db97b..5caafce85 100644 --- a/src/components/display/displayView.jsx +++ b/src/components/display/displayView.jsx @@ -6,12 +6,14 @@ import * as cornerstoneWADOImageLoader from "cornerstone-wado-image-loader"; import * as dcmjs from "dcmjs"; import _ from "lodash"; import CornerstoneViewport from "react-cornerstone-viewport"; -import { FaExpandArrowsAlt, FaPen, FaTag, FaTimes } from "react-icons/fa"; +import { FaExpandArrowsAlt, FaPen, FaTag, FaTimes, FaRegSquare, FaCheckSquare } from "react-icons/fa"; +import { unstable_batchedUpdates } from "react-dom"; import { connect } from "react-redux"; import { Redirect } from "react-router"; import { withRouter } from "react-router-dom"; import PropagateLoader from "react-spinners/PropagateLoader"; import { deleteAnnotation, getAnnotation } from "../../services/annotationServices"; +import { getSignificantSeries, setSignificantSeries } from "../../services/seriesServices"; import { refreshToken } from "../../services/authService"; import { getImageMetadata } from "../../services/imageServices"; import { @@ -28,19 +30,25 @@ import { aimDelete, changeActivePort, clearAimId, + clearGrid, clearMultiFrameAimJumpFlags, clearSelection, closeSerie, getSingleSerie, jumpToAim, setSegLabelMapIndex, - setSeriesData + setSeriesData, + setMammogramPage, + setMammogramSeries, + setPageOrder, + setPageOrderSeries, // fillSeriesDescfullData - , updateGridWithMultiFrameInfo, updateImageId, updateSubpath } from "../annotationsList/action"; +import { openSeriesInDisplay } from "../common/openSeriesHelper"; +import { isSupportedModality } from "../../Utils/aid.js"; import { arrow } from "./Arrow"; import { bidirectional } from "./Bidirectional"; import { circle } from "./Circle"; @@ -53,6 +61,7 @@ import "./viewport.css"; import { toast } from "react-toastify"; import SeriesDropDown from "./SeriesDropDown"; import getVPDimensions from "./ViewportCalculations"; +import SeriesOrderModal from "./SeriesOrderModal"; let mode; let wadoUrl; @@ -162,6 +171,10 @@ const mapStateToProps = (state) => { lastLocation: state.annotationsListReducer.lastLocation, projectMap: state.annotationsListReducer.projectMap, showingPHI: state.annotationsListReducer.showingPHI, + mammogramSeries: state.annotationsListReducer.mammogramSeries, + mammogramPageIndex: state.annotationsListReducer.mammogramPageIndex, + pageOrderSeries: state.annotationsListReducer.pageOrderSeries, + currentPageOrder: state.annotationsListReducer.currentPageOrder, }; }; @@ -204,6 +217,15 @@ class DisplayView extends Component { dataIndexMap: {}, aimEdited: false, isVisible: true, + selectedPorts: new Set(), + mammoExpanded: false, + hiddenPorts: new Set(), + expandedOrder: null, + trimMode: false, + trimmedDimensions: {}, + showReorderModal: false, + showSaveStatusWarning: false, + pendingSaveStatusData: null, }; } @@ -233,6 +255,8 @@ class DisplayView extends Component { this.setState({ wwwc: { ww, wc } }); }; + _explicitlyReset = new Set(); + componentDidMount() { const { series, onSwitchView } = this.props; // if (series.length < 1) { @@ -240,6 +264,10 @@ class DisplayView extends Component { // } this.props.dispatch(clearSelection()); this.getViewports(); + this._explicitlyReset.clear(); + sessionStorage.setItem('invertMap', JSON.stringify({})); + sessionStorage.setItem('imgStatus', JSON.stringify([])); + this.applyDisplayStateToSession(); this.getData(undefined, undefined, "componentDidMount"); this.formInvertMap(); if (series.length > 0) { @@ -337,6 +365,12 @@ class DisplayView extends Component { return; } + // Clear mammogram dot selections when a new study replaces the current one. + if (prevProps.mammogramSeries !== this.props.mammogramSeries && + (this.state.selectedPorts.size > 0 || this.state.mammoExpanded)) { + this.clearMammoSelection(); + } + const { projectID, studyUID } = series[activePort]; let { seriesUID } = series[activePort]; if (cornerstone.getEnabledElements()[activePort]) { @@ -425,6 +459,7 @@ class DisplayView extends Component { mfIndex = `${seriesAddition[activePort].multiFrameIndex}-${activePort}`; frame = 0; } + this.applyDisplayStateToSession(); this.getData(mfIndex, frame, "didupdated 2", refreshPage); this.formInvertMap(); } @@ -1097,9 +1132,10 @@ class DisplayView extends Component { indexKey = `${indexKey}-${mfIndexFinal}` } - const dataExistsInState = parseInt(dataIndexMap[indexKey]) >= 0; + const cachedIdx = parseInt(dataIndexMap[indexKey]); + const dataExistsInState = cachedIdx >= 0 && cachedIdx < this.state.data.length && !!this.state.data[cachedIdx]; - if (!dataExistsInState || force) { + if (!dataExistsInState || force) { const promise = this.getImageStack( series[i], i, @@ -2695,6 +2731,7 @@ class DisplayView extends Component { const max = parseInt(maxPort); imgStatus = imgStatus ? JSON.parse(imgStatus) : new Array(max); imgStatus[this.props.activePort] = null; + this._explicitlyReset.add(this.props.activePort); this.formInvertMap(null, null, true); sessionStorage.setItem("imgStatus", JSON.stringify(imgStatus)); }; @@ -2903,7 +2940,6 @@ class DisplayView extends Component { }; toggleOverlay = (e, i) => { - if (!this.props.showingPHI && mode === 'teaching') return; const showHide = { ...this.state.isOverlayVisible }; const index = i || i === 0 ? i : this.props.activePort; if (showHide[index]) delete showHide[index]; @@ -2965,6 +3001,488 @@ class DisplayView extends Component { } }; + // --- Mammogram pagination --- + + /** True when the currently open series are all mammograms and there is stored page data. */ + isMammogramOpen = () => { + const { series, mammogramSeries } = this.props; + const hasMammoSeries = !!(mammogramSeries && mammogramSeries.length > 0); + const hasOpenMG = !!(series && series.some(s => s && (s.examType || s.modality)?.toUpperCase() === 'MG')); + return hasMammoSeries && hasOpenMG; + }; + + /** True when pageOrder navigation is active (Case 1). */ + isPageOrderNav = () => this.props.pageOrderSeries && this.props.pageOrderSeries.length > 0; + + /** True when there is at least one more page of mammogram series to load. */ + hasNextMammoPage = () => { + if (this.isPageOrderNav()) return false; + const { mammogramSeries, mammogramPageIndex } = this.props; + return (mammogramPageIndex + 1) * parseInt(maxPort) < mammogramSeries.length; + }; + + /** True when the user is past the first page and can go back. */ + hasPrevMammoPage = () => { + if (this.isPageOrderNav()) return false; + return this.props.mammogramPageIndex > 0; + }; + + /** True when there is a higher pageOrder page available. */ + hasNextPageOrderPage = () => { + const { pageOrderSeries, currentPageOrder } = this.props; + return pageOrderSeries.some(s => s.pageOrder === currentPageOrder + 1); + }; + + /** True when the user is past page 1 in pageOrder navigation. */ + hasPrevPageOrderPage = () => { + return this.props.currentPageOrder > 1; + }; + + /** Clears the grid and loads the next pageOrder page. */ + clearPageSessionStorage = () => { + this._explicitlyReset.clear(); + sessionStorage.setItem('invertMap', JSON.stringify({})); + sessionStorage.setItem('imgStatus', JSON.stringify([])); + }; + + handlePageOrderNext = () => { + const { pageOrderSeries, currentPageOrder } = this.props; + const nextPage = currentPageOrder + 1; + const nextSeries = pageOrderSeries + .filter(s => s.pageOrder === nextPage) + .sort((a, b) => (a.significanceOrder || 0) - (b.significanceOrder || 0)); + if (!nextSeries.length) return; + + this.clearMammoSelection(); + this.clearPageSessionStorage(); + this.props.dispatch(clearGrid()); + this.props.dispatch(setPageOrder(nextPage)); + openSeriesInDisplay({ + dispatch: this.props.dispatch, + navigate: () => {}, + openSeries: [], + series: nextSeries, + existingData: pageOrderSeries, + }); + }; + + /** Clears the grid and loads the previous pageOrder page. */ + handlePageOrderPrev = () => { + const { pageOrderSeries, currentPageOrder } = this.props; + const prevPage = currentPageOrder - 1; + if (prevPage < 1) return; + + const prevSeries = pageOrderSeries + .filter(s => s.pageOrder === prevPage) + .sort((a, b) => (a.significanceOrder || 0) - (b.significanceOrder || 0)); + + this.clearMammoSelection(); + this.clearPageSessionStorage(); + this.props.dispatch(clearGrid()); + this.props.dispatch(setPageOrder(prevPage)); + openSeriesInDisplay({ + dispatch: this.props.dispatch, + navigate: () => {}, + openSeries: [], + series: prevSeries, + existingData: pageOrderSeries, + }); + }; + + /** Clears the grid and loads the next group of MAMMO_PAGE_SIZE series. */ + handleMammoNext = () => { + const { mammogramSeries, mammogramPageIndex } = this.props; + const nextPage = mammogramPageIndex + 1; + const pageSize = parseInt(maxPort); + const start = nextPage * pageSize; + const nextSeries = mammogramSeries.slice(start, start + pageSize); + if (!nextSeries.length) return; + + this.clearMammoSelection(); + this.clearPageSessionStorage(); + this.props.dispatch(clearGrid()); + this.props.dispatch(setMammogramPage(nextPage)); + openSeriesInDisplay({ + dispatch: this.props.dispatch, + navigate: () => {}, + openSeries: [], + series: nextSeries, + existingData: mammogramSeries, + }); + }; + + /** Clears the grid and loads the previous group of MAMMO_PAGE_SIZE series. */ + handleMammoPrev = () => { + const { mammogramSeries, mammogramPageIndex } = this.props; + const prevPage = mammogramPageIndex - 1; + if (prevPage < 0) return; + + const pageSize = parseInt(maxPort); + const start = prevPage * pageSize; + const prevSeries = mammogramSeries.slice(start, start + pageSize); + + this.clearMammoSelection(); + this.clearPageSessionStorage(); + this.props.dispatch(clearGrid()); + this.props.dispatch(setMammogramPage(prevPage)); + openSeriesInDisplay({ + dispatch: this.props.dispatch, + navigate: () => {}, + openSeries: [], + series: prevSeries, + existingData: mammogramSeries, + }); + }; + + /** Toggle selection of a viewport dot. Max 2 at a time. */ + handleMammoDotClick = (index) => { + const { selectedPorts } = this.state; + const next = new Set(selectedPorts); + if (next.has(index)) { + next.delete(index); + } else { + if (next.size >= 2) { + toast.warn("Only two viewports can be selected at a time.", { + position: "top-right", + autoClose: 3000, + }); + return; + } + next.add(index); + } + this.setState({ selectedPorts: next }); + }; + + /** + * EXPAND: if two viewports are selected, show them side by side. + * Otherwise fall back to the standard single-viewport hideShow. + * RESTORE: return to the standard grid layout. + */ + handleMammoExpand = () => { + const { activePort } = this.props; + const { selectedPorts, mammoExpanded, containerHeight, data } = this.state; + + if (mammoExpanded) { + this.restoreMammoExpand(); + return; + } + + if (selectedPorts.size === 2) { + // selectedArr preserves insertion order: [firstSelected, secondSelected] + const selectedArr = Array.from(selectedPorts); + const hidden = new Set(data.map((_, i) => i).filter(i => !selectedArr.includes(i))); + this.setState( + { mammoExpanded: true, width: "50%", height: containerHeight, hiddenPorts: hidden, expandedOrder: selectedArr }, + () => window.dispatchEvent(new CustomEvent("resize", { detail: { isMaximize: true } })) + ); + } else { + this.hideShow(activePort); + } + }; + + /** Restore all viewports to the standard grid layout and clear dot selections. */ + restoreMammoExpand = () => { + this.setState( + { mammoExpanded: false, selectedPorts: new Set(), hiddenPorts: new Set(), expandedOrder: null, trimMode: false, trimmedDimensions: {} }, + () => { + this.getViewports(); + window.dispatchEvent(new CustomEvent("resize", { detail: { isMaximize: false } })); + } + ); + }; + + /** Clear mammogram selection state (dots + expand). Called on NEXT and new study. */ + clearMammoSelection = () => { + this.setState({ selectedPorts: new Set(), mammoExpanded: false, hiddenPorts: new Set(), expandedOrder: null, trimMode: false, trimmedDimensions: {} }); + }; + + handleTrimMode = () => { + const next = !this.state.trimMode; + const { expandedOrder, hiddenPorts, data } = this.state; + // When expanded: trim only the 2 visible expanded viewports. + // When not expanded: trim all visible viewports. + const targetIndices = expandedOrder + ? expandedOrder + : data.map((_, i) => i).filter(i => !hiddenPorts.has(i)); + + const resizeTargets = () => { + setTimeout(() => { + targetIndices.forEach(i => { + try { + const elements = cornerstone.getEnabledElements(); + const el = elements[i] && elements[i].element; + if (el) cornerstone.resize(el, true); + } catch (e) {} + }); + }, 50); + }; + + if (!next) { + this.setState({ trimMode: false, trimmedDimensions: {} }, resizeTargets); + return; + } + + const trimmedDimensions = {}; + targetIndices.forEach(i => { + try { + const containerEl = this.viewportRefs[i] && this.viewportRefs[i].current; + const elements = cornerstone.getEnabledElements(); + const el = elements[i] && elements[i].element; + if (!containerEl || !el) return; + const enabledEl = cornerstone.getEnabledElement(el); + const image = enabledEl && enabledEl.image; + if (!image) return; + const imageAspect = image.columns / image.rows; + const containerW = containerEl.clientWidth; + const containerH = containerEl.clientHeight; + if (imageAspect < containerW / containerH) { + // Portrait image: black bars on sides → shrink width + trimmedDimensions[i] = { width: Math.round(containerH * imageAspect) + 'px', height: null }; + } else { + // Landscape image: black bars top/bottom → shrink height + trimmedDimensions[i] = { width: null, height: Math.round(containerW / imageAspect) + 'px' }; + } + } catch (e) {} + }); + + this.setState({ trimMode: true, trimmedDimensions }, resizeTargets); + }; + + // --- End mammogram pagination --- + + /** Re-fetch series after reorder save and reload the display with page 1. */ + refreshAfterReorder = async () => { + const active = this.props.series[this.props.activePort] || {}; + const { projectID, patientID, studyUID } = active; + if (!projectID || !patientID || !studyUID) return; + try { + let { data: series } = await getSeries(projectID, patientID, studyUID, false); + if (!series || series.length === 0) { + ({ data: series } = await getSeries(projectID, patientID, studyUID, true)); + } + series = (series || []).filter(isSupportedModality); + if (series.length === 0) return; + + const significant = series.filter(s => s.significanceOrder != null); + const hasPageOrder = significant.length > 0 && significant.some(s => s.pageOrder != null); + const maxPort = parseInt(sessionStorage.getItem('maxPort')); + + // Batch dispatches so React doesn't render the transient empty-openSeries state + // (which would trigger back to the search/list view). + unstable_batchedUpdates(() => { + this.clearMammoSelection(); + this.clearPageSessionStorage(); + this.props.dispatch(clearGrid()); + this.props.dispatch(setSeriesData(projectID, patientID, studyUID, series, true)); + + if (hasPageOrder) { + this.props.dispatch(setPageOrderSeries(significant)); + const pageOne = significant + .filter(s => s.pageOrder === 1) + .sort((a, b) => (a.significanceOrder || 0) - (b.significanceOrder || 0)); + openSeriesInDisplay({ + dispatch: this.props.dispatch, + navigate: () => {}, + openSeries: [], + series: pageOne.length ? pageOne : significant.slice(0, maxPort), + existingData: series, + }); + } else { + this.props.dispatch(setMammogramSeries(series, studyUID)); + const toDisplay = significant.length > 0 + ? significant.sort((a, b) => (a.significanceOrder || 0) - (b.significanceOrder || 0)) + : series.slice(0, maxPort); + openSeriesInDisplay({ + dispatch: this.props.dispatch, + navigate: () => {}, + openSeries: [], + series: toDisplay, + existingData: series, + }); + } + }); + // After React commits the batched dispatches, force the local viewport + // data state to reload the new series' image stacks. + setTimeout(() => { + this.setState({ isLoading: true, data: [], dataIndexMap: {} }, () => { + this.getViewports(); + this.getData(null, null, 'afterReorder', true); + }); + }, 0); + } catch (err) { + console.error('refreshAfterReorder error', err); + } + }; + + // --- Display State Restore --- + + applyDisplayStateToSession = () => { + const { series, pageOrderSeries } = this.props; + if (!series || series.length === 0) return; + + // Build seriesUID → displayState map. pageOrderSeries carries the full + // significant-series records from the backend (including displayState). + // openSeries entries (series[i]) may also carry it if addToGrid preserved it. + const displayStateMap = {}; + (pageOrderSeries || []).forEach(s => { + if (s && s.displayState) displayStateMap[s.seriesUID] = s.displayState; + }); + series.forEach(s => { + if (s && s.displayState) displayStateMap[s.seriesUID] = s.displayState; + }); + + if (Object.keys(displayStateMap).length === 0) return; + + const invertMap = JSON.parse(sessionStorage.getItem('invertMap') || '{}'); + const imgStatus = JSON.parse(sessionStorage.getItem('imgStatus') || '[]'); + let changed = false; + + for (let i = 0; i < series.length; i++) { + const s = series[i]; + if (!s) continue; + const displayState = displayStateMap[s.seriesUID]; + if (!displayState) continue; + // Only apply saved state if no live session value exists for this viewport + if (displayState.invertMap !== undefined && invertMap[i] == null && !this._explicitlyReset.has(i)) { + invertMap[i] = displayState.invertMap; + changed = true; + } + if (displayState.imageStatus && imgStatus[i] == null && !this._explicitlyReset.has(i)) { + imgStatus[i] = displayState.imageStatus; + changed = true; + } + } + + if (changed) { + sessionStorage.setItem('invertMap', JSON.stringify(invertMap)); + sessionStorage.setItem('imgStatus', JSON.stringify(imgStatus)); + } + }; + + // --- End Display State Restore --- + + // --- Save Image Status --- + + handleSaveState = async () => { + const { series } = this.props; + if (!series || series.length === 0) return; + + const invertMap = JSON.parse(sessionStorage.getItem('invertMap') || '{}'); + const imgStatus = JSON.parse(sessionStorage.getItem('imgStatus') || '[]'); + + // Collect series with any modified state + const modifiedSeries = []; + for (let i = 0; i < series.length; i++) { + if (!series[i]) continue; + const hasInvert = invertMap[i] !== undefined; + const hasImgStatus = imgStatus[i] !== undefined && imgStatus[i] !== null; + if (hasInvert || hasImgStatus) { + modifiedSeries.push({ + ...series[i], + viewportIndex: i, + displayState: { + invertMap: invertMap[i] || false, + imageStatus: imgStatus[i] || {}, + }, + }); + } + } + + if (modifiedSeries.length === 0) return; + + const ref = series.find(s => s) || {}; + const projectID = ref.projectID; + const patientID = ref.patientID || ref.subjectID; + const studyUID = ref.studyUID; + + try { + const { data: currentSigSeries } = await getSignificantSeries(projectID, patientID, studyUID); + const sigUIDs = new Set((currentSigSeries || []).map(s => s.seriesUID)); + const notYetSignificant = modifiedSeries.filter(s => !sigUIDs.has(s.seriesUID)); + const saveData = { modifiedSeries, currentSigSeries: currentSigSeries || [], projectID, patientID, studyUID }; + + if (notYetSignificant.length > 0) { + this.setState({ showSaveStatusWarning: true, pendingSaveStatusData: saveData }); + } else { + await this.executeSaveStatus(saveData); + } + } catch (err) { + console.error(err); + toast.error('Failed to fetch significant series.'); + } + }; + + executeSaveStatus = async ({ modifiedSeries, currentSigSeries, projectID, patientID, studyUID }) => { + // Build a map of existing significant series to preserve their order/pageOrder + const sigMap = {}; + currentSigSeries.forEach(s => { sigMap[s.seriesUID] = { ...s }; }); + + const maxSigOrder = currentSigSeries.reduce((m, s) => Math.max(m, s.significanceOrder || 0), 0); + const maxPage = currentSigSeries.reduce((m, s) => Math.max(m, s.pageOrder || 0), 0); + let nextSigOrder = maxSigOrder + 1; + + modifiedSeries.forEach(s => { + if (sigMap[s.seriesUID]) { + sigMap[s.seriesUID] = { ...sigMap[s.seriesUID], displayState: s.displayState }; + } else { + sigMap[s.seriesUID] = { + seriesUID: s.seriesUID, + significanceOrder: nextSigOrder++, + pageOrder: maxPage > 0 ? maxPage : 1, + displayState: s.displayState, + }; + } + }); + + try { + await setSignificantSeries(projectID, patientID, studyUID, Object.values(sigMap), true); + + // Clear saved entries from session storage + const invertMap = JSON.parse(sessionStorage.getItem('invertMap') || '{}'); + const imgStatus = JSON.parse(sessionStorage.getItem('imgStatus') || '[]'); + const savedUIDs = new Set(modifiedSeries.map(s => s.seriesUID)); + const { series } = this.props; + for (let i = 0; i < series.length; i++) { + if (series[i] && savedUIDs.has(series[i].seriesUID)) { + delete invertMap[i]; + imgStatus[i] = null; + } + } + sessionStorage.setItem('invertMap', JSON.stringify(invertMap)); + sessionStorage.setItem('imgStatus', JSON.stringify(imgStatus)); + + // Fetch updated state and reconstruct session storage + const { data: updatedSigSeries } = await getSignificantSeries(projectID, patientID, studyUID); + const newInvertMap = { ...invertMap }; + const newImgStatus = [...imgStatus]; + + (updatedSigSeries || []).forEach(sig => { + if (!sig.displayState) return; + const vpIndex = series.findIndex(s => s && s.seriesUID === sig.seriesUID); + if (vpIndex === -1) return; + if (sig.displayState.invertMap !== undefined) newInvertMap[vpIndex] = sig.displayState.invertMap; + if (sig.displayState.imageStatus) newImgStatus[vpIndex] = sig.displayState.imageStatus; + }); + + sessionStorage.setItem('invertMap', JSON.stringify(newInvertMap)); + sessionStorage.setItem('imgStatus', JSON.stringify(newImgStatus)); + + // Trigger Cornerstone re-render to reflect saved state + const elements = cornerstone.getEnabledElements(); + elements.forEach(({ element }) => { + try { cornerstone.updateImage(element); } catch (e) {} + }); + + toast.success('Image status saved.'); + this.setState({ showSaveStatusWarning: false, pendingSaveStatusData: null }); + } catch (err) { + console.error(err); + toast.error('Failed to save image status.'); + } + }; + + // --- End Save Image Status --- + openNextWLStudy = async (worklistID, studyUID) => { try { const sortedData = JSON.parse(sessionStorage.getItem("sortedListMap")) || {}; @@ -3020,6 +3538,11 @@ class DisplayView extends Component { const redirect = mode === "teaching" ? "search" : "list"; let invertMap = sessionStorage.getItem("invertMap"); invertMap = invertMap ? JSON.parse(invertMap) : {}; + const { trimMode, width: stateWidth } = this.state; + const vpGridCols = trimMode ? (Math.round(100 / parseFloat(stateWidth)) || 2) : undefined; + const vpWrapperStyle = trimMode + ? { display: 'grid', gridTemplateColumns: `repeat(${vpGridCols}, 1fr)`, alignContent: 'start' } + : { display: 'flex', flexWrap: 'wrap', alignContent: 'flex-start' }; return !Object.entries(series).length ? ( @@ -3044,7 +3567,54 @@ class DisplayView extends Component { onFuseNewImage={this.newImageFuse} onOpenSeries={this.props.openSeries} openNextWLStudy={this.openNextWLStudy} - /> + onReorder={() => this.setState({ showReorderModal: true })} + onSaveState={this.handleSaveState} + > +
+ + + + + {/* */} +
+ {this.state.isLoading && (
{ +
+ {data.map((data, i) => { + const { hiddenPorts, expandedOrder, trimMode, trimmedDimensions } = this.state; + const trimDim = trimMode && trimmedDimensions && trimmedDimensions[i]; + // In trim/grid mode, align paired viewports toward each other (no gap between them) + const visualCol = trimMode + ? (expandedOrder ? expandedOrder.indexOf(i) : i) % vpGridCols + : -1; + const justifySelf = visualCol >= 0 ? (visualCol % 2 === 0 ? 'end' : 'start') : undefined; return (
this.setActive(i)} > @@ -3164,6 +3744,16 @@ class DisplayView extends Component { > + {this.props.series && this.props.series[i] && (this.props.series[i].examType || this.props.series[i].modality)?.toUpperCase() === 'MG' && ( + { e.stopPropagation(); this.handleMammoDotClick(i); }} + title={this.state.selectedPorts.has(i) ? "Deselect viewport" : "Select viewport for expand"} + > + {this.state.selectedPorts.has(i) ? : } + + )}
{data.stack && data.stack.imageIds && ); })} +
} {/* */} + {this.state.showReorderModal && (() => { + const active = this.props.series[this.props.activePort] || {}; + return ( + this.setState({ showReorderModal: false })} + onSaved={this.refreshAfterReorder} + projectID={active.projectID} + subjectUID={active.patientID} + studyUID={active.studyUID} + /> + ); + })()} + {this.state.showSaveStatusWarning && ( +
+
+
⚠ Series Will Be Added to Significant Series
+

+ One or more series with saved states are not yet in your display order. Saving will add them as significant series. +

+
+ + +
+
+
+ )} ); // diff --git a/src/components/display/viewport.css b/src/components/display/viewport.css index 950a6274e..ecea24063 100644 --- a/src/components/display/viewport.css +++ b/src/components/display/viewport.css @@ -107,7 +107,7 @@ } */ .column.middle-right { - width: 65%; + width: 60%; display: grid; grid-template-columns: 10em 1fr; grid-template-rows: 1fr; @@ -119,9 +119,11 @@ } .column.right { - width: 15%; + width: 20%; + min-width: 48px; float: right; padding-top: 2px; + white-space: nowrap; } /* label { @@ -142,6 +144,25 @@ margin-left: 5px; } +/* Mammogram viewport selection dot */ +.mammo-select-dot { + background: transparent; + border-radius: 0; + color: #aaa; + font-size: 14px; + cursor: pointer; + line-height: 14px; + transition: color 0.15s ease-in-out; +} + +.mammo-select-dot--checked { + color: #4a7ab5; +} + +.mammo-select-dot:hover { + color: #aadcff; +} + .slice-number { margin: -8px; } diff --git a/src/components/flexView/index.jsx b/src/components/flexView/index.jsx index b7a6ba756..416c5c131 100644 --- a/src/components/flexView/index.jsx +++ b/src/components/flexView/index.jsx @@ -14,12 +14,15 @@ import SelectSerieModal from "../annotationsList/selectSerieModal"; import SeriesTable from "./SeriesTable"; import { isSupportedModality } from "../../Utils/aid.js"; import { - addToGrid, - getSingleSerie, clearSelection, setSeriesData, - setLastLocation + setLastLocation, + setMammogramSeries, + clearMammogramSeries, + setPageOrderSeries, + clearPageOrderSeries, } from "../annotationsList/action"; +import { openSeriesInDisplay, isMammogramStudy } from "../common/openSeriesHelper"; import "react-table-v6/react-table.css"; // import "../annotationSearch/annotationSearch.css"; // import "./flexView.css"; @@ -110,9 +113,14 @@ class FlexView extends React.Component { : null; try { + this.props.dispatch(clearPageOrderSeries()); + this.props.dispatch(clearMammogramSeries()); if (!dataExists && seriesCallSent !== studyUID) { this.setState({ loading: true }); - ({ data: series } = await getSeries(projectID, patientID, studyUID)); + ({ data: series } = await getSeries(projectID, patientID, studyUID, false)); + if (!series || series.length === 0) { + ({ data: series } = await getSeries(projectID, patientID, studyUID, true)); + } this.setState({ loading: false }); seriesCallSent = studyUID; this.props.dispatch( @@ -124,38 +132,39 @@ class FlexView extends React.Component { } catch (err) { console.log("Error => getting series of the study", err); } - if (this.props.openSeries.length === this.maxPort) { - this.setState({ showSeriesTable: true, series }); - return; - } - //get only unopen series - if (series.length > 0) series = this.excludeOpenSeries(series); - // filter series that have displayable modality series = series.filter(isSupportedModality); if (series.length === 0) { this.setState({ showWarning: true }); - } else { - //check if there is enough room - if (series.length + this.props.openSeries.length > this.maxPort) { - //if there is not bring the modal - this.setState({ showSeriesTable: true, series }); - // TODO show toast - } else { - //if there is enough room - //add serie to the grid - const promiseArr = []; - for (let i = 0; i < series.length; i++) { - this.props.dispatch(addToGrid(series[i])); - promiseArr.push(this.props.dispatch(getSingleSerie(series[i], null, null, existingData))); - } - //getsingleSerie - Promise.all(promiseArr) - .then(() => { - this.props.history.push("/display"); - }) - .catch((err) => console.error(err)); - } + return; } + + // Mammogram studies load only the first page; remaining series paginate via NEXT. + if (isMammogramStudy(series)) { + this.props.dispatch(setMammogramSeries(series, studyUID)); + openSeriesInDisplay({ + dispatch: this.props.dispatch, + navigate: () => this.props.history.push("/display"), + openSeries: this.props.openSeries, + series: series.slice(0, parseInt(maxPort)), + existingData: series, + }); + return; + } + + // If the study has pageOrder series, store them now so NEXT/PREV work after the modal confirms. + const significant = series.filter(s => s.significanceOrder != null); + if (significant.length > 0 && significant.some(s => s.pageOrder != null)) { + this.props.dispatch(setPageOrderSeries(significant)); + } + + openSeriesInDisplay({ + dispatch: this.props.dispatch, + navigate: () => this.props.history.push("/display"), + openSeries: this.props.openSeries, + series, + existingData, + onGridFull: pending => this.setState({ showSeriesTable: true, series: pending }), + }); }; excludeOpenSeries = (allSeriesArr) => { diff --git a/src/components/searchView/Annotations.jsx b/src/components/searchView/Annotations.jsx index 602f07f85..cc1575e00 100644 --- a/src/components/searchView/Annotations.jsx +++ b/src/components/searchView/Annotations.jsx @@ -8,13 +8,11 @@ import { getAnnotations } from '../../services/annotationServices'; import { formatDate } from '../flexView/helperMethods'; import SelectSerieModal from '../annotationsList/selectSerieModal'; import { - getSingleSerie, clearSelection, selectAnnotation, changeActivePort, - addToGrid, - jumpToAim } from '../annotationsList/action'; +import { openSeriesInDisplay } from '../common/openSeriesHelper'; function Table({ columns, data }) { const { @@ -97,51 +95,16 @@ function Annotations(props) { }; const displayAnnotations = async (selected) => { - const { projectID, studyUID, seriesUID, aimID } = selected; + const { aimID } = selected; setSelected(selected); - const patientID = selected.subjectID; - const { openSeries } = props; - const maxPort = parseInt(sessionStorage.getItem('maxPort')); - // const serieObj = { projectID, patientID, studyUID, seriesUID, aimID }; - //check if there is enough space in the grid - let isGridFull = openSeries.length === maxPort; - //check if the serie is already open - if (checkIfSerieOpen(seriesUID).isOpen) { - const { index } = checkIfSerieOpen(seriesUID); - props.dispatch(changeActivePort(index)); - props.dispatch(jumpToAim(seriesUID, aimID, index)); - props.dispatch(clearSelection()); - props.history.push('/display'); - } else { - if (isGridFull) { - setShowSelectSerie(true); - } else { - props.dispatch(addToGrid(selected, aimID)); - props - .dispatch(getSingleSerie(selected, aimID)) - .then(() => { }) - .catch(err => console.error(err)); - //if grid is NOT full check if patient data exists - // -----> Delete after v1.0 <----- - // if (!props.patients[patientID]) { - // // props.dispatch(getWholeData(null, null, selected)); - // getWholeData(null, null, selected); - // } else { - // props.dispatch( - // updatePatient( - // 'annotation', - // true, - // patientID, - // studyUID, - // seriesUID, - // aimID - // ) - // ); - // } - props.dispatch(clearSelection()); - props.history.push('/display'); - } - } + openSeriesInDisplay({ + dispatch: props.dispatch, + navigate: () => props.history.push('/display'), + openSeries: props.openSeries, + series: selected, + aimID, + onGridFull: () => setShowSelectSerie(true), + }); }; const selectRow = (e, data) => { diff --git a/src/components/searchView/Report.jsx b/src/components/searchView/Report.jsx index 659cd1b28..d76ece8c0 100644 --- a/src/components/searchView/Report.jsx +++ b/src/components/searchView/Report.jsx @@ -13,10 +13,9 @@ import { changeActivePort, clearGrid, jumpToAim, - addToGrid, - getSingleSerie, updateImageId } from '../annotationsList/action'; +import { openSeriesInDisplay } from '../common/openSeriesHelper'; const maxPort = parseInt(sessionStorage.getItem('maxPort')); // let waterfallOptions = sessionStorage.getItem('waterfallOptions'); @@ -374,15 +373,16 @@ const Report = props => { const openAims = (seriesToOpen, projectID, patientID) => { try { setSelectedSeries(seriesToOpen); - const array = + const series = projectID && patientID ? seriesToOpen.map(el => ({ ...el, projectID, patientID })) : seriesToOpen; - for (let series of array) { - props.dispatch(addToGrid(series, series.aimID || series.aimUID)); - props.dispatch(getSingleSerie(series, series.aimID || series.aimUID)); - } - props.history.push('/display'); + openSeriesInDisplay({ + dispatch: props.dispatch, + navigate: () => props.history.push('/display'), + openSeries: props.openSeries, + series, + }); } catch (err) { console.error(err); } diff --git a/src/components/searchView/Series.jsx b/src/components/searchView/Series.jsx index ba9045b7c..0ebf55ecc 100644 --- a/src/components/searchView/Series.jsx +++ b/src/components/searchView/Series.jsx @@ -9,16 +9,13 @@ import Annotations from "./Annotations"; import { getSeries } from "../../services/seriesServices"; import SelectSerieModal from "../annotationsList/selectSerieModal"; import { - alertViewPortFull, - getSingleSerie, - changeActivePort, selectSerie, clearSelection, - addToGrid, getWholeData, updatePatient, selectAnnotation, } from "../annotationsList/action"; +import { openSeriesInDisplay } from "../common/openSeriesHelper"; function Table({ columns, @@ -165,71 +162,21 @@ function Series(props) { const dispatchSerieDisplay = (selected) => { const { seriesData } = props; - const openSeries = Object.values(props.openSeries); const { patientID, studyUID, projectID } = selected; const dataExists = - seriesData[projectID] && - seriesData[projectID][patientID] && - seriesData[projectID][patientID][studyUID] && - seriesData[projectID][patientID][studyUID].list; - - const existingData = dataExists - ? seriesData[projectID][patientID][studyUID].list - : null; - - let isSerieOpen = false; - const maxPort = parseInt(sessionStorage.getItem("maxPort")); - - //check if there is enough space in the grid - let isGridFull = openSeries.length === maxPort; - //check if the serie is already open - - if (openSeries.length > 0) { - for (let i = 0; i < openSeries.length; i++) { - if (openSeries[i].seriesUID === selected.seriesUID) { - isSerieOpen = true; - props.dispatch(changeActivePort(i)); - break; - } - // } - } - } + seriesData[projectID] && + seriesData[projectID][patientID] && + seriesData[projectID][patientID][studyUID] && + seriesData[projectID][patientID][studyUID].list; - //serie is not already open; - if (!isSerieOpen) { - //if the grid is full show warning - if (isGridFull) { - setSerie(selected); - setShowSelectSerie(true); - // props.dispatch(alertViewPortFull()); - } else { - props.dispatch(addToGrid(selected)); - props - .dispatch(getSingleSerie(selected, null, null, existingData)) - .then(() => {}) - .catch((err) => console.error(err)); - //if grid is NOT full check if patient data exists - // -----> Delete after v1.0 <----- - // if (!props.patients[selected.patientID]) { - // props.dispatch(getWholeData(selected)); - // // getWholeData(selected); - // } else { - // props.dispatch( - // updatePatient( - // 'serie', - // true, - // patientID, - // studyUID, - // selected.seriesUID - // ) - // ); - // } - props.history.push("/display"); - } - } else { - props.history.push("/display"); - } - props.dispatch(clearSelection()); + openSeriesInDisplay({ + dispatch: props.dispatch, + navigate: () => props.history.push("/display"), + openSeries: Object.values(props.openSeries), + series: selected, + existingData: dataExists ? seriesData[projectID][patientID][studyUID].list : null, + onGridFull: () => { setSerie(selected); setShowSelectSerie(true); }, + }); }; const handleCheckboxSelect = (row) => { diff --git a/src/components/searchView/Studies.jsx b/src/components/searchView/Studies.jsx index 648088f8c..547d2ea4f 100644 --- a/src/components/searchView/Studies.jsx +++ b/src/components/searchView/Studies.jsx @@ -11,20 +11,22 @@ import { formatDate } from "../flexView/helperMethods"; import { getSeries } from "../../services/seriesServices"; import { clearCarets, isSupportedModality } from "../../Utils/aid.js"; import { - getSingleSerie, selectStudy, clearSelection, startLoading, loadCompleted, annotationsLoadingError, - addToGrid, getWholeData, alertViewPortFull, updatePatient, selectSerie, selectAnnotation, setSeriesData, + setMammogramSeries, + clearMammogramSeries, + clearPageOrderSeries, } from "../annotationsList/action"; +import { openSeriesInDisplay, isMammogramStudy } from "../common/openSeriesHelper"; function Table({ columns, @@ -199,11 +201,10 @@ function Studies(props) { try { if (!dataExists) { - const { data: series } = await getSeries( - projectID, - patientID, - studyUID - ); + let { data: series } = await getSeries(projectID, patientID, studyUID, false); + if (!series || series.length === 0) { + ({ data: series } = await getSeries(projectID, patientID, studyUID, true)); + } props.dispatch(loadCompleted()); props.dispatch( setSeriesData(projectID, patientID, studyUID, series, true) @@ -216,61 +217,36 @@ function Studies(props) { }; const displaySeries = async (selected) => { - const maxPort = parseInt(sessionStorage.getItem("maxPort")); - const { patientID, studyUID } = selected; + props.dispatch(clearPageOrderSeries()); + props.dispatch(clearMammogramSeries()); let seriesArr = await getSeriesData(selected); - const list = seriesArr.length > 0 ? seriesArr : null; - //check if the patient is there (create a patient exist flag) - // const patientExists = props.patients[patientID]; - //if there is patient iterate over the series object of the study (form an array of series) - // if (patientExists) { - // seriesArr = Object.values( - // props.patients[patientID].studies[studyUID].series - // ); - // //if there is not a patient get series data of the study and (form an array of series) - // } else { - - // } - // filter the nondisplayable modalities seriesArr = seriesArr.filter(isSupportedModality); - //get extraction of the series (extract unopen series) - // if (seriesArr.length > 0) seriesArr = excludeOpenSeries(seriesArr); - //check if there is enough room - if (seriesArr.length + props.openSeries.length > maxPort) { - //if there is not bring the modal - // await setState({ - // isSerieSelectionOpen: true, - // selectedStudy: [seriesArr], - // studyName: selected.studyDescription - // }); - setIsSerieSelectionOpen(true); - setSelectedStudy([seriesArr]); - setStudyName(selected.studyDescription); - } else { - //if there is enough room - //add serie to the grid - const promiseArr = []; - for (let serie of seriesArr) { - props.dispatch(addToGrid(serie)); - promiseArr.push(props.dispatch(getSingleSerie(serie, null, null, list))); - } - //getsingleSerie - Promise.all(promiseArr) - .then(() => {}) - .catch((err) => console.error(err)); - //if patient doesnot exist get patient - // -----> Delete after v1.0 <----- - // if (!patientExists) { - // // props.dispatch(getWholeData(null, selected)); - // getWholeData(null, selected); - // } else { - // //check if study exist - // props.dispatch(updatePatient('study', true, patientID, studyUID)); - // } - props.history.push("/display"); + // Mammogram studies load only the first page; remaining series paginate via NEXT. + if (isMammogramStudy(seriesArr)) { + props.dispatch(setMammogramSeries(seriesArr, selected.studyUID)); + openSeriesInDisplay({ + dispatch: props.dispatch, + navigate: () => props.history.push("/display"), + openSeries: props.openSeries, + series: seriesArr.slice(0, parseInt(sessionStorage.getItem('maxPort'))), + existingData: seriesArr, + }); + return; } - props.dispatch(clearSelection()); + + openSeriesInDisplay({ + dispatch: props.dispatch, + navigate: () => props.history.push("/display"), + openSeries: props.openSeries, + series: seriesArr, + existingData: seriesArr.length > 0 ? seriesArr : null, + onGridFull: pending => { + setIsSerieSelectionOpen(true); + setSelectedStudy([pending]); + setStudyName(selected.studyDescription); + }, + }); }; const columns = React.useMemo( diff --git a/src/components/sideBar/sideBarWorklist.jsx b/src/components/sideBar/sideBarWorklist.jsx index a8f23fc08..0222429e3 100644 --- a/src/components/sideBar/sideBarWorklist.jsx +++ b/src/components/sideBar/sideBarWorklist.jsx @@ -24,7 +24,8 @@ import { getSeries } from "../../services/seriesServices"; // Component imports import DeleteAlert from "../management/common/alertDeletionModal"; import SelectSeriesModal from "../annotationsList/selectSerieModal"; -import { addToGrid, getSingleSerie, alertViewPortFull, clearSelection, changeActivePort, selectPatient, setSeriesData, clearGrid } from "../annotationsList/action"; +import { alertViewPortFull, clearSelection, changeActivePort, selectPatient, setSeriesData, clearGrid, setMammogramSeries, clearMammogramSeries, setPageOrderSeries, clearPageOrderSeries } from "../annotationsList/action"; +import { openSeriesInDisplay, isMammogramStudy } from "../common/openSeriesHelper"; import { isSupportedModality, filterProjects, pseudo, generalizeDate } from "../../Utils/aid.js"; // CSS import import "./style.css"; @@ -189,53 +190,27 @@ let seriesCallSent; } const viewSelection = async (seriesArr) => { - const { seriesData } = props; + if (!seriesArr.length) return; const maxPort = parseInt(sessionStorage.getItem("maxPort")); - const notOpenSeries = []; - // const selectedSeries = Object.values(seriesObj); - const selectedSeries = seriesArr; - if (selectedSeries.length > 0) { - //check if enough room to display selection - for (let serie of selectedSeries) { - if (!checkIfSerieOpen(serie.seriesUID).isOpen) { - notOpenSeries.push(serie); - } - } - //if all ports are full - if ( - notOpenSeries.length > 0 && - props.openSeries.length === maxPort - ) { - props.dispatch(alertViewPortFull()); - } else { - //if all series already open update active port - if (notOpenSeries.length === 0) { - let index = checkIfSerieOpen(selectedSeries[0].seriesUID).index; - props.dispatch(changeActivePort(index)); - props.history.push("/display"); - props.dispatch(clearSelection()); - } else { - if (selectedSeries.length + props.openSeries.length > maxPort) { - // alert user about the num of open series a the moment and told only maxPort is allowed - const openPorts = props.openSeries.length; - setError(`Already ${openPorts} viewers open. You can open ${maxPort} at a time`); - } else { - //else get data for each serie for display - selectedSeries.forEach((serie) => { - const list = getExistingSeriesData(serie); - props.dispatch(addToGrid(serie, null, null, props.match.params.wid)); - props.dispatch(getSingleSerie(serie, null, null, list)); - }); - props.history.push("/display"); - props.dispatch(clearSelection()); - } - } - } - } + + openSeriesInDisplay({ + dispatch: props.dispatch, + navigate: () => props.history.push("/display"), + openSeries: props.openSeries, + series: seriesArr, + worklistID: props.match.params.wid, + existingData: serie => getExistingSeriesData(serie), + onGridFull: pending => { + const openPorts = props.openSeries.length; + setError(`Already ${openPorts} viewers open. You can open ${maxPort} at a time`); + }, + }); }; const handleOpenClick = async (study) => { if (mode === 'teaching') props.dispatch(clearGrid()); + props.dispatch(clearPageOrderSeries()); + props.dispatch(clearMammogramSeries()); const { seriesData } = props; const { projectID, subjectID, studyUID, studyDescription } = study; let series; @@ -248,15 +223,69 @@ let seriesCallSent; try { const isTeaching = mode === 'teaching'; if (!dataExists && seriesCallSent !== studyUID) { - ({ data: series } = await getSeries(projectID, subjectID, studyUID)); - if (series.length === 0 && isTeaching) - ({ data: series } = await getSeries(projectID, subjectID, studyUID, isTeaching)); + ({ data: series } = await getSeries(projectID, subjectID, studyUID, false)); + if (!series || series.length === 0) + ({ data: series } = await getSeries(projectID, subjectID, studyUID, true)); props.dispatch(setSeriesData(projectID, subjectID, studyUID, series, true)); seriesCallSent = studyUID; } else series = seriesData[projectID][subjectID][studyUID].list; series = series.filter(isSupportedModality); const maxPort = parseInt(sessionStorage.getItem("maxPort")); const { openSeries } = props; + + const significant = series.filter(s => s.significanceOrder != null); + const hasPageOrder = significant.length > 0 && significant.some(s => s.pageOrder != null); + + if (significant.length > 0 && hasPageOrder) { + // Case 1: pageOrder navigation. + props.dispatch(setPageOrderSeries(significant)); + const toDisplay = significant + .filter(s => s.pageOrder === 1) + .sort((a, b) => (a.significanceOrder || 0) - (b.significanceOrder || 0)); + openSeriesInDisplay({ + dispatch: props.dispatch, + navigate: () => props.history.push("/display"), + openSeries: props.openSeries, + series: toDisplay.length > 0 ? toDisplay : significant.slice(0, maxPort), + worklistID: props.match.params.wid, + existingData: series, + }); + return; + } + + if (isMammogramStudy(series)) { + // Case 3 (no significant) or Case 6 (significant, no pageOrder, MG). + props.dispatch(clearPageOrderSeries()); + props.dispatch(setMammogramSeries(series, studyUID)); + const toDisplay = significant.length > 0 + ? significant.sort((a, b) => (a.significanceOrder || 0) - (b.significanceOrder || 0)) // Case 6 + : series.slice(0, maxPort); // Case 3 + openSeriesInDisplay({ + dispatch: props.dispatch, + navigate: () => props.history.push("/display"), + openSeries: props.openSeries, + series: toDisplay, + worklistID: props.match.params.wid, + existingData: series, + }); + return; + } + + if (significant.length > 0) { + // Case 2: significant, no pageOrder, non-MG — load directly, no Next/Prev. + props.dispatch(clearPageOrderSeries()); + const toDisplay = significant.sort((a, b) => (a.significanceOrder || 0) - (b.significanceOrder || 0)); + openSeriesInDisplay({ + dispatch: props.dispatch, + navigate: () => props.history.push("/display"), + openSeries: props.openSeries, + series: toDisplay, + worklistID: props.match.params.wid, + existingData: series, + }); + return; + } + const alreadyOpenViews = isTeaching ? 0 : openSeries.length; if (alreadyOpenViews + series.length <= maxPort) { setSeries(series); diff --git a/src/services/httpService.js b/src/services/httpService.js index 021a6cb40..cdce3ada5 100644 --- a/src/services/httpService.js +++ b/src/services/httpService.js @@ -3,8 +3,6 @@ import axios from "axios"; import auth from "./authService"; // axios.defaults.withCredentials = false; -axios.defaults.headers.common["Content-Type"] = - "application/json, multipart/form-data"; axios.interceptors.request.use( async (config) => { @@ -12,7 +10,7 @@ axios.interceptors.request.use( const apikey = sessionStorage.getItem("API_KEY"); const user = sessionStorage.getItem("username"); if (apikey && user) { - config.params = {}; + config.params = { ...config.params }; config.params["user"] = user; } const header = await auth.getAuthHeader(); @@ -60,10 +58,10 @@ function mode() { } export default { - get: axios.get, - post: axios.post, - put: axios.put, - delete: axios.delete, + get: axios.get.bind(axios), + post: axios.post.bind(axios), + put: axios.put.bind(axios), + delete: axios.delete.bind(axios), apiUrl, wadoUrl, mode,