chore: 3.11 Recovery 1 (#5514)

This commit is contained in:
Joe Boccanfuso 2025-10-28 09:48:32 -04:00 committed by GitHub
parent 522531cc5c
commit 93dabb3805
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
147 changed files with 2948 additions and 5922 deletions

View File

@ -15,22 +15,13 @@ defaults: &defaults
commands: commands:
install_bun: install_bun:
steps: steps:
- restore_cache:
keys:
- bun-cache-v2-{{ arch }}-latest
- run: - run:
name: Install Bun name: Install Bun
command: | command: |
if [ ! -d "$HOME/.bun" ]; then curl -fsSL https://bun.sh/install | bash -s "bun-v1.2.23"
curl -fsSL https://bun.sh/install | bash
fi
echo 'export BUN_INSTALL="$HOME/.bun"' >> $BASH_ENV echo 'export BUN_INSTALL="$HOME/.bun"' >> $BASH_ENV
echo 'export PATH="$BUN_INSTALL/bin:$PATH"' >> $BASH_ENV echo 'export PATH="$BUN_INSTALL/bin:$PATH"' >> $BASH_ENV
source $BASH_ENV source $BASH_ENV
- save_cache:
key: bun-cache-v2-{{ arch }}-latest
paths:
- ~/.bun
jobs: jobs:
UNIT_TESTS: UNIT_TESTS:
@ -121,6 +112,34 @@ jobs:
- checkout - checkout
- attach_workspace: - attach_workspace:
at: ~/repo at: ~/repo
# SECURITY AUDIT
- run:
name: 'Security Audit - High Risk Vulnerabilities'
command: |
echo "🔍 Running bun audit for security vulnerabilities..."
echo "Checking for HIGH-RISK vulnerabilities..."
if bun audit --audit-level high; then
echo "✅ No high-risk vulnerabilities found"
echo "🎉 Security audit passed!"
else
echo ""
echo "❌ HIGH-RISK VULNERABILITIES DETECTED!"
echo "======================================"
echo ""
echo "🔧 To fix these issues:"
echo " 1. Run: bun audit"
echo " 2. Review the vulnerability details"
echo " 3. Update affected packages to secure versions"
echo " 4. Test your changes"
echo " 5. Re-run: bun audit --audit-level high"
echo ""
echo "📋 Full audit report:"
bun audit --audit-level low || true
echo ""
echo "❌ This build cannot proceed until high-risk vulnerabilities are resolved."
exit 1
fi
- run: - run:
name: Install Dependencies name: Install Dependencies
command: bun install --frozen-lockfile command: bun install --frozen-lockfile
@ -455,8 +474,13 @@ workflows:
filters: filters:
branches: branches:
only: master only: master
# - HOLD_FOR_APPROVAL:
# type: approval
# requires:
# - BUILD
- NPM_PUBLISH: - NPM_PUBLISH:
requires: requires:
# - HOLD_FOR_APPROVAL
- BUILD - BUILD
- DOCKER_BETA_PUBLISH: - DOCKER_BETA_PUBLISH:
requires: requires:

View File

@ -23,6 +23,8 @@ jobs:
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- uses: oven-sh/setup-bun@v2 - uses: oven-sh/setup-bun@v2
with:
bun-version: 1.2.23
- uses: actions/setup-node@v4 - uses: actions/setup-node@v4
with: with:
node-version: 20 # Or your desired Node version node-version: 20 # Or your desired Node version

View File

@ -1,7 +1,7 @@
name: Playwright Tests name: Playwright Tests
on: on:
pull_request: pull_request:
branches: [master] branches: [master, release/*]
concurrency: concurrency:
group: ${{ github.workflow }}-${{ github.ref }} group: ${{ github.workflow }}-${{ github.ref }}
@ -18,6 +18,8 @@ jobs:
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- uses: oven-sh/setup-bun@v2 - uses: oven-sh/setup-bun@v2
with:
bun-version: 1.2.23
- uses: actions/setup-node@v4 - uses: actions/setup-node@v4
with: with:
node-version: ${{ matrix.node-version }} node-version: ${{ matrix.node-version }}

View File

@ -10,6 +10,6 @@
"deploy": "netlify deploy --prod --dir ./../platform/app/dist" "deploy": "netlify deploy --prod --dir ./../platform/app/dist"
}, },
"devDependencies": { "devDependencies": {
"netlify-cli": "^2.21.0" "netlify-cli": "2.21.0"
} }
} }

View File

@ -49,8 +49,8 @@ RUN apt-get update && apt-get install -y build-essential python3
RUN mkdir /usr/src/app RUN mkdir /usr/src/app
WORKDIR /usr/src/app WORKDIR /usr/src/app
RUN npm install -g bun RUN npm install -g bun@1.2.23
# RUN npm install -g lerna@7.4.2 RUN npm install -g lerna@7.4.2
ENV PATH=/usr/src/app/node_modules/.bin:$PATH ENV PATH=/usr/src/app/node_modules/.bin:$PATH
# Do an initial install and then a final install # Do an initial install and then a final install
@ -60,6 +60,7 @@ COPY --parents ./addOns/package.json ./addOns/*/*/package.json ./extensions/*/pa
RUN bun pm cache rm RUN bun pm cache rm
RUN bun install RUN bun install
RUN bun add ajv@8.12.0
# Copy the local directory # Copy the local directory
COPY --link --exclude=yarn.lock --exclude=package.json --exclude=Dockerfile . . COPY --link --exclude=yarn.lock --exclude=package.json --exclude=Dockerfile . .
@ -98,7 +99,7 @@ COPY --from=builder /usr/src/app/platform/app/dist/dicom-microscopy-viewer /usr/
# In entrypoint.sh, app-config.js might be overwritten, so chmod it to be writeable. # In entrypoint.sh, app-config.js might be overwritten, so chmod it to be writeable.
# The nginx user cannot chmod it, so change to root. # The nginx user cannot chmod it, so change to root.
USER root USER root
RUN chown -R nginx:nginx /usr/share/nginx/html RUN chown -R nginx:nginx /usr/share/nginx/html && chmod -R 777 /usr/share/nginx/html
USER nginx USER nginx
ENTRYPOINT ["/usr/src/entrypoint.sh"] ENTRYPOINT ["/usr/src/entrypoint.sh"]
CMD ["nginx", "-g", "daemon off;"] CMD ["nginx", "-g", "daemon off;"]

View File

@ -9,85 +9,85 @@
"yarn": ">=1.19.1" "yarn": ">=1.19.1"
}, },
"dependencies": { "dependencies": {
"@babel/runtime": "^7.20.13", "@babel/runtime": "7.28.2",
"@kitware/vtk.js": "32.12.0", "@kitware/vtk.js": "32.12.0",
"clsx": "^2.1.1", "clsx": "2.1.1",
"core-js": "^3.2.1", "core-js": "3.45.1",
"moment": "^2.30.1" "moment": "2.30.1"
}, },
"peerDependencies": { "peerDependencies": {
"react": "^18.3.1", "react": "18.3.1",
"react-dom": "^18.3.1" "react-dom": "18.3.1"
}, },
"devDependencies": { "devDependencies": {
"@babel/eslint-parser": "7.28.0", "@babel/eslint-parser": "7.28.0",
"@pmmmwh/react-refresh-webpack-plugin": "^0.5.15", "@pmmmwh/react-refresh-webpack-plugin": "0.5.17",
"@rsbuild/core": "^1.3.16", "@rsbuild/core": "1.5.1",
"@rsbuild/plugin-node-polyfill": "^1.3.0", "@rsbuild/plugin-node-polyfill": "1.4.2",
"@rsbuild/plugin-react": "^1.3.1", "@rsbuild/plugin-react": "1.4.0",
"@svgr/webpack": "^8.1.0", "@svgr/webpack": "8.1.0",
"@swc/helpers": "^0.5.15", "@swc/helpers": "0.5.17",
"@types/jest": "^27.5.0", "@types/jest": "27.5.2",
"@typescript-eslint/eslint-plugin": "^6.3.0", "@typescript-eslint/eslint-plugin": "6.21.0",
"@typescript-eslint/parser": "^6.3.0", "@typescript-eslint/parser": "6.21.0",
"autoprefixer": "^10.4.4", "autoprefixer": "10.4.21",
"babel-loader": "^8.2.4", "babel-loader": "8.4.1",
"babel-plugin-module-resolver": "^5.0.0", "babel-plugin-module-resolver": "5.0.2",
"clean-webpack-plugin": "^3.0.0", "clean-webpack-plugin": "3.0.0",
"copy-webpack-plugin": "^9.0.1", "copy-webpack-plugin": "9.1.0",
"cross-env": "^5.2.0", "cross-env": "7.0.3",
"css-loader": "^6.8.1", "css-loader": "6.11.0",
"dotenv": "^8.1.0", "dotenv": "8.6.0",
"eslint": "^8.39.0", "eslint": "8.57.1",
"eslint-config-prettier": "7.2.0", "eslint-config-prettier": "7.2.0",
"eslint-config-react-app": "^6.0.0", "eslint-config-react-app": "6.0.0",
"eslint-plugin-cypress": "^2.12.1", "eslint-plugin-cypress": "2.15.2",
"eslint-plugin-flowtype": "^7.0.0", "eslint-plugin-flowtype": "7.0.0",
"eslint-plugin-import": "^2.26.0", "eslint-plugin-import": "2.32.0",
"eslint-plugin-jsx-a11y": "^6.5.1", "eslint-plugin-jsx-a11y": "6.10.2",
"eslint-plugin-node": "^11.1.0", "eslint-plugin-node": "11.1.0",
"eslint-plugin-prettier": "5.5.1", "eslint-plugin-prettier": "5.5.1",
"eslint-plugin-promise": "^5.2.0", "eslint-plugin-promise": "5.2.0",
"eslint-plugin-react": "^7.29.4", "eslint-plugin-react": "7.37.5",
"eslint-plugin-react-hooks": "^4.4.0", "eslint-plugin-react-hooks": "4.6.2",
"eslint-plugin-tsdoc": "^0.2.11", "eslint-plugin-tsdoc": "0.2.17",
"eslint-webpack-plugin": "^2.5.3", "eslint-webpack-plugin": "2.7.0",
"execa": "^8.0.1", "execa": "8.0.1",
"extract-css-chunks-webpack-plugin": "^4.5.4", "extract-css-chunks-webpack-plugin": "4.10.0",
"html-webpack-plugin": "^5.3.2", "html-webpack-plugin": "5.6.3",
"husky": "^3.0.0", "husky": "3.1.0",
"jest": "^29.5.0", "jest": "29.7.0",
"jest-canvas-mock": "^2.1.0", "jest-canvas-mock": "2.5.2",
"jest-environment-jsdom": "^29.5.0", "jest-environment-jsdom": "29.7.0",
"jest-junit": "^6.4.0", "jest-junit": "6.4.0",
"lerna": "^7.2.0", "lerna": "7.4.2",
"lint-staged": "^9.0.2", "lint-staged": "9.5.0",
"mini-css-extract-plugin": "^2.1.0", "mini-css-extract-plugin": "2.9.2",
"optimize-css-assets-webpack-plugin": "^6.0.1", "optimize-css-assets-webpack-plugin": "6.0.1",
"postcss": "^8.3.5", "postcss": "8.5.6",
"postcss-import": "^14.0.2", "postcss-import": "14.1.0",
"postcss-loader": "^6.1.1", "postcss-loader": "6.2.1",
"postcss-preset-env": "^7.4.3", "postcss-preset-env": "7.8.3",
"prettier": "3.6.2", "prettier": "3.6.2",
"prettier-plugin-tailwindcss": "0.6.9", "prettier-plugin-tailwindcss": "0.6.9",
"react-refresh": "^0.14.2", "react-refresh": "0.14.2",
"semver": "^7.5.1", "semver": "7.7.2",
"serve": "^14.2.4", "serve": "14.2.4",
"shader-loader": "^1.3.1", "shader-loader": "1.3.1",
"shx": "^0.3.3", "shx": "0.3.4",
"source-map-loader": "^4.0.1", "source-map-loader": "4.0.2",
"style-loader": "^1.0.0", "style-loader": "1.3.0",
"terser-webpack-plugin": "^5.1.4", "terser-webpack-plugin": "5.3.14",
"typescript": "5.5.4", "typescript": "5.5.4",
"unused-webpack-plugin": "2.4.0", "unused-webpack-plugin": "2.4.0",
"webpack": "^5.94.0", "webpack": "5.95.0",
"webpack-bundle-analyzer": "^4.8.0", "webpack-bundle-analyzer": "4.10.2",
"webpack-cli": "^5.0.1", "webpack-cli": "5.1.4",
"webpack-dev-server": "5.2.2", "webpack-dev-server": "5.2.2",
"webpack-hot-middleware": "^2.25.0", "webpack-hot-middleware": "2.26.1",
"webpack-merge": "^5.7.3", "webpack-merge": "5.10.0",
"workbox-webpack-plugin": "^6.1.5", "workbox-webpack-plugin": "6.6.1",
"worker-loader": "^3.0.8" "worker-loader": "3.0.8"
}, },
"scripts": { "scripts": {
"build": "Included as direct dependency" "build": "Included as direct dependency"

View File

@ -4,6 +4,6 @@
"version": "3.11.0", "version": "3.11.0",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"dicom-microscopy-viewer": "^0.46.1" "dicom-microscopy-viewer": "0.46.1"
} }
} }

View File

@ -22,30 +22,29 @@
}, },
"devDependencies": { "devDependencies": {
"@babel/core": "7.28.0", "@babel/core": "7.28.0",
"@babel/plugin-transform-class-properties": "^7.27.1", "@babel/plugin-transform-class-properties": "7.27.1",
"@babel/plugin-transform-object-rest-spread": "^7.28.0", "@babel/plugin-transform-object-rest-spread": "7.28.0",
"@babel/plugin-transform-private-methods": "^7.27.1", "@babel/plugin-transform-private-methods": "7.27.1",
"@babel/plugin-transform-private-property-in-object": "^7.27.1", "@babel/plugin-transform-private-property-in-object": "7.27.1",
"@babel/plugin-syntax-dynamic-import": "^7.8.3", "@babel/plugin-syntax-dynamic-import": "7.8.3",
"@babel/plugin-transform-arrow-functions": "^7.16.7", "@babel/plugin-transform-arrow-functions": "7.27.1",
"@babel/plugin-transform-regenerator": "^7.16.7", "@babel/plugin-transform-regenerator": "7.28.1",
"@babel/plugin-transform-runtime": "7.28.0", "@babel/plugin-transform-runtime": "7.28.0",
"@babel/plugin-transform-typescript": "^7.28.0", "@babel/plugin-transform-typescript": "7.28.0",
"@babel/preset-env": "7.28.0", "@babel/preset-env": "7.28.0",
"@babel/preset-react": "^7.27.1", "@babel/preset-react": "7.27.1",
"@babel/preset-typescript": "^7.27.1" "@babel/preset-typescript": "7.27.1"
}, },
"resolutions": { "resolutions": {
"**/@babel/runtime": "^7.20.13", "**/@babel/runtime": "7.28.2",
"commander": "8.3.0", "commander": "8.3.0",
"dcmjs": "0.43.1", "dcmjs": "0.43.1",
"dicomweb-client": ">=0.10.4", "dicomweb-client": ">=0.10.4",
"nth-check": "^2.1.1", "nth-check": "2.1.1",
"trim-newlines": "^5.0.0", "trim-newlines": "5.0.0",
"glob-parent": "^6.0.2", "glob-parent": "6.0.2",
"trim": "^1.0.0", "trim": "1.0.1",
"package-json": "^8.1.0", "package-json": "8.1.1",
"typescript": "5.5.4", "typescript": "5.5.4"
"sharp": "^0.32.6"
} }
} }

2473
bun.lock

File diff suppressed because it is too large Load Diff

View File

@ -37,18 +37,18 @@
"@ohif/extension-cornerstone": "3.11.0", "@ohif/extension-cornerstone": "3.11.0",
"@ohif/extension-default": "3.11.0", "@ohif/extension-default": "3.11.0",
"@ohif/i18n": "3.11.0", "@ohif/i18n": "3.11.0",
"prop-types": "^15.6.2", "prop-types": "15.8.1",
"react": "^18.3.1", "react": "18.3.1",
"react-dom": "^18.3.1", "react-dom": "18.3.1",
"react-i18next": "^12.2.2", "react-i18next": "12.3.1",
"react-router": "^6.8.1", "react-router": "6.30.1",
"react-router-dom": "^6.8.1" "react-router-dom": "6.30.1"
}, },
"dependencies": { "dependencies": {
"@babel/runtime": "^7.20.13", "@babel/runtime": "7.28.2",
"@cornerstonejs/adapters": "^3.32.5", "@cornerstonejs/adapters": "4.5.13",
"@cornerstonejs/core": "^3.32.5", "@cornerstonejs/core": "4.5.13",
"@kitware/vtk.js": "32.12.0", "@kitware/vtk.js": "32.12.0",
"react-color": "^2.19.3" "react-color": "2.19.3"
} }
} }

View File

@ -37,15 +37,15 @@
"@ohif/extension-cornerstone": "3.11.0", "@ohif/extension-cornerstone": "3.11.0",
"@ohif/extension-default": "3.11.0", "@ohif/extension-default": "3.11.0",
"@ohif/i18n": "3.11.0", "@ohif/i18n": "3.11.0",
"prop-types": "^15.6.2", "prop-types": "15.8.1",
"react": "^18.3.1", "react": "18.3.1",
"react-dom": "^18.3.1", "react-dom": "18.3.1",
"react-i18next": "^10.11.0", "react-i18next": "12.3.1",
"react-router": "^6.23.1", "react-router": "6.30.1",
"react-router-dom": "^6.23.1" "react-router-dom": "6.30.1"
}, },
"dependencies": { "dependencies": {
"@babel/runtime": "^7.20.13", "@babel/runtime": "7.28.2",
"react-color": "^2.19.3" "react-color": "2.19.3"
} }
} }

View File

@ -37,18 +37,18 @@
"@ohif/extension-cornerstone": "3.11.0", "@ohif/extension-cornerstone": "3.11.0",
"@ohif/extension-default": "3.11.0", "@ohif/extension-default": "3.11.0",
"@ohif/i18n": "3.11.0", "@ohif/i18n": "3.11.0",
"prop-types": "^15.6.2", "prop-types": "15.8.1",
"react": "^18.3.1", "react": "18.3.1",
"react-dom": "^18.3.1", "react-dom": "18.3.1",
"react-i18next": "^12.2.2", "react-i18next": "12.3.1",
"react-router": "^6.23.1", "react-router": "6.30.1",
"react-router-dom": "^6.23.1" "react-router-dom": "6.30.1"
}, },
"dependencies": { "dependencies": {
"@babel/runtime": "^7.20.13", "@babel/runtime": "7.28.2",
"@cornerstonejs/adapters": "^3.32.5", "@cornerstonejs/adapters": "4.5.13",
"@cornerstonejs/core": "^3.32.5", "@cornerstonejs/core": "4.5.13",
"@kitware/vtk.js": "32.12.0", "@kitware/vtk.js": "32.12.0",
"react-color": "^2.19.3" "react-color": "2.19.3"
} }
} }

View File

@ -39,16 +39,16 @@
"@ohif/extension-measurement-tracking": "3.11.0", "@ohif/extension-measurement-tracking": "3.11.0",
"@ohif/ui": "3.11.0", "@ohif/ui": "3.11.0",
"dcmjs": "0.43.1", "dcmjs": "0.43.1",
"dicom-parser": "^1.8.9", "dicom-parser": "1.8.21",
"hammerjs": "^2.0.8", "hammerjs": "2.0.8",
"prop-types": "^15.6.2", "prop-types": "15.8.1",
"react": "^18.3.1" "react": "18.3.1"
}, },
"dependencies": { "dependencies": {
"@babel/runtime": "^7.20.13", "@babel/runtime": "7.28.2",
"@cornerstonejs/adapters": "^3.32.5", "@cornerstonejs/adapters": "4.5.13",
"@cornerstonejs/core": "^3.32.5", "@cornerstonejs/core": "4.5.13",
"@cornerstonejs/tools": "^3.32.5", "@cornerstonejs/tools": "4.5.13",
"classnames": "^2.3.2" "classnames": "2.5.1"
} }
} }

View File

@ -35,15 +35,15 @@
"@ohif/i18n": "3.11.0", "@ohif/i18n": "3.11.0",
"@ohif/ui": "3.11.0", "@ohif/ui": "3.11.0",
"dcmjs": "0.43.1", "dcmjs": "0.43.1",
"dicom-parser": "^1.8.21", "dicom-parser": "1.8.21",
"hammerjs": "^2.0.8", "hammerjs": "2.0.8",
"prop-types": "^15.6.2", "prop-types": "15.8.1",
"react": "^18.3.1" "react": "18.3.1"
}, },
"dependencies": { "dependencies": {
"@babel/runtime": "^7.20.13", "@babel/runtime": "7.28.2",
"@cornerstonejs/core": "^3.32.5", "@cornerstonejs/core": "4.5.13",
"@cornerstonejs/tools": "^3.32.5", "@cornerstonejs/tools": "4.5.13",
"classnames": "^2.3.2" "classnames": "2.5.1"
} }
} }

View File

@ -31,41 +31,43 @@
"dev:cornerstone": "yarn run dev", "dev:cornerstone": "yarn run dev",
"build": "cross-env NODE_ENV=production webpack --progress --config .webpack/webpack.prod.js", "build": "cross-env NODE_ENV=production webpack --progress --config .webpack/webpack.prod.js",
"build:package-1": "yarn run build", "build:package-1": "yarn run build",
"start": "yarn run dev" "start": "yarn run dev",
"test:unit": "jest --watchAll",
"test:unit:ci": "jest --ci --runInBand --collectCoverage --passWithNoTests"
}, },
"peerDependencies": { "peerDependencies": {
"@cornerstonejs/codec-charls": "^1.2.3", "@cornerstonejs/codec-charls": "1.2.3",
"@cornerstonejs/codec-libjpeg-turbo-8bit": "^1.2.2", "@cornerstonejs/codec-libjpeg-turbo-8bit": "1.2.2",
"@cornerstonejs/codec-openjpeg": "^1.2.4", "@cornerstonejs/codec-openjpeg": "1.2.4",
"@cornerstonejs/codec-openjph": "^2.4.5", "@cornerstonejs/codec-openjph": "2.4.7",
"@cornerstonejs/dicom-image-loader": "^3.32.5", "@cornerstonejs/dicom-image-loader": "4.5.13",
"@ohif/core": "3.11.0", "@ohif/core": "3.11.0",
"@ohif/ui": "3.11.0", "@ohif/ui": "3.11.0",
"dcmjs": "0.43.1", "dcmjs": "0.43.1",
"dicom-parser": "^1.8.21", "dicom-parser": "1.8.21",
"hammerjs": "^2.0.8", "hammerjs": "2.0.8",
"prop-types": "^15.6.2", "prop-types": "15.8.1",
"react": "^18.3.1", "react": "18.3.1",
"react-dom": "^18.3.1", "react-dom": "18.3.1",
"react-resize-detector": "^10.0.1" "react-resize-detector": "10.0.1"
}, },
"dependencies": { "dependencies": {
"@babel/runtime": "^7.20.13", "@babel/runtime": "7.28.2",
"@cornerstonejs/adapters": "^3.32.5", "@cornerstonejs/adapters": "4.5.13",
"@cornerstonejs/ai": "^3.32.5", "@cornerstonejs/ai": "4.5.13",
"@cornerstonejs/core": "^3.32.5", "@cornerstonejs/core": "4.5.13",
"@cornerstonejs/labelmap-interpolation": "^3.32.5", "@cornerstonejs/labelmap-interpolation": "4.5.13",
"@cornerstonejs/polymorphic-segmentation": "^3.32.5", "@cornerstonejs/polymorphic-segmentation": "4.5.13",
"@cornerstonejs/tools": "^3.32.5", "@cornerstonejs/tools": "4.5.13",
"@itk-wasm/morphological-contour-interpolation": "1.1.0", "@itk-wasm/morphological-contour-interpolation": "1.1.0",
"@kitware/vtk.js": "32.12.0", "@kitware/vtk.js": "32.12.0",
"html2canvas": "^1.4.1", "html2canvas": "1.4.1",
"lodash.compact": "^3.0.1", "lodash.compact": "3.0.1",
"lodash.debounce": "^4.0.8", "lodash.debounce": "4.0.8",
"lodash.flatten": "^4.4.0", "lodash.flatten": "4.4.0",
"lodash.merge": "^4.6.2", "lodash.merge": "4.6.2",
"lodash.zip": "^4.2.0", "lodash.zip": "4.2.0",
"shader-loader": "^1.3.1", "shader-loader": "1.3.1",
"worker-loader": "^3.0.8" "worker-loader": "3.0.8"
} }
} }

View File

@ -245,10 +245,35 @@ function commandsModule({
referencedDisplaySetInstanceUID referencedDisplaySetInstanceUID
); );
storePositionPresentation(referencedDisplaySet); storePositionPresentation(referencedDisplaySet);
return commandsManager.runCommand('loadSegmentationDisplaySetsForViewport', {
const results = commandsManager.runCommand('loadSegmentationDisplaySetsForViewport', {
viewportId, viewportId,
displaySetInstanceUIDs: [referencedDisplaySet.displaySetInstanceUID], displaySetInstanceUIDs: [referencedDisplaySet.displaySetInstanceUID],
}); });
const disableEditing = customizationService.getCustomization(
'panelSegmentation.disableEditing'
);
if (disableEditing) {
const segmentationRepresentations = segmentationService.getSegmentationRepresentations(
viewportId,
{
segmentationId: displaySet.displaySetInstanceUID,
}
);
segmentationRepresentations.forEach(representation => {
const segmentIndices = Object.keys(representation.segments);
segmentIndices.forEach(segmentIndex => {
segmentationService.setSegmentLocked(
representation.segmentationId,
parseInt(segmentIndex),
true
);
});
});
}
return results;
} else if (displaySet.Modality === 'SR') { } else if (displaySet.Modality === 'SR') {
const results = commandsManager.runCommand('hydrateStructuredReport', { const results = commandsManager.runCommand('hydrateStructuredReport', {
displaySetInstanceUID: displaySet.displaySetInstanceUID, displaySetInstanceUID: displaySet.displaySetInstanceUID,

View File

@ -1754,7 +1754,10 @@ class SegmentationService extends PubSubService {
return `${volumeLoaderSchema}:${displaySet.displaySetInstanceUID}`; return `${volumeLoaderSchema}:${displaySet.displaySetInstanceUID}`;
} }
private _getSegmentCenter(segmentationId, segmentIndex) { private _getSegmentCenter(
segmentationId: string,
segmentIndex: number
): { image?: csTypes.Point3; world: csTypes.Point3 } | undefined {
const segmentation = this.getSegmentation(segmentationId); const segmentation = this.getSegmentation(segmentationId);
if (!segmentation) { if (!segmentation) {
@ -1765,19 +1768,17 @@ class SegmentationService extends PubSubService {
const { cachedStats } = segments[segmentIndex]; const { cachedStats } = segments[segmentIndex];
if (!cachedStats || !cachedStats?.center || !cachedStats?.namedStats?.center) { if (cachedStats?.center) {
return; const { center } = cachedStats;
return center as { image: csTypes.Point3; world: csTypes.Point3 };
} }
const { center } = cachedStats; if (cachedStats?.namedStats?.center) {
if (!center) {
return { return {
world: cachedStats.namedStats.center.value, world: cachedStats.namedStats.center.value,
}; };
} }
return center;
} }
private _setSegmentLockedStatus(segmentationId: string, segmentIndex: number, isLocked: boolean) { private _setSegmentLockedStatus(segmentationId: string, segmentIndex: number, isLocked: boolean) {

View File

@ -37,19 +37,19 @@
"@ohif/core": "3.11.0", "@ohif/core": "3.11.0",
"@ohif/i18n": "3.11.0", "@ohif/i18n": "3.11.0",
"dcmjs": "0.43.1", "dcmjs": "0.43.1",
"dicomweb-client": "^0.10.4", "dicomweb-client": "0.10.4",
"prop-types": "^15.6.2", "prop-types": "15.8.1",
"react": "^18.3.1", "react": "18.3.1",
"react-dom": "^18.3.1", "react-dom": "18.3.1",
"react-i18next": "^12.2.2", "react-i18next": "12.3.1",
"react-window": "^1.8.9", "react-window": "1.8.11",
"webpack": "5.89.0", "webpack": "5.89.0",
"webpack-merge": "^5.7.3" "webpack-merge": "5.10.0"
}, },
"dependencies": { "dependencies": {
"@babel/runtime": "^7.20.13", "@babel/runtime": "7.28.2",
"@cornerstonejs/calculate-suv": "^1.1.0", "@cornerstonejs/calculate-suv": "1.1.0",
"lodash.get": "^4.4.2", "lodash.get": "4.4.2",
"lodash.uniqby": "^4.7.0" "lodash.uniqby": "4.7.0"
} }
} }

View File

@ -262,17 +262,19 @@ function createDicomJSONApi(dicomJsonConfig) {
const { StudyInstanceUID, SeriesInstanceUID } = displaySet; const { StudyInstanceUID, SeriesInstanceUID } = displaySet;
const study = findStudies('StudyInstanceUID', StudyInstanceUID)[0]; const study = findStudies('StudyInstanceUID', StudyInstanceUID)[0];
const series = study.series.find(s => s.SeriesInstanceUID === SeriesInstanceUID) || []; const series = study.series.find(s => s.SeriesInstanceUID === SeriesInstanceUID) || {};
const instanceMap = new Map(); const instanceMap = new Map();
series.instances.forEach(instance => { if (series.instances) {
if (instance?.metadata?.SOPInstanceUID) { series.instances.forEach(instance => {
const { metadata, url } = instance; if (instance?.metadata?.SOPInstanceUID) {
const existingInstances = instanceMap.get(metadata.SOPInstanceUID) || []; const { metadata, url } = instance;
existingInstances.push({ ...metadata, url }); const existingInstances = instanceMap.get(metadata.SOPInstanceUID) || [];
instanceMap.set(metadata.SOPInstanceUID, existingInstances); existingInstances.push({ ...metadata, url });
} instanceMap.set(metadata.SOPInstanceUID, existingInstances);
}); }
});
}
displaySet.images.forEach(instance => { displaySet.images.forEach(instance => {
const NumberOfFrames = instance.NumberOfFrames || 1; const NumberOfFrames = instance.NumberOfFrames || 1;

View File

@ -65,6 +65,11 @@ const commandsModule = ({
* @param options.removeFirst - Optional flag to remove the display set first if it's already added * @param options.removeFirst - Optional flag to remove the display set first if it's already added
*/ */
addDisplaySetAsLayer: ({ viewportId, displaySetInstanceUID, removeFirst = false }) => { addDisplaySetAsLayer: ({ viewportId, displaySetInstanceUID, removeFirst = false }) => {
if (!viewportId) {
const { activeViewportId } = servicesManager.services.viewportGridService.getState();
viewportId = activeViewportId;
}
if (!viewportId || !displaySetInstanceUID) { if (!viewportId || !displaySetInstanceUID) {
console.warn('Missing required parameters for addDisplaySetAsLayer command'); console.warn('Missing required parameters for addDisplaySetAsLayer command');
return; return;

View File

@ -8,31 +8,13 @@ export default {
id: 'tagBrowser', id: 'tagBrowser',
label: 'Tag Browser', label: 'Tag Browser',
iconName: 'DicomTagBrowser', iconName: 'DicomTagBrowser',
onClick: ({ commandsManager, displaySetInstanceUID }: withAppTypes) => { commands: 'openDICOMTagViewer',
commandsManager.runCommand('openDICOMTagViewer', {
displaySetInstanceUID,
});
},
}, },
{ {
id: 'addAsLayer', id: 'addAsLayer',
label: 'Add as Layer', label: 'Add as Layer',
iconName: 'ViewportViews', iconName: 'ViewportViews',
onClick: ({ commandsManager, displaySetInstanceUID, servicesManager }: withAppTypes) => { commands: 'addDisplaySetAsLayer',
const { viewportGridService } = servicesManager.services;
// Get the active viewport
const { activeViewportId } = viewportGridService.getState();
if (!activeViewportId) {
return;
}
// Use the new command to add the display set as a layer
commandsManager.runCommand('addDisplaySetAsLayer', {
viewportId: activeViewportId,
displaySetInstanceUID,
});
},
}, },
], ],
'studyBrowser.sortFunctions': [ 'studyBrowser.sortFunctions': [

View File

@ -34,20 +34,20 @@
"@ohif/extension-default": "3.11.0", "@ohif/extension-default": "3.11.0",
"@ohif/i18n": "3.11.0", "@ohif/i18n": "3.11.0",
"@ohif/ui": "3.11.0", "@ohif/ui": "3.11.0",
"prop-types": "^15.6.2", "prop-types": "15.8.1",
"react": "^18.3.1", "react": "18.3.1",
"react-dom": "^18.3.1", "react-dom": "18.3.1",
"react-i18next": "^10.11.0", "react-i18next": "12.3.1",
"react-router": "^6.23.1", "react-router": "6.30.1",
"react-router-dom": "^6.23.1" "react-router-dom": "6.30.1"
}, },
"dependencies": { "dependencies": {
"@babel/runtime": "^7.20.13", "@babel/runtime": "7.28.2",
"@cornerstonejs/codec-charls": "^1.2.3", "@cornerstonejs/codec-charls": "1.2.3",
"@cornerstonejs/codec-libjpeg-turbo-8bit": "^1.2.2", "@cornerstonejs/codec-libjpeg-turbo-8bit": "1.2.2",
"@cornerstonejs/codec-openjpeg": "^1.2.4", "@cornerstonejs/codec-openjpeg": "1.2.4",
"colormap": "^2.3", "colormap": "2.3.2",
"lodash.debounce": "^4.0.8", "lodash.debounce": "4.0.8",
"mathjs": "^12.4.2" "mathjs": "12.4.3"
} }
} }

View File

@ -33,13 +33,13 @@
"@ohif/core": "3.11.0", "@ohif/core": "3.11.0",
"@ohif/ui": "3.11.0", "@ohif/ui": "3.11.0",
"dcmjs": "0.43.1", "dcmjs": "0.43.1",
"dicom-parser": "^1.8.9", "dicom-parser": "1.8.21",
"hammerjs": "^2.0.8", "hammerjs": "2.0.8",
"prop-types": "^15.6.2", "prop-types": "15.8.1",
"react": "^18.3.1" "react": "18.3.1"
}, },
"dependencies": { "dependencies": {
"@babel/runtime": "^7.20.13", "@babel/runtime": "7.28.2",
"classnames": "^2.3.2" "classnames": "2.5.1"
} }
} }

View File

@ -33,13 +33,13 @@
"@ohif/core": "3.11.0", "@ohif/core": "3.11.0",
"@ohif/ui": "3.11.0", "@ohif/ui": "3.11.0",
"dcmjs": "0.43.1", "dcmjs": "0.43.1",
"dicom-parser": "^1.8.9", "dicom-parser": "1.8.21",
"hammerjs": "^2.0.8", "hammerjs": "2.0.8",
"prop-types": "^15.6.2", "prop-types": "15.8.1",
"react": "^18.3.1" "react": "18.3.1"
}, },
"dependencies": { "dependencies": {
"@babel/runtime": "^7.20.13", "@babel/runtime": "7.28.2",
"classnames": "^2.3.2" "classnames": "2.5.1"
} }
} }

View File

@ -32,25 +32,25 @@
"start": "yarn run dev" "start": "yarn run dev"
}, },
"peerDependencies": { "peerDependencies": {
"@cornerstonejs/core": "^3.32.5", "@cornerstonejs/core": "4.5.13",
"@cornerstonejs/tools": "^3.32.5", "@cornerstonejs/tools": "4.5.13",
"@ohif/core": "3.11.0", "@ohif/core": "3.11.0",
"@ohif/extension-cornerstone-dicom-sr": "3.11.0", "@ohif/extension-cornerstone-dicom-sr": "3.11.0",
"@ohif/extension-default": "3.11.0", "@ohif/extension-default": "3.11.0",
"@ohif/ui": "3.11.0", "@ohif/ui": "3.11.0",
"classnames": "^2.3.2", "classnames": "2.5.1",
"dcmjs": "0.43.1", "dcmjs": "0.43.1",
"lodash.debounce": "^4.0.8", "lodash.debounce": "4.0.8",
"prop-types": "^15.6.2", "prop-types": "15.8.1",
"react": "^18.3.1", "react": "18.3.1",
"react-dom": "^18.3.1", "react-dom": "18.3.1",
"webpack": "5.89.0", "webpack": "5.89.0",
"webpack-merge": "^5.7.3" "webpack-merge": "5.10.0"
}, },
"dependencies": { "dependencies": {
"@babel/runtime": "^7.20.13", "@babel/runtime": "7.28.2",
"@ohif/ui": "3.11.0", "@ohif/ui": "3.11.0",
"@xstate/react": "^3.2.2", "@xstate/react": "3.2.2",
"xstate": "^4.10.0" "xstate": "4.38.3"
} }
} }

View File

@ -33,13 +33,13 @@
"@ohif/core": "3.11.0", "@ohif/core": "3.11.0",
"@ohif/ui": "3.11.0", "@ohif/ui": "3.11.0",
"dcmjs": "0.43.1", "dcmjs": "0.43.1",
"dicom-parser": "^1.8.9", "dicom-parser": "1.8.21",
"hammerjs": "^2.0.8", "hammerjs": "2.0.8",
"prop-types": "^15.6.2", "prop-types": "15.8.1",
"react": "^18.3.1" "react": "18.3.1"
}, },
"dependencies": { "dependencies": {
"@babel/runtime": "^7.20.13", "@babel/runtime": "7.28.2",
"classnames": "^2.3.2" "classnames": "2.5.1"
} }
} }

View File

@ -33,13 +33,13 @@
"@ohif/core": "3.11.0", "@ohif/core": "3.11.0",
"@ohif/ui": "3.11.0", "@ohif/ui": "3.11.0",
"dcmjs": "0.43.1", "dcmjs": "0.43.1",
"dicom-parser": "^1.8.9", "dicom-parser": "1.8.21",
"hammerjs": "^2.0.8", "hammerjs": "2.0.8",
"prop-types": "^15.6.2", "prop-types": "15.8.1",
"react": "^18.3.1" "react": "18.3.1"
}, },
"dependencies": { "dependencies": {
"@babel/runtime": "^7.20.13", "@babel/runtime": "7.28.2",
"classnames": "^2.3.2" "classnames": "2.5.1"
} }
} }

View File

@ -31,19 +31,19 @@
"start": "yarn run dev" "start": "yarn run dev"
}, },
"peerDependencies": { "peerDependencies": {
"prop-types": "^15.6.2", "prop-types": "15.8.1",
"react": "^18.3.1", "react": "18.3.1",
"react-dom": "^18.3.1", "react-dom": "18.3.1",
"react-i18next": "^12.2.2", "react-i18next": "12.3.1",
"react-router": "^6.23.1", "react-router": "6.30.1",
"react-router-dom": "^6.23.1", "react-router-dom": "6.30.1",
"webpack": "5.95.0", "webpack": "5.95.0",
"webpack-merge": "^5.7.3" "webpack-merge": "5.10.0"
}, },
"dependencies": { "dependencies": {
"@babel/runtime": "^7.20.13", "@babel/runtime": "7.28.2",
"@cornerstonejs/core": "^3.32.5", "@cornerstonejs/core": "4.5.13",
"@cornerstonejs/tools": "^3.32.5", "@cornerstonejs/tools": "4.5.13",
"@ohif/core": "3.11.0", "@ohif/core": "3.11.0",
"@ohif/extension-cornerstone": "3.11.0", "@ohif/extension-cornerstone": "3.11.0",
"@ohif/extension-default": "3.11.0", "@ohif/extension-default": "3.11.0",
@ -53,29 +53,29 @@
"devDependencies": { "devDependencies": {
"@babel/core": "7.28.0", "@babel/core": "7.28.0",
"@babel/eslint-parser": "7.28.0", "@babel/eslint-parser": "7.28.0",
"@babel/plugin-syntax-dynamic-import": "^7.8.3", "@babel/plugin-syntax-dynamic-import": "7.8.3",
"@babel/plugin-transform-arrow-functions": "^7.16.7", "@babel/plugin-transform-arrow-functions": "7.27.1",
"@babel/plugin-transform-class-properties": "^7.27.1", "@babel/plugin-transform-class-properties": "7.27.1",
"@babel/plugin-transform-object-rest-spread": "^7.28.0", "@babel/plugin-transform-object-rest-spread": "7.28.0",
"@babel/plugin-transform-private-methods": "^7.27.1", "@babel/plugin-transform-private-methods": "7.27.1",
"@babel/plugin-transform-private-property-in-object": "^7.27.1", "@babel/plugin-transform-private-property-in-object": "7.27.1",
"@babel/plugin-transform-regenerator": "^7.16.7", "@babel/plugin-transform-regenerator": "7.28.1",
"@babel/plugin-transform-runtime": "7.28.0", "@babel/plugin-transform-runtime": "7.28.0",
"@babel/plugin-transform-typescript": "^7.28.0", "@babel/plugin-transform-typescript": "7.28.0",
"@babel/preset-env": "7.28.0", "@babel/preset-env": "7.28.0",
"@babel/preset-react": "^7.27.1", "@babel/preset-react": "7.27.1",
"@babel/preset-typescript": "^7.27.1", "@babel/preset-typescript": "7.27.1",
"@svgr/webpack": "^8.1.0", "@svgr/webpack": "8.1.0",
"babel-loader": "^8.2.4", "babel-loader": "8.4.1",
"babel-plugin-module-resolver": "^5.0.0", "babel-plugin-module-resolver": "5.0.2",
"clean-webpack-plugin": "^4.0.0", "clean-webpack-plugin": "3.0.0",
"copy-webpack-plugin": "^10.2.0", "copy-webpack-plugin": "9.1.0",
"cross-env": "^7.0.3", "cross-env": "7.0.3",
"dotenv": "^14.1.0", "dotenv": "8.6.0",
"eslint": "^8.39.0", "eslint": "8.57.1",
"eslint-loader": "^2.0.0", "eslint-loader": "2.2.1",
"webpack": "5.95.0", "webpack": "5.95.0",
"webpack-cli": "^5.0.2", "webpack-cli": "5.1.4",
"webpack-merge": "^5.7.3" "webpack-merge": "5.10.0"
} }
} }

View File

@ -39,11 +39,11 @@
"@ohif/extension-dicom-video": "3.11.0" "@ohif/extension-dicom-video": "3.11.0"
}, },
"dependencies": { "dependencies": {
"@babel/runtime": "^7.20.13", "@babel/runtime": "7.28.2",
"i18next": "^17.0.3" "i18next": "17.3.1"
}, },
"devDependencies": { "devDependencies": {
"webpack": "5.95.0", "webpack": "5.95.0",
"webpack-merge": "^5.7.3" "webpack-merge": "5.10.0"
} }
} }

View File

@ -44,11 +44,11 @@
"@ohif/extension-test": "3.11.0" "@ohif/extension-test": "3.11.0"
}, },
"dependencies": { "dependencies": {
"@babel/runtime": "^7.20.13", "@babel/runtime": "7.28.2",
"i18next": "^17.0.3" "i18next": "17.3.1"
}, },
"devDependencies": { "devDependencies": {
"webpack": "5.95.0", "webpack": "5.95.0",
"webpack-merge": "^5.7.3" "webpack-merge": "5.10.0"
} }
} }

View File

@ -45,11 +45,11 @@
"@ohif/extension-measurement-tracking": "3.11.0" "@ohif/extension-measurement-tracking": "3.11.0"
}, },
"dependencies": { "dependencies": {
"@babel/runtime": "^7.20.13", "@babel/runtime": "7.28.2",
"i18next": "^17.0.3" "i18next": "17.3.1"
}, },
"devDependencies": { "devDependencies": {
"webpack": "5.95.0", "webpack": "5.95.0",
"webpack-merge": "^5.7.3" "webpack-merge": "5.10.0"
} }
} }

View File

@ -39,7 +39,7 @@
"@ohif/extension-dicom-microscopy": "3.11.0" "@ohif/extension-dicom-microscopy": "3.11.0"
}, },
"dependencies": { "dependencies": {
"@babel/runtime": "^7.20.13", "@babel/runtime": "7.28.2",
"i18next": "^17.0.3" "i18next": "17.3.1"
} }
} }

View File

@ -41,10 +41,10 @@
"@ohif/extension-tmtv": "3.11.0" "@ohif/extension-tmtv": "3.11.0"
}, },
"dependencies": { "dependencies": {
"@babel/runtime": "^7.20.13" "@babel/runtime": "7.28.2"
}, },
"devDependencies": { "devDependencies": {
"webpack": "5.95.0", "webpack": "5.95.0",
"webpack-merge": "^5.7.3" "webpack-merge": "5.10.0"
} }
} }

View File

@ -45,33 +45,33 @@
"@ohif/extension-dicom-video": "3.11.0" "@ohif/extension-dicom-video": "3.11.0"
}, },
"dependencies": { "dependencies": {
"@babel/runtime": "^7.20.13", "@babel/runtime": "7.28.2",
"i18next": "^17.0.3" "i18next": "17.3.1"
}, },
"devDependencies": { "devDependencies": {
"@babel/core": "7.28.0", "@babel/core": "7.28.0",
"@babel/eslint-parser": "7.28.0", "@babel/eslint-parser": "7.28.0",
"@babel/plugin-syntax-dynamic-import": "^7.8.3", "@babel/plugin-syntax-dynamic-import": "7.8.3",
"@babel/plugin-transform-arrow-functions": "^7.16.7", "@babel/plugin-transform-arrow-functions": "7.27.1",
"@babel/plugin-transform-class-properties": "^7.27.1", "@babel/plugin-transform-class-properties": "7.27.1",
"@babel/plugin-transform-object-rest-spread": "^7.28.0", "@babel/plugin-transform-object-rest-spread": "7.28.0",
"@babel/plugin-transform-private-methods": "^7.27.1", "@babel/plugin-transform-private-methods": "7.27.1",
"@babel/plugin-transform-regenerator": "^7.16.7", "@babel/plugin-transform-regenerator": "7.28.1",
"@babel/plugin-transform-runtime": "7.28.0", "@babel/plugin-transform-runtime": "7.28.0",
"@babel/plugin-transform-typescript": "^7.28.0", "@babel/plugin-transform-typescript": "7.28.0",
"@babel/preset-env": "7.28.0", "@babel/preset-env": "7.28.0",
"@babel/preset-react": "^7.27.1", "@babel/preset-react": "7.27.1",
"@babel/preset-typescript": "^7.27.1", "@babel/preset-typescript": "7.27.1",
"@svgr/webpack": "^8.1.0", "@svgr/webpack": "8.1.0",
"babel-loader": "^8.0.0-beta.4", "babel-loader": "8.4.1",
"clean-webpack-plugin": "^4.0.0", "clean-webpack-plugin": "3.0.0",
"copy-webpack-plugin": "^10.2.0", "copy-webpack-plugin": "9.1.0",
"cross-env": "^7.0.3", "cross-env": "7.0.3",
"dotenv": "^14.1.0", "dotenv": "8.6.0",
"eslint": "^8.39.0", "eslint": "8.57.1",
"eslint-loader": "^2.0.0", "eslint-loader": "2.2.1",
"webpack": "5.95.0", "webpack": "5.95.0",
"webpack-cli": "^4.7.2", "webpack-cli": "5.1.4",
"webpack-merge": "^5.7.3" "webpack-merge": "5.10.0"
} }
} }

View File

@ -43,11 +43,11 @@
"@ohif/extension-measurement-tracking": "3.11.0" "@ohif/extension-measurement-tracking": "3.11.0"
}, },
"dependencies": { "dependencies": {
"@babel/runtime": "^7.20.13", "@babel/runtime": "7.28.2",
"i18next": "^17.0.3" "i18next": "17.3.1"
}, },
"devDependencies": { "devDependencies": {
"webpack": "5.95.0", "webpack": "5.95.0",
"webpack-merge": "^5.7.3" "webpack-merge": "5.10.0"
} }
} }

View File

@ -33,38 +33,38 @@
"test:unit:ci": "jest --ci --runInBand --collectCoverage --passWithNoTests" "test:unit:ci": "jest --ci --runInBand --collectCoverage --passWithNoTests"
}, },
"dependencies": { "dependencies": {
"@babel/runtime": "^7.20.13", "@babel/runtime": "7.28.2",
"@cornerstonejs/core": "^3.32.5", "@cornerstonejs/core": "4.5.13",
"@cornerstonejs/tools": "^3.32.5", "@cornerstonejs/tools": "4.5.13",
"@ohif/core": "3.11.0", "@ohif/core": "3.11.0",
"@ohif/extension-cornerstone-dicom-sr": "3.11.0", "@ohif/extension-cornerstone-dicom-sr": "3.11.0",
"@ohif/extension-ultrasound-pleura-bline": "3.11.0", "@ohif/extension-ultrasound-pleura-bline": "3.11.0",
"i18next": "^17.0.3" "i18next": "17.3.1"
}, },
"devDependencies": { "devDependencies": {
"@babel/core": "7.28.0", "@babel/core": "7.28.0",
"@babel/eslint-parser": "7.28.0", "@babel/eslint-parser": "7.28.0",
"@babel/plugin-syntax-dynamic-import": "^7.8.3", "@babel/plugin-syntax-dynamic-import": "7.8.3",
"@babel/plugin-transform-arrow-functions": "^7.16.7", "@babel/plugin-transform-arrow-functions": "7.27.1",
"@babel/plugin-transform-class-properties": "^7.27.1", "@babel/plugin-transform-class-properties": "7.27.1",
"@babel/plugin-transform-object-rest-spread": "^7.28.0", "@babel/plugin-transform-object-rest-spread": "7.28.0",
"@babel/plugin-transform-private-methods": "^7.27.1", "@babel/plugin-transform-private-methods": "7.27.1",
"@babel/plugin-transform-regenerator": "^7.16.7", "@babel/plugin-transform-regenerator": "7.28.1",
"@babel/plugin-transform-runtime": "7.28.0", "@babel/plugin-transform-runtime": "7.28.0",
"@babel/plugin-transform-typescript": "^7.28.0", "@babel/plugin-transform-typescript": "7.28.0",
"@babel/preset-env": "7.28.0", "@babel/preset-env": "7.28.0",
"@babel/preset-react": "^7.27.1", "@babel/preset-react": "7.27.1",
"@babel/preset-typescript": "^7.27.1", "@babel/preset-typescript": "7.27.1",
"@svgr/webpack": "^8.1.0", "@svgr/webpack": "8.1.0",
"babel-loader": "^8.0.0-beta.4", "babel-loader": "8.4.1",
"clean-webpack-plugin": "^4.0.0", "clean-webpack-plugin": "3.0.0",
"copy-webpack-plugin": "^10.2.0", "copy-webpack-plugin": "9.1.0",
"cross-env": "^7.0.3", "cross-env": "7.0.3",
"dotenv": "^14.1.0", "dotenv": "8.6.0",
"eslint": "^8.39.0", "eslint": "8.57.1",
"eslint-loader": "^2.0.0", "eslint-loader": "2.2.1",
"webpack": "5.95.0", "webpack": "5.95.0",
"webpack-cli": "^5.0.2", "webpack-cli": "5.1.4",
"webpack-merge": "^5.7.3" "webpack-merge": "5.10.0"
} }
} }

View File

@ -55,13 +55,13 @@
"test:unit:ci": "lerna run test:unit:ci --parallel --stream", "test:unit:ci": "lerna run test:unit:ci --parallel --stream",
"test:e2e": "lerna run test:e2e --stream", "test:e2e": "lerna run test:e2e --stream",
"test:e2e:coverage": "nyc --reporter=html yarn run test:e2e:ci", "test:e2e:coverage": "nyc --reporter=html yarn run test:e2e:ci",
"test:e2e:ci": "TEST_ENV=true npx playwright test", "test:e2e:ci": "cross-env TEST_ENV=true npx playwright test",
"test:e2e:ui": "TEST_ENV=true npx playwright test --ui", "test:e2e:ui": "cross-env TEST_ENV=true npx playwright test --ui",
"test:e2e:reporter": "TEST_ENV=true npx playwright test --reporter=html", "test:e2e:reporter": "cross-env TEST_ENV=true npx playwright test --reporter=html",
"test:e2e:update": "TEST_ENV=true npx playwright test --reporter=html --update-snapshots -g shouldUpdateThis", "test:e2e:update": "cross-env TEST_ENV=true npx playwright test --reporter=html --update-snapshots -g shouldUpdateThis",
"test:e2e:update:debug": "TEST_ENV=true npx playwright test -g @debug --reporter=html --update-snapshots -g shouldUpdateThis", "test:e2e:update:debug": "cross-env TEST_ENV=true npx playwright test -g @debug --reporter=html --update-snapshots -g shouldUpdateThis",
"test:e2e:headed": "TEST_ENV=true npx playwright test --headed", "test:e2e:headed": "cross-env TEST_ENV=true npx playwright test --headed",
"test:e2e:debug": "TEST_ENV=true npx playwright test --debug", "test:e2e:debug": "cross-env TEST_ENV=true npx playwright test --debug",
"test:e2e:dist": "lerna run test:e2e:dist --stream", "test:e2e:dist": "lerna run test:e2e:dist --stream",
"test:e2e:serve": "yarn test:data && lerna run test:e2e:serve --stream", "test:e2e:serve": "yarn test:data && lerna run test:e2e:serve --stream",
"see-changed": "lerna changed", "see-changed": "lerna changed",
@ -75,39 +75,50 @@
"link-list": "npm ls --depth=0 --link=true" "link-list": "npm ls --depth=0 --link=true"
}, },
"dependencies": { "dependencies": {
"execa": "^8.0.1" "execa": "8.0.1"
}, },
"optionalDependencies": { "optionalDependencies": {
"@percy/cypress": "^3.1.1", "@percy/cypress": "3.1.6",
"@playwright/test": "^1.48.0" "@playwright/test": "1.56.1"
}, },
"devDependencies": { "devDependencies": {
"@babel/core": "7.28.0", "@babel/core": "7.28.0",
"@babel/plugin-transform-class-properties": "^7.27.1", "@babel/plugin-syntax-dynamic-import": "7.8.3",
"@babel/plugin-transform-object-rest-spread": "^7.28.0", "@babel/plugin-transform-arrow-functions": "7.27.1",
"@babel/plugin-transform-private-methods": "^7.27.1", "@babel/plugin-transform-class-properties": "7.27.1",
"@babel/plugin-transform-private-property-in-object": "^7.27.1", "@babel/plugin-transform-for-of": "7.27.1",
"@babel/plugin-syntax-dynamic-import": "^7.8.3", "@babel/plugin-transform-object-rest-spread": "7.28.0",
"@babel/plugin-transform-arrow-functions": "^7.16.7", "@babel/plugin-transform-private-methods": "7.27.1",
"@babel/plugin-transform-for-of": "^7.27.1", "@babel/plugin-transform-private-property-in-object": "7.27.1",
"@babel/plugin-transform-regenerator": "^7.16.7", "@babel/plugin-transform-regenerator": "7.28.1",
"@babel/plugin-transform-runtime": "7.28.0", "@babel/plugin-transform-runtime": "7.28.0",
"@babel/plugin-transform-typescript": "^7.28.0", "@babel/plugin-transform-typescript": "7.28.0",
"@babel/preset-env": "7.28.0", "@babel/preset-env": "7.28.0",
"@babel/preset-react": "^7.27.1", "@babel/preset-react": "7.27.1",
"@babel/preset-typescript": "^7.27.1", "@babel/preset-typescript": "7.27.1",
"babel-plugin-istanbul": "^7.0.0", "@istanbuljs/nyc-config-typescript": "1.0.2",
"@istanbuljs/nyc-config-typescript": "^1.0.2", "@pmmmwh/react-refresh-webpack-plugin": "0.5.17",
"babel-loader": "8.4.1",
"babel-plugin-istanbul": "7.0.0",
"babel-plugin-transform-import-meta": "2.3.3",
"cross-env": "7.0.3",
"css-loader": "6.11.0",
"cypress": "14.5.2", "cypress": "14.5.2",
"cypress-file-upload": "^5.0.8", "cypress-file-upload": "5.0.8",
"file-loader": "6.2.0",
"istanbul": "0.4.5", "istanbul": "0.4.5",
"nyc": "^17.1.0", "nyc": "17.1.0",
"patch-package": "^8.0.0", "patch-package": "8.0.0",
"playwright-test-coverage": "^1.2.12", "playwright-test-coverage": "1.2.12",
"prettier": "^3.3.3", "postcss-loader": "6.2.1",
"prettier": "3.6.2",
"prettier-plugin-tailwindcss": "0.6.9", "prettier-plugin-tailwindcss": "0.6.9",
"source-map-support": "^0.5.21", "shader-loader": "1.3.1",
"ts-node": "^10.9.2" "source-map-support": "0.5.21",
"style-loader": "1.3.0",
"terser-webpack-plugin": "5.3.14",
"ts-node": "10.9.2",
"worker-loader": "3.0.8"
}, },
"husky": { "husky": {
"hooks": { "hooks": {
@ -122,17 +133,22 @@
}, },
"resolutions": { "resolutions": {
"commander": "8.3.0", "commander": "8.3.0",
"cross-env": "7.0.3",
"cross-spawn": "7.0.6",
"dcmjs": "0.43.1", "dcmjs": "0.43.1",
"path-to-regexp": "0.1.12", "path-to-regexp": "0.1.12",
"nth-check": "^2.1.1", "nth-check": "2.1.1",
"trim-newlines": "^5.0.0", "trim-newlines": "5.0.0",
"glob-parent": "^6.0.2", "glob-parent": "6.0.2",
"trim": "^1.0.0", "trim": "1.0.1",
"package-json": "^8.1.0", "package-json": "8.1.1",
"sharp": "^0.32.6",
"rollup": "2.79.2", "rollup": "2.79.2",
"body-parser": "1.20.3", "body-parser": "1.20.3",
"form-data": ">=4.0.4" "form-data": ">=4.0.4",
"axios": "1.12.0",
"core-js": "3.45.1",
"@babel/runtime-corejs2": "7.26.10",
"tapable": "2.2.2"
}, },
"packageManager": "yarn@1.22.22+sha512.a6b2f7906b721bba3d67d4aff083df04dad64c399707841b7acf00f6b133b7ac24255f2652fa22ae3534329dc6180534e98d17432037ff6fd140556e2bb3137e" "packageManager": "yarn@1.22.22+sha512.a6b2f7906b721bba3d67d4aff083df04dad64c399707841b7acf00f6b133b7ac24255f2652fa22ae3534329dc6180534e98d17432037ff6fd140556e2bb3137e"
} }

View File

@ -48,13 +48,13 @@
"README.md" "README.md"
], ],
"dependencies": { "dependencies": {
"@babel/runtime": "^7.20.13", "@babel/runtime": "7.28.2",
"@cornerstonejs/codec-charls": "^1.2.3", "@cornerstonejs/codec-charls": "1.2.3",
"@cornerstonejs/codec-libjpeg-turbo-8bit": "^1.2.2", "@cornerstonejs/codec-libjpeg-turbo-8bit": "1.2.2",
"@cornerstonejs/codec-openjpeg": "^1.2.4", "@cornerstonejs/codec-openjpeg": "1.2.4",
"@cornerstonejs/codec-openjph": "^2.4.5", "@cornerstonejs/codec-openjph": "2.4.7",
"@cornerstonejs/dicom-image-loader": "^3.32.5", "@cornerstonejs/dicom-image-loader": "4.5.13",
"@emotion/serialize": "^1.1.3", "@emotion/serialize": "1.3.3",
"@ohif/core": "3.11.0", "@ohif/core": "3.11.0",
"@ohif/extension-cornerstone": "3.11.0", "@ohif/extension-cornerstone": "3.11.0",
"@ohif/extension-cornerstone-dicom-rt": "3.11.0", "@ohif/extension-cornerstone-dicom-rt": "3.11.0",
@ -74,41 +74,52 @@
"@ohif/mode-ultrasound-pleura-bline": "3.11.0", "@ohif/mode-ultrasound-pleura-bline": "3.11.0",
"@ohif/ui": "3.11.0", "@ohif/ui": "3.11.0",
"@ohif/ui-next": "3.11.0", "@ohif/ui-next": "3.11.0",
"@svgr/webpack": "^8.1.0", "@svgr/webpack": "8.1.0",
"@types/react": "^18.3.3", "@types/react": "18.3.23",
"classnames": "^2.3.2", "classnames": "2.5.1",
"core-js": "*", "core-js": "3.45.1",
"cornerstone-math": "^0.1.9", "cornerstone-math": "0.1.10",
"dcmjs": "0.43.1", "dcmjs": "0.43.1",
"detect-gpu": "^4.0.16", "detect-gpu": "4.0.50",
"dicom-parser": "^1.8.9", "dicom-parser": "1.8.21",
"dotenv-webpack": "^1.7.0", "dotenv-webpack": "1.8.0",
"file-loader": "^6.2.0", "file-loader": "6.2.0",
"hammerjs": "^2.0.8", "hammerjs": "2.0.8",
"history": "^5.3.0", "history": "5.3.0",
"i18next": "^17.0.3", "i18next": "17.3.1",
"i18next-browser-languagedetector": "^3.0.1", "i18next-browser-languagedetector": "3.1.1",
"lodash.isequal": "4.5.0", "lodash.isequal": "4.5.0",
"oidc-client": "1.11.5", "oidc-client": "1.11.5",
"oidc-client-ts": "^3.0.1", "oidc-client-ts": "3.3.0",
"prop-types": "^15.7.2", "prop-types": "15.8.1",
"query-string": "^6.12.1", "query-string": "6.14.1",
"react": "^18.3.1", "react": "18.3.1",
"react-dom": "^18.3.1", "react-dom": "18.3.1",
"react-dropzone": "^10.1.7", "react-dropzone": "10.2.2",
"react-i18next": "^12.2.2", "react-i18next": "12.3.1",
"react-resize-detector": "^10.0.1", "react-resize-detector": "10.0.1",
"react-router": "^6.23.1", "react-router": "6.30.1",
"react-router-dom": "^6.8.1", "react-router-dom": "6.30.1",
"react-shepherd": "6.1.1", "react-shepherd": "6.1.1",
"shepherd.js": "13.0.3", "shepherd.js": "13.0.3",
"url-loader": "^4.1.1", "url-loader": "4.1.1",
"zustand": "4.5.5" "zustand": "4.5.5"
}, },
"devDependencies": { "devDependencies": {
"@babel/plugin-transform-private-methods": "^7.27.1", "@babel/plugin-transform-private-methods": "7.27.1",
"@types/node": "^20.12.12", "@pmmmwh/react-refresh-webpack-plugin": "0.5.17",
"@types/node": "20.19.9",
"clean-webpack-plugin": "4.0.0",
"copy-webpack-plugin": "10.2.4",
"cross-env": "7.0.3",
"extract-css-chunks-webpack-plugin": "4.10.0",
"html-webpack-plugin": "5.6.3",
"identity-obj-proxy": "3.0.x", "identity-obj-proxy": "3.0.x",
"tailwindcss": "3.2.4" "mini-css-extract-plugin": "2.9.2",
"shx": "0.3.4",
"tailwindcss": "3.2.4",
"terser-webpack-plugin": "5.3.14",
"webpack-dev-server": "5.2.2",
"workbox-webpack-plugin": "6.6.1"
} }
} }

View File

@ -25,6 +25,7 @@ window.config = {
// above, the number of requests can be go a lot higher. // above, the number of requests can be go a lot higher.
prefetch: 25, prefetch: 25,
}, },
showErrorDetails: 'always', // 'always', 'dev', 'production'
// filterQueryParam: false, // filterQueryParam: false,
// Defines multi-monitor layouts // Defines multi-monitor layouts
multimonitor: [ multimonitor: [

View File

@ -128,11 +128,17 @@ const createRoutes = ({
]; ];
function RouteWithErrorBoundary({ route, ...rest }) { function RouteWithErrorBoundary({ route, ...rest }) {
const [appConfig] = useAppConfig();
const { showErrorDetails } = appConfig;
history.navigate = useNavigate(); history.navigate = useNavigate();
// eslint-disable-next-line react/jsx-props-no-spreading // eslint-disable-next-line react/jsx-props-no-spreading
return ( return (
<ErrorBoundary context={`Route ${route.path}`}> <ErrorBoundary
context={`Route ${route.path}`}
showErrorDetails={showErrorDetails}
>
<route.children <route.children
{...rest} {...rest}
{...route.props} {...route.props}

View File

@ -21,20 +21,20 @@
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@babel/core": "7.28.0", "@babel/core": "7.28.0",
"axios": "^1.11.0", "axios": "1.12.0",
"chalk": "^5.0.0", "chalk": "5.4.1",
"execa": "^8.0.1", "execa": "8.0.1",
"gitignore": "^0.7.0", "gitignore": "0.7.0",
"inquirer": "^8.2.0", "inquirer": "8.2.6",
"listr": "^0.14.3", "listr": "0.14.3",
"mustache": "^4.2.0", "mustache": "4.2.0",
"ncp": "^2.0.0", "ncp": "2.0.0",
"node-fetch": "^3.1.1", "node-fetch": "3.3.2",
"pkg-install": "^1.0.0", "pkg-install": "1.0.0",
"registry-url": "^6.0.0", "registry-url": "6.0.1",
"spdx-license-list": "^6.4.0", "spdx-license-list": "6.10.0",
"util": "^0.12.4", "util": "0.12.5",
"yarn-programmatic": "^0.1.2" "yarn-programmatic": "0.1.2"
}, },
"files": [ "files": [
"bin/", "bin/",

View File

@ -33,33 +33,33 @@
"test:unit:ci": "jest --ci --runInBand --collectCoverage" "test:unit:ci": "jest --ci --runInBand --collectCoverage"
}, },
"peerDependencies": { "peerDependencies": {
"@cornerstonejs/codec-charls": "^1.2.3", "@cornerstonejs/codec-charls": "1.2.3",
"@cornerstonejs/codec-libjpeg-turbo-8bit": "^1.2.2", "@cornerstonejs/codec-libjpeg-turbo-8bit": "1.2.2",
"@cornerstonejs/codec-openjpeg": "^1.2.4", "@cornerstonejs/codec-openjpeg": "1.2.4",
"@cornerstonejs/codec-openjph": "^2.4.5", "@cornerstonejs/codec-openjph": "2.4.7",
"@cornerstonejs/core": "^3.32.5", "@cornerstonejs/core": "4.5.13",
"@cornerstonejs/dicom-image-loader": "^3.32.5", "@cornerstonejs/dicom-image-loader": "4.5.13",
"@ohif/ui": "3.11.0", "@ohif/ui": "3.11.0",
"cornerstone-math": "0.1.9", "cornerstone-math": "0.1.9",
"dicom-parser": "^1.8.21" "dicom-parser": "1.8.21"
}, },
"dependencies": { "dependencies": {
"@babel/runtime": "^7.20.13", "@babel/runtime": "7.28.2",
"dcmjs": "0.43.1", "dcmjs": "0.43.1",
"dicomweb-client": "^0.10.4", "dicomweb-client": "0.10.4",
"gl-matrix": "^3.4.3", "gl-matrix": "3.4.3",
"immutability-helper": "^3.1.1", "immutability-helper": "3.1.1",
"isomorphic-base64": "^1.0.2", "isomorphic-base64": "1.0.2",
"lodash.clonedeep": "^4.5.0", "lodash.clonedeep": "4.5.0",
"lodash.isequal": "^4.5.0", "lodash.isequal": "4.5.0",
"moment": "^2.30.1", "moment": "2.30.1",
"object-hash": "2.1.1", "object-hash": "2.1.1",
"query-string": "^6.14.0", "query-string": "6.14.1",
"react-shepherd": "6.1.1", "react-shepherd": "6.1.1",
"shepherd.js": "13.0.3", "shepherd.js": "13.0.3",
"validate.js": "^0.12.0" "validate.js": "0.12.0"
}, },
"devDependencies": { "devDependencies": {
"webpack-merge": "*" "webpack-merge": "5.10.0"
} }
} }

View File

@ -196,6 +196,7 @@ if auth headers are used, a preflight request is required.
- `activateViewportBeforeInteraction`: (default to true), if set to false, tools can be used directly without the need to click and activate the viewport. - `activateViewportBeforeInteraction`: (default to true), if set to false, tools can be used directly without the need to click and activate the viewport.
- `autoPlayCine`: (default to false), if set to true, data sets with the DICOM frame time tag (i.e. (0018,1063)) will auto play when displayed - `autoPlayCine`: (default to false), if set to true, data sets with the DICOM frame time tag (i.e. (0018,1063)) will auto play when displayed
- `addWindowLevelActionMenu`: (default to true), if set to false, the window level action menu item is NOT added to the viewport action corners - `addWindowLevelActionMenu`: (default to true), if set to false, the window level action menu item is NOT added to the viewport action corners
- `showErrorDetails`: determines which runtime environments can display exception and error details caught at the `ErrorBoundary`; acceptable values include: `always`, `dev`, and `production`
- `dangerouslyUseDynamicConfig`: Dynamic config allows user to pass `configUrl` query string. This allows to load config without recompiling application. If the `configUrl` query string is passed, the worklist and modes will load from the referenced json rather than the default .env config. If there is no `configUrl` path provided, the default behaviour is used and there should not be any deviation from current user experience.<br/> - `dangerouslyUseDynamicConfig`: Dynamic config allows user to pass `configUrl` query string. This allows to load config without recompiling application. If the `configUrl` query string is passed, the worklist and modes will load from the referenced json rather than the default .env config. If there is no `configUrl` path provided, the default behaviour is used and there should not be any deviation from current user experience.<br/>
Points to consider while using `dangerouslyUseDynamicConfig`:<br/> Points to consider while using `dangerouslyUseDynamicConfig`:<br/>
- User have to enable this feature by setting `dangerouslyUseDynamicConfig.enabled:true`. By default it is `false`. - User have to enable this feature by setting `dangerouslyUseDynamicConfig.enabled:true`. By default it is `false`.

View File

@ -48,52 +48,52 @@
"@docusaurus/tsconfig": "3.0.0", "@docusaurus/tsconfig": "3.0.0",
"@docusaurus/types": "3.0.0", "@docusaurus/types": "3.0.0",
"@mdx-js/react": "3.0.1", "@mdx-js/react": "3.0.1",
"@radix-ui/react-accordion": "^1.2.0", "@radix-ui/react-accordion": "1.2.11",
"@radix-ui/react-checkbox": "^1.1.1", "@radix-ui/react-checkbox": "1.3.2",
"@radix-ui/react-dialog": "^1.1.1", "@radix-ui/react-dialog": "1.1.14",
"@radix-ui/react-dropdown-menu": "^2.1.1", "@radix-ui/react-dropdown-menu": "2.1.15",
"@radix-ui/react-hover-card": "^1.1.6", "@radix-ui/react-hover-card": "1.1.14",
"@radix-ui/react-icons": "^1.3.0", "@radix-ui/react-icons": "1.3.2",
"@radix-ui/react-label": "^2.1.0", "@radix-ui/react-label": "2.1.7",
"@radix-ui/react-popover": "^1.0.7", "@radix-ui/react-popover": "1.1.14",
"@radix-ui/react-scroll-area": "^1.1.0", "@radix-ui/react-scroll-area": "1.2.9",
"@radix-ui/react-select": "^2.1.1", "@radix-ui/react-select": "2.2.5",
"@radix-ui/react-separator": "^1.1.0", "@radix-ui/react-separator": "1.1.7",
"@radix-ui/react-slider": "^1.2.0", "@radix-ui/react-slider": "1.3.5",
"@radix-ui/react-slot": "^1.0.2", "@radix-ui/react-slot": "1.2.3",
"@radix-ui/react-switch": "^1.1.0", "@radix-ui/react-switch": "1.2.5",
"@radix-ui/react-tabs": "^1.1.0", "@radix-ui/react-tabs": "1.1.12",
"@radix-ui/react-toggle": "^1.1.0", "@radix-ui/react-toggle": "1.1.9",
"@radix-ui/react-tooltip": "^1.1.2", "@radix-ui/react-tooltip": "1.2.7",
"@svgr/webpack": "^5.5.0", "@svgr/webpack": "8.1.0",
"@types/react": "^18.2.29", "@types/react": "18.3.23",
"autoprefixer": "^10.4.20", "autoprefixer": "10.4.21",
"class-variance-authority": "^0.7.0", "class-variance-authority": "0.7.1",
"classnames": "^2.3.2", "classnames": "2.5.1",
"clsx": "^1.1.1", "clsx": "2.1.1",
"cmdk": "^1.0.0", "cmdk": "1.1.1",
"date-fns": "^3.6.0", "date-fns": "3.6.0",
"docusaurus-plugin-image-zoom": "^1.0.1", "docusaurus-plugin-image-zoom": "1.0.1",
"file-loader": "^6.2.0", "file-loader": "6.2.0",
"framer-motion": "6.2.4", "framer-motion": "6.2.4",
"glob": "^10.3.10", "glob": "10.4.5",
"lucide-react": "^0.379.0", "lucide-react": "0.379.0",
"next-themes": "^0.3.0", "next-themes": "0.3.0",
"postcss": "^8.4.47", "postcss": "8.5.6",
"postcss-import": "^14.0.2", "postcss-import": "14.1.0",
"postcss-preset-env": "^7.4.3", "postcss-preset-env": "7.8.3",
"prism-react-renderer": "^2.1.0", "prism-react-renderer": "2.1.0",
"react": "^18.3.1", "react": "18.3.1",
"react-day-picker": "^8.10.1", "react-day-picker": "8.10.1",
"react-dom": "^18.3.1", "react-dom": "18.3.1",
"react-outside-click-handler": "^1.3.0", "react-outside-click-handler": "1.3.0",
"react-shepherd": "6.1.1", "react-shepherd": "6.1.1",
"shepherd.js": "13.0.3", "shepherd.js": "13.0.3",
"sonner": "^1.4.41", "sonner": "1.7.4",
"tailwind-merge": "^2.3.0", "tailwind-merge": "2.6.0",
"tailwindcss": "^3.4.13", "tailwindcss": "3.2.4",
"tailwindcss-animate": "^1.0.7", "tailwindcss-animate": "1.0.7",
"typescript": "~5.2.2", "typescript": "5.5.4",
"url-loader": "^4.1.1" "url-loader": "4.1.1"
} }
} }

View File

@ -31,24 +31,24 @@
"test:unit:ci": "echo 'platform/i18n: missing unit tests'" "test:unit:ci": "echo 'platform/i18n: missing unit tests'"
}, },
"peerDependencies": { "peerDependencies": {
"i18next": "^17.0.3", "i18next": "17.3.1",
"i18next-browser-languagedetector": "^3.0.1", "i18next-browser-languagedetector": "3.1.1",
"react": "^18.3.1", "react": "18.3.1",
"react-dom": "^18.3.1", "react-dom": "18.3.1",
"react-i18next": "^12.2.2" "react-i18next": "12.3.1"
}, },
"dependencies": { "dependencies": {
"@babel/runtime": "^7.20.13", "@babel/runtime": "7.28.2",
"i18next-locize-backend": "^2.0.0", "i18next-locize-backend": "2.2.2",
"locize-editor": "^2.0.0", "locize-editor": "2.2.2",
"locize-lastused": "^1.1.0" "locize-lastused": "1.1.1"
}, },
"devDependencies": { "devDependencies": {
"i18next": "^17.0.3", "i18next": "17.3.1",
"i18next-browser-languagedetector": "^3.0.1", "i18next-browser-languagedetector": "3.1.1",
"react": "^18.3.1", "react": "18.3.1",
"react-dom": "^18.3.1", "react-dom": "18.3.1",
"react-i18next": "^12.2.2", "react-i18next": "12.3.1",
"webpack-merge": "^5.7.3" "webpack-merge": "5.10.0"
} }
} }

View File

@ -12,7 +12,7 @@
"README.md" "README.md"
], ],
"scripts": { "scripts": {
"clean": "rm -rf node_modules/.cache/storybook && shx rm -rf dist", "clean": "shx rm -rf dist",
"clean:deep": "yarn run clean && shx rm -rf node_modules", "clean:deep": "yarn run clean && shx rm -rf node_modules",
"start": "yarn run build --watch", "start": "yarn run build --watch",
"dev": "cross-env NODE_ENV=development webpack serve --config .webpack/webpack.playground.js", "dev": "cross-env NODE_ENV=development webpack serve --config .webpack/webpack.playground.js",
@ -27,43 +27,44 @@
".": "./src/index.ts" ".": "./src/index.ts"
}, },
"dependencies": { "dependencies": {
"@radix-ui/react-accordion": "^1.2.0", "@radix-ui/react-accordion": "1.2.11",
"@radix-ui/react-checkbox": "^1.1.1", "@radix-ui/react-checkbox": "1.3.2",
"@radix-ui/react-context-menu": "^2.2.4", "@radix-ui/react-context-menu": "2.2.15",
"@radix-ui/react-dialog": "^1.1.1", "@radix-ui/react-dialog": "1.1.14",
"@radix-ui/react-dropdown-menu": "^2.1.1", "@radix-ui/react-dropdown-menu": "2.1.15",
"@radix-ui/react-hover-card": "^1.1.6", "@radix-ui/react-hover-card": "1.1.14",
"@radix-ui/react-icons": "^1.3.0", "@radix-ui/react-icons": "1.3.2",
"@radix-ui/react-label": "^2.1.0", "@radix-ui/react-label": "2.1.7",
"@radix-ui/react-popover": "^1.0.7", "@radix-ui/react-popover": "1.1.14",
"@radix-ui/react-scroll-area": "^1.1.0", "@radix-ui/react-scroll-area": "1.2.9",
"@radix-ui/react-select": "^2.1.1", "@radix-ui/react-select": "2.2.5",
"@radix-ui/react-separator": "^1.1.0", "@radix-ui/react-separator": "1.1.7",
"@radix-ui/react-slider": "^1.2.0", "@radix-ui/react-slider": "1.3.5",
"@radix-ui/react-slot": "^1.0.2", "@radix-ui/react-slot": "1.2.3",
"@radix-ui/react-switch": "^1.1.0", "@radix-ui/react-switch": "1.2.5",
"@radix-ui/react-tabs": "^1.1.0", "@radix-ui/react-tabs": "1.1.12",
"@radix-ui/react-toggle": "^1.1.0", "@radix-ui/react-toggle": "1.1.9",
"@radix-ui/react-tooltip": "^1.1.2", "@radix-ui/react-toggle-group": "1.1.10",
"class-variance-authority": "^0.7.0", "@radix-ui/react-tooltip": "1.2.7",
"clsx": "*", "class-variance-authority": "0.7.1",
"cmdk": "^1.0.0", "clsx": "2.1.1",
"date-fns": "^3.6.0", "cmdk": "1.1.1",
"date-fns": "3.6.0",
"framer-motion": "6.2.4", "framer-motion": "6.2.4",
"lucide-react": "^0.379.0", "lucide-react": "0.379.0",
"next-themes": "^0.3.0", "next-themes": "0.3.0",
"react": "^18.3.1", "react": "18.3.1",
"react-day-picker": "^8.10.1", "react-day-picker": "8.10.1",
"react-resizable-panels": "^2.1.7", "react-resizable-panels": "2.1.9",
"react-shepherd": "6.1.1", "react-shepherd": "6.1.1",
"shepherd.js": "13.0.3", "shepherd.js": "13.0.3",
"sonner": "^1.5.0", "sonner": "1.7.4",
"tailwind-merge": "^2.3.0", "tailwind-merge": "2.6.0",
"tailwindcss": "3.2.4", "tailwindcss": "3.2.4",
"tailwindcss-animate": "^1.0.7" "tailwindcss-animate": "1.0.7"
}, },
"devDependencies": { "devDependencies": {
"@babel/plugin-transform-private-property-in-object": "^7.27.1" "@babel/plugin-transform-private-property-in-object": "7.27.1"
}, },
"keywords": [], "keywords": [],
"author": "OHIF", "author": "OHIF",

View File

@ -1,4 +1,4 @@
import React, { useState, useRef } from 'react'; import React, { useState } from 'react';
import { Button } from '../../components/Button/Button'; import { Button } from '../../components/Button/Button';
import { import {
DropdownMenu, DropdownMenu,
@ -81,292 +81,291 @@ interface DataRowProps {
className?: string; className?: string;
} }
export const DataRow: React.FC<DataRowProps> = ({ const DataRow = React.forwardRef<HTMLDivElement, DataRowProps>(
number, (
title, {
colorHex, number,
details, title,
onSelect, colorHex,
isLocked, details,
onToggleVisibility, onSelect,
onToggleLocked, isLocked,
onRename, onToggleVisibility,
onDelete, onToggleLocked,
onColor, onRename,
isSelected = false, onDelete,
isVisible = true, onColor,
disableEditing = false, isSelected = false,
className, isVisible = true,
}) => { disableEditing = false,
const [isDropdownOpen, setIsDropdownOpen] = useState(false); className,
const isTitleLong = title?.length > 25; },
const rowRef = useRef<HTMLDivElement>(null); ref
) => {
const [isDropdownOpen, setIsDropdownOpen] = useState(false);
const isTitleLong = title?.length > 25;
// useEffect(() => { const handleAction = (action: string, e: React.MouseEvent) => {
// if (isSelected && rowRef.current) { e.stopPropagation();
// setTimeout(() => { switch (action) {
// rowRef.current?.scrollIntoView({ behavior: 'smooth', block: 'center' }); case 'Rename':
// }, 200); onRename(e);
// } break;
// }, [isSelected]); case 'Lock':
onToggleLocked(e);
break;
case 'Delete':
onDelete(e);
break;
case 'Color':
onColor(e);
break;
}
};
const handleAction = (action: string, e: React.MouseEvent) => { const decodeHTML = (html: string) => {
e.stopPropagation(); const txt = document.createElement('textarea');
switch (action) { txt.innerHTML = html;
case 'Rename': return txt.value;
onRename(e); };
break;
case 'Lock':
onToggleLocked(e);
break;
case 'Delete':
onDelete(e);
break;
case 'Color':
onColor(e);
break;
}
};
const decodeHTML = (html: string) => { const renderDetailText = (text: string, indent: number = 0) => {
const txt = document.createElement('textarea'); const indentation = ' '.repeat(indent);
txt.innerHTML = html; if (text === '') {
return txt.value; return (
}; <div
key={`empty-${indent}`}
const renderDetailText = (text: string, indent: number = 0) => { className="h-2"
const indentation = ' '.repeat(indent); ></div>
if (text === '') { );
}
const cleanText = decodeHTML(text);
return ( return (
<div <div
key={`empty-${indent}`} key={cleanText}
className="h-2" className="whitespace-pre-wrap"
></div> >
{indentation}
<span className="font-medium">{cleanText}</span>
</div>
); );
} };
const cleanText = decodeHTML(text);
return (
<div
key={cleanText}
className="whitespace-pre-wrap"
>
{indentation}
<span className="font-medium">{cleanText}</span>
</div>
);
};
const renderDetails = (details: string[]) => { const renderDetails = (details: string[]) => {
const visibleLines = details.slice(0, 4); const visibleLines = details.slice(0, 4);
const hiddenLines = details.slice(4); const hiddenLines = details.slice(4);
return ( return (
<Tooltip> <Tooltip>
<TooltipTrigger asChild> <TooltipTrigger asChild>
<div className="cursor-help"> <div className="cursor-help">
<div className="flex flex-col space-y-1"> <div className="flex flex-col space-y-1">
{visibleLines.map((line, lineIndex) => {visibleLines.map((line, lineIndex) =>
renderDetailText(line, line.startsWith(' ') ? 1 : 0)
)}
</div>
{hiddenLines.length > 0 && (
<div className="text-muted-foreground mt-1 flex items-center text-sm">
<span>...</span>
<Icons.Info className="mr-1 h-5 w-5" />
</div>
)}
</div>
</TooltipTrigger>
<TooltipContent
side="right"
align="start"
className="max-w-md"
>
<div className="text-secondary-foreground flex flex-col space-y-1 text-sm leading-normal">
{details.map((line, lineIndex) =>
renderDetailText(line, line.startsWith(' ') ? 1 : 0) renderDetailText(line, line.startsWith(' ') ? 1 : 0)
)} )}
</div> </div>
{hiddenLines.length > 0 && ( </TooltipContent>
<div className="text-muted-foreground mt-1 flex items-center text-sm"> </Tooltip>
<span>...</span> );
<Icons.Info className="mr-1 h-5 w-5" /> };
</div>
)}
</div>
</TooltipTrigger>
<TooltipContent
side="right"
align="start"
className="max-w-md"
>
<div className="text-secondary-foreground flex flex-col space-y-1 text-sm leading-normal">
{details.map((line, lineIndex) =>
renderDetailText(line, line.startsWith(' ') ? 1 : 0)
)}
</div>
</TooltipContent>
</Tooltip>
);
};
return ( return (
<div
ref={rowRef}
className={cn('flex flex-col', !isVisible && 'opacity-60', className)}
>
<div <div
className={`flex items-center ${ ref={ref}
isSelected ? 'bg-popover' : 'bg-muted' className={cn('flex flex-col', !isVisible && 'opacity-60', className)}
} group relative cursor-pointer`}
onClick={onSelect}
data-cy="data-row"
> >
{/* Hover Overlay */} <div
<div className="bg-primary/20 pointer-events-none absolute inset-0 opacity-0 transition-opacity group-hover:opacity-100"></div> className={`flex items-center ${
isSelected ? 'bg-popover' : 'bg-muted'
} group relative cursor-pointer`}
onClick={onSelect}
data-cy="data-row"
>
{/* Hover Overlay */}
<div className="bg-primary/20 pointer-events-none absolute inset-0 opacity-0 transition-opacity group-hover:opacity-100"></div>
{/* Number Box */} {/* Number Box */}
{number !== null && ( {number !== null && (
<div <div
className={`flex h-7 max-h-7 w-7 flex-shrink-0 items-center justify-center rounded-l border-r border-black text-base ${ className={`flex h-7 max-h-7 w-7 flex-shrink-0 items-center justify-center rounded-l border-r border-black text-base ${
isSelected ? 'bg-highlight text-black' : 'bg-muted text-muted-foreground' isSelected ? 'bg-highlight text-black' : 'bg-muted text-muted-foreground'
} overflow-hidden`} } overflow-hidden`}
> >
{number} {number}
</div> </div>
)} )}
{/* add some space if there is not segment index */} {/* add some space if there is not segment index */}
{number === null && <div className="ml-1 h-7"></div>} {number === null && <div className="ml-1 h-7"></div>}
{colorHex && ( {colorHex && (
<div className="flex h-7 w-5 items-center justify-center"> <div className="flex h-7 w-5 items-center justify-center">
<span <span
className="ml-2 h-2 w-2 rounded-full" className="ml-2 h-2 w-2 rounded-full"
style={{ backgroundColor: colorHex }} style={{ backgroundColor: colorHex }}
></span> ></span>
</div> </div>
)} )}
{/* Label with Conditional Tooltip */} {/* Label with Conditional Tooltip */}
<div className="ml-2 flex-1 overflow-hidden"> <div className="ml-2 flex-1 overflow-hidden">
{isTitleLong ? ( {isTitleLong ? (
<Tooltip> <Tooltip>
<TooltipTrigger asChild> <TooltipTrigger asChild>
<span <span
className={`cursor-default text-base ${ className={`cursor-default text-base ${
isSelected ? 'text-highlight' : 'text-muted-foreground' isSelected ? 'text-highlight' : 'text-muted-foreground'
} [overflow:hidden] [display:-webkit-box] [-webkit-line-clamp:2] [-webkit-box-orient:vertical]`} } [overflow:hidden] [display:-webkit-box] [-webkit-line-clamp:2] [-webkit-box-orient:vertical]`}
>
{title}
</span>
</TooltipTrigger>
<TooltipContent
side="top"
align="center"
> >
{title} {title}
</span> </TooltipContent>
</TooltipTrigger> </Tooltip>
<TooltipContent ) : (
side="top" <span
align="center" className={`text-base ${
isSelected ? 'text-highlight' : 'text-muted-foreground'
} [overflow:hidden] [display:-webkit-box] [-webkit-line-clamp:2] [-webkit-box-orient:vertical]`}
> >
{title} {title}
</TooltipContent> </span>
</Tooltip> )}
) : ( </div>
<span
className={`text-base ${ {/* Actions and Visibility Toggle */}
isSelected ? 'text-highlight' : 'text-muted-foreground' <div className="relative ml-2 flex items-center space-x-1">
} [overflow:hidden] [display:-webkit-box] [-webkit-line-clamp:2] [-webkit-box-orient:vertical]`} {/* Visibility Toggle Icon */}
<Button
size="icon"
variant="ghost"
className={`h-6 w-6 transition-opacity ${
isSelected || !isVisible ? 'opacity-100' : 'opacity-0 group-hover:opacity-100'
}`}
aria-label={isVisible ? 'Hide' : 'Show'}
onClick={e => {
e.stopPropagation();
onToggleVisibility(e);
}}
> >
{title} {isVisible ? <Icons.Hide className="h-6 w-6" /> : <Icons.Show className="h-6 w-6" />}
</span> </Button>
)}
</div>
{/* Actions and Visibility Toggle */} {/* Lock Icon (if needed) */}
<div className="relative ml-2 flex items-center space-x-1"> {isLocked && !disableEditing && (
{/* Visibility Toggle Icon */} <Icons.Lock className="text-muted-foreground h-6 w-6" />
<Button )}
size="icon"
variant="ghost"
className={`h-6 w-6 transition-opacity ${
isSelected || !isVisible ? 'opacity-100' : 'opacity-0 group-hover:opacity-100'
}`}
aria-label={isVisible ? 'Hide' : 'Show'}
onClick={e => {
e.stopPropagation();
onToggleVisibility(e);
}}
>
{isVisible ? <Icons.Hide className="h-6 w-6" /> : <Icons.Show className="h-6 w-6" />}
</Button>
{/* Lock Icon (if needed) */} {/* Actions Dropdown Menu */}
{isLocked && !disableEditing && <Icons.Lock className="text-muted-foreground h-6 w-6" />} {disableEditing && <div className="h-6 w-6"></div>}
{!disableEditing && (
{/* Actions Dropdown Menu */} <DropdownMenu onOpenChange={open => setIsDropdownOpen(open)}>
{disableEditing && <div className="h-6 w-6"></div>} <DropdownMenuTrigger asChild>
{!disableEditing && ( <Button
<DropdownMenu onOpenChange={open => setIsDropdownOpen(open)}> size="icon"
<DropdownMenuTrigger asChild> variant="ghost"
<Button className={`h-6 w-6 transition-opacity ${
size="icon" isSelected || isDropdownOpen
variant="ghost" ? 'opacity-100'
className={`h-6 w-6 transition-opacity ${ : 'opacity-0 group-hover:opacity-100'
isSelected || isDropdownOpen }`}
? 'opacity-100' aria-label="Actions"
: 'opacity-0 group-hover:opacity-100' dataCY="actionsMenuTrigger"
}`} onClick={e => e.stopPropagation()} // Prevent row selection on button click
aria-label="Actions" >
dataCY="actionsMenuTrigger" <Icons.More className="h-6 w-6" />
onClick={e => e.stopPropagation()} // Prevent row selection on button click </Button>
</DropdownMenuTrigger>
<DropdownMenuContent
align="end"
// this was causing issue for auto focus on input dialog
onCloseAutoFocus={e => e.preventDefault()}
> >
<Icons.More className="h-6 w-6" /> <>
</Button> <DropdownMenuItem onClick={e => handleAction('Rename', e)}>
</DropdownMenuTrigger> <Icons.Rename className="text-foreground" />
<DropdownMenuContent
align="end"
// this was causing issue for auto focus on input dialog
onCloseAutoFocus={e => e.preventDefault()}
>
<>
<DropdownMenuItem onClick={e => handleAction('Rename', e)}>
<Icons.Rename className="text-foreground" />
<span
className="pl-2"
data-cy="Rename"
>
Rename
</span>
</DropdownMenuItem>
<DropdownMenuItem onClick={e => handleAction('Delete', e)}>
<Icons.Delete className="text-foreground" />
<span
className="pl-2"
data-cy="Delete"
>
Delete
</span>
</DropdownMenuItem>
{onColor && (
<DropdownMenuItem onClick={e => handleAction('Color', e)}>
<Icons.ColorChange className="text-foreground" />
<span <span
className="pl-2" className="pl-2"
data-cy="Change Color" data-cy="Rename"
> >
Change Color Rename
</span> </span>
</DropdownMenuItem> </DropdownMenuItem>
)} <DropdownMenuItem onClick={e => handleAction('Delete', e)}>
<DropdownMenuItem onClick={e => handleAction('Lock', e)}> <Icons.Delete className="text-foreground" />
<Icons.Lock className="text-foreground" /> <span
<span className="pl-2"
className="pl-2" data-cy="Delete"
data-cy="LockToggle" >
> Delete
{isLocked ? 'Unlock' : 'Lock'} </span>
</span> </DropdownMenuItem>
</DropdownMenuItem> {onColor && (
</> <DropdownMenuItem onClick={e => handleAction('Color', e)}>
</DropdownMenuContent> <Icons.ColorChange className="text-foreground" />
</DropdownMenu> <span
)} className="pl-2"
</div> data-cy="Change Color"
</div> >
Change Color
{/* Details Section */} </span>
{details && (details.primary?.length > 0 || details.secondary?.length > 0) && ( </DropdownMenuItem>
<div className="ml-7 px-2 py-2"> )}
<div className="text-secondary-foreground flex items-center gap-1 text-base leading-normal"> <DropdownMenuItem onClick={e => handleAction('Lock', e)}>
{details.primary?.length > 0 && renderDetails(details.primary)} <Icons.Lock className="text-foreground" />
{details.secondary?.length > 0 && ( <span
<div className="text-muted-foreground ml-auto text-sm"> className="pl-2"
{renderDetails(details.secondary)} data-cy="LockToggle"
</div> >
{isLocked ? 'Unlock' : 'Lock'}
</span>
</DropdownMenuItem>
</>
</DropdownMenuContent>
</DropdownMenu>
)} )}
</div> </div>
</div> </div>
)}
</div> {/* Details Section */}
); {details && (details.primary?.length > 0 || details.secondary?.length > 0) && (
}; <div className="ml-7 px-2 py-2">
<div className="text-secondary-foreground flex items-center gap-1 text-base leading-normal">
{details.primary?.length > 0 && renderDetails(details.primary)}
{details.secondary?.length > 0 && (
<div className="text-muted-foreground ml-auto text-sm">
{renderDetails(details.secondary)}
</div>
)}
</div>
</div>
)}
</div>
);
}
);
export default DataRow; export default DataRow;
export { DataRow };

View File

@ -112,10 +112,17 @@ interface ErrorBoundaryError extends Error {
stack?: string; stack?: string;
} }
enum ShowErrorDetails {
always = 'always',
dev = 'dev',
production = 'production',
}
interface DefaultFallbackProps extends FallbackProps { interface DefaultFallbackProps extends FallbackProps {
error: ErrorBoundaryError; error: ErrorBoundaryError;
context: string; context: string;
resetErrorBoundary: () => void; resetErrorBoundary: () => void;
showErrorDetails?: ShowErrorDetails;
} }
interface ErrorBoundaryProps { interface ErrorBoundaryProps {
@ -126,13 +133,21 @@ interface ErrorBoundaryProps {
children: React.ReactNode; children: React.ReactNode;
fallbackRoute?: string | null; fallbackRoute?: string | null;
isPage?: boolean; isPage?: boolean;
showErrorDetails?: ShowErrorDetails;
} }
const DefaultFallback = ({ const DefaultFallback = ({
error, error,
context, context,
resetErrorBoundary = () => {}, resetErrorBoundary = () => {},
showErrorDetails,
}: DefaultFallbackProps) => { }: DefaultFallbackProps) => {
const isShowDetailsButtonVisible =
showErrorDetails == null ||
showErrorDetails === ShowErrorDetails.always ||
(showErrorDetails === ShowErrorDetails.dev && !isProduction) ||
(showErrorDetails === ShowErrorDetails.production && isProduction);
const { t } = useTranslation('ErrorBoundary'); const { t } = useTranslation('ErrorBoundary');
const [showDetails, setShowDetails] = useState(false); const [showDetails, setShowDetails] = useState(false);
const { show } = useNotification(); const { show } = useNotification();
@ -165,17 +180,15 @@ const DefaultFallback = ({
type: 'error', type: 'error',
duration: 0, duration: 0,
id: errorId, id: errorId,
action: { action: isShowDetailsButtonVisible
label: t('Show Details'), ? {
onClick: () => setShowDetails(true), label: t('Show Details'),
}, onClick: () => setShowDetails(true),
}
: undefined,
}); });
}, [error, errorTitle, subtitle, t, title, show]); }, [error, errorTitle, subtitle, t, title, show]);
if (isProduction) {
return null;
}
return ( return (
<Dialog <Dialog
open={showDetails} open={showDetails}
@ -249,6 +262,7 @@ const ErrorBoundary = ({
onError = _error => {}, onError = _error => {},
fallbackComponent: FallbackComponent = DefaultFallback, fallbackComponent: FallbackComponent = DefaultFallback,
children, children,
showErrorDetails,
}: ErrorBoundaryProps) => { }: ErrorBoundaryProps) => {
const [error, setError] = useState<ErrorBoundaryError | null>(null); const [error, setError] = useState<ErrorBoundaryError | null>(null);
@ -303,6 +317,7 @@ const ErrorBoundary = ({
<FallbackComponent <FallbackComponent
{...props} {...props}
context={context} context={context}
showErrorDetails={showErrorDetails}
/> />
)} )}
onReset={onResetHandler} onReset={onResetHandler}
@ -315,6 +330,7 @@ const ErrorBoundary = ({
error={error} error={error}
context={context} context={context}
resetErrorBoundary={() => setError(null)} resetErrorBoundary={() => setError(null)}
showErrorDetails={showErrorDetails}
/> />
)} )}
</> </>

View File

@ -54,7 +54,10 @@ const renderRangeSetting = option => {
key={option.id} key={option.id}
> >
<div className="w-1/3 text-[13px]">{option.name}</div> <div className="w-1/3 text-[13px]">{option.name}</div>
<div className="w-2/3"> <div
className="w-2/3"
data-cy={option.id}
>
<RowInputRange <RowInputRange
minValue={option.min} minValue={option.min}
maxValue={option.max} maxValue={option.max}

View File

@ -1,4 +1,4 @@
import React from 'react'; import React, { useEffect } from 'react';
import { ScrollArea, DataRow } from '../../components'; import { ScrollArea, DataRow } from '../../components';
import { HoverCard, HoverCardTrigger, HoverCardContent } from '../../components/HoverCard'; import { HoverCard, HoverCardTrigger, HoverCardContent } from '../../components/HoverCard';
import { useSegmentationTableContext, useSegmentationExpanded } from './contexts'; import { useSegmentationTableContext, useSegmentationExpanded } from './contexts';
@ -23,6 +23,11 @@ export const SegmentationSegments = ({ children = null }: { children?: React.Rea
let segmentation; let segmentation;
let representation; let representation;
const activeSegmentRef = React.useRef<{
element: HTMLElement | null;
index: number | null;
}>({ element: null, index: null });
try { try {
// Try to use the SegmentationExpanded context if available // Try to use the SegmentationExpanded context if available
const segmentationInfo = useSegmentationExpanded('SegmentationSegments'); const segmentationInfo = useSegmentationExpanded('SegmentationSegments');
@ -38,108 +43,163 @@ export const SegmentationSegments = ({ children = null }: { children?: React.Rea
} }
const segments = Object.values(representation.segments); const segments = Object.values(representation.segments);
// Find the active segment to scroll to it when it changes
const activeSegment = segments.find(segment => {
if (!segment) {
return false;
}
const segmentFromSegmentation = segmentation.segments[segment.segmentIndex];
return segmentFromSegmentation?.active;
});
const isActiveSegmentation = segmentation.segmentationId === activeSegmentationId; const isActiveSegmentation = segmentation.segmentationId === activeSegmentationId;
const { ref: scrollableContainerRef, maxHeight } = useDynamicMaxHeight(segments); const { ref: scrollableContainerRef, maxHeight } = useDynamicMaxHeight(segments);
useEffect(() => {
const activeSegmentIndex = activeSegmentRef.current.index;
if (!activeSegmentIndex || activeSegmentIndex !== activeSegment?.segmentIndex) {
return;
}
const activeSegmentElement = activeSegmentRef.current.element;
if (!activeSegmentElement) {
return;
}
// Check if the active segment is already visible.
const activeSegmentElementBounds = activeSegmentElement.getBoundingClientRect();
const scrollableContainerRect = scrollableContainerRef.current.getBoundingClientRect();
if (
activeSegmentElementBounds.top > scrollableContainerRect.top &&
activeSegmentElementBounds.bottom < scrollableContainerRect.bottom
) {
// The active segment is already visible, so we don't need to scroll.
return;
}
activeSegmentElement.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
}, [activeSegment?.segmentIndex, scrollableContainerRef]);
if (!representation || !segmentation) { if (!representation || !segmentation) {
return null; return null;
} }
return ( return (
<ScrollArea <div ref={scrollableContainerRef}>
className={`bg-bkg-low space-y-px`} <ScrollArea
showArrows={true} className={`bg-bkg-low space-y-px`}
> showArrows={
<div scrollableContainerRef?.current
ref={scrollableContainerRef} ? scrollableContainerRef?.current?.offsetHeight >= parseFloat(maxHeight)
style={{ maxHeight: maxHeight }} : false
}
> >
{segments.map(segment => { <div style={{ maxHeight: maxHeight }}>
if (!segment) { {segments.map(segment => {
return null; if (!segment) {
} return null;
const { segmentIndex, color, visible } = segment as { }
segmentIndex: number; const { segmentIndex, color, visible } = segment as {
color: number[]; segmentIndex: number;
visible: boolean; color: number[];
}; visible: boolean;
const segmentFromSegmentation = segmentation.segments[segmentIndex]; };
const segmentFromSegmentation = segmentation.segments[segmentIndex];
if (!segmentFromSegmentation) { if (!segmentFromSegmentation) {
return null; return null;
} }
const { locked, active, label, displayText } = segmentFromSegmentation; const { locked, active, label, displayText } = segmentFromSegmentation;
const cssColor = `rgb(${color[0]},${color[1]},${color[2]})`; const cssColor = `rgb(${color[0]},${color[1]},${color[2]})`;
const hasStats = segmentFromSegmentation.cachedStats?.namedStats; const hasStats = segmentFromSegmentation.cachedStats?.namedStats;
const DataRowComponent = (
<DataRow const segmentRowRef = (element: HTMLElement) => {
key={segmentIndex} if (!active) {
number={showSegmentIndex ? segmentIndex : null} return;
title={label}
// details={displayText}
description={displayText}
colorHex={cssColor}
isSelected={active}
isVisible={visible}
isLocked={locked}
disableEditing={disableEditing}
className={!isActiveSegmentation ? 'opacity-80' : ''}
onColor={() => onSegmentColorClick(segmentation.segmentationId, segmentIndex)}
onToggleVisibility={() =>
onToggleSegmentVisibility(
segmentation.segmentationId,
segmentIndex,
representation.type
)
} }
onToggleLocked={() => onToggleSegmentLock(segmentation.segmentationId, segmentIndex)}
onSelect={() => onSegmentClick(segmentation.segmentationId, segmentIndex)}
onRename={() => onSegmentEdit(segmentation.segmentationId, segmentIndex)}
onDelete={() => onSegmentDelete(segmentation.segmentationId, segmentIndex)}
/>
);
return hasStats ? ( if (element) {
<HoverCard activeSegmentRef.current = { element, index: segmentIndex };
key={`hover-${segmentIndex}`} } else {
openDelay={300} activeSegmentRef.current = { element: null, index: null };
> }
<HoverCardTrigger asChild> };
<div>{DataRowComponent}</div>
</HoverCardTrigger>
<HoverCardContent
side="left"
align="start"
className="w-72 border"
>
<div className="mb-4 flex items-center space-x-2">
<div
className="h-2.5 w-2.5 flex-shrink-0 rounded-full"
style={{ backgroundColor: cssColor }}
></div>
<h3 className="text-muted-foreground break-words font-semibold">{label}</h3>
</div>
<SegmentStatistics const DataRowComponent = (
segment={{ <DataRow
...segmentFromSegmentation, ref={segmentRowRef}
key={segmentIndex}
number={showSegmentIndex ? segmentIndex : null}
title={label}
// details={displayText}
description={displayText}
colorHex={cssColor}
isSelected={active}
isVisible={visible}
isLocked={locked}
disableEditing={disableEditing}
className={!isActiveSegmentation ? 'opacity-80' : ''}
onColor={() => onSegmentColorClick(segmentation.segmentationId, segmentIndex)}
onToggleVisibility={() =>
onToggleSegmentVisibility(
segmentation.segmentationId,
segmentIndex, segmentIndex,
}} representation.type
segmentationId={segmentation.segmentationId} )
}
onToggleLocked={() =>
onToggleSegmentLock(segmentation.segmentationId, segmentIndex)
}
onSelect={() => onSegmentClick(segmentation.segmentationId, segmentIndex)}
onRename={() => onSegmentEdit(segmentation.segmentationId, segmentIndex)}
onDelete={() => onSegmentDelete(segmentation.segmentationId, segmentIndex)}
/>
);
return hasStats ? (
<HoverCard
key={`hover-${segmentIndex}`}
openDelay={300}
>
<HoverCardTrigger asChild>
<div>{DataRowComponent}</div>
</HoverCardTrigger>
<HoverCardContent
side="left"
align="start"
className="w-72 border"
> >
{children} <div className="mb-4 flex items-center space-x-2">
</SegmentStatistics> <div
</HoverCardContent> className="h-2.5 w-2.5 flex-shrink-0 rounded-full"
</HoverCard> style={{ backgroundColor: cssColor }}
) : ( ></div>
DataRowComponent <h3 className="text-muted-foreground break-words font-semibold">{label}</h3>
); </div>
})}
</div> <SegmentStatistics
</ScrollArea> segment={{
...segmentFromSegmentation,
segmentIndex,
}}
segmentationId={segmentation.segmentationId}
>
{children}
</SegmentStatistics>
</HoverCardContent>
</HoverCard>
) : (
DataRowComponent
);
})}
</div>
</ScrollArea>
</div>
); );
}; };

View File

@ -1,5 +1,29 @@
import { useRef, useState, useEffect, RefObject } from 'react'; import { useRef, useState, useEffect, RefObject } from 'react';
const _getMovementIntersectionObserver = ({
callback,
rootMargin,
threshold,
}: {
callback: () => void;
rootMargin: string;
threshold: number[];
}): IntersectionObserver => {
return new IntersectionObserver(
entries => {
entries.forEach(entry => {
if (entry.isIntersecting) {
callback();
}
});
},
{
threshold,
rootMargin,
}
);
};
/** /**
* Calculates the maximum height for an element based on its position * Calculates the maximum height for an element based on its position
* relative to the bottom of the viewport. * relative to the bottom of the viewport.
@ -31,20 +55,49 @@ export function useDynamicMaxHeight(
} }
}; };
// Calculate initially // Two intersection observers to trigger a recalculation when the target element
// Use requestAnimationFrame to ensure layout is stable after initial render // moves up or down. One for moving up and one for moving down.
const rafId = requestAnimationFrame(calculateMaxHeight); // Note that with this approach we don't need to use a resize observer nor
// a window resize listener.
// Recalculate on window resize // The trick is to use a margin for the IntersectionObserver to detect movement.
window.addEventListener('resize', calculateMaxHeight); // See more below.
const rootMarginHeight = maxHeight === '100vh' ? `${window.innerHeight}px` : `${maxHeight}`;
// Note that we use a fine grained threshold because we don't know how
// much it will move and we want any movement to trigger the intersection observer.
const threshold = [0, 0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 1.0];
// The trick here is to use the calculated maxHeight as the root margin height
// so that any movement of the target element down (i.e. "out of the" viewport)
// will trigger the intersection observer.
const moveDownIntersectionObserver = _getMovementIntersectionObserver({
callback: calculateMaxHeight,
rootMargin: `0px 0px ${rootMarginHeight} 0px`,
threshold,
});
// The trick here is to use the calculated maxHeight as the negative
// root margin height so that any movement of the target element up
// (i.e. "into the" viewport) will trigger the intersection observer.
const moveUpIntersectionObserver = _getMovementIntersectionObserver({
callback: calculateMaxHeight,
rootMargin: `0px 0px -${rootMarginHeight} 0px`,
threshold,
});
if (ref.current) {
moveUpIntersectionObserver.observe(ref.current);
moveDownIntersectionObserver.observe(ref.current);
}
// Cleanup listener and requestAnimationFrame on component unmount // Cleanup listener and requestAnimationFrame on component unmount
return () => { return () => {
window.removeEventListener('resize', calculateMaxHeight); moveUpIntersectionObserver.disconnect();
cancelAnimationFrame(rafId); moveDownIntersectionObserver.disconnect();
}; };
// Dependencies: buffer, minHeight, and data. // Dependencies: buffer, minHeight, and data.
}, [data, buffer, minHeight]); }, [data, buffer, minHeight, maxHeight]);
return { ref, maxHeight }; return { ref, maxHeight };
} }

View File

@ -19,23 +19,20 @@
"README.md" "README.md"
], ],
"scripts": { "scripts": {
"clean": "rm -rf node_modules/.cache/storybook && shx rm -rf dist", "clean": "shx rm -rf dist",
"clean:deep": "yarn run clean && shx rm -rf node_modules", "clean:deep": "yarn run clean && shx rm -rf node_modules",
"start": "yarn run build --watch", "start": "yarn run build --watch",
"build": "cross-env NODE_ENV=production webpack --config .webpack/webpack.prod.js", "build": "cross-env NODE_ENV=production webpack --config .webpack/webpack.prod.js",
"build:package": "yarn run build", "build:package": "yarn run build"
"storybook": "storybook dev -p 6006",
"dev": "storybook dev -p 6006",
"build-storybook": "storybook build"
}, },
"peerDependencies": { "peerDependencies": {
"react": "^18.3.1", "react": "18.3.1",
"react-dom": "^18.3.1" "react-dom": "18.3.1"
}, },
"dependencies": { "dependencies": {
"@testing-library/react": "^13.1.0", "@testing-library/react": "13.4.0",
"browser-detect": "^0.2.28", "browser-detect": "0.2.28",
"classnames": "^2.3.2", "classnames": "2.5.1",
"d3-array": "3", "d3-array": "3",
"d3-axis": "3", "d3-axis": "3",
"d3-scale": "4", "d3-scale": "4",
@ -43,45 +40,36 @@
"d3-selection": "3", "d3-selection": "3",
"d3-shape": "3", "d3-shape": "3",
"d3-zoom": "3", "d3-zoom": "3",
"lodash.clonedeep": "^4.5.0", "lodash.clonedeep": "4.5.0",
"lodash.debounce": "^4.0.8", "lodash.debounce": "4.0.8",
"lodash.merge": "^4.6.1", "lodash.merge": "4.6.2",
"moment": "^2.30.1", "moment": "2.30.1",
"mousetrap": "^1.6.5", "mousetrap": "1.6.5",
"react": "^18.3.1", "react": "18.3.1",
"react-dates": "^21.8.0", "react-dates": "21.8.0",
"react-dnd": "14.0.2", "react-dnd": "14.0.2",
"react-dnd-html5-backend": "14.0.0", "react-dnd-html5-backend": "14.0.0",
"react-dom": "^18.3.1", "react-dom": "18.3.1",
"react-draggable": "^4.4.6", "react-draggable": "4.5.0",
"react-error-boundary": "^3.1.3", "react-error-boundary": "3.1.4",
"react-modal": "3.11.2", "react-modal": "3.11.2",
"react-outside-click-handler": "^1.3.0", "react-outside-click-handler": "1.3.0",
"react-select": "5.7.4", "react-select": "5.7.4",
"react-test-renderer": "^18.3.1", "react-test-renderer": "18.3.1",
"react-window": "^1.8.9", "react-window": "1.8.11",
"react-with-direction": "^1.3.1", "react-with-direction": "1.4.0",
"swiper": "^8.4.2", "swiper": "8.4.7",
"webpack": "5.95.0" "webpack": "5.95.0"
}, },
"devDependencies": { "devDependencies": {
"@babel/core": "7.28.0", "@babel/core": "7.28.0",
"@storybook/addon-actions": "^7.6.10", "autoprefixer": "10.4.21",
"@storybook/addon-docs": "^7.6.10", "babel-loader": "8.4.1",
"@storybook/addon-essentials": "^7.6.10", "dotenv-webpack": "1.8.0",
"@storybook/addon-links": "^7.6.10", "postcss": "8.5.6",
"@storybook/cli": "^7.6.10", "postcss-loader": "6.2.1",
"@storybook/react": "^7.6.10", "prop-types": "15.8.1",
"@storybook/react-webpack5": "^7.6.10", "remark-gfm": "3.0.1",
"@storybook/source-loader": "^7.6.10",
"autoprefixer": "^10.4.14",
"babel-loader": "^9.1.2",
"dotenv-webpack": "^8.0.1",
"postcss": "^8.4.23",
"postcss-loader": "^7.2.4",
"prop-types": "^15.8.1",
"remark-gfm": "^3.0.1",
"storybook": "^7.6.10",
"tailwindcss": "3.2.4" "tailwindcss": "3.2.4"
} }
} }

View File

@ -0,0 +1,144 @@
import { test, expect } from 'playwright-test-coverage';
import { visitStudy } from './utils';
import { viewportSVGPathLocator } from './utils/locators';
import { simulateNormalizedDragOnElement } from './utils/simulateDragOnElement';
const studyInstanceUID = '1.2.840.113619.2.290.3.3767434740.226.1600859119.501';
test('should not allow contours to be edited in basic viewer mode', async ({ page }) => {
const mode = 'viewer';
await visitStudy(page, studyInstanceUID, mode, 2000);
await page.getByTestId('side-panel-header-right').click();
await page.getByTestId('study-browser-thumbnail-no-image').dblclick();
// Wait for the segmentation to be loaded.
await page.waitForTimeout(5000);
await page.getByTestId('yes-hydrate-btn').click();
// Wait for the segmentation to hydrate.
await page.waitForTimeout(5000);
const svgPathLocatorPreEdit = viewportSVGPathLocator({ page, viewportId: 'default' });
expect(
await svgPathLocatorPreEdit.count(),
'Expected exactly 1 path element in the viewport'
).toBe(1);
const expectedPathCommands = await svgPathLocatorPreEdit.getAttribute('d');
// Try to drag one of the edges of the rectangular contour.
await simulateNormalizedDragOnElement({
locator: svgPathLocatorPreEdit,
start: { x: 0.1, y: 0 },
end: { x: 0.1, y: -0.2 },
});
const svgPathLocatorPostEdit = viewportSVGPathLocator({ page, viewportId: 'default' });
expect(
await svgPathLocatorPostEdit.getAttribute('d'),
'Expected the path commands to be the same as the pre-edit path commands'
).toBe(expectedPathCommands);
});
test('should not allow contours to be edited when panelSegmentation.disableEditing is true', async ({
page,
}) => {
const mode = 'segmentation';
await visitStudy(page, studyInstanceUID, mode, 2000);
await page.getByTestId('side-panel-header-right').click();
await page.getByTestId('study-browser-thumbnail-no-image').dblclick();
// Wait for the segmentation to be loaded.
await page.waitForTimeout(5000);
// disable editing of segmentations via the customization service
await page.evaluate(() => {
window.services.customizationService.setGlobalCustomization(
'panelSegmentation.disableEditing',
{
$set: true,
}
);
});
await page.getByTestId('yes-hydrate-btn').click();
// Wait for the segmentation to hydrate.
await page.waitForTimeout(5000);
const svgPathLocatorPreEdit = viewportSVGPathLocator({ page, viewportId: 'default' });
expect(
await svgPathLocatorPreEdit.count(),
'Expected exactly 1 path element in the viewport'
).toBe(1);
const expectedPathCommands = await svgPathLocatorPreEdit.getAttribute('d');
// Try to drag one of the edges of the rectangular contour.
await simulateNormalizedDragOnElement({
locator: svgPathLocatorPreEdit,
start: { x: 0.1, y: 0 },
end: { x: 0.1, y: -0.2 },
});
const svgPathLocatorPostEdit = viewportSVGPathLocator({ page, viewportId: 'default' });
expect(
await svgPathLocatorPostEdit.getAttribute('d'),
'Expected the path commands to be the same as the pre-edit path commands'
).toBe(expectedPathCommands);
});
test('should allow contours to be edited when panelSegmentation.disableEditing is false', async ({
page,
}) => {
const mode = 'segmentation';
await visitStudy(page, studyInstanceUID, mode, 2000);
await page.getByTestId('side-panel-header-right').click();
await page.getByTestId('study-browser-thumbnail-no-image').dblclick();
// Wait for the segmentation to be loaded.
await page.waitForTimeout(5000);
// disable editing of segmentations via the customization service
await page.evaluate(() => {
window.services.customizationService.setGlobalCustomization(
'panelSegmentation.disableEditing',
{
$set: false,
}
);
});
await page.getByTestId('yes-hydrate-btn').click();
// Wait for the segmentation to hydrate.
await page.waitForTimeout(5000);
const svgPathLocatorPreEdit = viewportSVGPathLocator({ page, viewportId: 'default' });
expect(
await svgPathLocatorPreEdit.count(),
'Expected exactly 1 path element in the viewport'
).toBe(1);
const preEditPathCommands = await svgPathLocatorPreEdit.getAttribute('d');
// Try to drag one of the edges of the rectangular contour.
await simulateNormalizedDragOnElement({
locator: svgPathLocatorPreEdit,
start: { x: 0.1, y: 0 },
end: { x: 0.1, y: -0.2 },
});
const svgPathLocatorPostEdit = viewportSVGPathLocator({ page, viewportId: 'default' });
expect(
await svgPathLocatorPostEdit.getAttribute('d'),
'Not expecting the path commands to be the same as the pre-edit path commands'
).not.toBe(preEditPathCommands);
});

View File

@ -0,0 +1,118 @@
import { test } from 'playwright-test-coverage';
import { visitStudy, checkForScreenshot, screenShotPaths } from './utils';
import { press } from './utils/keyboardUtils';
import { simulateNormalizedDragOnElement } from './utils/simulateDragOnElement';
import { viewportLocator } from './utils/locators';
test.beforeEach(async ({ page }) => {
const studyInstanceUID = '1.3.6.1.4.1.14519.5.2.1.256467663913010332776401703474716742458';
const mode = 'segmentation';
await visitStudy(page, studyInstanceUID, mode, 2000);
});
test('should prevent editing of label map segmentations when panelSegmentation.disableEditing is true', async ({
page,
}) => {
// disable editing of segmentations via the customization service
await page.evaluate(() => {
window.services.customizationService.setGlobalCustomization(
'panelSegmentation.disableEditing',
{
$set: true,
}
);
});
await page.getByTestId('study-browser-thumbnail-no-image').dblclick();
// Wait for the segmentation to be loaded.
await page.waitForTimeout(5000);
await page.getByTestId('yes-hydrate-btn').click();
// Wait for the segmentation to hydrate.
await page.waitForTimeout(5000);
// navigate to the 12th image and ensure the correct overlay is displayed
await press({ page, key: 'ArrowDown', nTimes: 11 });
await checkForScreenshot(page, page, screenShotPaths.labelMapSegLocking.globalLockedSegPreEdit);
// Attempt to erase the segmentations.
await page.getByTestId('Eraser-btn').click();
// Use the largest eraser radius to help ensure the entire image is erased.
await page.locator(`css=div[data-cy="eraser-radius"] input`).fill('1000');
// Attempt to erase the segmentations by dragging the eraser tool across the image several times.
await simulateNormalizedDragOnElement({
locator: viewportLocator({ page, viewportId: 'default' }),
start: { x: 0.01, y: 0.25 },
end: { x: 1.0, y: 0.25 },
});
await simulateNormalizedDragOnElement({
locator: viewportLocator({ page, viewportId: 'default' }),
start: { x: 0.01, y: 0.5 },
end: { x: 1.0, y: 0.5 },
});
await simulateNormalizedDragOnElement({
locator: viewportLocator({ page, viewportId: 'default' }),
start: { x: 0.01, y: 0.75 },
end: { x: 1.0, y: 0.75 },
});
await checkForScreenshot(page, page, screenShotPaths.labelMapSegLocking.globalLockedSegPostEdit);
});
test('should allow editing of label map segmentations when panelSegmentation.disableEditing is false', async ({
page,
}) => {
// disable editing of segmentations via the customization service
await page.evaluate(() => {
window.services.customizationService.setGlobalCustomization(
'panelSegmentation.disableEditing',
{
$set: false,
}
);
});
await page.getByTestId('study-browser-thumbnail-no-image').dblclick();
// Wait for the segmentation to be loaded.
await page.waitForTimeout(5000);
await page.getByTestId('yes-hydrate-btn').click();
// Wait for the segmentation to hydrate.
await page.waitForTimeout(5000);
// navigate to the 12th image and ensure the correct overlay is displayed
await press({ page, key: 'ArrowDown', nTimes: 11 });
await checkForScreenshot(page, page, screenShotPaths.labelMapSegLocking.globalUnlockedSegPreEdit);
// Attempt to erase the segmentations.
await page.getByTestId('Eraser-btn').click();
// Use the largest eraser radius to help ensure the eraser passes over the entire image.
await page.locator(`css=div[data-cy="eraser-radius"] input`).fill('1000');
// Attempt to erase the segmentations by dragging the eraser tool across the image several times.
await simulateNormalizedDragOnElement({
locator: viewportLocator({ page, viewportId: 'default' }),
start: { x: 0.01, y: 0.25 },
end: { x: 1.0, y: 0.25 },
});
await simulateNormalizedDragOnElement({
locator: viewportLocator({ page, viewportId: 'default' }),
start: { x: 0.01, y: 0.5 },
end: { x: 1.0, y: 0.5 },
});
await simulateNormalizedDragOnElement({
locator: viewportLocator({ page, viewportId: 'default' }),
start: { x: 0.01, y: 0.75 },
end: { x: 1.0, y: 0.75 },
});
await checkForScreenshot(
page,
page,
screenShotPaths.labelMapSegLocking.globalUnlockedSegPostEdit
);
});

View File

@ -16,7 +16,7 @@ test('should launch MPR with unhydrated RTSTRUCT chosen from the data overlay me
await page.getByTestId('MPR').click(); await page.getByTestId('MPR').click();
// Wait 5 seconds for MPR to load. This is necessary in particular when screen shots are added or replaced. // Wait 5 seconds for MPR to load. This is necessary in particular when screen shots are added or replaced.
await page.waitForTimeout(5000); await page.waitForTimeout(10000);
await checkForScreenshot( await checkForScreenshot(
page, page,

View File

@ -10,9 +10,14 @@ test.beforeEach(async ({ page }) => {
test('should hydrate RT reports correctly', async ({ page }) => { test('should hydrate RT reports correctly', async ({ page }) => {
await page.getByTestId('side-panel-header-right').click(); await page.getByTestId('side-panel-header-right').click();
await page.getByTestId('study-browser-thumbnail-no-image').dblclick(); await page.getByTestId('study-browser-thumbnail-no-image').dblclick();
await page.waitForTimeout(5000);
await checkForScreenshot(page, page, screenShotPaths.rtHydration.rtPreHydration); await checkForScreenshot(page, page, screenShotPaths.rtHydration.rtPreHydration);
await page.getByTestId('yes-hydrate-btn').click(); await page.getByTestId('yes-hydrate-btn').click();
await page.waitForTimeout(5000);
await checkForScreenshot(page, page, screenShotPaths.rtHydration.rtPostHydration); await checkForScreenshot(page, page, screenShotPaths.rtHydration.rtPostHydration);
await page.getByText('Small Sphere').click(); await page.getByText('Small Sphere').click();
await page.waitForTimeout(5000);
await checkForScreenshot(page, page, screenShotPaths.rtHydration.rtJumpToStructure); await checkForScreenshot(page, page, screenShotPaths.rtHydration.rtJumpToStructure);
}); });

View File

@ -13,7 +13,7 @@ test('should hydrate an RTSTRUCT from MPR', async ({ page }) => {
await page.getByTestId('Layout').click(); await page.getByTestId('Layout').click();
await page.getByTestId('MPR').click(); await page.getByTestId('MPR').click();
await page.waitForTimeout(5000); await page.waitForTimeout(10000);
await checkForScreenshot(page, page, screenShotPaths.rtHydrationFromMPR.mprBeforeRT); await checkForScreenshot(page, page, screenShotPaths.rtHydrationFromMPR.mprBeforeRT);

View File

@ -2,6 +2,7 @@ import { test, expect } from 'playwright-test-coverage';
import { visitStudy, simulateNormalizedClickOnElement } from './utils/index'; import { visitStudy, simulateNormalizedClickOnElement } from './utils/index';
import { viewportLocator } from './utils/locators'; import { viewportLocator } from './utils/locators';
import { downloadAsString } from './utils/download'; import { downloadAsString } from './utils/download';
import toArray from 'extensions/dicom-microscopy/src/utils/toArray';
test('should create and download the TMTV CSV report correctly', async ({ page }) => { test('should create and download the TMTV CSV report correctly', async ({ page }) => {
const studyInstanceUID = '1.2.840.113619.2.290.3.3767434740.226.1600859119.501'; const studyInstanceUID = '1.2.840.113619.2.290.3.3767434740.226.1600859119.501';
@ -26,14 +27,22 @@ test('should create and download the TMTV CSV report correctly', async ({ page }
'202009231_tmtv.csv' '202009231_tmtv.csv'
); );
const tmtvCSVReportContent = await downloadAsString(download); const tmtvCSVReportContent: string = await downloadAsString(download);
const expectedHeaders =
'PatientID,PatientName,SeriesInstanceUID,StudyDate,StudyInstanceUID,center,count,id,kurtosis,label,lesionGlycolysis,max,maxLPS,mean,median,min,minLPS,peakLPS,peakValue,skewness,stdDev,volume';
const tmtvCSVReportHeaders = tmtvCSVReportContent
.substring(0, expectedHeaders.length)
.split(',')
.sort()
.join(',');
expect( expect(
tmtvCSVReportContent, tmtvCSVReportHeaders,
'Expected the file to start with specific column/value headers' 'Expected the file to start with specific column/value headers'
).toMatch( ).toBe(expectedHeaders);
/^id,label,min,max,mean,stdDev,median,skewness,kurtosis,count,maxLPS,minLPS,center,volume,peakValue,peakLPS,lesionGlycolysis,PatientID,PatientName,StudyInstanceUID,SeriesInstanceUID,StudyDate/
);
expect(tmtvCSVReportContent, 'Expected the patient name to be present').toContain( expect(tmtvCSVReportContent, 'Expected the patient name to be present').toContain(
'Water Phantom' 'Water Phantom'
); );

Binary file not shown.

Before

Width:  |  Height:  |  Size: 324 KiB

After

Width:  |  Height:  |  Size: 265 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 289 KiB

After

Width:  |  Height:  |  Size: 248 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 307 KiB

After

Width:  |  Height:  |  Size: 210 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 297 KiB

After

Width:  |  Height:  |  Size: 293 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 250 KiB

After

Width:  |  Height:  |  Size: 257 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 251 KiB

After

Width:  |  Height:  |  Size: 271 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 250 KiB

After

Width:  |  Height:  |  Size: 270 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 247 KiB

After

Width:  |  Height:  |  Size: 268 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 316 KiB

After

Width:  |  Height:  |  Size: 341 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 252 KiB

After

Width:  |  Height:  |  Size: 259 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 256 KiB

After

Width:  |  Height:  |  Size: 267 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 250 KiB

After

Width:  |  Height:  |  Size: 256 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 256 KiB

After

Width:  |  Height:  |  Size: 257 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 253 KiB

After

Width:  |  Height:  |  Size: 254 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 250 KiB

After

Width:  |  Height:  |  Size: 238 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 210 KiB

After

Width:  |  Height:  |  Size: 213 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 207 KiB

After

Width:  |  Height:  |  Size: 211 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 209 KiB

After

Width:  |  Height:  |  Size: 215 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 184 KiB

After

Width:  |  Height:  |  Size: 195 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 195 KiB

After

Width:  |  Height:  |  Size: 198 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 208 KiB

After

Width:  |  Height:  |  Size: 210 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 200 KiB

After

Width:  |  Height:  |  Size: 201 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 190 KiB

After

Width:  |  Height:  |  Size: 193 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 199 KiB

After

Width:  |  Height:  |  Size: 200 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 207 KiB

After

Width:  |  Height:  |  Size: 209 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 256 KiB

After

Width:  |  Height:  |  Size: 265 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 132 KiB

After

Width:  |  Height:  |  Size: 147 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 247 KiB

After

Width:  |  Height:  |  Size: 253 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 260 KiB

After

Width:  |  Height:  |  Size: 284 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 218 KiB

After

Width:  |  Height:  |  Size: 230 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 192 KiB

After

Width:  |  Height:  |  Size: 182 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 192 KiB

After

Width:  |  Height:  |  Size: 182 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 218 KiB

After

Width:  |  Height:  |  Size: 230 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 304 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 297 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 282 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 299 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 249 KiB

After

Width:  |  Height:  |  Size: 256 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 253 KiB

After

Width:  |  Height:  |  Size: 262 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 207 KiB

After

Width:  |  Height:  |  Size: 211 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 157 KiB

After

Width:  |  Height:  |  Size: 173 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 182 KiB

After

Width:  |  Height:  |  Size: 166 KiB

Some files were not shown because too many files have changed in this diff Show More