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:
install_bun:
steps:
- restore_cache:
keys:
- bun-cache-v2-{{ arch }}-latest
- run:
name: Install Bun
command: |
if [ ! -d "$HOME/.bun" ]; then
curl -fsSL https://bun.sh/install | bash
fi
curl -fsSL https://bun.sh/install | bash -s "bun-v1.2.23"
echo 'export BUN_INSTALL="$HOME/.bun"' >> $BASH_ENV
echo 'export PATH="$BUN_INSTALL/bin:$PATH"' >> $BASH_ENV
source $BASH_ENV
- save_cache:
key: bun-cache-v2-{{ arch }}-latest
paths:
- ~/.bun
jobs:
UNIT_TESTS:
@ -121,6 +112,34 @@ jobs:
- checkout
- attach_workspace:
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:
name: Install Dependencies
command: bun install --frozen-lockfile
@ -455,8 +474,13 @@ workflows:
filters:
branches:
only: master
# - HOLD_FOR_APPROVAL:
# type: approval
# requires:
# - BUILD
- NPM_PUBLISH:
requires:
# - HOLD_FOR_APPROVAL
- BUILD
- DOCKER_BETA_PUBLISH:
requires:

View File

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

View File

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

View File

@ -10,6 +10,6 @@
"deploy": "netlify deploy --prod --dir ./../platform/app/dist"
},
"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
WORKDIR /usr/src/app
RUN npm install -g bun
# RUN npm install -g lerna@7.4.2
RUN npm install -g bun@1.2.23
RUN npm install -g lerna@7.4.2
ENV PATH=/usr/src/app/node_modules/.bin:$PATH
# 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 install
RUN bun add ajv@8.12.0
# Copy the local directory
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.
# The nginx user cannot chmod it, so change to 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
ENTRYPOINT ["/usr/src/entrypoint.sh"]
CMD ["nginx", "-g", "daemon off;"]

View File

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

View File

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

View File

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

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-default": "3.11.0",
"@ohif/i18n": "3.11.0",
"prop-types": "^15.6.2",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"react-i18next": "^12.2.2",
"react-router": "^6.8.1",
"react-router-dom": "^6.8.1"
"prop-types": "15.8.1",
"react": "18.3.1",
"react-dom": "18.3.1",
"react-i18next": "12.3.1",
"react-router": "6.30.1",
"react-router-dom": "6.30.1"
},
"dependencies": {
"@babel/runtime": "^7.20.13",
"@cornerstonejs/adapters": "^3.32.5",
"@cornerstonejs/core": "^3.32.5",
"@babel/runtime": "7.28.2",
"@cornerstonejs/adapters": "4.5.13",
"@cornerstonejs/core": "4.5.13",
"@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-default": "3.11.0",
"@ohif/i18n": "3.11.0",
"prop-types": "^15.6.2",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"react-i18next": "^10.11.0",
"react-router": "^6.23.1",
"react-router-dom": "^6.23.1"
"prop-types": "15.8.1",
"react": "18.3.1",
"react-dom": "18.3.1",
"react-i18next": "12.3.1",
"react-router": "6.30.1",
"react-router-dom": "6.30.1"
},
"dependencies": {
"@babel/runtime": "^7.20.13",
"react-color": "^2.19.3"
"@babel/runtime": "7.28.2",
"react-color": "2.19.3"
}
}

View File

@ -37,18 +37,18 @@
"@ohif/extension-cornerstone": "3.11.0",
"@ohif/extension-default": "3.11.0",
"@ohif/i18n": "3.11.0",
"prop-types": "^15.6.2",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"react-i18next": "^12.2.2",
"react-router": "^6.23.1",
"react-router-dom": "^6.23.1"
"prop-types": "15.8.1",
"react": "18.3.1",
"react-dom": "18.3.1",
"react-i18next": "12.3.1",
"react-router": "6.30.1",
"react-router-dom": "6.30.1"
},
"dependencies": {
"@babel/runtime": "^7.20.13",
"@cornerstonejs/adapters": "^3.32.5",
"@cornerstonejs/core": "^3.32.5",
"@babel/runtime": "7.28.2",
"@cornerstonejs/adapters": "4.5.13",
"@cornerstonejs/core": "4.5.13",
"@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/ui": "3.11.0",
"dcmjs": "0.43.1",
"dicom-parser": "^1.8.9",
"hammerjs": "^2.0.8",
"prop-types": "^15.6.2",
"react": "^18.3.1"
"dicom-parser": "1.8.21",
"hammerjs": "2.0.8",
"prop-types": "15.8.1",
"react": "18.3.1"
},
"dependencies": {
"@babel/runtime": "^7.20.13",
"@cornerstonejs/adapters": "^3.32.5",
"@cornerstonejs/core": "^3.32.5",
"@cornerstonejs/tools": "^3.32.5",
"classnames": "^2.3.2"
"@babel/runtime": "7.28.2",
"@cornerstonejs/adapters": "4.5.13",
"@cornerstonejs/core": "4.5.13",
"@cornerstonejs/tools": "4.5.13",
"classnames": "2.5.1"
}
}

View File

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

View File

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

View File

@ -245,10 +245,35 @@ function commandsModule({
referencedDisplaySetInstanceUID
);
storePositionPresentation(referencedDisplaySet);
return commandsManager.runCommand('loadSegmentationDisplaySetsForViewport', {
const results = commandsManager.runCommand('loadSegmentationDisplaySetsForViewport', {
viewportId,
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') {
const results = commandsManager.runCommand('hydrateStructuredReport', {
displaySetInstanceUID: displaySet.displaySetInstanceUID,

View File

@ -1754,7 +1754,10 @@ class SegmentationService extends PubSubService {
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);
if (!segmentation) {
@ -1765,19 +1768,17 @@ class SegmentationService extends PubSubService {
const { cachedStats } = segments[segmentIndex];
if (!cachedStats || !cachedStats?.center || !cachedStats?.namedStats?.center) {
return;
if (cachedStats?.center) {
const { center } = cachedStats;
return center as { image: csTypes.Point3; world: csTypes.Point3 };
}
const { center } = cachedStats;
if (!center) {
if (cachedStats?.namedStats?.center) {
return {
world: cachedStats.namedStats.center.value,
};
}
return center;
}
private _setSegmentLockedStatus(segmentationId: string, segmentIndex: number, isLocked: boolean) {

View File

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

View File

@ -262,17 +262,19 @@ function createDicomJSONApi(dicomJsonConfig) {
const { StudyInstanceUID, SeriesInstanceUID } = displaySet;
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();
series.instances.forEach(instance => {
if (instance?.metadata?.SOPInstanceUID) {
const { metadata, url } = instance;
const existingInstances = instanceMap.get(metadata.SOPInstanceUID) || [];
existingInstances.push({ ...metadata, url });
instanceMap.set(metadata.SOPInstanceUID, existingInstances);
}
});
if (series.instances) {
series.instances.forEach(instance => {
if (instance?.metadata?.SOPInstanceUID) {
const { metadata, url } = instance;
const existingInstances = instanceMap.get(metadata.SOPInstanceUID) || [];
existingInstances.push({ ...metadata, url });
instanceMap.set(metadata.SOPInstanceUID, existingInstances);
}
});
}
displaySet.images.forEach(instance => {
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
*/
addDisplaySetAsLayer: ({ viewportId, displaySetInstanceUID, removeFirst = false }) => {
if (!viewportId) {
const { activeViewportId } = servicesManager.services.viewportGridService.getState();
viewportId = activeViewportId;
}
if (!viewportId || !displaySetInstanceUID) {
console.warn('Missing required parameters for addDisplaySetAsLayer command');
return;

View File

@ -8,31 +8,13 @@ export default {
id: 'tagBrowser',
label: 'Tag Browser',
iconName: 'DicomTagBrowser',
onClick: ({ commandsManager, displaySetInstanceUID }: withAppTypes) => {
commandsManager.runCommand('openDICOMTagViewer', {
displaySetInstanceUID,
});
},
commands: 'openDICOMTagViewer',
},
{
id: 'addAsLayer',
label: 'Add as Layer',
iconName: 'ViewportViews',
onClick: ({ commandsManager, displaySetInstanceUID, servicesManager }: withAppTypes) => {
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,
});
},
commands: 'addDisplaySetAsLayer',
},
],
'studyBrowser.sortFunctions': [

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -39,11 +39,11 @@
"@ohif/extension-dicom-video": "3.11.0"
},
"dependencies": {
"@babel/runtime": "^7.20.13",
"i18next": "^17.0.3"
"@babel/runtime": "7.28.2",
"i18next": "17.3.1"
},
"devDependencies": {
"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"
},
"dependencies": {
"@babel/runtime": "^7.20.13",
"i18next": "^17.0.3"
"@babel/runtime": "7.28.2",
"i18next": "17.3.1"
},
"devDependencies": {
"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"
},
"dependencies": {
"@babel/runtime": "^7.20.13",
"i18next": "^17.0.3"
"@babel/runtime": "7.28.2",
"i18next": "17.3.1"
},
"devDependencies": {
"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"
},
"dependencies": {
"@babel/runtime": "^7.20.13",
"i18next": "^17.0.3"
"@babel/runtime": "7.28.2",
"i18next": "17.3.1"
}
}

View File

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

View File

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

View File

@ -55,13 +55,13 @@
"test:unit:ci": "lerna run test:unit:ci --parallel --stream",
"test:e2e": "lerna run test:e2e --stream",
"test:e2e:coverage": "nyc --reporter=html yarn run test:e2e:ci",
"test:e2e:ci": "TEST_ENV=true npx playwright test",
"test:e2e:ui": "TEST_ENV=true npx playwright test --ui",
"test:e2e:reporter": "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:debug": "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:debug": "TEST_ENV=true npx playwright test --debug",
"test:e2e:ci": "cross-env TEST_ENV=true npx playwright test",
"test:e2e:ui": "cross-env TEST_ENV=true npx playwright test --ui",
"test:e2e:reporter": "cross-env TEST_ENV=true npx playwright test --reporter=html",
"test:e2e:update": "cross-env TEST_ENV=true npx playwright test --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": "cross-env TEST_ENV=true npx playwright test --headed",
"test:e2e:debug": "cross-env TEST_ENV=true npx playwright test --debug",
"test:e2e:dist": "lerna run test:e2e:dist --stream",
"test:e2e:serve": "yarn test:data && lerna run test:e2e:serve --stream",
"see-changed": "lerna changed",
@ -75,39 +75,50 @@
"link-list": "npm ls --depth=0 --link=true"
},
"dependencies": {
"execa": "^8.0.1"
"execa": "8.0.1"
},
"optionalDependencies": {
"@percy/cypress": "^3.1.1",
"@playwright/test": "^1.48.0"
"@percy/cypress": "3.1.6",
"@playwright/test": "1.56.1"
},
"devDependencies": {
"@babel/core": "7.28.0",
"@babel/plugin-transform-class-properties": "^7.27.1",
"@babel/plugin-transform-object-rest-spread": "^7.28.0",
"@babel/plugin-transform-private-methods": "^7.27.1",
"@babel/plugin-transform-private-property-in-object": "^7.27.1",
"@babel/plugin-syntax-dynamic-import": "^7.8.3",
"@babel/plugin-transform-arrow-functions": "^7.16.7",
"@babel/plugin-transform-for-of": "^7.27.1",
"@babel/plugin-transform-regenerator": "^7.16.7",
"@babel/plugin-syntax-dynamic-import": "7.8.3",
"@babel/plugin-transform-arrow-functions": "7.27.1",
"@babel/plugin-transform-class-properties": "7.27.1",
"@babel/plugin-transform-for-of": "7.27.1",
"@babel/plugin-transform-object-rest-spread": "7.28.0",
"@babel/plugin-transform-private-methods": "7.27.1",
"@babel/plugin-transform-private-property-in-object": "7.27.1",
"@babel/plugin-transform-regenerator": "7.28.1",
"@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-react": "^7.27.1",
"@babel/preset-typescript": "^7.27.1",
"babel-plugin-istanbul": "^7.0.0",
"@istanbuljs/nyc-config-typescript": "^1.0.2",
"@babel/preset-react": "7.27.1",
"@babel/preset-typescript": "7.27.1",
"@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-file-upload": "^5.0.8",
"cypress-file-upload": "5.0.8",
"file-loader": "6.2.0",
"istanbul": "0.4.5",
"nyc": "^17.1.0",
"patch-package": "^8.0.0",
"playwright-test-coverage": "^1.2.12",
"prettier": "^3.3.3",
"nyc": "17.1.0",
"patch-package": "8.0.0",
"playwright-test-coverage": "1.2.12",
"postcss-loader": "6.2.1",
"prettier": "3.6.2",
"prettier-plugin-tailwindcss": "0.6.9",
"source-map-support": "^0.5.21",
"ts-node": "^10.9.2"
"shader-loader": "1.3.1",
"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": {
"hooks": {
@ -122,17 +133,22 @@
},
"resolutions": {
"commander": "8.3.0",
"cross-env": "7.0.3",
"cross-spawn": "7.0.6",
"dcmjs": "0.43.1",
"path-to-regexp": "0.1.12",
"nth-check": "^2.1.1",
"trim-newlines": "^5.0.0",
"glob-parent": "^6.0.2",
"trim": "^1.0.0",
"package-json": "^8.1.0",
"sharp": "^0.32.6",
"nth-check": "2.1.1",
"trim-newlines": "5.0.0",
"glob-parent": "6.0.2",
"trim": "1.0.1",
"package-json": "8.1.1",
"rollup": "2.79.2",
"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"
}

View File

@ -48,13 +48,13 @@
"README.md"
],
"dependencies": {
"@babel/runtime": "^7.20.13",
"@cornerstonejs/codec-charls": "^1.2.3",
"@cornerstonejs/codec-libjpeg-turbo-8bit": "^1.2.2",
"@cornerstonejs/codec-openjpeg": "^1.2.4",
"@cornerstonejs/codec-openjph": "^2.4.5",
"@cornerstonejs/dicom-image-loader": "^3.32.5",
"@emotion/serialize": "^1.1.3",
"@babel/runtime": "7.28.2",
"@cornerstonejs/codec-charls": "1.2.3",
"@cornerstonejs/codec-libjpeg-turbo-8bit": "1.2.2",
"@cornerstonejs/codec-openjpeg": "1.2.4",
"@cornerstonejs/codec-openjph": "2.4.7",
"@cornerstonejs/dicom-image-loader": "4.5.13",
"@emotion/serialize": "1.3.3",
"@ohif/core": "3.11.0",
"@ohif/extension-cornerstone": "3.11.0",
"@ohif/extension-cornerstone-dicom-rt": "3.11.0",
@ -74,41 +74,52 @@
"@ohif/mode-ultrasound-pleura-bline": "3.11.0",
"@ohif/ui": "3.11.0",
"@ohif/ui-next": "3.11.0",
"@svgr/webpack": "^8.1.0",
"@types/react": "^18.3.3",
"classnames": "^2.3.2",
"core-js": "*",
"cornerstone-math": "^0.1.9",
"@svgr/webpack": "8.1.0",
"@types/react": "18.3.23",
"classnames": "2.5.1",
"core-js": "3.45.1",
"cornerstone-math": "0.1.10",
"dcmjs": "0.43.1",
"detect-gpu": "^4.0.16",
"dicom-parser": "^1.8.9",
"dotenv-webpack": "^1.7.0",
"file-loader": "^6.2.0",
"hammerjs": "^2.0.8",
"history": "^5.3.0",
"i18next": "^17.0.3",
"i18next-browser-languagedetector": "^3.0.1",
"detect-gpu": "4.0.50",
"dicom-parser": "1.8.21",
"dotenv-webpack": "1.8.0",
"file-loader": "6.2.0",
"hammerjs": "2.0.8",
"history": "5.3.0",
"i18next": "17.3.1",
"i18next-browser-languagedetector": "3.1.1",
"lodash.isequal": "4.5.0",
"oidc-client": "1.11.5",
"oidc-client-ts": "^3.0.1",
"prop-types": "^15.7.2",
"query-string": "^6.12.1",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"react-dropzone": "^10.1.7",
"react-i18next": "^12.2.2",
"react-resize-detector": "^10.0.1",
"react-router": "^6.23.1",
"react-router-dom": "^6.8.1",
"oidc-client-ts": "3.3.0",
"prop-types": "15.8.1",
"query-string": "6.14.1",
"react": "18.3.1",
"react-dom": "18.3.1",
"react-dropzone": "10.2.2",
"react-i18next": "12.3.1",
"react-resize-detector": "10.0.1",
"react-router": "6.30.1",
"react-router-dom": "6.30.1",
"react-shepherd": "6.1.1",
"shepherd.js": "13.0.3",
"url-loader": "^4.1.1",
"url-loader": "4.1.1",
"zustand": "4.5.5"
},
"devDependencies": {
"@babel/plugin-transform-private-methods": "^7.27.1",
"@types/node": "^20.12.12",
"@babel/plugin-transform-private-methods": "7.27.1",
"@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",
"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.
prefetch: 25,
},
showErrorDetails: 'always', // 'always', 'dev', 'production'
// filterQueryParam: false,
// Defines multi-monitor layouts
multimonitor: [

View File

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

View File

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

View File

@ -33,33 +33,33 @@
"test:unit:ci": "jest --ci --runInBand --collectCoverage"
},
"peerDependencies": {
"@cornerstonejs/codec-charls": "^1.2.3",
"@cornerstonejs/codec-libjpeg-turbo-8bit": "^1.2.2",
"@cornerstonejs/codec-openjpeg": "^1.2.4",
"@cornerstonejs/codec-openjph": "^2.4.5",
"@cornerstonejs/core": "^3.32.5",
"@cornerstonejs/dicom-image-loader": "^3.32.5",
"@cornerstonejs/codec-charls": "1.2.3",
"@cornerstonejs/codec-libjpeg-turbo-8bit": "1.2.2",
"@cornerstonejs/codec-openjpeg": "1.2.4",
"@cornerstonejs/codec-openjph": "2.4.7",
"@cornerstonejs/core": "4.5.13",
"@cornerstonejs/dicom-image-loader": "4.5.13",
"@ohif/ui": "3.11.0",
"cornerstone-math": "0.1.9",
"dicom-parser": "^1.8.21"
"dicom-parser": "1.8.21"
},
"dependencies": {
"@babel/runtime": "^7.20.13",
"@babel/runtime": "7.28.2",
"dcmjs": "0.43.1",
"dicomweb-client": "^0.10.4",
"gl-matrix": "^3.4.3",
"immutability-helper": "^3.1.1",
"isomorphic-base64": "^1.0.2",
"lodash.clonedeep": "^4.5.0",
"lodash.isequal": "^4.5.0",
"moment": "^2.30.1",
"dicomweb-client": "0.10.4",
"gl-matrix": "3.4.3",
"immutability-helper": "3.1.1",
"isomorphic-base64": "1.0.2",
"lodash.clonedeep": "4.5.0",
"lodash.isequal": "4.5.0",
"moment": "2.30.1",
"object-hash": "2.1.1",
"query-string": "^6.14.0",
"query-string": "6.14.1",
"react-shepherd": "6.1.1",
"shepherd.js": "13.0.3",
"validate.js": "^0.12.0"
"validate.js": "0.12.0"
},
"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.
- `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
- `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/>
Points to consider while using `dangerouslyUseDynamicConfig`:<br/>
- 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/types": "3.0.0",
"@mdx-js/react": "3.0.1",
"@radix-ui/react-accordion": "^1.2.0",
"@radix-ui/react-checkbox": "^1.1.1",
"@radix-ui/react-dialog": "^1.1.1",
"@radix-ui/react-dropdown-menu": "^2.1.1",
"@radix-ui/react-hover-card": "^1.1.6",
"@radix-ui/react-icons": "^1.3.0",
"@radix-ui/react-label": "^2.1.0",
"@radix-ui/react-popover": "^1.0.7",
"@radix-ui/react-scroll-area": "^1.1.0",
"@radix-ui/react-select": "^2.1.1",
"@radix-ui/react-separator": "^1.1.0",
"@radix-ui/react-slider": "^1.2.0",
"@radix-ui/react-slot": "^1.0.2",
"@radix-ui/react-switch": "^1.1.0",
"@radix-ui/react-tabs": "^1.1.0",
"@radix-ui/react-toggle": "^1.1.0",
"@radix-ui/react-tooltip": "^1.1.2",
"@svgr/webpack": "^5.5.0",
"@types/react": "^18.2.29",
"autoprefixer": "^10.4.20",
"class-variance-authority": "^0.7.0",
"classnames": "^2.3.2",
"clsx": "^1.1.1",
"cmdk": "^1.0.0",
"date-fns": "^3.6.0",
"docusaurus-plugin-image-zoom": "^1.0.1",
"file-loader": "^6.2.0",
"@radix-ui/react-accordion": "1.2.11",
"@radix-ui/react-checkbox": "1.3.2",
"@radix-ui/react-dialog": "1.1.14",
"@radix-ui/react-dropdown-menu": "2.1.15",
"@radix-ui/react-hover-card": "1.1.14",
"@radix-ui/react-icons": "1.3.2",
"@radix-ui/react-label": "2.1.7",
"@radix-ui/react-popover": "1.1.14",
"@radix-ui/react-scroll-area": "1.2.9",
"@radix-ui/react-select": "2.2.5",
"@radix-ui/react-separator": "1.1.7",
"@radix-ui/react-slider": "1.3.5",
"@radix-ui/react-slot": "1.2.3",
"@radix-ui/react-switch": "1.2.5",
"@radix-ui/react-tabs": "1.1.12",
"@radix-ui/react-toggle": "1.1.9",
"@radix-ui/react-tooltip": "1.2.7",
"@svgr/webpack": "8.1.0",
"@types/react": "18.3.23",
"autoprefixer": "10.4.21",
"class-variance-authority": "0.7.1",
"classnames": "2.5.1",
"clsx": "2.1.1",
"cmdk": "1.1.1",
"date-fns": "3.6.0",
"docusaurus-plugin-image-zoom": "1.0.1",
"file-loader": "6.2.0",
"framer-motion": "6.2.4",
"glob": "^10.3.10",
"lucide-react": "^0.379.0",
"next-themes": "^0.3.0",
"postcss": "^8.4.47",
"postcss-import": "^14.0.2",
"postcss-preset-env": "^7.4.3",
"prism-react-renderer": "^2.1.0",
"react": "^18.3.1",
"react-day-picker": "^8.10.1",
"react-dom": "^18.3.1",
"react-outside-click-handler": "^1.3.0",
"glob": "10.4.5",
"lucide-react": "0.379.0",
"next-themes": "0.3.0",
"postcss": "8.5.6",
"postcss-import": "14.1.0",
"postcss-preset-env": "7.8.3",
"prism-react-renderer": "2.1.0",
"react": "18.3.1",
"react-day-picker": "8.10.1",
"react-dom": "18.3.1",
"react-outside-click-handler": "1.3.0",
"react-shepherd": "6.1.1",
"shepherd.js": "13.0.3",
"sonner": "^1.4.41",
"tailwind-merge": "^2.3.0",
"tailwindcss": "^3.4.13",
"tailwindcss-animate": "^1.0.7",
"typescript": "~5.2.2",
"url-loader": "^4.1.1"
"sonner": "1.7.4",
"tailwind-merge": "2.6.0",
"tailwindcss": "3.2.4",
"tailwindcss-animate": "1.0.7",
"typescript": "5.5.4",
"url-loader": "4.1.1"
}
}

View File

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

View File

@ -12,7 +12,7 @@
"README.md"
],
"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",
"start": "yarn run build --watch",
"dev": "cross-env NODE_ENV=development webpack serve --config .webpack/webpack.playground.js",
@ -27,43 +27,44 @@
".": "./src/index.ts"
},
"dependencies": {
"@radix-ui/react-accordion": "^1.2.0",
"@radix-ui/react-checkbox": "^1.1.1",
"@radix-ui/react-context-menu": "^2.2.4",
"@radix-ui/react-dialog": "^1.1.1",
"@radix-ui/react-dropdown-menu": "^2.1.1",
"@radix-ui/react-hover-card": "^1.1.6",
"@radix-ui/react-icons": "^1.3.0",
"@radix-ui/react-label": "^2.1.0",
"@radix-ui/react-popover": "^1.0.7",
"@radix-ui/react-scroll-area": "^1.1.0",
"@radix-ui/react-select": "^2.1.1",
"@radix-ui/react-separator": "^1.1.0",
"@radix-ui/react-slider": "^1.2.0",
"@radix-ui/react-slot": "^1.0.2",
"@radix-ui/react-switch": "^1.1.0",
"@radix-ui/react-tabs": "^1.1.0",
"@radix-ui/react-toggle": "^1.1.0",
"@radix-ui/react-tooltip": "^1.1.2",
"class-variance-authority": "^0.7.0",
"clsx": "*",
"cmdk": "^1.0.0",
"date-fns": "^3.6.0",
"@radix-ui/react-accordion": "1.2.11",
"@radix-ui/react-checkbox": "1.3.2",
"@radix-ui/react-context-menu": "2.2.15",
"@radix-ui/react-dialog": "1.1.14",
"@radix-ui/react-dropdown-menu": "2.1.15",
"@radix-ui/react-hover-card": "1.1.14",
"@radix-ui/react-icons": "1.3.2",
"@radix-ui/react-label": "2.1.7",
"@radix-ui/react-popover": "1.1.14",
"@radix-ui/react-scroll-area": "1.2.9",
"@radix-ui/react-select": "2.2.5",
"@radix-ui/react-separator": "1.1.7",
"@radix-ui/react-slider": "1.3.5",
"@radix-ui/react-slot": "1.2.3",
"@radix-ui/react-switch": "1.2.5",
"@radix-ui/react-tabs": "1.1.12",
"@radix-ui/react-toggle": "1.1.9",
"@radix-ui/react-toggle-group": "1.1.10",
"@radix-ui/react-tooltip": "1.2.7",
"class-variance-authority": "0.7.1",
"clsx": "2.1.1",
"cmdk": "1.1.1",
"date-fns": "3.6.0",
"framer-motion": "6.2.4",
"lucide-react": "^0.379.0",
"next-themes": "^0.3.0",
"react": "^18.3.1",
"react-day-picker": "^8.10.1",
"react-resizable-panels": "^2.1.7",
"lucide-react": "0.379.0",
"next-themes": "0.3.0",
"react": "18.3.1",
"react-day-picker": "8.10.1",
"react-resizable-panels": "2.1.9",
"react-shepherd": "6.1.1",
"shepherd.js": "13.0.3",
"sonner": "^1.5.0",
"tailwind-merge": "^2.3.0",
"sonner": "1.7.4",
"tailwind-merge": "2.6.0",
"tailwindcss": "3.2.4",
"tailwindcss-animate": "^1.0.7"
"tailwindcss-animate": "1.0.7"
},
"devDependencies": {
"@babel/plugin-transform-private-property-in-object": "^7.27.1"
"@babel/plugin-transform-private-property-in-object": "7.27.1"
},
"keywords": [],
"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 {
DropdownMenu,
@ -81,292 +81,291 @@ interface DataRowProps {
className?: string;
}
export const DataRow: React.FC<DataRowProps> = ({
number,
title,
colorHex,
details,
onSelect,
isLocked,
onToggleVisibility,
onToggleLocked,
onRename,
onDelete,
onColor,
isSelected = false,
isVisible = true,
disableEditing = false,
className,
}) => {
const [isDropdownOpen, setIsDropdownOpen] = useState(false);
const isTitleLong = title?.length > 25;
const rowRef = useRef<HTMLDivElement>(null);
const DataRow = React.forwardRef<HTMLDivElement, DataRowProps>(
(
{
number,
title,
colorHex,
details,
onSelect,
isLocked,
onToggleVisibility,
onToggleLocked,
onRename,
onDelete,
onColor,
isSelected = false,
isVisible = true,
disableEditing = false,
className,
},
ref
) => {
const [isDropdownOpen, setIsDropdownOpen] = useState(false);
const isTitleLong = title?.length > 25;
// useEffect(() => {
// if (isSelected && rowRef.current) {
// setTimeout(() => {
// rowRef.current?.scrollIntoView({ behavior: 'smooth', block: 'center' });
// }, 200);
// }
// }, [isSelected]);
const handleAction = (action: string, e: React.MouseEvent) => {
e.stopPropagation();
switch (action) {
case 'Rename':
onRename(e);
break;
case 'Lock':
onToggleLocked(e);
break;
case 'Delete':
onDelete(e);
break;
case 'Color':
onColor(e);
break;
}
};
const handleAction = (action: string, e: React.MouseEvent) => {
e.stopPropagation();
switch (action) {
case 'Rename':
onRename(e);
break;
case 'Lock':
onToggleLocked(e);
break;
case 'Delete':
onDelete(e);
break;
case 'Color':
onColor(e);
break;
}
};
const decodeHTML = (html: string) => {
const txt = document.createElement('textarea');
txt.innerHTML = html;
return txt.value;
};
const decodeHTML = (html: string) => {
const txt = document.createElement('textarea');
txt.innerHTML = html;
return txt.value;
};
const renderDetailText = (text: string, indent: number = 0) => {
const indentation = ' '.repeat(indent);
if (text === '') {
const renderDetailText = (text: string, indent: number = 0) => {
const indentation = ' '.repeat(indent);
if (text === '') {
return (
<div
key={`empty-${indent}`}
className="h-2"
></div>
);
}
const cleanText = decodeHTML(text);
return (
<div
key={`empty-${indent}`}
className="h-2"
></div>
key={cleanText}
className="whitespace-pre-wrap"
>
{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 visibleLines = details.slice(0, 4);
const hiddenLines = details.slice(4);
const renderDetails = (details: string[]) => {
const visibleLines = details.slice(0, 4);
const hiddenLines = details.slice(4);
return (
<Tooltip>
<TooltipTrigger asChild>
<div className="cursor-help">
<div className="flex flex-col space-y-1">
{visibleLines.map((line, lineIndex) =>
return (
<Tooltip>
<TooltipTrigger asChild>
<div className="cursor-help">
<div className="flex flex-col space-y-1">
{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)
)}
</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)
)}
</div>
</TooltipContent>
</Tooltip>
);
};
</TooltipContent>
</Tooltip>
);
};
return (
<div
ref={rowRef}
className={cn('flex flex-col', !isVisible && 'opacity-60', className)}
>
return (
<div
className={`flex items-center ${
isSelected ? 'bg-popover' : 'bg-muted'
} group relative cursor-pointer`}
onClick={onSelect}
data-cy="data-row"
ref={ref}
className={cn('flex flex-col', !isVisible && 'opacity-60', className)}
>
{/* Hover Overlay */}
<div className="bg-primary/20 pointer-events-none absolute inset-0 opacity-0 transition-opacity group-hover:opacity-100"></div>
<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 !== null && (
<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 ${
isSelected ? 'bg-highlight text-black' : 'bg-muted text-muted-foreground'
} overflow-hidden`}
>
{number}
</div>
)}
{/* Number Box */}
{number !== null && (
<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 ${
isSelected ? 'bg-highlight text-black' : 'bg-muted text-muted-foreground'
} overflow-hidden`}
>
{number}
</div>
)}
{/* add some space if there is not segment index */}
{number === null && <div className="ml-1 h-7"></div>}
{colorHex && (
<div className="flex h-7 w-5 items-center justify-center">
<span
className="ml-2 h-2 w-2 rounded-full"
style={{ backgroundColor: colorHex }}
></span>
</div>
)}
{/* add some space if there is not segment index */}
{number === null && <div className="ml-1 h-7"></div>}
{colorHex && (
<div className="flex h-7 w-5 items-center justify-center">
<span
className="ml-2 h-2 w-2 rounded-full"
style={{ backgroundColor: colorHex }}
></span>
</div>
)}
{/* Label with Conditional Tooltip */}
<div className="ml-2 flex-1 overflow-hidden">
{isTitleLong ? (
<Tooltip>
<TooltipTrigger asChild>
<span
className={`cursor-default text-base ${
isSelected ? 'text-highlight' : 'text-muted-foreground'
} [overflow:hidden] [display:-webkit-box] [-webkit-line-clamp:2] [-webkit-box-orient:vertical]`}
{/* Label with Conditional Tooltip */}
<div className="ml-2 flex-1 overflow-hidden">
{isTitleLong ? (
<Tooltip>
<TooltipTrigger asChild>
<span
className={`cursor-default text-base ${
isSelected ? 'text-highlight' : 'text-muted-foreground'
} [overflow:hidden] [display:-webkit-box] [-webkit-line-clamp:2] [-webkit-box-orient:vertical]`}
>
{title}
</span>
</TooltipTrigger>
<TooltipContent
side="top"
align="center"
>
{title}
</span>
</TooltipTrigger>
<TooltipContent
side="top"
align="center"
</TooltipContent>
</Tooltip>
) : (
<span
className={`text-base ${
isSelected ? 'text-highlight' : 'text-muted-foreground'
} [overflow:hidden] [display:-webkit-box] [-webkit-line-clamp:2] [-webkit-box-orient:vertical]`}
>
{title}
</TooltipContent>
</Tooltip>
) : (
<span
className={`text-base ${
isSelected ? 'text-highlight' : 'text-muted-foreground'
} [overflow:hidden] [display:-webkit-box] [-webkit-line-clamp:2] [-webkit-box-orient:vertical]`}
</span>
)}
</div>
{/* Actions and Visibility Toggle */}
<div className="relative ml-2 flex items-center space-x-1">
{/* 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}
</span>
)}
</div>
{isVisible ? <Icons.Hide className="h-6 w-6" /> : <Icons.Show className="h-6 w-6" />}
</Button>
{/* Actions and Visibility Toggle */}
<div className="relative ml-2 flex items-center space-x-1">
{/* 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);
}}
>
{isVisible ? <Icons.Hide className="h-6 w-6" /> : <Icons.Show className="h-6 w-6" />}
</Button>
{/* Lock Icon (if needed) */}
{isLocked && !disableEditing && (
<Icons.Lock className="text-muted-foreground h-6 w-6" />
)}
{/* Lock Icon (if needed) */}
{isLocked && !disableEditing && <Icons.Lock className="text-muted-foreground h-6 w-6" />}
{/* Actions Dropdown Menu */}
{disableEditing && <div className="h-6 w-6"></div>}
{!disableEditing && (
<DropdownMenu onOpenChange={open => setIsDropdownOpen(open)}>
<DropdownMenuTrigger asChild>
<Button
size="icon"
variant="ghost"
className={`h-6 w-6 transition-opacity ${
isSelected || isDropdownOpen
? 'opacity-100'
: 'opacity-0 group-hover:opacity-100'
}`}
aria-label="Actions"
dataCY="actionsMenuTrigger"
onClick={e => e.stopPropagation()} // Prevent row selection on button click
{/* Actions Dropdown Menu */}
{disableEditing && <div className="h-6 w-6"></div>}
{!disableEditing && (
<DropdownMenu onOpenChange={open => setIsDropdownOpen(open)}>
<DropdownMenuTrigger asChild>
<Button
size="icon"
variant="ghost"
className={`h-6 w-6 transition-opacity ${
isSelected || isDropdownOpen
? 'opacity-100'
: 'opacity-0 group-hover:opacity-100'
}`}
aria-label="Actions"
dataCY="actionsMenuTrigger"
onClick={e => e.stopPropagation()} // Prevent row selection on button click
>
<Icons.More className="h-6 w-6" />
</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>
</DropdownMenuTrigger>
<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" />
<>
<DropdownMenuItem onClick={e => handleAction('Rename', e)}>
<Icons.Rename className="text-foreground" />
<span
className="pl-2"
data-cy="Change Color"
data-cy="Rename"
>
Change Color
Rename
</span>
</DropdownMenuItem>
)}
<DropdownMenuItem onClick={e => handleAction('Lock', e)}>
<Icons.Lock className="text-foreground" />
<span
className="pl-2"
data-cy="LockToggle"
>
{isLocked ? 'Unlock' : 'Lock'}
</span>
</DropdownMenuItem>
</>
</DropdownMenuContent>
</DropdownMenu>
)}
</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>
<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
className="pl-2"
data-cy="Change Color"
>
Change Color
</span>
</DropdownMenuItem>
)}
<DropdownMenuItem onClick={e => handleAction('Lock', e)}>
<Icons.Lock className="text-foreground" />
<span
className="pl-2"
data-cy="LockToggle"
>
{isLocked ? 'Unlock' : 'Lock'}
</span>
</DropdownMenuItem>
</>
</DropdownMenuContent>
</DropdownMenu>
)}
</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 { DataRow };

View File

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

View File

@ -54,7 +54,10 @@ const renderRangeSetting = option => {
key={option.id}
>
<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
minValue={option.min}
maxValue={option.max}

View File

@ -1,4 +1,4 @@
import React from 'react';
import React, { useEffect } from 'react';
import { ScrollArea, DataRow } from '../../components';
import { HoverCard, HoverCardTrigger, HoverCardContent } from '../../components/HoverCard';
import { useSegmentationTableContext, useSegmentationExpanded } from './contexts';
@ -23,6 +23,11 @@ export const SegmentationSegments = ({ children = null }: { children?: React.Rea
let segmentation;
let representation;
const activeSegmentRef = React.useRef<{
element: HTMLElement | null;
index: number | null;
}>({ element: null, index: null });
try {
// Try to use the SegmentationExpanded context if available
const segmentationInfo = useSegmentationExpanded('SegmentationSegments');
@ -38,108 +43,163 @@ export const SegmentationSegments = ({ children = null }: { children?: React.Rea
}
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 { 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) {
return null;
}
return (
<ScrollArea
className={`bg-bkg-low space-y-px`}
showArrows={true}
>
<div
ref={scrollableContainerRef}
style={{ maxHeight: maxHeight }}
<div ref={scrollableContainerRef}>
<ScrollArea
className={`bg-bkg-low space-y-px`}
showArrows={
scrollableContainerRef?.current
? scrollableContainerRef?.current?.offsetHeight >= parseFloat(maxHeight)
: false
}
>
{segments.map(segment => {
if (!segment) {
return null;
}
const { segmentIndex, color, visible } = segment as {
segmentIndex: number;
color: number[];
visible: boolean;
};
const segmentFromSegmentation = segmentation.segments[segmentIndex];
<div style={{ maxHeight: maxHeight }}>
{segments.map(segment => {
if (!segment) {
return null;
}
const { segmentIndex, color, visible } = segment as {
segmentIndex: number;
color: number[];
visible: boolean;
};
const segmentFromSegmentation = segmentation.segments[segmentIndex];
if (!segmentFromSegmentation) {
return null;
}
if (!segmentFromSegmentation) {
return null;
}
const { locked, active, label, displayText } = segmentFromSegmentation;
const cssColor = `rgb(${color[0]},${color[1]},${color[2]})`;
const { locked, active, label, displayText } = segmentFromSegmentation;
const cssColor = `rgb(${color[0]},${color[1]},${color[2]})`;
const hasStats = segmentFromSegmentation.cachedStats?.namedStats;
const DataRowComponent = (
<DataRow
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,
representation.type
)
const hasStats = segmentFromSegmentation.cachedStats?.namedStats;
const segmentRowRef = (element: HTMLElement) => {
if (!active) {
return;
}
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"
>
<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>
if (element) {
activeSegmentRef.current = { element, index: segmentIndex };
} else {
activeSegmentRef.current = { element: null, index: null };
}
};
<SegmentStatistics
segment={{
...segmentFromSegmentation,
const DataRowComponent = (
<DataRow
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,
}}
segmentationId={segmentation.segmentationId}
representation.type
)
}
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}
</SegmentStatistics>
</HoverCardContent>
</HoverCard>
) : (
DataRowComponent
);
})}
</div>
</ScrollArea>
<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
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';
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
* relative to the bottom of the viewport.
@ -31,20 +55,49 @@ export function useDynamicMaxHeight(
}
};
// Calculate initially
// Use requestAnimationFrame to ensure layout is stable after initial render
const rafId = requestAnimationFrame(calculateMaxHeight);
// Two intersection observers to trigger a recalculation when the target element
// moves up or down. One for moving up and one for moving down.
// Note that with this approach we don't need to use a resize observer nor
// a window resize listener.
// Recalculate on window resize
window.addEventListener('resize', calculateMaxHeight);
// The trick is to use a margin for the IntersectionObserver to detect movement.
// 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
return () => {
window.removeEventListener('resize', calculateMaxHeight);
cancelAnimationFrame(rafId);
moveUpIntersectionObserver.disconnect();
moveDownIntersectionObserver.disconnect();
};
// Dependencies: buffer, minHeight, and data.
}, [data, buffer, minHeight]);
}, [data, buffer, minHeight, maxHeight]);
return { ref, maxHeight };
}

View File

@ -19,23 +19,20 @@
"README.md"
],
"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",
"start": "yarn run build --watch",
"build": "cross-env NODE_ENV=production webpack --config .webpack/webpack.prod.js",
"build:package": "yarn run build",
"storybook": "storybook dev -p 6006",
"dev": "storybook dev -p 6006",
"build-storybook": "storybook build"
"build:package": "yarn run build"
},
"peerDependencies": {
"react": "^18.3.1",
"react-dom": "^18.3.1"
"react": "18.3.1",
"react-dom": "18.3.1"
},
"dependencies": {
"@testing-library/react": "^13.1.0",
"browser-detect": "^0.2.28",
"classnames": "^2.3.2",
"@testing-library/react": "13.4.0",
"browser-detect": "0.2.28",
"classnames": "2.5.1",
"d3-array": "3",
"d3-axis": "3",
"d3-scale": "4",
@ -43,45 +40,36 @@
"d3-selection": "3",
"d3-shape": "3",
"d3-zoom": "3",
"lodash.clonedeep": "^4.5.0",
"lodash.debounce": "^4.0.8",
"lodash.merge": "^4.6.1",
"moment": "^2.30.1",
"mousetrap": "^1.6.5",
"react": "^18.3.1",
"react-dates": "^21.8.0",
"lodash.clonedeep": "4.5.0",
"lodash.debounce": "4.0.8",
"lodash.merge": "4.6.2",
"moment": "2.30.1",
"mousetrap": "1.6.5",
"react": "18.3.1",
"react-dates": "21.8.0",
"react-dnd": "14.0.2",
"react-dnd-html5-backend": "14.0.0",
"react-dom": "^18.3.1",
"react-draggable": "^4.4.6",
"react-error-boundary": "^3.1.3",
"react-dom": "18.3.1",
"react-draggable": "4.5.0",
"react-error-boundary": "3.1.4",
"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-test-renderer": "^18.3.1",
"react-window": "^1.8.9",
"react-with-direction": "^1.3.1",
"swiper": "^8.4.2",
"react-test-renderer": "18.3.1",
"react-window": "1.8.11",
"react-with-direction": "1.4.0",
"swiper": "8.4.7",
"webpack": "5.95.0"
},
"devDependencies": {
"@babel/core": "7.28.0",
"@storybook/addon-actions": "^7.6.10",
"@storybook/addon-docs": "^7.6.10",
"@storybook/addon-essentials": "^7.6.10",
"@storybook/addon-links": "^7.6.10",
"@storybook/cli": "^7.6.10",
"@storybook/react": "^7.6.10",
"@storybook/react-webpack5": "^7.6.10",
"@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",
"autoprefixer": "10.4.21",
"babel-loader": "8.4.1",
"dotenv-webpack": "1.8.0",
"postcss": "8.5.6",
"postcss-loader": "6.2.1",
"prop-types": "15.8.1",
"remark-gfm": "3.0.1",
"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();
// 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(
page,

View File

@ -10,9 +10,14 @@ test.beforeEach(async ({ page }) => {
test('should hydrate RT reports correctly', async ({ page }) => {
await page.getByTestId('side-panel-header-right').click();
await page.getByTestId('study-browser-thumbnail-no-image').dblclick();
await page.waitForTimeout(5000);
await checkForScreenshot(page, page, screenShotPaths.rtHydration.rtPreHydration);
await page.getByTestId('yes-hydrate-btn').click();
await page.waitForTimeout(5000);
await checkForScreenshot(page, page, screenShotPaths.rtHydration.rtPostHydration);
await page.getByText('Small Sphere').click();
await page.waitForTimeout(5000);
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('MPR').click();
await page.waitForTimeout(5000);
await page.waitForTimeout(10000);
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 { viewportLocator } from './utils/locators';
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 }) => {
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'
);
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(
tmtvCSVReportContent,
tmtvCSVReportHeaders,
'Expected the file to start with specific column/value headers'
).toMatch(
/^id,label,min,max,mean,stdDev,median,skewness,kurtosis,count,maxLPS,minLPS,center,volume,peakValue,peakLPS,lesionGlycolysis,PatientID,PatientName,StudyInstanceUID,SeriesInstanceUID,StudyDate/
);
).toBe(expectedHeaders);
expect(tmtvCSVReportContent, 'Expected the patient name to be present').toContain(
'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