Compare commits

...
Author SHA1 Message Date
ohif-bot cb6cdafd24 chore(version): Update package versions to 3.13.0-beta.109 [skip ci] 2026-06-30 13:28:46 +00:00
Alireza aa1ba771e0 fix(test): stabilize DICOM Tag Browser scrollbar screenshot (#6118) 2026-06-30 09:25:37 -04:00
ohif-bot d4153f12b2 chore(version): Update package versions to 3.13.0-beta.108 [skip ci] 2026-06-29 16:58:29 +00:00
Alireza 71c50db9b5 fix(ci): deploy docs with Netlify CLI monorepo filter (#6115) 2026-06-29 12:55:01 -04:00
ohif-bot 4cdcd34a33 chore(version): Update package versions to 3.13.0-beta.107 [skip ci] 2026-06-29 16:42:34 +00:00
Dan Rukas 0dfd321326 feat(ui-next): Adds appearance dialog with theme presets and custom theme support (#6041) 2026-06-29 12:38:46 -04:00
ohif-bot 0fcebe05c6 chore(version): Update package versions to 3.13.0-beta.106 [skip ci] 2026-06-29 16:36:28 +00:00
Dan Rukas ac45a9497f docs(site): add ui-next component documentation and upgrade Docusaurus (#6102) 2026-06-29 12:33:16 -04:00
Dan Rukas 03b180f185 fix(colors): Replace hardcoded colors with theme tokens (#6040) 2026-06-29 12:32:40 -04:00
ohif-bot e1bc625ab8 chore(version): Update package versions to 3.13.0-beta.105 [skip ci] 2026-06-26 12:23:19 +00:00
Joe Boccanfuso 661ecb5b2e fix(hotkeyBindings): consolidate Escape hotkey behavior for contour drawing tools (#6104)
* fix(hotkeyBindings): consolidate Escape hotkey behavior for contour drawing tools

* PR feedback.

* PR feedback.
2026-06-26 08:20:22 -04:00
ohif-bot b7c8b865dd chore(version): Update package versions to 3.13.0-beta.104 [skip ci] 2026-06-24 19:21:51 +00:00
ee165b4d8e Fix: Segmentation Hide All fails when switching from 2D to 3D fourUp (#5995)
* Fix: Segmentation "Hide All" fails when switching from 2D to 3D four-up viewport

* Update based on the review comment on segmentation load in 3d fourup

Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>

* fix: simplify the conditional logic

* fix: playwright test

* fix: playwright test

* fix: render timeout in test

---------

Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>
Co-authored-by: Belbin-GK <150322972+Belbin-GK@users.noreply.github.com>
Co-authored-by: Devu Jayalekshmi <devu.jayalekshmi@trenser.com>
Co-authored-by: Bill Wallace <wayfarer3130@gmail.com>
2026-06-24 15:18:16 -04:00
ohif-bot 589d6a1506 chore(version): Update package versions to 3.13.0-beta.103 [skip ci] 2026-06-24 15:08:56 +00:00
diattamo 95200ec6e8 test(segmentation): switch getSegmentCount to expect(rows).toHaveCount in panel specs (#6105) 2026-06-24 11:05:27 -04:00
ohif-bot 8f6947b75d chore(version): Update package versions to 3.13.0-beta.102 [skip ci] 2026-06-24 14:39:34 +00:00
diattamo 79cb2356e1 chore(testing): Playwright tests for contour segmentation create, update, delete interactions (#6091) 2026-06-24 10:36:16 -04:00
ohif-bot 20632f97cf chore(version): Update package versions to 3.13.0-beta.101 [skip ci] 2026-06-23 06:25:18 +00:00
Joe Boccanfuso c205965f87 chore(testing): Flock lock for playwright tests; Pin node version for OHIF; add more workers (#6099)
* update Cypress apt deps for Ubuntu Noble (drop libgconf-2-4, libasound2→libasound2t64)
2026-06-23 02:21:18 -04:00
ohif-bot 75488298cd chore(version): Update package versions to 3.13.0-beta.100 [skip ci] 2026-06-23 05:56:18 +00:00
diattamo b24f039735 test(contour): Add the ContourSegmentToggleLock.spec.ts test file to test contour locking (#6072) 2026-06-23 01:52:35 -04:00
ohif-bot 1f6670c602 chore(version): Update package versions to 3.13.0-beta.99 [skip ci] 2026-06-20 15:33:27 +00:00
diattamo b9cde4981a feat(testing): OHIF Test Agent Skills (#5993) 2026-06-20 11:30:27 -04:00
ohif-bot fc994fe525 chore(version): Update package versions to 3.13.0-beta.98 [skip ci] 2026-06-20 15:17:28 +00:00
diattamoandBill Wallace ada2da34f0 test(contour): contour interactions delete segment (#6069)
---------

Co-authored-by: Bill Wallace <wayfarer3130@gmail.com>
2026-06-20 11:14:07 -04:00
diattamo 5955151ba1 test(contour): contour color change coverage (#6042) 2026-06-20 11:11:57 -04:00
ohif-bot 419366bd25 chore(version): Update package versions to 3.13.0-beta.97 [skip ci] 2026-06-19 16:37:27 +00:00
Bill Wallace b59eb30b0f fix: Release deploy of docs (#6095)
A netlify fix that needs to be verified on master
2026-06-19 12:34:07 -04:00
ohif-bot 57d03fc722 chore(version): Update package versions to 3.13.0-beta.96 [skip ci] 2026-06-19 15:36:51 +00:00
Bill Wallace 17e12d53e5 fix: pnpm related dependency bugs - nothing funcitonal (#6094)
Testing this needs a release and there are no functional changes, only pnpm migration issues, so merging early for test.

* fix: pnpm related dependency bugs - nothing funcitonal

* Undo peer dependency due to non-pnpm integration issues
2026-06-19 11:33:45 -04:00
ohif-bot ffd7884ed1 chore(version): Update package versions to 3.13.0-beta.95 [skip ci] 2026-06-18 15:25:00 +00:00
Joe Boccanfuso 0dd6571505 chore(testing): Setup new self-hosted test runner (#6067) 2026-06-18 11:21:41 -04:00
ohif-bot 6e2d53f280 chore(version): Update package versions to 3.13.0-beta.94 [skip ci] 2026-06-17 14:54:34 +00:00
Joe Boccanfuso 6acbbc0271 fix(security): Update dependencies to fix security vulnerabilities. (#6085) 2026-06-17 10:51:03 -04:00
ohif-bot 780d172201 chore(version): Update package versions to 3.13.0-beta.93 [skip ci] 2026-06-16 16:02:34 +00:00
Bill WallaceandClaude Opus 4.8 256b8347e7 fix(docs): remove stray tool-call tags breaking the MDX build (#6081)
This is a fix to pnpm deployment which needs testing as the final part of origin/master release
No functional changes

* fix(docs): remove stray tool-call tags breaking the MDX build

platform/docs/docs/migration-guide/3p12-to-3p13/build-tooling.md ended with
two orphan closing tags (leftover tool-call serialization artifacts):

  </content>
  </invoke>

Docusaurus compiles Markdown as MDX (JSX-aware), so the orphan closing tag
failed the docs build:

  MDX compilation failed ... Unexpected closing slash in tag, expected an
  open tag first (build-tooling.md line 402)

This was the remaining blocker for build-and-deploy-docs once the
--no-frozen-lockfile change let the install step succeed. A scan of the docs
tree found no other such artifacts. Verified locally: docusaurus build now
generates static files with no MDX errors.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* Update lockfile and avoid freshness check on every command

* fix(release): keep workspace:* in the repo, concretize only at publish

The release flow rewrote internal @ohif/* dependency specifiers to the concrete
version and committed them, so pnpm-lock.yaml (which records workspace links)
drifted from the manifests on every version bump. The resulting
ERR_PNPM_OUTDATED_LOCKFILE broke every frozen install: Netlify (viewer-dev),
the docs deploy, pnpm's pre-run deps check, and post-merge installs.

Keep workspace:* everywhere in the committed repo and move the concrete-version
substitution to publish time only:

- publish-version.mjs: bump each package's own `version` field only; stop
  rewriting @ohif/* dependency/peerDependency specifiers.
- publish-package.mjs: publish with `pnpm publish --no-git-checks` instead of
  `npm publish`. pnpm rewrites workspace:* to the exact version in the published
  tarball; npm would publish the literal "workspace:*", which npm/yarn consumers
  cannot resolve.
- One-time: revert the 25 workspace manifests' @ohif/* specifiers to workspace:*
  (version fields untouched) and regenerate pnpm-lock.yaml to match.

Because internal deps are workspace:* (links, not versions in the lockfile),
version bumps no longer change pnpm-lock.yaml, so it stays in sync and frozen
installs keep working.

Verified: `pnpm install --frozen-lockfile` passes, and `pnpm pack` of @ohif/core
emits a tarball whose @ohif/ui dependency is the exact version (3.13.0-beta.92),
not workspace:*.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* docs(ci): correct build-docs install comment for workspace:* release flow

publish-version.mjs no longer rewrites @ohif/* deps to concrete versions, so
the old comment was stale. Internal deps stay workspace:* and the lockfile
stays consistent; pnpm publish concretizes only the published tarball.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* ci(docs): use --frozen-lockfile now that the lockfile no longer drifts

With internal deps as workspace:* the lockfile stays in sync across version
bumps, so the docs deploy can install frozen -- failing fast on genuine
lockfile drift instead of silently reconciling. The --no-frozen-lockfile
workaround is no longer needed.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* ci: use --frozen-lockfile in CI install steps now that the lockfile is stable

Internal @ohif/* deps are workspace:* so pnpm-lock.yaml no longer drifts; the
UNIT_TESTS/BUILD/NPM_PUBLISH installs can run frozen and fail fast on genuine
drift. Kept --no-frozen-lockfile only where it is still required: the Dockerfile
(platform/docs is excluded from the build context) and the playwright CS3D-version
step (mutates @cornerstonejs versions before installing).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* test: stability of seg load mpr test

* Better drag fix for crosshairs

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-16 11:59:04 -04:00
ohif-bot da9fd9057d chore(version): Update package versions to 3.13.0-beta.92 [skip ci] 2026-06-13 01:01:06 +00:00
Bill WallaceandClaude Opus 4.8 5cb2fb41f8 fix: pnpm deploy bugs for docs and viewer-dev (#6077)
Merging for testing since this can't be tested without a merge to master

fix(ci): use --no-frozen-lockfile for docs deploy (pnpm migration)

The Build and Deploy Docs workflow ran `pnpm install --frozen-lockfile` and
failed on every master push after the pnpm migration:

  [ERR_PNPM_OUTDATED_LOCKFILE] pnpm-lock.yaml is not up to date with
  platform/core/package.json
    - @ohif/ui (lockfile: workspace:*, manifest: 3.13.0-beta.90)

publish-version.mjs rewrites @ohif/* workspace deps from "workspace:*" to the
concrete release version and commits that bump to master, so the committed
manifests intentionally drift from pnpm-lock.yaml. Every CircleCI job already
passes --no-frozen-lockfile for exactly this reason (pnpm-workspace.yaml sets
frozenLockfile:true as the default); the docs workflow was the lone job still
using frozen and so broke the docs deploy.

Switch the docs install to --no-frozen-lockfile to match the rest of CI.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@

* fix(ci): write npm auth token to ~/.npmrc so publishes authenticate

NPM_PUBLISH wrote the registry auth token to ~/repo/.npmrc (the repo root),
but publish-package.mjs does process.chdir(packageDirectory) and runs
`npm publish` from inside each package (platform/ui, extensions/*, ...). npm
reads the project .npmrc from that package dir and the user .npmrc from
$HOME -- it never walks up to ~/repo/.npmrc -- so every publish failed with
ENEEDAUTH ("need auth ... requires you to be logged in").

publish-package.mjs catches and swallows per-package publish errors, so
NPM_PUBLISH still exited 0 and reported green; the breakage was silent. As a
result no @ohif/* package newer than 3.13.0-beta.89 (the last publish before
the pnpm migration) reached npm -- beta.90 and beta.91 are missing and the
beta dist-tag is stuck at beta.89.

Write the token to ~/.npmrc (npm per-user config, read regardless of cwd)
instead. This also stops clobbering the committed workspace-config .npmrc
(node-linker=hoisted) at the repo root, which the in-job pnpm install/build
relies on. Applied to all three auth steps for consistency.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(docker): copy preinstall.js before pnpm install in builder stage

The builder stage copies only the package.json manifests, runs
`pnpm install --no-frozen-lockfile`, and copies the rest of the source
afterward (for layer caching). But the root package.json defines a
"preinstall" lifecycle script (node preinstall.js) that pnpm runs at the
start of install -- before the source copy -- so the script file was absent
and install aborted:

  . preinstall$ node preinstall.js
  Error: Cannot find module '/usr/src/app/preinstall.js' (MODULE_NOT_FOUND)
  ERROR: process "pnpm install --no-frozen-lockfile" did not complete

This surfaced once the .npmrc COPY fix (#6076) let the build advance past
the earlier COPY failure. preinstall.js is self-contained (it no-ops without
GITHUB_TOKEN and guards the AGENTS.md/CLAUDE.md symlink with existsSync), so
copying just the script into the early manifest layer is sufficient.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(build): declare postcss plugins in platform/app for Docker build

The shared root postcss.config.js (which platform/app re-exports) loads
postcss-import, postcss-preset-env, and cssnano, but none were declared in
platform/app or the root. They only reached node_modules by being hoisted
from platform/docs (postcss-import, postcss-preset-env) and transitively
(cssnano). The Docker image excludes platform/docs via .dockerignore and
libs/@cornerstonejs is not a workspace member, so under pnpm's stricter
node-linker=hoisted the app build failed to resolve the plugins:

  Loading PostCSS "postcss-preset-env" plugin failed:
  Cannot find module 'postcss-preset-env'

(The accompanying "Can't resolve assets/woff2/latin.woff2" error was a
cascade from the broken PostCSS chain and clears with this fix.)

Declare the three plugins the config explicitly loads as devDependencies of
platform/app, pinned to the versions already resolved by working builds
(postcss-import@14.1.0, postcss-preset-env@7.8.3, cssnano@5.1.15), so the
build no longer depends on incidental hoisting from docs.

The lockfile was regenerated against a workspace:* baseline so the only
@ohif change is none -- the diff adds just the postcss plugin trees, keeping
specifiers at workspace:* per the repo's convention. Verified by building the
full Docker image locally: rspack compiles with 0 errors and the image
exports successfully.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-12 20:57:11 -04:00
ohif-bot a219822ad1 chore(version): Update package versions to 3.13.0-beta.91 [skip ci] 2026-06-13 00:10:16 +00:00
Bill WallaceandClaude Opus 4.8 e391e6de5f fix: Fix the deployment to viewer-dev caused by missing .npmrc (#6076)
@
fix(ci): restore committed .npmrc in Docker build jobs (pnpm migration)

The pnpm migration (#6031) made the Dockerfile COPY .npmrc into the
builder stage because pnpm needs the workspace config it carries
(node-linker=hoisted, link-workspace-packages). But the Docker jobs still
ran `rm -f ./.npmrc` immediately before `docker build` -- a leftover from
when .npmrc only held npm credentials. NPM_PUBLISH overwrites the tracked
.npmrc with a publish auth token and persists it to the workspace, so the
Docker jobs deleted the file the Dockerfile requires:

  ERROR [builder 6/13] COPY package.json pnpm-lock.yaml pnpm-workspace.yaml .npmrc ./
  failed to compute cache key: "/.npmrc": not found

DEPLOY_MASTER therefore failed at DOCKER_BETA_PUBLISH on every master
build after the migration (NPM_PUBLISH still pushed the version bump first,
so npm packages shipped but the latest-beta image never did -- leaving
viewer-dev.ohif.org stuck on the prior beta).

Replace `rm -f ./.npmrc` with `git checkout -- .npmrc` in the four Docker
*build* jobs so the committed, token-free workspace-config .npmrc is
restored before the build. The two manifest jobs keep `rm` since they do
not build from the Dockerfile.


@

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-12 20:05:44 -04:00
ohif-bot a64fceb009 chore(version): Update package versions to 3.13.0-beta.90 [skip ci] 2026-06-12 20:08:30 +00:00
f1cc2ee13a feat: Update to use pnpm (#6031)
* Initial pnpm change

* Install/update in pnpm sort of works

* Attempt to fix install

* fix pnpm

* Refactor build process to use RSPack instead of Webpack across multiple extensions and modes. Update package.json scripts for development and production builds, and adjust webpack configuration files to accommodate new plugin imports and settings.

* Update RSPack dependencies to version 2.0.0 across the project, enhancing compatibility and performance. Refactor webpack configuration in multiple extensions and modes to utilize the new library structure for UMD output. Adjust package.json scripts and settings for improved build processes.

* Implement migration guide for OHIF 3.13, detailing the transition from Webpack to Rspack v2, the shift to pnpm workspaces, and the increase in minimum Node.js version to 24. Include new build commands, plugin replacements, and updates to package configurations across the monorepo.

* Fix some dev:fast bugs

* Move netlify to top level and remove webpack builds

* Update version script to deal with lerna missing

* chore(tests): Update multiple screenshot test images for various specs

* feat(screenshot-reviewer): Add screenshot review tool and update package.json scripts

* fix(DICOMSRDisplayTool): Improve actor presence check in viewport

* chore(tests): Update multiple screenshot assets for various specs

* chore(tests): Integrate waitForPaintToSettle and waitForViewportsRendered in multiple specs for improved rendering stability

* chore(tests): Update screenshot assets for SEGHydration and SEGNoHydration specs

* test: update progressive loading screenshots

* jest 30 test fixes for compatibility with pnpm cs3d

* Use correct setDisplaySets instead of setDataId

* fix: Naming change for LegacyVolumeViewport3D

* Update to allow tolerance for contour tests

* update

* fix

* refactor: Replace instanceof checks with utility functions for viewport type validation

* fix: Update createSegmentationForViewport to handle undefined displaySetInstanceUID gracefully

* bun lock

* chore(pnpm): align workspace setup to cornerstone3D and address PR review

- .npmrc / pnpm-workspace.yaml: mirror cs3d (node-linker hoisted,
  strict-peer-dependencies=false, link/prefer workspace packages,
  minimumReleaseAge, frozenLockfile); sync axios 1.17.0 + tmp override
- root: packageManager pnpm@11.4.0, engines pnpm >=11, wire preinstall.js
- restore preinstall.js (token/private-repo + CLAUDE->AGENTS symlink)
- playwright.yml: keep base CS3D-integration workflow, switch only the
  package manager (bun/yarn -> pnpm) and node 20 -> 24
- webpack.base.js: revert prod devtool to source-map (drop hidden-source-map)
- remove @percy/cypress; .netlify + cli templates engines pnpm >=11
- Dockerfile pin pnpm@11; document tests/globalSetup.ts warmup
- drop dicom-sr -> measurement-tracking edge (breaks pnpm cyclic dep)

* chore(pnpm): regenerate lockfile and restore cs3d:* dev scripts

- Regenerate pnpm-lock.yaml against the resolved workspace (cycle removed,
  cs3d 4.22.10, workspace:* internal deps).
- Restore cs3d:checkout/check/build/watch/install/link/unlink helper scripts
  (dropped when taking the PR's root scripts), converted yarn -> pnpm since
  the local cornerstone3D checkout is now pnpm too.

* Merge base branch issues

* Link ohif app

* ci: pin pnpm to 11.4.0 in action-setup (was version: latest)

version: latest floats across runs and breaks reproducibility. Pin to the repo's packageManager version (pnpm@11.4.0) in both playwright and build-docs workflows.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix: Accidental commit of .npmrc config

* Fix build issues

* build fix

* Dependency fixes

* fix: Dependency bug in app on extensions/modes

* Fix output configuration

* Replace percy screenshots with native cypress

* Remove percy screenshot entirely

* fix: Build issues

* force click to prevent canvas cover issues

* Enable swiftshader

* Update launch of electron

* fix: Broken size calculation

* Empty change to force a re-build

* fix(cypress): generate ui-next tailwind classes; drop unsupported electron arg

Tailwind only scanned @ohif/ui-next via ../../node_modules/@ohif/ui-next, which no longer resolves under the pnpm layout, so ui-next-unique classes (e.g. toolbar split-button sizing) were never generated. Those buttons' hit-boxes collapsed, producing Cypress 'covered by element' failures on toolbar interactions. Scan ui-next by direct filesystem path like ui/extensions/modes.

Also stop pushing --enable-unsafe-swiftshader into Electron's launchOptions.args (Electron ignores it and warns); it is supplied via ELECTRON_EXTRA_LAUNCH_ARGS in CI. The arg is still pushed for non-electron chromium (local chrome).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix(e2e): disable dev-server overlay during coverage runs

The rspack dev-server client overlay injects an iframe (id=rspack-dev-server-client-overlay) that intercepts pointer events, causing Playwright/Cypress clicks on toolbar buttons to time out (e.g. MicroscopyPanel). Disable the overlay when COVERAGE=true (e2e/Playwright webServer) while keeping it for normal local dev.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix(microscopy): provide dicom-microscopy-viewer runtime assets

The microscopy viewer is loaded at runtime via peerImport('dicom-microscopy-viewer'), which fetches the copied asset served at /dicom-microscopy-viewer/. Two issues prevented those assets from reaching dist, so the viewer never initialized and drawing produced no measurement row:

1) dicom-microscopy-viewer was declared in no workspace package.json (only under libs/@cornerstonejs), so it was not installed at root node_modules and the pluginConfig copy source did not exist. Declare it (0.48.6, matching libs) in the microscopy extension.

2) createCopyPluginToDist appended the public/dist folder name to entries that specify an explicit directory, breaking the dicom-microscopy-viewer public entry (looked for .../dynamic-import/public). Use an explicit directory as-is; only append the folder name for package-derived entries.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* Update to pnpm 11.5.2 and cs3d 5.0.2

* Lock file

* fix: Update to newer versions of actions to try to fix hang

* Remove unnecessary version

* fix: Tests that are flaky

* PR comments

* Incorrect run name

* docs: Explain pluginConfig tooling

* Updated notes on pluginConfig and PR comments

* fix: resolve plugins in rsbuild build and fix directory-based asset copy

rsbuild.config.ts: merge getPluginResolveAliases() and an @ohif/app$ alias into resolve.alias, and add resolve.modules (root, platform/app, platform/ui node_modules) so extensions resolve their shared @ohif/* imports. Brings the rsbuild (dev:fast) path to parity with webpack.pwa.js / webpack.base.js.

writePluginImportsFile.js: treat a 'directory' on an extension/mode entry as the package root and copy its public/ and dist/ subdirs instead of the whole directory; public-section entries keep copying their directory verbatim via a new literalDirectory flag.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* refactor: share resolve alias/modules between webpack and rsbuild configs

Extract the resolve.alias (@ohif/app, @, @components, ...) and the node_modules search paths into .webpack/resolveConfig.js, the single source of truth consumed by both webpack.base.js (and every per-package webpack.prod/.dev.js that merges it) and rsbuild.config.ts. getModules(srcDir) appends the building package's own source root, preserving the previous per-package behavior. Ends the drift where the rsbuild path kept missing aliases/module paths that webpack.base.js already had.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* Add a timeout on git fetch and avoid all the unnecessary mirrros

* PR comments and tests

* Remove cache to prevent corrupting the cache key/setup.

* Fix stale/corrupted pnpm installer

---------

Co-authored-by: Alireza <ar.sedghi@gmail.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-12 16:04:56 -04:00
ohif-bot 571c2b4ff0 chore(version): Update package versions to 3.13.0-beta.89 [skip ci] 2026-06-10 00:29:36 +00:00
AlirezaandBill Wallace 6dd150d401 fix: ohif tests to run with cornerstone 3d 5.0 (#6043)
* chore(tests): Update multiple screenshot test images for various specs

* feat(screenshot-reviewer): Add screenshot review tool and update package.json scripts

* fix(DICOMSRDisplayTool): Improve actor presence check in viewport

* chore(tests): Update multiple screenshot assets for various specs

* chore(tests): Integrate waitForPaintToSettle and waitForViewportsRendered in multiple specs for improved rendering stability

* chore(tests): Update screenshot assets for SEGHydration and SEGNoHydration specs

* test: update progressive loading screenshots

* jest 30 test fixes for compatibility with pnpm cs3d

* Use correct setDisplaySets instead of setDataId

* fix: Naming change for LegacyVolumeViewport3D

* Update to allow tolerance for contour tests

* update

* fix

* refactor: Replace instanceof checks with utility functions for viewport type validation

* fix: Update createSegmentationForViewport to handle undefined displaySetInstanceUID gracefully

* bun lock

* fix: Install cs3d with pnpm instead of bun

* Update node version for playwright

* Update to v5.0.0 of cs3d

* fix: Build dependency

* audit

* Change to a web await retry assert

* Fix timing related test failures

* fix: Freehand close

---------

Co-authored-by: Bill Wallace <wayfarer3130@gmail.com>
2026-06-09 20:25:14 -04:00
ohif-bot a27f779f0b chore(version): Update package versions to 3.13.0-beta.88 [skip ci] 2026-06-05 12:51:24 +00:00
Joe Boccanfuso 99c6154343 fix(security): Patch axios vulnerabilities. (#6063) 2026-06-05 08:46:09 -04:00
ohif-bot 0885468bb9 chore(version): Update package versions to 3.13.0-beta.87 [skip ci] 2026-05-29 00:13:12 +00:00
Joe Boccanfuso 9396e01107 chore: Fix netlify deploy by updating various @babel dependencies. (#6049) 2026-05-28 20:08:43 -04:00
ohif-bot 1aaae125ea chore(version): Update package versions to 3.13.0-beta.86 [skip ci] 2026-05-28 19:03:08 +00:00
Joe Boccanfuso ff3e855809 chore: Fix netlify deploy. (#6047) 2026-05-28 14:57:52 -04:00
ohif-bot 550dec742d chore(version): Update package versions to 3.13.0-beta.85 [skip ci] 2026-05-28 16:33:24 +00:00
daae4c144e feat(WorkList): New Study List (WorkList based on ui-next); old study list renamed to LegacyWorklist (#6005)
---------

Co-authored-by: Dan Rukas <dan.rukas@gmail.com>
Co-authored-by: Bill Wallace <wayfarer3130@gmail.com>
2026-05-28 12:26:30 -04:00
ohif-bot 9e7a3586ce chore(version): Update package versions to 3.13.0-beta.84 [skip ci] 2026-05-27 14:43:26 +00:00
Joe Boccanfuso bdcafc12de fix(security): Add tmp as a resolution to fix security vulnerability. (#6044) 2026-05-27 10:38:45 -04:00
ohif-bot 5ea22fc34f chore(version): Update package versions to 3.13.0-beta.83 [skip ci] 2026-05-26 16:28:06 +00:00
Ghadeer Albattarnianddiattamo 0c1ca6b04a fix(segmentation): restore navigation for duplicated contour segments (#6038)
---------

Co-authored-by: diattamo <mmddiatta@gmail.com>
2026-05-26 12:22:46 -04:00
ohif-bot 9a2d2c3d13 chore(version): Update package versions to 3.13.0-beta.82 [skip ci] 2026-05-22 17:53:48 +00:00
Ghadeer Albattarni 5762095cfa test(contour): add test for RT contour position after viewport maximize/restore (#6036) 2026-05-22 13:49:00 -04:00
ohif-bot becd208924 chore(version): Update package versions to 3.13.0-beta.81 [skip ci] 2026-05-21 01:24:47 +00:00
diattamo 2258c7957a feat(tests): Add tests for duplicating contour segments (#6002) 2026-05-20 21:18:58 -04:00
629 changed files with 55619 additions and 58535 deletions

No files matched your search

+346
View File
@@ -0,0 +1,346 @@
---
name: ohif-test-agent
description: Generate runnable Playwright E2E tests for the OHIF Viewer using its custom fixture system, page objects, and normalized WebGL viewport coordinates. Use this skill whenever the user asks to write, add, modify, or debug tests in an OHIF/Viewers context — including vague asks like "write a test for X" when working in the OHIF repo, tests touching platform/app/tests/, or anything involving Cornerstone viewports, DICOM studies, measurements, segmentations, or OHIF modes/extensions. Prefer this skill over generic test-writing even if the user doesn't say "Playwright" or "E2E" explicitly.
---
# OHIF Test Agent
This skill teaches you to generate correct, runnable Playwright end-to-end tests for the OHIF Viewer. Follow the workflow below.
## Environment model
This package follows the agentskills.io SKILL.md convention. `SKILL.md` is the entire behavior contract — there is no separate runtime entrypoint.
## Workflow: how to write a new OHIF test
1. **Classify the feature.** What area does the test belong to — a measurement tool, segmentation hydration, contour panel interaction, MPR layout, crosshairs, tag browser, etc.? The area determines the mode, the StudyInstanceUID, and the seed spec you'll read.
2. **Read the seed spec.** Consult [references/patterns-by-feature.md](references/patterns-by-feature.md) to find the canonical existing spec for that area. Read it end-to-end before writing. This is the single most important step — OHIF specs follow consistent idioms that are easier to mimic than to reconstruct from first principles. (This mirrors Playwright's own agent guidance: use seed tests as the example for generated tests.)
3. **Scaffold from the template.** Start from [assets/spec-template.ts](assets/spec-template.ts) — or copy the seed spec and adapt.
4. **Look up specifics in the source, not from memory.** The reference files [page-objects.md](references/page-objects.md) and [utilities.md](references/utilities.md) capture the **stable rules** — fixture keys, import conventions, access idioms, the reasons certain things trip people up. They deliberately do not enumerate methods. For the current method surface or a utility's exact signature, open the relevant file under `tests/pages/` or `tests/utils/` — the source evolves, and the source is always right. The seed spec you picked in step 2 is usually the fastest second source, because it co-evolves with the API.
5. **Run the test when execution is available.** `yarn test:e2e:ci` runs the whole suite, but for iteration use `yarn playwright test tests/YourNew.spec.ts` (or via the Playwright VS Code extension).
6. **If runtime execution is unavailable, do static validation.** Validate import source, fixture keys, normalized viewport usage, UID/mode pairing, and hydration/tracking prompt handling. Then report clearly that execution was not performed.
7. **If it fails, triage before debugging.** Use [references/failure-triage.md](references/failure-triage.md) — most OHIF test failures are timing / hydration, not real regressions.
## Architecture
OHIF uses Playwright with a custom fixture system. Tests are **not** vanilla Playwright — they import `test`, `expect`, and utilities from `./utils`, which re-exports an extended test runner that injects page objects.
```text
playwright.config.ts → Chromium-only, port 3335, data-cy as testId
└─ tests/utils/fixture.ts → Extends playwright-test-coverage, injects page objects
└─ tests/*.spec.ts → Each imports { test, expect, ... } from './utils'
├─ tests/pages/ → Page objects (ViewportPageObject, MainToolbarPageObject, …)
└─ tests/utils/ → Utilities (visitStudy, checkForScreenshot, screenShotPaths, …)
```
Why the custom fixture matters: the page objects are created for each test and bound to the right Playwright `page`. If you `new ViewportPageObject(page)` manually, you skip the fixture wiring and some sub-objects won't resolve correctly.
### Import rule
```ts
// Correct
import { test, expect, visitStudy, checkForScreenshot, screenShotPaths } from './utils';
// Wrong — will compile but fixtures won't be injected
import { test, expect } from '@playwright/test';
```
A few utilities (`press`, `downloadAsString`, the `assert*` helpers) are NOT re-exported from `./utils`. See [references/utilities.md](references/utilities.md) for the correct import path per utility.
## The viewport is WebGL
OHIF renders medical images onto a WebGL canvas. You cannot query *canvas* pixels by CSS selector. Use **normalized coordinates** (0–1 range, top-left is `{x:0, y:0}`) for clicks and drags, and **visual regression** (screenshot comparison) for canvas assertions. (Not everything in the viewport is canvas — some overlays render as SVG you *can* query via DOM, e.g. a vector overlay's color through `getSvgAttribute`. The canvas rule is about raster output painted onto the WebGL surface.)
```ts
const activeViewport = await viewportPageObject.active;
await activeViewport.normalizedClickAt([{ x: 0.5, y: 0.5 }]); // click center
await activeViewport.normalizedClickAt([{ x: 0.3, y: 0.3 }, { x: 0.7, y: 0.7 }]); // draw two points
await activeViewport.normalizedClickAt([{ x: 0.5, y: 0.5 }], 'right'); // right-click
await activeViewport.normalizedDragAt({
start: { x: 0.3, y: 0.3 },
end: { x: 0.7, y: 0.7 },
});
```
Pixel coordinates (`clickAt`, `doubleClickAt`) exist but prefer normalized for portability across viewport sizes.
For DOM-rendered state (panel counts, dialog text, overlay text values, button enabled states), assert directly:
```ts
await expect(activeViewport.overlayText.bottomRight.instanceNumber).toContainText('17/');
const count = await rightPanelPageObject.measurementsPanel.panel.getMeasurementCount();
expect(count).toBe(1);
```
## Study loading lifecycle
Every test follows this sequence. Skipping steps causes flakiness:
```ts
test.beforeEach(async ({ page }) => {
const studyInstanceUID = '1.3.6.1.4.1.25403.345050719074.3824.20170125095438.5';
const mode = 'viewer';
await visitStudy(page, studyInstanceUID, mode, 2000); // 2s delay is the community norm
});
```
`visitStudy` navigates to `/{mode}/ohif?StudyInstanceUIDs={uid}`, waits for `domcontentloaded`, then `networkidle`, then the explicit delay. Default delay is `0`, but most specs pass `2000` to let the first render settle.
Delay by scene type (observed across the current suite, not a rule to apply blindly):
| Scene | Delay |
|-------|-------|
| Default (viewer mode, 2D/MPR/3D layouts, crosshairs) | `2000` |
| `mode: 'tmtv'` | `10000` — PET fusion + SUV calculation takes noticeably longer |
Start at the convention for your scene; ramp only if the test flakes on initial render. 3D layouts in `viewer` mode already stay at `2000` — the stabilization problem there is solved with `attemptAction(() => reduce3DViewportSize(page), 10, 100)`, not with a longer `visitStudy` delay.
If the study has DICOM SEG, RT, or SR data, OHIF asks whether to hydrate. Handle it:
```ts
await leftPanelPageObject.loadSeriesByModality('SEG'); // or 'RTSTRUCT', 'SR'
await page.waitForTimeout(3000); // allow the prompt to appear
await expect(DOMOverlayPageObject.viewport.segmentationHydration.locator).toBeVisible();
await DOMOverlayPageObject.viewport.segmentationHydration.yes.click();
```
The first measurement you create triggers a "start tracking?" prompt:
```ts
await DOMOverlayPageObject.viewport.measurementTracking.confirm.click();
```
For 3D / MPR scenes, wrap stabilization in `attemptAction(() => reduce3DViewportSize(page), 10, 100)` or insert a `page.waitForTimeout(...)` after the layout change before asserting.
## Wait for renders, don't sleep
`page.waitForTimeout(...)` after an action that re-renders the viewport is a smell. The viewports tell us when they're done — use that signal. `tests/utils/waitForViewportsRendered.ts` exposes three helpers, all barrel-exported from `./utils`:
- `waitForViewportRenderCycle(page)` — wait for the next full cycle: a viewport enters `needsRender`, then **all** viewports report `rendered` (and volumes are loaded, by default).
- `waitForViewportsRendered(page)` — only the second half: wait until all viewports are `rendered`. Use this when the action has already requested a render before you started waiting (e.g. a layout change or `loadSeriesByDescription`).
- `waitForAnyViewportNeedsRender(page)` — only the first half. Rarely needed directly.
The canonical idiom — **start the watcher before the action, await it after**:
```ts
// start watching for the next render cycle
const viewportRenderCycle = waitForViewportRenderCycle(page);
await action(); // e.g. segmentationHydration.yes.click(), layoutSelection.MPR.click(), addSegmentation, etc.
// wait for the render to finish
await viewportRenderCycle;
await check(); // e.g. checkForScreenshot, count assertion, overlay text
```
Why "start before"? `waitForViewportRenderCycle` first waits for a viewport to enter `needsRender`. If you start it **after** the action, that transition may already be over and you'll hang until the timeout. Starting it first captures the cycle the action is about to trigger.
When to use which:
| Situation | Helper |
|-----------|--------|
| Click that triggers a re-render and you want to assert after | `waitForViewportRenderCycle(page)` started before the click |
| Layout switch / series load — render already in flight | `await waitForViewportsRendered(page)` after the call |
| Compose with another await (e.g. screenshot the same time as load) | Save the promise, `await` it later |
Replace patterns like this:
```ts
// ❌ Sleep-and-pray
await action();
await page.waitForTimeout(5000);
await checkForScreenshot(...);
// ✅ Wait on the actual signal
const cycle = waitForViewportRenderCycle(page);
await action();
await cycle;
await checkForScreenshot(...);
```
This shaves real wall-clock time off the suite and removes a class of flake (sleep too short → flake; sleep too long → slow). `tests/SEGHydrationFromMPR.spec.ts` is the canonical seed for this pattern.
Caveats:
- These helpers wait on Cornerstone viewport state. They won't help for purely DOM-side state (panel rows appearing, dialogs opening) — for those, prefer `expect(locator).toHaveCount(n)` / `toBeVisible()` which auto-retry, or `expect.toPass({ timeout })`.
- For some actions (hanging-protocol changes are the documented example) the viewport doesn't transition through `needsRender` synchronously — those still need a short `waitForTimeout`. The source comment in `waitForViewportsRendered.ts` calls this out.
## Fixture-injected page objects
Destructure these from the test function argument. **Never `new` them manually.**
```ts
test('my test', async ({
page,
viewportPageObject,
mainToolbarPageObject,
leftPanelPageObject,
rightPanelPageObject,
DOMOverlayPageObject, // note the capital D — this matches the fixture key
notFoundStudyPageObject,
}) => { ... });
```
Two page objects are **not** fixture-injected:
- `DataOverlayPageObject` — reach via `viewportPageObject.getById(id).overlayMenu.dataOverlay`.
- `DicomTagBrowserPageObject` — reach via `DOMOverlayPageObject.dialog.dicomTagBrowser`.
See [references/page-objects.md](references/page-objects.md) for fixture rules and a map of which file covers which concern; read the `.ts` file under `tests/pages/` for the current method surface.
## When the control you need has no page object yet
A spec must not reach for `page.getByTestId(...)` / `getByRole(...)` directly for
application controls. If the button, menu, dialog, or field you need isn't already
exposed by a page object, **add it to one — or create a new page object — instead of
inlining a raw selector.** Raw selectors in a spec are the clearest sign a test was
written without reading the existing suite: they duplicate locators, bypass the
fixture wiring, and rot silently when the DOM changes.
Where new coverage goes:
| What you need | Where it belongs |
|---|---|
| A toolbar button or tool (Zoom, Pan) | a getter on `MainToolbarPageObject`, next to `crosshairs` / `measurementTools` |
| A menu, prompt, context menu, or small dialog | `DOMOverlayPageObject` |
| A substantial dialog with its own fields (User Preferences) | its **own** page object class, reached through `DOMOverlayPageObject` — follow `DicomTagBrowserPageObject` (`DOMOverlayPageObject.dialog.dicomTagBrowser`) |
| Each field/row inside that dialog | a method or sub-object on the dialog's page object — not a raw selector in the spec |
| A side-panel control | `LeftPanelPageObject` / `RightPanelPageObject` |
If the control has no `data-cy`, **add `data-cy` to the source component** and target
it — don't fall back to `getByRole`/text selectors, which are brittle and
locale-sensitive (`testIdAttribute` is `data-cy`, so `getByTestId('Zoom')` resolves
`[data-cy="Zoom"]`). Call out any `data-cy` you add so it ships in the same PR.
**Worked example — "set the Zoom hotkey in User Preferences":** the options menu, the
preferences dialog, each preference field, and the Zoom toolbar button should all be
page-object surface — e.g. `mainToolbarPageObject.zoom`,
`DOMOverlayPageObject.optionsMenu.settings.click()`,
`DOMOverlayPageObject.dialog.userPreferences.hotkey('Zoom').set('q')`. The spec then
reads as intent, not as a pile of `getByTestId` calls.
## Assert the effect, not just the attribute
Prefer asserting the actual rendered result over a proxy attribute. Activating Zoom and
checking `data-active="true"` confirms the *button* toggled — not that zoom works. Drag
on the viewport and assert the image actually zoomed (a viewport-scoped screenshot, or a
measurable state change). Attribute checks are fine as a secondary signal, not the whole
test.
## Visual regression
**Direction:** the suite is moving off *full-app* screenshots — not off screenshots
altogether. "Avoid screenshots" means: don't screenshot the whole page, and don't
screenshot something that has a faithful DOM/state signal. It does **not** mean avoid
screenshots for output that is genuinely canvas-only — for that output a screenshot is
the correct and required tool, and you should use it without apology. Older specs that
screenshot the whole page are the legacy pattern being phased out; viewport-scoped
screenshots are not.
### Screenshot vs. DOM assertion — how to choose
Reach for the cheapest *faithful* signal, in this order:
1. **A faithful DOM/SVG/state signal exists → assert on it.** Panel counts, dialog and
overlay text, enabled/disabled state, and any overlay that renders as SVG (a vector
overlay's color is readable via `getSvgAttribute`) all have a DOM representation — assert
on it directly, no screenshot.
2. **The thing under test is painted onto the WebGL canvas with no DOM representation → a
screenshot is correct and required.** Raster output on the canvas exposes no attribute to
read for a painted pixel. Scope a `checkForScreenshot` to the viewport (pane or grid) and
assert it — this is the right tool, not a last resort, whenever what you're verifying is
the rendered canvas itself.
3. **Never substitute a service/state read for a render assertion.** Reading a service's
state (any `window.services...`) asserts the *data model*, not the pixels the user sees —
it passes even when rendering is broken. `page.evaluate(() => window.services...)` is an
escape hatch for *setup*, not for *appearance* assertions.
For anything drawn onto the WebGL canvas with no DOM signal, compare a screenshot scoped to a specific viewport or the viewport grid:
```ts
await checkForScreenshot({
page,
locator: viewportPageObject.grid, // scope to the viewport grid — not the whole page
screenshotPath: screenShotPaths.length.lengthDisplayedCorrectly,
});
```
`checkForScreenshot` retries up to 10 times at 500 ms intervals. Use `screenShotPaths.<category>.<name>` rather than a hand-typed string — the tree of valid keys lives in `tests/utils/screenShotPaths.ts`.
Rules (apply to all new screenshot assertions):
- **Use the object form.** The positional form is legacy; don't introduce it in new code, and don't treat existing positional-form usage as a pattern to copy.
- **Never screenshot the full app.** Full-page screenshots include panels, toolbars, and dialogs that drift independently of what's under test and make baselines fragile. Scope by passing a `locator` — `viewportPageObject.grid` for the grid, or a specific viewport pane. A bare `normalizedClip: { x: 0, y: 0, width: 1, height: 1 }` with no `locator` is **not** scoping — it clips to the full page. Use `normalizedClip` only to target a sub-region *of a locator* (e.g. a scrollbar strip). If you reach for `fullPage: true`, stop and pick a locator.
- **Do not tune `maxDiffPixelRatio` or `threshold`** to make a screenshot pass. If a baseline mismatches, regenerate it after a human review of the diff, or fix the underlying flake.
## Playwright config facts worth remembering
| Setting | Value | Why |
|---------|-------|-----|
| `baseURL` | `http://localhost:3335` | OHIF e2e uses 3335, not 3000 |
| `testIdAttribute` | `data-cy` | `getByTestId(...)` maps to `[data-cy="..."]` |
| `browser` | Chromium only | Firefox/WebKit disabled (SharedArrayBuffer + stability) |
| `retries` | 3 in CI, 0 locally | Flaky rendering needs CI retries |
| `workers` | 6 in CI, undefined locally | Parallel execution |
| `globalTimeout` / `timeout` | 800_000 ms | Medical image loads are slow |
| `actionTimeout` | 10_000 ms | Per-action cap |
| `webServer.command` | `cross-env APP_CONFIG=config/e2e.js COVERAGE=true OHIF_PORT=3335 nyc yarn start` | e2e config + coverage |
## Test data: which study for which test
| StudyInstanceUID | Mode | Used for |
|------------------|------|----------|
| `1.3.6.1.4.1.25403.345050719074.3824.20170125095438.5` | `viewer` | General measurements, annotations, context menu |
| `1.3.6.1.4.1.14519.5.2.1.1706.8374.643249677828306008300337414785` | `viewer` | 3D, MPR, crosshairs |
| `1.3.6.1.4.1.14519.5.2.1.256467663913010332776401703474716742458` | `viewer` or `segmentation` | Labelmap SEG |
| `1.2.840.113619.2.290.3.3767434740.226.1600859119.501` | `viewer` / `segmentation` / `tmtv` | RTSTRUCT/contour and TMTV |
| `1.3.6.1.4.1.14519.5.2.1.7695.4007.324475281161490036195179843543` | `viewer` | SR hydration |
Full mapping in [references/patterns-by-feature.md](references/patterns-by-feature.md). **Do not invent UIDs** — they must exist on the e2e data server.
## Rules (short, so they're actually read)
1. Import `test`, `expect`, and utilities from `./utils`, not `@playwright/test`.
2. Destructure fixture-injected page objects; don't `new` them.
3. Use normalized coordinates (0–1) for viewport interactions.
4. Use `visitStudy` with a real UID, correct mode, and a non-zero delay (2000 is conventional).
5. Handle hydration and measurement-tracking prompts where applicable.
6. Choose the faithful signal: DOM/SVG assertions where the rendered result has one (panels, dialogs, overlay text, SVG/vector overlays), a viewport-scoped screenshot when what you're verifying is canvas-only raster output, and never a `window.services` state read standing in for a render check. Screenshots use the object form, scoped via a `locator` — never the full app.
7. Use `data-cy` selectors (already wired via `testIdAttribute`).
8. When an assertion needs retry tolerance, wrap it in `expect.toPass({ timeout })`.
9. Test in the correct mode — segmentation tools aren't available in `viewer` mode.
10. If a utility isn't exported from `./utils`, import from the deeper path (see [references/utilities.md](references/utilities.md)).
11. After an action that re-renders the viewport, prefer `waitForViewportRenderCycle(page)` (started before the action) over `page.waitForTimeout(...)`. See the "Wait for renders, don't sleep" section.
12. Don't inline raw `page.getByTestId(...)` / `getByRole(...)` for app controls. If a control has no page object, create or augment one (see "When the control you need has no page object yet"), adding a `data-cy` to the source if needed.
13. Assert the actual effect (e.g. the image zoomed), not just a proxy attribute like `data-active`.
## Pre-output self-check (mandatory)
Before returning a generated OHIF test, confirm all items:
1. Imports `test`/`expect` from `./utils` (not `@playwright/test`).
2. Uses fixture-injected keys and exact casing (especially `DOMOverlayPageObject`).
3. Uses normalized viewport interactions (`normalizedClickAt` / `normalizedDragAt`) unless there is a strong reason otherwise.
4. Uses a valid canonical StudyInstanceUID and compatible mode.
5. Handles hydration or measurement tracking prompts when the workflow requires them.
6. Uses the faithful signal for each assertion — DOM/SVG where the result has a DOM representation, a viewport-scoped screenshot when what's verified is canvas-only raster output, and never a `window.services` state read in place of a render check. Any `checkForScreenshot` call uses the object form, scoped via a `locator` (viewport pane or grid) — no full-app screenshots.
7. Replaces `page.waitForTimeout(...)` after viewport-rendering actions with `waitForViewportRenderCycle(page)` (started before the action) — keeps `waitForTimeout` only for non-render waits like the hydration prompt in `beforeEach`.
8. If execution was skipped, states that explicitly and provides concrete run commands.
9. Every application control is reached through a page object — no raw `getByTestId`/`getByRole` in the spec for buttons, menus, dialogs, or fields. Any control not already covered was added to the right page object (or a new one), with a source `data-cy` if it lacked one.
10. Assertions verify the real effect where feasible (e.g. the image visibly zoomed), not only an attribute toggle.
## Output contract (for non-executing agents)
When execution cannot be performed in the current environment, the response should include:
1. The test code.
2. Assumptions made (if any).
3. Static checks that were verified.
4. What still must be run locally and exact commands to run.
## When to consult each reference
- **Before writing** → [references/patterns-by-feature.md](references/patterns-by-feature.md). Pick the seed spec for the feature area and read it. The seed spec is the closest thing to a live API example because it co-evolves with the code.
- **For a stable rule or idiom** (fixture keys, import paths, panel-access order, capital-D quirk, object-param convention) → [references/page-objects.md](references/page-objects.md), [references/utilities.md](references/utilities.md).
- **For a method name, property, or signature** → read the source under `tests/pages/` or `tests/utils/`. Do not rely on a static table for these; they drift as the code is refactored.
- **When a test fails** → [references/failure-triage.md](references/failure-triage.md).
@@ -0,0 +1,54 @@
import {
checkForScreenshot,
expect,
screenShotPaths,
test,
visitStudy,
waitForViewportRenderCycle,
} from './utils';
test.beforeEach(async ({ page }) => {
// Pick the right UID + mode for your feature (see references/patterns-by-feature.md)
const studyInstanceUID = '1.3.6.1.4.1.25403.345050719074.3824.20170125095438.5';
const mode = 'viewer';
await visitStudy(page, studyInstanceUID, mode, 2000);
});
test.describe('FEATURE NAME', () => {
test('describes the behaviour in one sentence', async ({
page,
viewportPageObject,
mainToolbarPageObject,
rightPanelPageObject,
DOMOverlayPageObject, // capital D on purpose
}) => {
// 1. Arrange — select a tool, open a panel, etc.
// 2. Act — interact with the viewport.
// Prefer normalized (0–1) coordinates:
// const activeViewport = await viewportPageObject.active;
// await activeViewport.normalizedClickAt([{ x: 0.3, y: 0.3 }, { x: 0.7, y: 0.7 }]);
// For actions that re-render the viewport, gate on the render cycle
// instead of sleeping — start the watcher BEFORE the action:
// const cycle = waitForViewportRenderCycle(page);
// await action();
// await cycle;
// 3. Handle prompts (first measurement prompts for tracking; SEG/RT/SR prompts for hydration)
// await DOMOverlayPageObject.viewport.measurementTracking.confirm.click();
// 4. Assert canvas output via visual regression — object form, scoped to the
// viewport via a locator (not the full page). normalizedClip is only for
// clipping to a sub-region of that locator.
// await checkForScreenshot({
// page,
// locator: viewportPageObject.grid,
// screenshotPath: screenShotPaths.YOUR_CATEGORY.YOUR_KEY,
// });
// 5. Assert DOM state directly
// const count = await rightPanelPageObject.measurementsPanel.panel.getMeasurementCount();
// expect(count).toBe(1);
});
});
@@ -0,0 +1,70 @@
# Failure triage
Before debugging, classify. Most OHIF test failures are timing or hydration — not real regressions.
| Category | Symptom | Fix |
|----------|---------|-----|
| Timing | Element not visible, action timeout | Add / increase the `delay` param of `visitStudy`; for actions that re-render viewports, use `waitForViewportRenderCycle(page)` (started before the action) instead of `waitForTimeout`; wrap the assertion in `expect.toPass({ timeout })` |
| Selector | Element not found | Verify `data-cy` on the target; confirm the panel is open (`toggle()` / `select()` before interacting); check for capital `D` in `DOMOverlayPageObject` when destructuring |
| Hydration | Segmentation/RT not interactive | Ensure the `segmentationHydration.yes.click()` fired; add `waitForTimeout(3000)` after `loadSeriesByModality('SEG'\|'RTSTRUCT'\|'SR')` |
| Data | Study not found, empty viewport | Confirm the UID is in the canonical list (see [patterns-by-feature.md](patterns-by-feature.md)); confirm the mode supports the feature (segmentation tools aren't in `viewer` mode) |
| Visual drift | Screenshot mismatch but feature works | Have a human review the diff, then regenerate the baseline with `yarn playwright test --update-snapshots`. Do not adjust `maxDiffPixelRatio` or `threshold` to make a failing screenshot pass. |
| Real regression | Feature is actually broken | Report as a bug — this is the test doing its job |
## Prefer render-cycle waits over sleeps
If you're tempted to add `await page.waitForTimeout(2000)` after an action, ask whether the action re-rendered the viewport. If it did, use:
```ts
const cycle = waitForViewportRenderCycle(page);
await action();
await cycle;
await check();
```
The watcher must be created **before** the action — it waits for `needsRender` first, and that transition is gone by the time the action returns. See the "Wait for renders, don't sleep" section in [SKILL.md](../SKILL.md) and `tests/SEGHydrationFromMPR.spec.ts`.
### When the cycle helper times out at `waitForAnyViewportNeedsRender`
Symptom: the test fails inside `waitForAnyViewportNeedsRender` after 5s, with the action having actually completed in the UI. The action just doesn't transition the viewport through `needsRender` synchronously. Known cases:
- Hanging-protocol changes.
- **RTSTRUCT / contour segmentation hydration confirm.** SEG (labelmap) hydration does fire `needsRender`; contour does not. The fix is to gate on the actual end-state — for hydration in `beforeEach`, `await expect(page.getByTestId('data-row')).toHaveCount(N)` is the right wait.
Don't react by raising the cycle's timeout — the transition isn't coming. Replace the cycle wrapper with an auto-retrying DOM/SVG assertion, or `expect.toPass({ timeout })` around the assertion block.
## The `toPass` pattern
When an assertion needs to wait for async render / propagation:
```ts
await expect(async () => {
await rightPanelPageObject.labelMapSegmentationPanel.panel.segmentByText('Spleen').click();
await expect(activeViewport.overlayText.bottomRight.instanceNumber).toContainText('17/');
}).toPass({ timeout: 10_000 });
```
`toPass` reruns the assertion block until it succeeds or the timeout expires — cleaner than a hand-rolled retry loop and surfaces the last failure reason if it times out.
## Common `DOMOverlayPageObject` mistake
The fixture key is capital-D `DOMOverlayPageObject`, not lowercase. If your destructure is silently `undefined`, check the casing.
## `press` import mistake
`press` is NOT re-exported from `./utils`. `import { press } from './utils'` resolves to `undefined` and fails at runtime. Use:
```ts
import { press } from './utils/keyboardUtils';
await press({ page, key: 'ArrowDown', nTimes: 50 }); // object param, not (page, key)
```
## Visual regression iteration
Screenshots live under `tests/screenshots/chromium/<testFilePath>/`. To accept new output as the baseline:
```sh
yarn playwright test tests/YourSpec.spec.ts --update-snapshots
```
Review the resulting PNGs carefully — an agent-accepted baseline that's subtly wrong is worse than a failing test.
@@ -0,0 +1,120 @@
# OHIF Page Object guide
> This file documents the **stable structural rules** of the page object system. For the current list of methods and properties on any class, **read the source under `tests/pages/`** — it is always authoritative, and it evolves as the product does. A static method table in a reference file goes stale the moment someone refactors; the source does not.
## How to discover the API of a page object
1. Find the relevant class in `tests/pages/`. File names match class names.
2. Read it end-to-end once — most are under a few hundred lines.
3. Some classes compose sub-objects (e.g. `RightPanelPageObject` holds a measurementsPanel, contourSegmentationPanel, labelMapSegmentationPanel, tmtvPanel, etc.). Those sub-objects usually live in the same file or a sibling under `tests/pages/`.
4. To see how a method is actually used, grep `tests/` or open the seed spec listed in [patterns-by-feature.md](patterns-by-feature.md). Real usage beats a synthesized signature every time.
Do not try to memorize a method surface from this file — it intentionally does not list one. It lists only the rules you cannot derive from the source by reading a single file.
---
## Stable rules
### Fixture keys (case-sensitive)
These are injected via `tests/utils/fixture.ts`. Destructure them from the test function's first argument — do not `new` them, because the fixture wires sub-objects to the correct `page` and hand-constructed instances skip that wiring.
- `viewportPageObject`
- `mainToolbarPageObject`
- `leftPanelPageObject`
- `rightPanelPageObject`
- `DOMOverlayPageObject` — **capital D**. A silent `undefined` destructure is almost always a casing typo here.
- `notFoundStudyPageObject`
If the fixture file is updated and new keys are added, they will show up there first — check it if something feels missing.
### Non-fixture page objects
Some page object classes are not fixture-injected. They are reached through an injected fixture:
- `DicomTagBrowserPageObject` → via `DOMOverlayPageObject.dialog.dicomTagBrowser`
- `DataOverlayPageObject` → via `viewportPageObject.getById(viewportId).overlayMenu.dataOverlay`
Both can be constructed manually (`new DataOverlayPageObject(page)`) if a test really needs a fresh instance, but the accessor path is the idiomatic one.
### Viewport wrapper vs. viewport instance
`viewportPageObject` is a **wrapper**. You almost always want a specific viewport out of it first:
- `await viewportPageObject.active` — the currently focused viewport
- `viewportPageObject.getAll()` — every viewport in the grid
- `viewportPageObject.getNth(i)` — zero-indexed
- `viewportPageObject.getById(cornerstoneViewportId)` — e.g. `'default'`, `'ctAXIAL'`
The object these return is the one with `normalizedClickAt`, `normalizedDragAt`, `overlayText`, `nthAnnotation`, etc. Reach for the viewport instance first, then call methods on it.
### Panel access order
Every `rightPanelPageObject` sub-panel follows the same three-step idiom: **open the side panel, `.select()` the sub-panel tab, then interact with `.panel.*`**. Skipping either of the first two is the most common cause of "element not found".
```ts
await rightPanelPageObject.toggle();
await rightPanelPageObject.measurementsPanel.select();
const count = await rightPanelPageObject.measurementsPanel.panel.getMeasurementCount();
```
The exact row/action methods vary by panel — check the source file for the one you need.
### Layout identifiers are camelCase JS properties
`mainToolbarPageObject.layoutSelection.<layout>.click()` — access layouts by camelCase property name (e.g. `threeDFourUp`, `axialPrimary`), not with bracket-escaped DICOM-ish strings like `['3DFourUp']`. This is a convention enforced by how the class exposes its tools.
### Sub-tools auto-open their dropdown
Tools nested inside a toolbar dropdown (measurement tools, more tools, layouts) each expose a `.click()` that opens the parent menu for you. You almost never need to open the menu first. `await mainToolbarPageObject.measurementTools.length.click()` does both the expand and the select.
### When the control you need isn't covered yet — create or augment
The page objects here cover what the suite currently exercises. When your test needs a
control that isn't exposed, **extend the page object system rather than dropping a raw
`page.getByTestId(...)` into the spec.** A raw selector in a spec is the clearest tell
that the author didn't read the existing tests — it duplicates a locator that should
live in one place and bypasses the fixture wiring.
| What you need | Where it goes | Precedent to copy |
|---|---|---|
| A toolbar button/tool (Zoom, Pan) | a getter on `MainToolbarPageObject` | `crosshairs`, `measurementTools` |
| A menu / prompt / context menu / small dialog | `DOMOverlayPageObject` | `viewport.measurementTracking`, `dialog.input` |
| A large dialog with its own fields (User Preferences) | a **new** page object class reached via `DOMOverlayPageObject` | `DicomTagBrowserPageObject` (`DOMOverlayPageObject.dialog.dicomTagBrowser`) |
| Individual fields/rows in that dialog | methods/sub-objects on the dialog's page object | `DicomTagBrowserPageObject.seriesSelect` |
| Side-panel controls | `LeftPanelPageObject` / `RightPanelPageObject` | existing sub-panels |
Two more expectations:
- **Missing `data-cy`?** Add it to the source component and target it; don't fall back
to `getByRole`/text. `testIdAttribute` is `data-cy`, so `getByTestId('Zoom')` resolves
`[data-cy="Zoom"]`. Mention any attribute you added so it ships in the same PR.
- **Mirror the existing shape.** A new getter should look like its neighbors (return a
locator, or a small object with `.click()` etc.) so the new surface is
indistinguishable from what was already there.
Example — a "set the Zoom hotkey" test should make `mainToolbarPageObject.zoom`, an
options-menu accessor on `DOMOverlayPageObject`, and a `userPreferences` dialog page
object (with a per-hotkey field accessor) exist — then the spec reads as steps, not
selectors.
---
## Page object map — what each class is *for*
This table exists to help you pick the right file to open, not to enumerate methods. Source of truth for any specific method remains the `.ts` file.
| Class | File | Covers |
|-------|------|--------|
| ViewportPageObject | `tests/pages/ViewportPageObject.ts` | Cornerstone viewports — clicks, drags, overlays, annotations, crosshairs |
| MainToolbarPageObject | `tests/pages/MainToolbarPageObject.ts` | Top toolbar — measurement tools, more tools, layouts, crosshairs, pan |
| LeftPanelPageObject | `tests/pages/LeftPanelPageObject.ts` | Study browser — thumbnails, load by modality or description |
| RightPanelPageObject | `tests/pages/RightPanelPageObject.ts` | Side panels — measurements, contour seg, labelmap seg, TMTV, microscopy |
| DOMOverlayPageObject | `tests/pages/DOMOverlayPageObject.ts` | DOM overlays — dialogs, hydration/tracking prompts, context menus, tag-browser accessor |
| NotFoundStudyPageObject | `tests/pages/NotFoundStudyPageObject.ts` | Study-not-found error page |
| DicomTagBrowserPageObject | `tests/pages/DicomTagBrowserPageObject.ts` | Tag-browser dialog (non-fixture; reach via `DOMOverlayPageObject.dialog`) |
| DataOverlayPageObject | `tests/pages/DataOverlayPageObject.ts` | Data-overlay menu (non-fixture; reach via `viewport.overlayMenu`) |
If the directory adds or renames a file, that diff is your first clue and this table is your second — trust the directory.
For live usage, the seed spec in [patterns-by-feature.md](patterns-by-feature.md) shows how a class is actually called; the source file tells you everything else that's on it.
@@ -0,0 +1,148 @@
# Seed specs by feature area
> When writing a new OHIF test, find the closest feature area below and read the listed spec in full before writing. Playwright's own guidance says the seed test "serves as an example of all the generated tests" — that applies here.
>
> **If a spec listed below has moved or been renamed**, grep `tests/` for a remaining example (e.g. `grep -rn "loadSeriesByModality('RTSTRUCT')" tests/`). The pattern matters more than the exact filename — specs get renamed, the feature area persists.
>
> **No close match below?** This list only covers areas with a seed worth copying; don't add a stub for every untested area. See [Feature area not listed above?](#feature-area-not-listed-above-no-existing-seed).
## Canonical study UIDs
| UID | Mode(s) | What it has |
|-----|---------|-------------|
| `1.3.6.1.4.1.25403.345050719074.3824.20170125095438.5` | `viewer` | CT — default for measurement/annotation tests |
| `1.3.6.1.4.1.14519.5.2.1.1706.8374.643249677828306008300337414785` | `viewer` | CT volume for 3D/MPR/crosshairs |
| `1.3.6.1.4.1.14519.5.2.1.256467663913010332776401703474716742458` | `viewer` or `segmentation` | CT + SEG for labelmap tests |
| `1.2.840.113619.2.290.3.3767434740.226.1600859119.501` | `viewer` / `segmentation` / `tmtv` | CT + RTSTRUCT + PET, used for contour and TMTV |
| `1.3.6.1.4.1.14519.5.2.1.7695.4007.324475281161490036195179843543` | `viewer` | SR structured report |
Do not invent UIDs — they must exist on the e2e data server.
---
## 1. Simple measurement tools (length, angle, bidirectional, rectangle, ellipse, circle)
**Seed:** `tests/Length.spec.ts`, `tests/Angle.spec.ts`
Pattern: select tool via `mainToolbarPageObject.measurementTools.<tool>.click()`, place N points via `activeViewport.normalizedClickAt([...])`, confirm the tracking prompt, screenshot via `screenShotPaths.<name>.<name>DisplayedCorrectly`.
## 2. Freehand / spline / livewire ROIs
**Seed:** `tests/FreehandROI.spec.ts`, `tests/Livewire.spec.ts`, `tests/Spline.spec.ts`
Pattern: `normalizedDragAt({ start, end, config: { steps: 20, delay: 30 } })` for smooth strokes; `subscribeToMeasurementAdded` to assert the event fires; `activeViewport.nthAnnotation(0)` to reference what was drawn.
## 3. Annotations (arrow, probe)
**Seed:** `tests/ArrowAnnotate.spec.ts`, `tests/Probe.spec.ts`
Arrow annotate opens `DOMOverlayPageObject.dialog.input` for the label. Use `fillAndSave(label)`.
## 4. Measurement panel interactions
**Seed:** `tests/MeasurementPanel.spec.ts`
Panel access: `rightPanelPageObject.toggle()` → `.measurementsPanel.select()` → `.panel.nthMeasurement(i)` → `.actions.rename|delete|toggleLock|duplicate|...`. Also demonstrates `addLengthMeasurement(page)` and panel-row `click()` for jump-to.
## 5. Context menu (right-click on annotation)
**Seed:** `tests/ContextMenu.spec.ts`
Two ways to open: `activeViewport.normalizedClickAt([{...}], 'right')` on an empty area, or `activeViewport.nthAnnotation(0).contextMenu.open()` on a drawn annotation. Then `DOMOverlayPageObject.viewport.annotationContextMenu.addLabel|delete.click()`.
## 6. Labelmap segmentation (SEG) hydration
**Seed:** `tests/SEGHydration.spec.ts`, `tests/SEGHydrationThenMPR.spec.ts`
Flow: `leftPanelPageObject.loadSeriesByModality('SEG')` → `waitForTimeout(3000)` → `DOMOverlayPageObject.viewport.segmentationHydration.yes.click()`. Often pokes Cornerstone state directly via `page.evaluate(() => window.cornerstone...)` for zoom/render.
## 7. Contour segmentation (RTSTRUCT) + interactions
**Seeds:**
- Hydration: `tests/RTHydration.spec.ts`
- Rename: `tests/ContourSegmentRename.spec.ts`
- Navigation between segments: `tests/ContourSegNavigation.spec.ts`
- Duplicate: `tests/ContourSegmentDuplicate.spec.ts`
- Visibility: `tests/ContourSegmentToggleVisibility.spec.ts`
- Locking: `tests/ContourSegLocking.spec.ts`
Pattern: load RTSTRUCT series → hydrate → use `rightPanelPageObject.contourSegmentationPanel.panel.nthSegment(i)` / `.segmentByText('Small Sphere')` and their `.actions.rename|delete`, or the segment's `.click()` to jump.
## 8. Labelmap segmentation editing (brush / eraser / threshold)
**Seed:** `tests/LabelMapSegLocking.spec.ts`, `tests/SEGDrawingToolsResizing.spec.ts`
Pattern: `rightPanelPageObject.labelMapSegmentationPanel.tools.brush.setRadius(n)` / `.click()`, then `activeViewport.normalizedDragAt(...)` to paint.
## 9. TMTV / PET
**Seeds:** `tests/TMTVCSVReport.spec.ts`, `tests/TMTVSUV.spec.ts`, `tests/TMTVAlignment.spec.ts`, `tests/TMTVRendering.spec.ts`
Visit `mode: 'tmtv'` with a longer delay (`10000`). `rightPanelPageObject.tmtvPanel` for the side panel. Exporting a report uses `page.waitForEvent('download')` + `downloadAsString(download)`.
## 10. MPR and 3D layouts
**Seeds:** `tests/MPR.spec.ts`, `tests/3DOnly.spec.ts`, `tests/3DFourUp.spec.ts`, `tests/3DMain.spec.ts`, `tests/AxialPrimary.spec.ts`
Pattern: `mainToolbarPageObject.layoutSelection.<layoutName>.click()`. For 3D, wrap stabilization with `attemptAction(() => reduce3DViewportSize(page), 10, 100)` to settle the render before asserting.
## 11. Crosshairs
**Seed:** `tests/Crosshairs.spec.ts`
Pattern: `initializeMousePositionTracker(page)` in `beforeEach`, `mainToolbarPageObject.crosshairs.click()`, then `viewportPageObject.crosshairs.axial.rotate()` / `.increase()`.
## 12. Overlays — data overlay menu, window/level, orientation
**Seeds:** `tests/DataOverlayMenu.spec.ts`, `tests/WindowLevelOverlayText.spec.ts`, `tests/MultipleSegmentationDataOverlays.spec.ts`
Pattern: `viewportPageObject.getById('default').overlayMenu.dataOverlay.toggle()`, then `.addSegmentation(name)` / `.changeSegmentation(from, to)` / `.remove(name)`. For keyboard navigation, remember `press({ page, key, nTimes })` imports from `./utils/keyboardUtils`.
## 13. DICOM Tag Browser
**Seed:** `tests/DicomTagBrowser.spec.ts`
Open with `mainToolbarPageObject.moreTools.tagBrowser.click()`, then interact via `DOMOverlayPageObject.dialog.dicomTagBrowser.waitVisible()` / `.seriesSelect.selectOption(i)` / `.seriesSelect.getOptionText(i)`.
## 14. Study validation / not-found / worklist
**Seeds:** `tests/StudyValidation.spec.ts`, `tests/Worklist.spec.ts`
These are the rare specs that use `page.goto(...)` directly instead of `visitStudy()` — because they test error states or non-study pages. `notFoundStudyPageObject` gives you `errorMessage`, `returnMessage`, `studyListLink`.
## 15. SR (Structured Report) hydration
**Seeds:** `tests/SRHydration.spec.ts`, `tests/SRHydrationThenReload.spec.ts`
Same shape as SEG hydration but load via `loadSeriesByModality('SR')`.
## Feature area not listed above? (no existing seed)
Don't add a stub section here for an untested area. When your target isn't listed:
1. **Look for an indirect seed.** Grep `tests/` for the controls or flow you need
(`grep -rn "options-menu" tests/`, a related panel/dialog). An adjacent area's seed
usually still shows how the harness is driven.
2. **If genuinely uncovered, you're *creating* page objects.** Read
[page-objects.md](page-objects.md) → "When the control you need isn't covered yet", then:
- One page object per new control. Dialogs follow `DicomTagBrowserPageObject` (reached
through `DOMOverlayPageObject`); toolbar buttons get a getter on `MainToolbarPageObject`.
- Add a `data-cy` to any control that lacks one.
- **Verify the real effect, not a proxy** — drag and screenshot that the image moved via
`locator: viewportPageObject.grid`, not just that a button gained `data-active`.
**Example — user preferences / hotkeys (no seed today):** open the options menu and User
Preferences dialog (options-menu accessor on `DOMOverlayPageObject` + a `userPreferences`
dialog page object reached through it), set the hotkey, save, then trigger it and confirm the
viewport effect (activate Zoom, drag, screenshot the zoom). Add the Zoom button as a getter on
`MainToolbarPageObject`.
---
## Advanced patterns worth knowing
- **`subscribeToMeasurementAdded`** (`tests/FreehandROI.spec.ts`) — for async "was a measurement actually added?" assertions. Always `try { ... } finally { await measurementAdded.unsubscribe() }`.
- **`attemptAction`** (`tests/3DOnly.spec.ts`) — retry flaky setup (3D render, heavy layout change) without silencing real failures.
- **`addOHIFConfiguration`** (`tests/RTHydrationDisableConfirmation.spec.ts`) — pre-load config overrides before `visitStudy`.
- **`page.evaluate(() => window.services...)`** — used in several SEG/SR specs to set customizations or poke viewport state. Treat as an escape hatch, not a default.
- **`expect.toPass({ timeout })`** — wrap flaky assertions (common for jump-to-measurement tests where rendering settles asynchronously).
@@ -0,0 +1,100 @@
# OHIF Test Utility guide
> This file documents the **stable import rules and conventions** around `tests/utils/`. For the current list of exported helpers and their exact signatures, **read `tests/utils/index.ts` and the files it re-exports** — the barrel is always current; a static table here is not. Utilities get added, renamed, and refactored; the rules below change much more slowly.
## How to discover what's available
1. Open `tests/utils/index.ts`. Every symbol exported from the barrel is importable as `import { foo } from './utils'`.
2. If what you need isn't in the barrel, look in the rest of `tests/utils/` — there are a handful of specialized files (`keyboardUtils.ts`, `assertions.ts`, `download.ts`, …). These need the **deeper import path**; see the rule below.
3. For the actual signature, read the utility's `.ts` file. It's one short function per file in most cases.
4. To see a utility in context, grep `tests/` (`grep -rn visitStudy tests/`) — or open the seed spec for the relevant feature area ([patterns-by-feature.md](patterns-by-feature.md)). Existing specs are the most reliable signature reference because they co-evolve with the API.
Do not guess parameter shapes from memory, and do not treat this file as an API catalog — it intentionally isn't one.
---
## Stable rules
### Barrel vs. deep imports
Two import styles exist. They are not interchangeable.
```ts
// Barrel — anything re-exported from tests/utils/index.ts
import { test, expect, visitStudy, checkForScreenshot, screenShotPaths } from './utils';
// Deep — for files NOT re-exported by the barrel
import { press } from './utils/keyboardUtils';
import { assertNumberOfModalityLoadBadges } from './utils/assertions';
import { downloadAsString } from './utils/download';
```
If a symbol isn't in the barrel, `import { x } from './utils'` **compiles**, resolves `x` to `undefined`, and blows up at the first call site. Confirm by opening `tests/utils/index.ts` for your working revision. At the time of this writing, `press`, `downloadAsString`, and the `assert*` helpers live outside the barrel — but maintainers can move things in or out, so treat `index.ts` as the ground truth rather than this note.
### Never import `test` / `expect` from `@playwright/test`
```ts
// ✅ Correct — gets the fixture-extended runner
import { test, expect } from './utils';
// ❌ Wrong — compiles, but every page-object fixture is silently undefined
import { test, expect } from '@playwright/test';
```
If your test function's destructured arguments (like `viewportPageObject`) are `undefined`, this import is almost always why.
### `visitStudy` — 2000 ms is a convention, not a default
```ts
await visitStudy(page, studyInstanceUID, mode, 2000);
```
The function's own default delay is `0`. Nearly every spec passes `2000` to let the first render settle. The one consistent exception is `mode: 'tmtv'`, where the suite uniformly uses `10000` because PET fusion and SUV calculation add real wall-clock cost before the UI is interactive.
Notably, 3D layouts in `viewer` mode (3DOnly, 3DFourUp, 3DMain, 3DPrimary) and MPR also use `2000` — they're not "heavy" in the `visitStudy` sense. Their stabilization problem is solved at the interaction layer with `attemptAction(() => reduce3DViewportSize(page), 10, 100)`, not by a longer visit delay.
So: pick `10000` when the mode is `tmtv`, `2000` otherwise, and only ramp up if a specific test flakes on first-render assertions. The delay is a good first lever for "not visible" flakes, but it's not a universal upgrade.
### `checkForScreenshot` — use the object form, never screenshot the full app
**This is the direction going forward** The suite is being migrated off *full-app* screenshots — not off screenshots altogether. Screenshots are the correct and required tool whenever what you're verifying is canvas-only raster output with no DOM signal; don't avoid them there. Avoid them only where a faithful DOM/SVG signal exists (e.g. a vector overlay's color via `getSvgAttribute`) or where you'd be capturing the whole app. See SKILL.md → "Screenshot vs. DOM assertion — how to choose". Any new spec must follow the rules below, and any modification to an older spec should bring it in line when reasonable.
- **Object form** (required for all new specs): `checkForScreenshot({ page, screenshotPath, normalizedClip?, ... })`
- **Positional form**: legacy. It still appears in older specs because they haven't been migrated yet. **Do not treat existing positional-form usage as a pattern to copy** — those specs are the thing being moved away from. Do not introduce the positional form in new code.
**Hard rules for new screenshots:**
1. Use the object form.
2. Scope by passing a `locator` — `viewportPageObject.grid` for the whole grid, or a specific viewport pane locator. **Never screenshot the full app.** `normalizedClip` is computed *relative to the locator* (and defaults to the full page when no locator is given), so `{ x: 0, y: 0, width: 1, height: 1 }` alone does not scope anything — reserve `normalizedClip` for clipping to a sub-region of a locator. If you find yourself reaching for `fullPage: true`, stop and pass a locator instead.
Do not tune `maxDiffPixelRatio` or `threshold` to make a screenshot pass — those are intentionally rarely touched and not the right knob for flakes. If a baseline mismatches, regenerate it (`--update-snapshots`) after a human review of the diff, or fix the underlying instability. Check the current signature in `tests/utils/checkForScreenshot.ts` if something looks off.
### `screenShotPaths` — use keys, not raw strings
```ts
await checkForScreenshot({
page,
locator: viewportPageObject.grid,
screenshotPath: screenShotPaths.length.lengthDisplayedCorrectly,
});
```
The full tree of categories lives in `tests/utils/screenShotPaths.ts`. When you need a new baseline, **add the key there and reference it by name** rather than typing a raw path. A typo in a key becomes a compile error instead of a silent mismatch, and other tests become discoverable through the object.
### Object-param convention
Many OHIF test utilities take a single **object argument** rather than positional arguments — notably `press({ page, key, nTimes? })`, the `simulate*` helpers, and the `assert*` helpers. If a call looks like it should work but throws "cannot read properties of undefined," check whether you're passing positional args to something that expects `{ page, ... }`.
Read the one-line signature at the top of the utility's source file before calling it — it's faster than guessing, and it's always right.
---
## Utility shapes worth flagging
Most utilities are obvious once you read the source; these earn a mention:
- **`waitForViewportRenderCycle(page, options?)`** — preferred replacement for `page.waitForTimeout(...)` after any viewport-mutating action (hydration confirm, layout change, segmentation add, series load, etc.). Start it **before** the action, await it after — it captures the `needsRender → rendered` transition the action triggers. Use `waitForViewportsRendered(page)` (the second-half-only variant) when the render is already in flight before you can attach a watcher. Source: `tests/utils/waitForViewportsRendered.ts`. Seed: `tests/SEGHydrationFromMPR.spec.ts`. The "Wait for renders, don't sleep" section in [SKILL.md](../SKILL.md) covers the idiom in full.
- **`subscribeToMeasurementAdded(page)`** — returns `{ waitFired(timeout?), unsubscribe() }`. Use in freehand/livewire/spline specs to assert the event fired. Always wrap in `try { ... } finally { await sub.unsubscribe() }` so a failing assertion doesn't leak the listener across tests.
- **`attemptAction(action, attempts?, delay?)`** — retries a flaky async action without masking real failures. Mainly used to stabilize 3D scenes (`attemptAction(() => reduce3DViewportSize(page), 10, 100)`).
For everything else, the pattern is: find a spec that uses it (see [patterns-by-feature.md](patterns-by-feature.md)), copy the shape, adapt.
+129 -106
View File
@@ -1,26 +1,30 @@
version: 2.1
# ci: re-trigger pipeline
orbs:
codecov: codecov/codecov@1.0.5
cypress: cypress-io/cypress@3.4.2
defaults: &defaults
docker:
- image: cimg/node:20.19.0
- image: cimg/node:24.15.0
environment:
TERM: xterm
QUICK_BUILD: true
working_directory: ~/repo
commands:
install_bun:
install_pnpm:
steps:
- run:
name: Install Bun
name: Install pnpm
command: |
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
# The cimg/node global modules dir (/usr/local/lib/node_modules) is
# root-owned, so a plain `npm install -g` fails with EACCES. Install
# with sudo so the global pnpm binary lands in the shared prefix.
sudo npm install -g pnpm@11.5.2
echo 'export PATH="$(pnpm store path)/../.bin:$PATH"' >> $BASH_ENV
source $BASH_ENV
jobs:
@@ -28,16 +32,16 @@ jobs:
<<: *defaults
resource_class: large
steps:
- install_bun
- install_pnpm
- run: node --version
- checkout
- run:
name: Install Dependencies
command: bun install --no-save
command: pnpm install --frozen-lockfile
# RUN TESTS
- run:
name: 'JavaScript Test Suite'
command: bun run test:unit:ci
command: pnpm run test:unit:ci
# platform/app
- run:
name: 'VIEWER: Combine report output'
@@ -71,17 +75,17 @@ jobs:
steps:
# Checkout code and ALL Git Tags
- checkout
- install_bun
- install_pnpm
- run:
name: Install Dependencies
command: bun install --no-save
command: pnpm install --frozen-lockfile
# Build & Test
- run:
name: 'Perform the versioning before build'
command: bun ./version.mjs
command: node ./version.mjs
- run:
name: 'Build the OHIF Viewer'
command: bun run build
command: pnpm run build
no_output_timeout: 45m
- run:
name: 'Upload SourceMaps, Send Deploy Notification'
@@ -107,14 +111,55 @@ jobs:
<<: *defaults
resource_class: large
steps:
- install_bun
- install_pnpm
# Checkout code and ALL Git Tags
- checkout
- attach_workspace:
at: ~/repo
# SECURITY AUDIT - only when pnpm-lock.yaml has changed
- run:
name: 'Security Audit - High Risk Vulnerabilities'
command: |
git fetch origin master 2>/dev/null || true
BASE_REF=$(git merge-base HEAD origin/master 2>/dev/null)
if [[ -z "$BASE_REF" ]]; then
echo "Could not determine base ref (e.g. shallow clone or no origin/master), skipping security audit."
exit 0
fi
CHANGED_FILES=$(git diff --name-only origin/master...HEAD 2>/dev/null || echo "")
if ! echo "$CHANGED_FILES" | grep -qx 'pnpm-lock.yaml'; then
echo "pnpm-lock.yaml unchanged - skipping security audit."
exit 0
fi
echo "pnpm-lock.yaml changed - running pnpm audit for security vulnerabilities..."
echo "Checking for HIGH-RISK vulnerabilities..."
if pnpm 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: pnpm audit"
echo " 2. Review the vulnerability details"
echo " 3. Update affected packages to secure versions"
echo " 4. Test your changes"
echo " 5. Re-run: pnpm audit --audit-level high"
echo ""
echo "Full audit report:"
pnpm audit || 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
command: pnpm install
- run:
name: Avoid hosts unknown for github
command: |
@@ -125,28 +170,34 @@ jobs:
git config --global user.name "ohif-bot"
- run:
name: Authenticate with NPM registry
command: echo "//registry.npmjs.org/:_authToken=$NPM_TOKEN" > ~/repo/.npmrc
# Write the auth token to npm's per-user config (~/.npmrc), not the
# repo root. publish-package.mjs chdir's into each package dir to run
# `npm publish`, and npm only reads the project .npmrc from that dir
# plus the user .npmrc from $HOME -- it never sees ~/repo/.npmrc, so a
# root-level token yields ENEEDAUTH. Using ~/.npmrc also avoids
# clobbering the committed workspace-config .npmrc (node-linker=hoisted).
command: echo "//registry.npmjs.org/:_authToken=$NPM_TOKEN" > ~/.npmrc
- run:
name: build half of the packages (to avoid out of memory in circleci)
command: |
bun run build:package-all
pnpm run build:package-all
- run:
name: build the other half of the packages
command: |
bun run build:package-all-1
pnpm run build:package-all-1
NPM_PUBLISH:
<<: *defaults
resource_class: large
steps:
- install_bun
- install_pnpm
# Checkout code and ALL Git Tags
- checkout
- attach_workspace:
at: ~/repo
- run:
name: Install Dependencies
command: bun install --no-save
command: pnpm install --frozen-lockfile
- run:
name: Avoid hosts unknown for github
command: |
@@ -157,15 +208,21 @@ jobs:
git config --global user.name "ohif-bot"
- run:
name: Authenticate with NPM registry
command: echo "//registry.npmjs.org/:_authToken=$NPM_TOKEN" > ~/repo/.npmrc
# Write the auth token to npm's per-user config (~/.npmrc), not the
# repo root. publish-package.mjs chdir's into each package dir to run
# `npm publish`, and npm only reads the project .npmrc from that dir
# plus the user .npmrc from $HOME -- it never sees ~/repo/.npmrc, so a
# root-level token yields ENEEDAUTH. Using ~/.npmrc also avoids
# clobbering the committed workspace-config .npmrc (node-linker=hoisted).
command: echo "//registry.npmjs.org/:_authToken=$NPM_TOKEN" > ~/.npmrc
- run:
name: build half of the packages (to avoid out of memory in circleci)
command: |
bun run build:package-all
pnpm run build:package-all
- run:
name: build the other half of the packages
command: |
bun run build:package-all-1
pnpm run build:package-all-1
- run:
name: increase min time out
command: |
@@ -177,14 +234,20 @@ jobs:
- run:
name: publish package versions
command: |
bun ./publish-version.mjs
node ./publish-version.mjs
- run:
name: Again set the NPM registry (was deleted in the version script)
command: echo "//registry.npmjs.org/:_authToken=$NPM_TOKEN" > ~/repo/.npmrc
name: Re-assert the NPM auth token before publishing
# Write the auth token to npm's per-user config (~/.npmrc), not the
# repo root. publish-package.mjs chdir's into each package dir to run
# `npm publish`, and npm only reads the project .npmrc from that dir
# plus the user .npmrc from $HOME -- it never sees ~/repo/.npmrc, so a
# root-level token yields ENEEDAUTH. Using ~/.npmrc also avoids
# clobbering the committed workspace-config .npmrc (node-linker=hoisted).
command: echo "//registry.npmjs.org/:_authToken=$NPM_TOKEN" > ~/.npmrc
- run:
name: publish package dist
command: |
bun ./publish-package.mjs
node ./publish-package.mjs
- persist_to_workspace:
root: ~/repo
paths:
@@ -206,8 +269,11 @@ jobs:
if [[ ! -e version.txt ]]; then
exit 0
else
# Remove npm config
rm -f ./.npmrc
# Restore the committed .npmrc (pnpm workspace config such as
# node-linker=hoisted). NPM_PUBLISH overwrote it with a publish
# auth token, but the Dockerfile COPYs .npmrc for the in-image
# pnpm install, so it needs the config file present and token-free.
git checkout -- .npmrc
# Set our version number using vars
export IMAGE_VERSION=$(cat version.txt)
export IMAGE_VERSION_FULL=v$IMAGE_VERSION
@@ -240,8 +306,11 @@ jobs:
if [[ ! -e version.txt ]]; then
exit 0
else
# Remove npm config
rm -f ./.npmrc
# Restore the committed .npmrc (pnpm workspace config such as
# node-linker=hoisted). NPM_PUBLISH overwrote it with a publish
# auth token, but the Dockerfile COPYs .npmrc for the in-image
# pnpm install, so it needs the config file present and token-free.
git checkout -- .npmrc
# Set our version number using vars
export IMAGE_VERSION=$(cat version.txt)
export IMAGE_VERSION_FULL=v$IMAGE_VERSION
@@ -278,7 +347,11 @@ jobs:
exit 0
else
echo "Building and pushing Docker image from the master branch (beta releases)"
rm -f ./.npmrc
# Restore the committed .npmrc (pnpm workspace config such as
# node-linker=hoisted). NPM_PUBLISH overwrote it with a publish
# auth token, but the Dockerfile COPYs .npmrc for the in-image
# pnpm install, so it needs the config file present and token-free.
git checkout -- .npmrc
# Set our version number using vars
export IMAGE_VERSION=$(cat version.txt)
@@ -312,7 +385,11 @@ jobs:
exit 0
else
echo "Building and pushing ARM64 Docker image from the master branch (beta releases)"
rm -f ./.npmrc
# Restore the committed .npmrc (pnpm workspace config such as
# node-linker=hoisted). NPM_PUBLISH overwrote it with a publish
# auth token, but the Dockerfile COPYs .npmrc for the in-image
# pnpm install, so it needs the config file present and token-free.
git checkout -- .npmrc
# Set our version number using vars
export IMAGE_VERSION=$(cat version.txt)
export IMAGE_VERSION_FULL=v$IMAGE_VERSION
@@ -330,12 +407,21 @@ jobs:
resource_class: large
parallelism: 8
steps:
- install_bun
- install_pnpm
- run:
name: Install System Dependencies
command: |
sudo apt-get update
sudo apt-get install -y xvfb libgtk2.0-0 libgtk-3-0 libgbm-dev libnotify-dev libgconf-2-4 libnss3 libxss1 libasound2 libxtst6
# CircleCI's base image registers third-party apt sources (git-lfs via
# packagecloud, git-core PPA via launchpad) that periodically hang
# `apt-get update`. The Cypress libs below all come from the Ubuntu
# archive, so drop those sources and bound the update with timeouts +
# retries so a slow mirror can't stall the job indefinitely.
sudo rm -f /etc/apt/sources.list.d/*git* || true
sudo apt-get update \
-o Acquire::Retries=3 \
-o Acquire::http::Timeout=20 \
-o Acquire::https::Timeout=20
sudo apt-get install -y xvfb libgtk2.0-0 libgtk-3-0 libgbm-dev libnotify-dev libnss3 libxss1 libasound2t64 libxtst6
- run:
name: Start Xvfb
command: Xvfb :99 -screen 0 1920x1080x24 &
@@ -344,74 +430,15 @@ jobs:
name: Export Display Variable
command: export DISPLAY=:99
- cypress/install:
install-command: yarn install --frozen-lockfile --no-save
install-command: pnpm install
- cypress/run-tests:
# CI runs headless under Xvfb with no GPU, so Electron uses software
# WebGL. Newer Chromium deprecated that implicit fallback (canvas
# rendering degrades and races with element clicks). ELECTRON_EXTRA_LAUNCH_ARGS
# is the reliable way to pass the opt-in flag to Cypress's Electron browser.
cypress-command: |
npx wait-on@latest http://localhost:3000 && cd platform/app && npx cypress run --record --parallel
start-command: yarn run test:data && yarn run test:e2e:serve
SECURITY_AUDIT:
<<: *defaults
resource_class: large
steps:
- install_bun
- checkout
- run:
name: 'Security Audit - High Risk Vulnerabilities'
command: |
git fetch origin master 2>/dev/null || true
BASE_REF=$(git merge-base HEAD origin/master 2>/dev/null)
if [[ -z "$BASE_REF" ]]; then
echo "Could not determine base ref (e.g. shallow clone or no origin/master), skipping security audit."
exit 0
fi
CHANGED_FILES=$(git diff --name-only origin/master...HEAD 2>/dev/null || echo "")
if ! echo "$CHANGED_FILES" | grep -qx 'bun.lock'; then
echo "⏭️ bun.lock unchanged - skipping security audit."
exit 0
fi
echo "🔍 bun.lock changed - running bun audit for security vulnerabilities..."
echo "Checking for HIGH-RISK vulnerabilities..."
# Define ignored vulnerabilities with comments
IGNORED_VULNS=(
"GHSA-3ppc-4f35-3m26" # CVE-2026-26996 - minimatch via itk-wasm and glob is safe because it does NOT use the CLI
# CVE-2026-26996 - minimatch via other packages are strictly for building and CI/CD purposes; no user supplied expressions are passed to minimatch
"GHSA-7r86-cg39-jmmj" # CVE-2026-27903 - minimatch same as above
"GHSA-23c5-xmqv-rm74" # CVE-2026-27904 - minimatch same as above
"GHSA-c2c7-rcm5-vvqj" # CVE-2026-33671 - picomatch is generally used for development and CI/CD purposes
)
# Build ignore flags
IGNORE_FLAGS=""
for vuln in "${IGNORED_VULNS[@]}"; do
IGNORE_FLAGS="$IGNORE_FLAGS --ignore=$vuln"
done
if bun audit $IGNORE_FLAGS --audit-level high; then
echo "✅ No high-risk vulnerabilities found"
echo "🎉 Security audit passed!"
exit 0
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 $IGNORE_FLAGS --audit-level low || true
echo ""
echo "❌ This build cannot proceed until high-risk vulnerabilities are resolved."
exit 1
fi
npx wait-on@latest http://localhost:3000 && cd platform/app && ELECTRON_EXTRA_LAUNCH_ARGS="--enable-unsafe-swiftshader" npx cypress run --record --parallel
start-command: pnpm run test:data && pnpm run test:e2e:serve
DOCKER_MULTIARCH_MANIFEST:
<<: *defaults
@@ -502,10 +529,6 @@ workflows:
- CYPRESS:
name: 'Cypress Tests'
context: cypress
- SECURITY_AUDIT:
filters:
branches:
ignore: master
# viewer-dev.ohif.org
DEPLOY_MASTER:
+1 -11
View File
@@ -1,18 +1,8 @@
version: 2
enable-beta-ecosystems: true
updates:
- package-ecosystem: 'bun'
# Disable all pull requests for bun version updates.
open-pull-requests-limit: 0
directory: '/'
schedule:
interval: 'daily'
labels: ['dependencies']
commit-message:
prefix: 'chore'
include: 'scope'
- package-ecosystem: 'npm'
# Disable all pull requests for npm version updates.
# Disable all pull requests for npm/pnpm version updates.
open-pull-requests-limit: 0
directory: '/'
schedule:
+44 -29
View File
@@ -21,16 +21,29 @@ jobs:
contents: read
pull-requests: read
steps:
- uses: actions/checkout@v4
- uses: oven-sh/setup-bun@v2
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
with:
bun-version: 1.2.23
- uses: actions/setup-node@v4
persist-credentials: false
- uses: pnpm/action-setup@0e279bb959325dab635dd2c09392533439d90093 # v6.0.8
- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
with:
node-version: 20 # Or your desired Node version
node-version: 24.15.0
cache: pnpm
- name: Install root dependencies
run: bun install --frozen-lockfile
- name: Configure git for private repos
run: git config --global url."https://${GITHUB_TOKEN}:x-oauth-basic@github.com/".insteadOf ssh://git@github.com/
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Install dependencies
# Internal @ohif/* deps are workspace:* in the committed manifests and the
# lockfile records them as links, so pnpm-lock.yaml stays in sync across
# version bumps and a frozen install passes. Frozen is the right choice
# for a deploy job: it fails fast if a PR ever changed a real dependency
# without committing the lockfile update, instead of silently reconciling.
# (Exact versions appear only in published tarballs, where pnpm publish
# rewrites workspace:* at pack time -- not publish-version.mjs.)
run: pnpm install --frozen-lockfile
# Removed Playwright tests and coverage generation steps
@@ -44,47 +57,44 @@ jobs:
# Note: This relies on the merge commit being directly pushed to main
PR_DATA=$(gh pr list --state merged --search "$MERGE_COMMIT_SHA" --json number,headRefOid --jq '.[0]')
if [ -z "$PR_DATA" ]; then
echo "Could not find merged PR for commit $MERGE_COMMIT_SHA."
# Decide how to handle - fail, or maybe generate coverage now?
# For now, let's fail.
exit 1
echo "::warning::Could not find merged PR for commit $MERGE_COMMIT_SHA. Skipping coverage embedding."
echo "coverage_found=false" >> $GITHUB_OUTPUT
exit 0
fi
PR_HEAD_SHA=$(echo "$PR_DATA" | jq -r '.headRefOid')
PR_NUMBER=$(echo "$PR_DATA" | jq -r '.number')
echo "Found PR Number: $PR_NUMBER"
echo "Found PR Head SHA: $PR_HEAD_SHA"
# Find the latest workflow run ID for the playwright workflow on the PR head commit
# Uses the workflow file name 'playwright.yml'
# Remove the --status success flag to find any run
RUN_ID=$(gh run list --workflow playwright.yml --commit "$PR_HEAD_SHA" --event pull_request --json databaseId --jq '.[0].databaseId')
# Find the latest *successful* playwright.yml run for the PR head commit so we
# never embed coverage from a failed/incomplete run.
RUN_ID=$(gh run list --workflow playwright.yml --commit "$PR_HEAD_SHA" --event pull_request --status success --json databaseId --jq '.[0].databaseId')
if [ -z "$RUN_ID" ]; then
echo "Could not find any 'playwright.yml' run for PR $PR_NUMBER (Head SHA: $PR_HEAD_SHA)."
# Decide how to handle - maybe try finding the artifact from the merge commit run if that exists?
# For now, let's fail.
exit 1
echo "::warning::Could not find a successful 'playwright.yml' run for PR $PR_NUMBER (Head SHA: $PR_HEAD_SHA). Skipping coverage embedding."
echo "coverage_found=false" >> $GITHUB_OUTPUT
exit 0
fi
echo "Found Run ID: $RUN_ID"
echo "run_id=$RUN_ID" >> $GITHUB_OUTPUT
echo "coverage_found=true" >> $GITHUB_OUTPUT
- name: Download coverage artifact from PR run
if: steps.find_pr_run.outputs.coverage_found == 'true'
env:
GH_TOKEN: ${{ github.token }}
run: |
mkdir -p ./coverage-artifact
gh run download ${{ steps.find_pr_run.outputs.run_id }} -n coverage-report-pr --dir ./coverage-artifact
# Check if download was successful (e.g., check if files exist)
if [ ! -f ./coverage-artifact/base.css ]; then
echo "Failed to download or find expected files in artifact 'coverage-report-pr' from run ${{ steps.find_pr_run.outputs.run_id }}."
# Verify the artifact contains an HTML coverage report rather than checking a single asset
if [ -z "$(ls -A ./coverage-artifact)" ] || ! ls ./coverage-artifact/*.html >/dev/null 2>&1; then
echo "Failed to download or find an HTML coverage report in artifact 'coverage-report-pr' from run ${{ steps.find_pr_run.outputs.run_id }}."
exit 1
fi
echo "Artifact downloaded successfully."
- name: Install docs dependencies
run: cd platform/docs && bun install
- name: Copy coverage to docs static directory
if: steps.find_pr_run.outputs.coverage_found == 'true'
run: |
# Copy files from the downloaded artifact directory
mkdir -p platform/docs/static/coverage
@@ -99,12 +109,17 @@ jobs:
cp ./coverage-artifact/sorter.js platform/docs/static/
- name: Build docs
run: cd platform/docs && bun run build
run: pnpm --filter ohif-docs run build
- name: Deploy to Netlify
run: |
cd platform/docs
npx netlify-cli deploy --dir=./build --prod
# The docs are already built by the "Build docs" step above, so pass
# --no-build: `netlify deploy` runs the build command by default, which
# would otherwise pick up the repo-root netlify.toml (configured for the
# main viewer app) and build the wrong project. --dir is resolved
# relative to the repo root (the netlify.toml base), not the cwd, so it
# must be the full path to the prebuilt docs output. --filter selects
# the ohif-docs package so Netlify CLI does not abort on monorepo detection.
run: npx netlify-cli deploy --filter ohif-docs --dir=platform/docs/build --prod --no-build
env:
NETLIFY_AUTH_TOKEN: ${{ secrets.NETLIFY_AUTH_TOKEN }}
NETLIFY_SITE_ID: ${{ secrets.NETLIFY_SITE_ID }}
+5 -3
View File
@@ -55,7 +55,9 @@ jobs:
# your codebase is analyzed, see https://docs.github.com/en/code-security/code-scanning/creating-an-advanced-setup-for-code-scanning/codeql-code-scanning-for-compiled-languages
steps:
- name: Checkout repository
uses: actions/checkout@v4
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
with:
persist-credentials: false
# Add any setup steps before running the `github/codeql-action/init` action.
# This includes steps like installing compilers or runtimes (`actions/setup-node`
@@ -65,7 +67,7 @@ jobs:
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
uses: github/codeql-action/init@v3
uses: github/codeql-action/init@dd903d2e4f5405488e5ef1422510ee31c8b32357 # v3.36.2
with:
languages: ${{ matrix.language }}
build-mode: ${{ matrix.build-mode }}
@@ -93,6 +95,6 @@ jobs:
exit 1
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v3
uses: github/codeql-action/analyze@dd903d2e4f5405488e5ef1422510ee31c8b32357 # v3.36.2
with:
category: "/language:${{matrix.language}}"
+42 -20
View File
@@ -23,21 +23,35 @@ concurrency:
jobs:
playwright-tests:
timeout-minutes: 120
runs-on: self-hosted
runs-on: [self-hosted, nashua]
strategy:
fail-fast: false
matrix:
node-version: [20]
node-version: [24.15.0]
steps:
- uses: actions/checkout@v4
- uses: oven-sh/setup-bun@v2
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
with:
bun-version: 1.2.23
- uses: actions/setup-node@v4
persist-credentials: false
- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
with:
node-version: ${{ matrix.node-version }}
- name: Install Yarn
run: npm install -g yarn@1.22.22
# No `cache: pnpm`: this is a self-hosted runner with a persistent
# pnpm store on local disk. The Actions cache doesn't preserve the
# pnpm self-install's links faithfully, dropping pnpm/dist/worker.js
# and breaking `pnpm install` with MODULE_NOT_FOUND.
# Install pnpm via Corepack instead of pnpm/action-setup. The action's
# self-installer is a JS action run under the runner's bundled
# externals/node{version} and derives the `npm` path from that runtime — which
# on this self-hosted runner is corrupted (Cannot find module
# '../lib/cli.js'), so it dies regardless of `standalone:true`. Corepack
# ships inside the Node that setup-node just installed, reads the pinned
# `packageManager` (pnpm@11.5.2) from package.json, and fetches pnpm via
# Node's own https — it never invokes the npm CLI.
- name: Enable Corepack (pnpm)
shell: bash
run: |
corepack enable
corepack prepare --activate
# ── CS3D integration: detect label and ref type ──────────────────────
- name: Check for CS3D integration label
@@ -87,11 +101,11 @@ jobs:
- name: Install & Build CS3D
if: steps.cs3d-check.outputs.enabled == 'true' && steps.cs3d-ref.outputs.type == 'branch'
working-directory: libs/@cornerstonejs
run: bun install --frozen-lockfile && bun run build:esm
run: pnpm install --frozen-lockfile && pnpm run build:esm
# ── Common: install OHIF dependencies ───────────────────────────────
- name: Install dependencies
run: bun install --frozen-lockfile
run: pnpm install --frozen-lockfile
# ── CS3D branch path: link packages after OHIF install ──────────────
- name: Link CS3D packages
@@ -104,17 +118,22 @@ jobs:
if: steps.cs3d-check.outputs.enabled == 'true' && steps.cs3d-ref.outputs.type == 'version'
run: |
node .scripts/cs3d-set-version.mjs "${CS3D_VERSION}"
bun install --config=./bunfig.update-lockfile.toml
pnpm install --no-frozen-lockfile
env:
CS3D_VERSION: ${{ steps.cs3d-ref.outputs.version }}
# ── Common: run tests ───────────────────────────────────────────────
- name: Install Playwright browsers
run: npx playwright install
# Only chromium is used (see playwright.config.ts and tests/globalSetup.ts);
# firefox/webkit projects are commented out. Scoping the install to chromium
# avoids downloading + dep-validating browsers we never launch (the WebKit
# validation is what surfaces the "missing libwoff1/libflite1/..." error on
# hosts without those libs).
run: npx playwright install chromium
- name: Run Playwright tests
run: |
export NODE_OPTIONS="--max_old_space_size=10192"
bun run test:e2e:coverage
bash .scripts/ci/with-nashua-lock.sh pnpm run test:e2e:coverage
# ── Common: collect test results and coverage ───────────────────────
- name: Create directory of test results
@@ -125,17 +144,17 @@ jobs:
cp -r ./tests/playwright-report packaged-test-results/ || true
- name: Upload directory of test results artifact
if: ${{ !cancelled() }}
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: playwright-results
path: packaged-test-results/
retention-days: 5
- name: create the coverage report
run: |
bun nyc report --reporter=lcov --reporter=text
pnpm exec nyc report --reporter=lcov --reporter=text
- name: Upload the coverage report to GitHub Actions Artifacts
if: ${{ !cancelled() }}
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: coverage-report-pr
path: coverage
@@ -152,12 +171,13 @@ jobs:
fi
node .scripts/log-build-context.mjs
env:
BUILD_TYPE: ${{ steps.cs3d-ref.outputs.type == 'branch' && 'ohif-downstream' || 'ohif-upstream' }}
BUILD_TYPE:
${{ steps.cs3d-ref.outputs.type == 'branch' && 'ohif-downstream' || 'ohif-upstream' }}
CS3D_REF_TYPE: ${{ steps.cs3d-ref.outputs.type }}
- name: Build OHIF viewer (CS3D preview)
if: steps.cs3d-check.outputs.enabled == 'true'
run: bun run build:ci
run: pnpm run build:ci
- name: Deploy CS3D preview to Netlify
if: steps.cs3d-check.outputs.enabled == 'true'
@@ -194,11 +214,13 @@ jobs:
# ── Separate job: block merge when using a CS3D branch ─────────────
cs3d-branch-merge-guard:
name: "CS3D Branch Merge Guard"
name: 'CS3D Branch Merge Guard'
runs-on: ubuntu-latest
timeout-minutes: 5
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
with:
persist-credentials: false
- name: Check for CS3D branch usage
run: bash .scripts/ci/cs3d-branch-merge-guard.sh
env:
+1 -2
View File
@@ -1,6 +1,7 @@
# Packages
node_modules
.cursor/
.claude/
.nyc_output/
# Output
build
@@ -60,8 +61,6 @@ tests/playwright-report/
/blob-report/
/playwright/.cache/
**/.claude/settings.local.json
# cornerstone3D local linking
libs/
+4 -10
View File
@@ -6,27 +6,21 @@ cd "$(dirname "$0")"
cd .. # Up to project root
# Helpful to verify which versions we're using
echo 'My yarn version is... '
echo 'My pnpm version is... '
yarn -v
pnpm -v
node -v
# Build && Move PWA Output
yarn run build:ci
pnpm run build:ci
mkdir -p ./.netlify/www/pwa
mv platform/app/dist/* .netlify/www/pwa -v
echo 'Web application built and copied'
# Build && Move Docusaurus Output (for the docs themselves)
cd platform/docs
yarn install --frozen-lockfile
yarn run build
cd ../..
pnpm --filter ohif-docs run build
mkdir -p ./.netlify/www/docs
mv platform/docs/build/* .netlify/www/docs -v
echo 'Docs built (docusaurus) and copied'
# Cache all of the node_module dependencies in
# extensions, modules, and platform packages
yarn run lerna:cache
echo 'Nothing left to see here. Go home, folks.'
+2 -3
View File
@@ -2,9 +2,8 @@
"name": "root",
"private": true,
"engines": {
"node": ">=14",
"npm": ">=6",
"yarn": ">=1.16.0"
"node": ">=24",
"pnpm": ">=11"
},
"scripts": {
"deploy": "netlify deploy --prod --dir ./../platform/app/dist"
+1 -1
View File
@@ -1 +1 @@
20.9.0
24.15.0
+5
View File
@@ -0,0 +1,5 @@
node-linker=hoisted
strict-peer-dependencies=false
link-workspace-packages=true
prefer-workspace-packages=true
manage-package-manager-versions=false
+54
View File
@@ -0,0 +1,54 @@
#!/usr/bin/env bash
#
# Run a command while holding the nashua Playwright mutex.
#
# Only one Playwright run may execute at a time on the shared nashua self-hosted
# runner — across THIS repo (OHIF) AND the cornerstone3D repo. GitHub's
# `concurrency:` is scoped per-repository and cannot coordinate across the two
# orgs, so the mutex lives on the box's filesystem.
#
# This is OHIF's own copy (the script can't be shared across separate repos). It
# MUST use the SAME lock path as cornerstone3D's copy (NASHUA_LOCK_FILE default
# below) — if the paths differ, the mutex silently does nothing.
#
# flock holds the lock via fd 9 and it releases automatically when the wrapped
# command exits — including on job cancel / timeout / SIGKILL — so there are no
# stale locks to clean up.
#
# Usage: .scripts/ci/with-nashua-lock.sh <command> [args...]
# Strict mode: -e abort on any unhandled command failure, -u treat use of an
# unset variable as an error, -o pipefail make a pipeline fail if ANY stage
# fails (not just the last). Catches mistakes early instead of pressing on.
set -euo pipefail
LOCK="${NASHUA_LOCK_FILE:-/var/tmp/nashua-playwright.lock}" # MUST match cornerstone3D's copy
LOCK_WAIT="${NASHUA_LOCK_WAIT:-5400}" # max seconds to wait
# Guard: a command to wrap is required. With no arguments there is nothing to
# run, so print usage and exit instead of silently grabbing and releasing the
# lock for no reason (which would mask a mis-wired workflow step).
if [ "$#" -eq 0 ]; then
echo "usage: $0 <command> [args...]" >&2
exit 2
fi
# Open the lock file on fd 9 (read-write, create if missing, never truncate).
exec 9<>"$LOCK" || { echo "::error::cannot open lock file $LOCK"; exit 1; }
# Try instantly; if busy, report who holds it, then block up to LOCK_WAIT.
if ! flock -n 9; then
echo "nashua Playwright runner busy — held by: $(cat "${LOCK}.info" 2>/dev/null || echo unknown). Waiting up to ${LOCK_WAIT}s…"
if ! flock -w "$LOCK_WAIT" 9; then
echo "::error::Timed out after ${LOCK_WAIT}s waiting for the nashua Playwright lock"
exit 1
fi
fi
# Record the current holder for other jobs' "held by" message (best-effort).
echo "${GITHUB_REPOSITORY:-local}#${GITHUB_RUN_ID:-0} @ $(date -u +%FT%TZ 2>/dev/null || true)" > "${LOCK}.info" 2>/dev/null || true
echo "✅ Acquired nashua Playwright lock ($LOCK); running: $*"
# Replace the shell with the command. fd 9 is inherited (not close-on-exec), so
# the lock is held for the whole command and released when it exits.
exec "$@"
+2 -2
View File
@@ -6,9 +6,9 @@ PROJECT=$1
if [ -z "$PROJECT" ]
then
# Default
npx lerna run dev:viewer
pnpm --filter @ohif/app run dev:viewer
else
eval "npx lerna run dev:$PROJECT"
pnpm --filter @ohif/app run "dev:$PROJECT"
fi
read -p 'Press [Enter] key to continue...'
+51
View File
@@ -0,0 +1,51 @@
const path = require('path');
// Shared module-resolution rules (alias + module search paths) used by BOTH
// build pipelines so they cannot drift apart:
// - the webpack/rspack build: webpack.base.js -> webpack.pwa.js, plus every
// per-package webpack.prod.js / webpack.dev.js that merges webpack.base.js
// - the rsbuild build: rsbuild.config.ts (dev:fast)
//
// Plugin aliases (writePluginImportsFile.getPluginResolveAliases) are NOT
// included here: they depend on pluginConfig.json and are merged in separately
// by each config.
//
// All paths are anchored to this file's location (the repo-root `.webpack/`
// directory), so the values are identical regardless of which package's build
// is doing the resolving.
const alias = {
// Some extensions import app-level utilities (e.g. history,
// preserveQueryParameters) from '@ohif/app'. pnpm's isolated layout does not
// expose the top-level app package to those extensions, and adding it as a
// workspace dependency would create an app<->default cycle, so we resolve the
// bare specifier to the app source here ($ = exact match).
'@ohif/app$': path.resolve(__dirname, '../platform/app/src/index.js'),
'@': path.resolve(__dirname, '../platform/app/src'),
'@components': path.resolve(__dirname, '../platform/app/src/components'),
'@hooks': path.resolve(__dirname, '../platform/app/src/hooks'),
'@routes': path.resolve(__dirname, '../platform/app/src/routes'),
'@state': path.resolve(__dirname, '../platform/app/src/state'),
};
// Directories to search when resolving modules. The leading bare 'node_modules'
// preserves the default importer-relative walk-up, which pnpm's isolated layout
// requires so that transitive deps (e.g. react-remove-scroll -> tslib 2.x)
// resolve to the sibling copy inside .pnpm/<pkg>/node_modules rather than a
// hoisted older version.
const moduleSearchPaths = [
'node_modules',
path.resolve(__dirname, '../node_modules'),
path.resolve(__dirname, '../../../node_modules'),
path.resolve(__dirname, '../platform/app/node_modules'),
path.resolve(__dirname, '../platform/ui/node_modules'),
];
// Build the resolve.modules array for a build. `srcDir` is the building
// package's source root; it is appended last (matching the previous inline
// behavior in webpack.base.js). Pass nothing to get just the shared paths.
function getModules(srcDir) {
return srcDir ? [...moduleSearchPaths, srcDir] : [...moduleSearchPaths];
}
module.exports = { alias, moduleSearchPaths, getModules };
+2 -2
View File
@@ -2,14 +2,14 @@ const autoprefixer = require('autoprefixer');
const path = require('path');
const tailwindcss = require('tailwindcss');
const tailwindConfigPath = path.resolve('../../platform/app/tailwind.config.js');
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
const rspack = require('@rspack/core');
const devMode = process.env.NODE_ENV !== 'production';
const cssToJavaScript = {
test: /\.css$/,
use: [
//'style-loader',
devMode ? 'style-loader' : MiniCssExtractPlugin.loader,
devMode ? 'style-loader' : rspack.CssExtractRspackPlugin.loader,
{ loader: 'css-loader', options: { importLoaders: 1 } },
{
loader: 'postcss-loader',
+2
View File
@@ -4,6 +4,8 @@ function transpileJavaScript(mode) {
const exclude =
mode === 'production'
? excludeNodeModulesExcept([
// Workspace packages (needed for pnpm shamefully-hoist where they resolve through node_modules)
'@ohif',
// 'dicomweb-client',
// https://github.com/react-dnd/react-dnd/blob/master/babel.config.js
'react-dnd',
+38 -33
View File
@@ -4,11 +4,10 @@ const dotenv = require('dotenv');
const path = require('path');
const fs = require('fs');
const webpack = require('webpack');
const webpack = require('@rspack/core');
// ~~ PLUGINS
// const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;
const TerserJSPlugin = require('terser-webpack-plugin');
// ~~ PackageJSON
// const vtkRules = require('vtk.js/Utilities/config/dependency.js').webpack.core
@@ -18,9 +17,15 @@ const TerserJSPlugin = require('terser-webpack-plugin');
const loadWebWorkersRule = require('./rules/loadWebWorkers.js');
const transpileJavaScriptRule = require('./rules/transpileJavaScript.js');
const cssToJavaScript = require('./rules/cssToJavaScript.js');
// Module-resolution rules shared with the rsbuild build (see rsbuild.config.ts).
const resolveConfig = require('./resolveConfig');
// Only uncomment for old v2 stylus
// const stylusToJavaScript = require('./rules/stylusToJavaScript.js');
const ReactRefreshWebpackPlugin = require('@pmmmwh/react-refresh-webpack-plugin');
let ReactRefreshWebpackPlugin;
try {
const mod = require('@rspack/plugin-react-refresh');
ReactRefreshWebpackPlugin = mod.ReactRefreshRspackPlugin || mod.default || mod;
} catch { ReactRefreshWebpackPlugin = null; }
// ~~ ENV VARS
const NODE_ENV = process.env.NODE_ENV;
@@ -66,6 +71,12 @@ module.exports = (env, argv, { SRC_DIR, ENTRY }) => {
const config = {
mode: isProdBuild ? 'production' : 'development',
devtool: isProdBuild ? 'source-map' : 'cheap-module-source-map',
// `rspack serve` (@rspack/cli) auto-enables lazyCompilation for web-only
// apps unless the config defines it explicitly. The on-demand proxy chunks
// it produces fail to load in the headless cypress/electron e2e run
// (ChunkLoadError on cornerstone vendor chunks), so disable it here to match
// the rsbuild build (see rsbuild.config.ts `dev.lazyCompilation: false`).
lazyCompilation: false,
entry: ENTRY,
optimization: {
// splitChunks: {
@@ -92,9 +103,7 @@ module.exports = (env, argv, { SRC_DIR, ENTRY }) => {
children: false,
warnings: true,
},
cache: {
type: 'filesystem',
},
cache: isProdBuild ? false : { type: 'filesystem' },
module: {
noParse: [/(dicomicc)/],
rules: [
@@ -168,9 +177,6 @@ module.exports = (env, argv, { SRC_DIR, ENTRY }) => {
},
},
cssToJavaScript,
// Note: Only uncomment the following if you are using the old style of stylus in v2
// Also you need to uncomment this platform/app/.webpack/rules/extractStyleChunks.js
// stylusToJavaScript,
{
test: /\.wasm/,
type: 'asset/resource',
@@ -194,27 +200,16 @@ module.exports = (env, argv, { SRC_DIR, ENTRY }) => {
},
resolve: {
mainFields: ['module', 'browser', 'main'],
// alias and modules are shared with the rsbuild build via ./resolveConfig
// so the two pipelines resolve identically.
alias: {
// Viewer project
'@': path.resolve(__dirname, '../platform/app/src'),
'@components': path.resolve(__dirname, '../platform/app/src/components'),
'@hooks': path.resolve(__dirname, '../platform/app/src/hooks'),
'@routes': path.resolve(__dirname, '../platform/app/src/routes'),
'@state': path.resolve(__dirname, '../platform/app/src/state'),
...resolveConfig.alias,
},
// Which directories to search when resolving modules
modules: [
// Modules specific to this package
path.resolve(__dirname, '../node_modules'),
// Hoisted Yarn Workspace Modules
path.resolve(__dirname, '../../../node_modules'),
path.resolve(__dirname, '../platform/app/node_modules'),
path.resolve(__dirname, '../platform/ui/node_modules'),
SRC_DIR,
],
modules: resolveConfig.getModules(SRC_DIR),
// Attempt to resolve these extensions in order.
extensions: ['.js', '.jsx', '.json', '.ts', '.tsx', '*'],
// symlinked resources are resolved to their real path, not their symlinked location
// Workspace packages use relative imports between sibling packages.
// Resolve symlinks to keep those imports anchored at the real package paths.
symlinks: true,
fallback: {
fs: false,
@@ -223,24 +218,34 @@ module.exports = (env, argv, { SRC_DIR, ENTRY }) => {
buffer: require.resolve('buffer'),
},
},
node: {
// Leave __filename / __dirname references alone. The previous 'mock'
// value triggers an rspack warning whenever bundled deps reference
// __dirname (e.g. Emscripten-compiled cornerstone codecs). Those refs
// sit inside `if (ENVIRONMENT_IS_NODE)` branches that never execute in
// the browser, so leaving them un-substituted is harmless at runtime.
__filename: false,
__dirname: false,
},
plugins: [
new webpack.DefinePlugin(defineValues),
new webpack.ProvidePlugin({
Buffer: ['buffer', 'Buffer'],
}),
...(isProdBuild ? [] : [new ReactRefreshWebpackPlugin({ overlay: false })]),
new webpack.IgnorePlugin({
resourceRegExp: /^(fs|path)$/,
contextRegExp: /@cornerstonejs[\\/]codec-/,
}),
...(isProdBuild || IS_COVERAGE || !ReactRefreshWebpackPlugin
? []
: [new ReactRefreshWebpackPlugin({ overlay: false })]),
// Uncomment to generate bundle analyzer
// new BundleAnalyzerPlugin(),
],
};
if (isProdBuild) {
config.optimization.minimizer = [
new TerserJSPlugin({
parallel: true,
terserOptions: {},
}),
];
config.optimization.minimizer = [new webpack.SwcJsMinimizerRspackPlugin()];
}
if (isQuickBuild) {
+4
View File
@@ -176,6 +176,10 @@ Always prioritrize pub sub, by calling a services subscribe over useEffects as i
### Never modify core architecture
Do not modify the core and always find a way to implement the solution via the extensions and modes, only modify core as a last resort if all other fail or there's an architectural constraint.
## Skills
The `ohif-test-agent` skill (Playwright E2E test guidance) lives at `.agents/skills/ohif-test-agent/`.
## Configuration
### Plugin Configuration
+90
View File
@@ -3,6 +3,96 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
# [3.13.0-beta.89](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.88...v3.13.0-beta.89) (2026-06-10)
### Bug Fixes
* ohif tests to run with cornerstone 3d 5.0 ([#6043](https://github.com/OHIF/Viewers/issues/6043)) ([6dd150d](https://github.com/OHIF/Viewers/commit/6dd150d401ad73d60632a23378b7dfd4b5142690))
# [3.13.0-beta.88](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.87...v3.13.0-beta.88) (2026-06-05)
### Bug Fixes
* **security:** Patch axios vulnerabilities. ([#6063](https://github.com/OHIF/Viewers/issues/6063)) ([99c6154](https://github.com/OHIF/Viewers/commit/99c615434309b113aed9ac9a977419eeae0528cb))
# [3.13.0-beta.87](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.86...v3.13.0-beta.87) (2026-05-29)
**Note:** Version bump only for package ohif-monorepo-root
# [3.13.0-beta.86](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.85...v3.13.0-beta.86) (2026-05-28)
**Note:** Version bump only for package ohif-monorepo-root
# [3.13.0-beta.85](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.84...v3.13.0-beta.85) (2026-05-28)
### Features
* **WorkList:** New Study List (WorkList based on ui-next); old study list renamed to LegacyWorklist ([#6005](https://github.com/OHIF/Viewers/issues/6005)) ([daae4c1](https://github.com/OHIF/Viewers/commit/daae4c144e7cb80bbee7c05d9fceef8b332ed2a0))
# [3.13.0-beta.84](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.83...v3.13.0-beta.84) (2026-05-27)
### Bug Fixes
* **security:** Add tmp as a resolution to fix security vulnerability. ([#6044](https://github.com/OHIF/Viewers/issues/6044)) ([bdcafc1](https://github.com/OHIF/Viewers/commit/bdcafc12de5e4f73a1a96cab9c0830d49315c044))
# [3.13.0-beta.83](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.82...v3.13.0-beta.83) (2026-05-26)
### Bug Fixes
* **segmentation:** restore navigation for duplicated contour segments ([#6038](https://github.com/OHIF/Viewers/issues/6038)) ([0c1ca6b](https://github.com/OHIF/Viewers/commit/0c1ca6b04a6c1aef478c7a7d9f360f9b9c50f188))
# [3.13.0-beta.82](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.81...v3.13.0-beta.82) (2026-05-22)
**Note:** Version bump only for package ohif-monorepo-root
# [3.13.0-beta.81](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.80...v3.13.0-beta.81) (2026-05-21)
### Features
* **tests:** Add tests for duplicating contour segments ([#6002](https://github.com/OHIF/Viewers/issues/6002)) ([2258c79](https://github.com/OHIF/Viewers/commit/2258c7957a18ed0f1a75d2bd5d379006797b2882))
# [3.13.0-beta.80](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.79...v3.13.0-beta.80) (2026-05-20)
**Note:** Version bump only for package ohif-monorepo-root
+19 -35
View File
@@ -20,49 +20,33 @@
#
# syntax=docker/dockerfile:1.7-labs
# This dockerfile is used to publish the `ohif/app` image on dockerhub.
#
# It's a good example of how to build our static application and package it
# with a web server capable of hosting it as static content.
#
# docker build
# --------------
# If you would like to use this dockerfile to build and tag an image, make sure
# you set the context to the project's root directory:
# https://docs.docker.com/engine/reference/commandline/build/
#
#
# SUMMARY
# --------------
# This dockerfile is used as an input for a second stage to make things run faster.
#
# Stage 1: Build the application
# docker build -t ohif/viewer:latest .
# Copy Files
FROM node:20.18.1-slim as builder
FROM node:24.15.0-slim as builder
RUN apt-get update && apt-get install -y build-essential python3
RUN apt-get update && apt-get install -y --no-install-recommends build-essential python3 \
&& rm -rf /var/lib/apt/lists/*
RUN npm install -g pnpm@11
RUN mkdir /usr/src/app
WORKDIR /usr/src/app
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
COPY package.json yarn.lock preinstall.js lerna.json ./
COPY --parents ./addOns/package.json ./addOns/*/*/package.json ./extensions/*/package.json ./modes/*/package.json ./platform/*/package.json ./
# Run the install before copying the rest of the files
RUN bun pm cache rm
RUN bun install
RUN bun add ajv@8.12.0
# Copy package manifests for install caching. preinstall.js is included because
# the root package.json's "preinstall" lifecycle script (node preinstall.js)
# runs during `pnpm install` below -- before the full source is copied -- so the
# script file must already be present or install fails with MODULE_NOT_FOUND.
COPY package.json pnpm-lock.yaml pnpm-workspace.yaml .npmrc preinstall.js ./
COPY --parents ./extensions/*/package.json ./modes/*/package.json ./platform/*/package.json ./
# Run the install before copying the rest of the files.
# Keep --no-frozen-lockfile here (unlike CI): .dockerignore excludes
# platform/docs, so the lockfile's docs importer has no manifest in the build
# context and a frozen install would fail. pnpm reconciles (drops docs) instead.
RUN pnpm install --no-frozen-lockfile
# Copy the local directory
COPY --link --exclude=yarn.lock --exclude=package.json --exclude=Dockerfile . .
COPY --link --exclude=pnpm-lock.yaml --exclude=package.json --exclude=Dockerfile . .
# Build here
# After install it should hopefully be stable until the local directory changes
@@ -72,14 +56,14 @@ ARG APP_CONFIG=config/default.js
ARG PUBLIC_URL=/
ENV PUBLIC_URL=${PUBLIC_URL}
RUN bun run show:config
RUN bun run build
RUN pnpm run show:config
RUN pnpm run build
# Precompress files
RUN chmod u+x .docker/compressDist.sh
RUN ./.docker/compressDist.sh
# Stage 3: Bundle the built application into a Docker container
# Stage 2: Bundle the built application into a Docker container
# which runs Nginx using Alpine Linux
FROM nginxinc/nginx-unprivileged:1.27-alpine as final
#RUN apk add --no-cache bash
-3
View File
@@ -1,3 +0,0 @@
# External Dependencies
This module contains optional dependencies and external dependencies for including in OHIF, such as the DICOM Microscopy Viewer component.
File diff suppressed because it is too large. Load diff
-91
View File
@@ -1,91 +0,0 @@
{
"name": "@externals/devDependencies",
"description": "External dev dependencies - put dev build dependencies here",
"version": "3.13.0-beta.80",
"license": "MIT",
"private": true,
"engines": {
"node": ">=12",
"yarn": ">=1.19.1"
},
"dependencies": {
"@babel/runtime": "7.28.2",
"@kitware/vtk.js": "34.15.1",
"clsx": "2.1.1",
"core-js": "3.45.1",
"moment": "2.30.1"
},
"peerDependencies": {
"react": "18.3.1",
"react-dom": "18.3.1"
},
"devDependencies": {
"@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": "8.56.0",
"@typescript-eslint/parser": "8.56.0",
"autoprefixer": "10.4.21",
"babel-loader": "8.4.1",
"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": "9.39.3",
"eslint-config-prettier": "7.2.0",
"eslint-config-react-app": "7.0.1",
"eslint-plugin-cypress": "2.15.2",
"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": "7.2.1",
"eslint-plugin-react": "7.37.5",
"eslint-plugin-react-hooks": "7.0.1",
"eslint-plugin-tsdoc": "0.2.17",
"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": "9.0.4",
"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.7.2",
"serve": "14.2.5",
"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.105.0",
"webpack-bundle-analyzer": "4.10.2",
"webpack-cli": "5.1.4",
"webpack-dev-server": "5.2.2",
"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"
}
}
File diff suppressed because it is too large. Load diff
-9
View File
@@ -1,9 +0,0 @@
{
"name": "@externals/dicom-microscopy-viewer",
"description": "External reference to dicom-microscopy-viewer",
"version": "3.13.0-beta.80",
"license": "MIT",
"dependencies": {
"dicom-microscopy-viewer": "0.48.17"
}
}
-50
View File
@@ -1,50 +0,0 @@
{
"name": "ohif-monorepo-root",
"private": true,
"packageManager": "yarn@1.22.22",
"workspaces": {
"packages": [
"../platform/i18n",
"../platform/core",
"../platform/ui",
"../platform/ui-next",
"../platform/app",
"../extensions/*",
"../modes/*",
"../addOns/externals/*"
],
"nohoist": [
"**/html-minifier-terser"
]
},
"scripts": {
"preinstall": "cd .. && node preinstall.js"
},
"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.27.1",
"@babel/plugin-transform-regenerator": "7.28.1",
"@babel/plugin-transform-runtime": "7.28.0",
"@babel/plugin-transform-typescript": "7.28.0",
"@babel/preset-env": "7.29.5",
"@babel/preset-react": "7.27.1",
"@babel/preset-typescript": "7.27.1"
},
"resolutions": {
"**/@babel/runtime": "7.28.2",
"commander": "8.3.0",
"dcmjs": "0.49.4",
"dicomweb-client": "0.10.4",
"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"
}
}
+3 -1
View File
@@ -26,7 +26,9 @@ module.exports = {
'@babel/preset-typescript',
],
plugins: [
'babel-plugin-istanbul',
// jest's babel coverage provider injects babel-plugin-istanbul when
// --collectCoverage is set; adding it here too makes babel 7 (pulled in
// by jest 30) throw "Duplicate plugin/preset detected".
'@babel/plugin-transform-object-rest-spread',
'@babel/plugin-syntax-dynamic-import',
'@babel/plugin-transform-regenerator',
-8176
View File
File diff suppressed because it is too large. Load diff
-2
View File
@@ -1,2 +0,0 @@
[install]
frozenLockfile = true
-2
View File
@@ -1,2 +0,0 @@
[install]
frozenLockfile = false
+1 -1
View File
@@ -1 +1 @@
09093b3f0a5eaaed660f0d20e624a2bad1c77257
6dd150d401ad73d60632a23378b7dfd4b5142690
@@ -1,8 +1,8 @@
const webpack = require('webpack');
const webpack = require('@rspack/core');
const { merge } = require('webpack-merge');
const path = require('path');
const webpackCommon = require('./../../../.webpack/webpack.base.js');
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
const MiniCssExtractPlugin = webpack.CssExtractRspackPlugin;
const pkg = require('./../package.json');
@@ -35,9 +35,11 @@ module.exports = (env, argv) => {
sideEffects: true,
},
output: {
library: {
name: 'ohif-extension-cornerstone-dicom-pmap',
type: 'umd',
},
path: ROOT_DIR,
library: 'ohif-extension-cornerstone-dicom-pmap',
libraryTarget: 'umd',
filename: pkg.main,
},
externals: [/\b(vtk.js)/, /\b(dcmjs)/, /\b(gl-matrix)/, /^@ohif/, /^@cornerstonejs/],
@@ -3,6 +3,81 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
# [3.13.0-beta.89](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.88...v3.13.0-beta.89) (2026-06-10)
### Bug Fixes
* ohif tests to run with cornerstone 3d 5.0 ([#6043](https://github.com/OHIF/Viewers/issues/6043)) ([6dd150d](https://github.com/OHIF/Viewers/commit/6dd150d401ad73d60632a23378b7dfd4b5142690))
# [3.13.0-beta.88](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.87...v3.13.0-beta.88) (2026-06-05)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-pmap
# [3.13.0-beta.87](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.86...v3.13.0-beta.87) (2026-05-29)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-pmap
# [3.13.0-beta.86](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.85...v3.13.0-beta.86) (2026-05-28)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-pmap
# [3.13.0-beta.85](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.84...v3.13.0-beta.85) (2026-05-28)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-pmap
# [3.13.0-beta.84](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.83...v3.13.0-beta.84) (2026-05-27)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-pmap
# [3.13.0-beta.83](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.82...v3.13.0-beta.83) (2026-05-26)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-pmap
# [3.13.0-beta.82](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.81...v3.13.0-beta.82) (2026-05-22)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-pmap
# [3.13.0-beta.81](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.80...v3.13.0-beta.81) (2026-05-21)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-pmap
# [3.13.0-beta.80](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.79...v3.13.0-beta.80) (2026-05-20)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-pmap
+26 -25
View File
@@ -1,42 +1,37 @@
{
"name": "@ohif/extension-cornerstone-dicom-pmap",
"version": "3.13.0-beta.80",
"version": "3.13.0-beta.109",
"description": "DICOM Parametric Map read workflow",
"author": "OHIF",
"license": "MIT",
"repository": "OHIF/Viewers",
"main": "dist/ohif-extension-cornerstone-dicom-pmap.umd.js",
"module": "src/index.tsx",
"engines": {
"node": ">=24"
},
"files": [
"dist/**",
"public/**",
"README.md"
],
"repository": "OHIF/Viewers",
"keywords": [
"ohif-extension"
],
"publishConfig": {
"access": "public"
},
"engines": {
"node": ">=14",
"npm": ">=6",
"yarn": ">=1.18.0"
},
"scripts": {
"clean": "shx rm -rf dist",
"clean:deep": "yarn run clean && shx rm -rf node_modules",
"dev": "cross-env NODE_ENV=development webpack --config .webpack/webpack.dev.js --watch --output-pathinfo",
"dev:dicom-pmap": "yarn run dev",
"build": "cross-env NODE_ENV=production webpack --config .webpack/webpack.prod.js",
"build:package-1": "yarn run build",
"start": "yarn run dev"
"clean:deep": "pnpm run clean && shx rm -rf node_modules",
"dev": "cross-env NODE_ENV=development rspack build --config .webpack/webpack.dev.js --watch",
"dev:dicom-pmap": "pnpm run dev",
"build": "cross-env NODE_ENV=production rspack build --config .webpack/webpack.prod.js",
"build:package-1": "pnpm run build",
"start": "pnpm run dev"
},
"peerDependencies": {
"@ohif/core": "3.13.0-beta.80",
"@ohif/extension-cornerstone": "3.13.0-beta.80",
"@ohif/extension-default": "3.13.0-beta.80",
"@ohif/i18n": "3.13.0-beta.80",
"@ohif/core": "workspace:*",
"@ohif/extension-cornerstone": "workspace:*",
"@ohif/extension-default": "workspace:*",
"@ohif/i18n": "workspace:*",
"prop-types": "15.8.1",
"react": "18.3.1",
"react-dom": "18.3.1",
@@ -45,9 +40,15 @@
"react-router-dom": "6.30.3"
},
"dependencies": {
"@babel/runtime": "7.28.2",
"@cornerstonejs/adapters": "4.22.8",
"@cornerstonejs/core": "4.22.8",
"@kitware/vtk.js": "34.15.1"
}
"@babel/runtime": "7.29.7",
"@cornerstonejs/adapters": "5.0.2",
"@cornerstonejs/core": "5.0.2",
"@kitware/vtk.js": "35.5.3"
},
"devDependencies": {
"cross-env": "7.0.3"
},
"keywords": [
"ohif-extension"
]
}
@@ -1,4 +1,4 @@
const webpack = require('webpack');
const webpack = require('@rspack/core');
const { merge } = require('webpack-merge');
const path = require('path');
const webpackCommon = require('./../../../.webpack/webpack.base.js');
@@ -32,9 +32,11 @@ module.exports = (env, argv) => {
sideEffects: false,
},
output: {
library: {
name: 'ohif-extension-cornerstone-dicom-rt',
type: 'umd',
},
path: ROOT_DIR,
library: 'ohif-extension-cornerstone-dicom-rt',
libraryTarget: 'umd',
filename: pkg.main,
},
externals: [/\b(vtk.js)/, /\b(dcmjs)/, /\b(gl-matrix)/, /^@ohif/, /^@cornerstonejs/],
@@ -3,6 +3,78 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
# [3.13.0-beta.89](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.88...v3.13.0-beta.89) (2026-06-10)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt
# [3.13.0-beta.88](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.87...v3.13.0-beta.88) (2026-06-05)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt
# [3.13.0-beta.87](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.86...v3.13.0-beta.87) (2026-05-29)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt
# [3.13.0-beta.86](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.85...v3.13.0-beta.86) (2026-05-28)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt
# [3.13.0-beta.85](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.84...v3.13.0-beta.85) (2026-05-28)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt
# [3.13.0-beta.84](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.83...v3.13.0-beta.84) (2026-05-27)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt
# [3.13.0-beta.83](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.82...v3.13.0-beta.83) (2026-05-26)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt
# [3.13.0-beta.82](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.81...v3.13.0-beta.82) (2026-05-22)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt
# [3.13.0-beta.81](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.80...v3.13.0-beta.81) (2026-05-21)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt
# [3.13.0-beta.80](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.79...v3.13.0-beta.80) (2026-05-20)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt
+23 -22
View File
@@ -1,11 +1,15 @@
{
"name": "@ohif/extension-cornerstone-dicom-rt",
"version": "3.13.0-beta.80",
"version": "3.13.0-beta.109",
"description": "DICOM RT read workflow",
"author": "OHIF",
"license": "MIT",
"repository": "OHIF/Viewers",
"main": "dist/ohif-extension-cornerstone-dicom-rt.umd.js",
"module": "src/index.tsx",
"engines": {
"node": ">=24"
},
"files": [
"dist/**",
"public/**",
@@ -14,29 +18,20 @@
"publishConfig": {
"access": "public"
},
"repository": "OHIF/Viewers",
"keywords": [
"ohif-extension"
],
"engines": {
"node": ">=14",
"npm": ">=6",
"yarn": ">=1.18.0"
},
"scripts": {
"clean": "shx rm -rf dist",
"clean:deep": "yarn run clean && shx rm -rf node_modules",
"dev": "cross-env NODE_ENV=development webpack --config .webpack/webpack.dev.js --watch --output-pathinfo",
"dev:dicom-seg": "yarn run dev",
"build": "cross-env NODE_ENV=production webpack --config .webpack/webpack.prod.js",
"build:package-1": "yarn run build",
"start": "yarn run dev"
"clean:deep": "pnpm run clean && shx rm -rf node_modules",
"dev": "cross-env NODE_ENV=development rspack build --config .webpack/webpack.dev.js --watch",
"dev:dicom-seg": "pnpm run dev",
"build": "cross-env NODE_ENV=production rspack build --config .webpack/webpack.prod.js",
"build:package-1": "pnpm run build",
"start": "pnpm run dev"
},
"peerDependencies": {
"@ohif/core": "3.13.0-beta.80",
"@ohif/extension-cornerstone": "3.13.0-beta.80",
"@ohif/extension-default": "3.13.0-beta.80",
"@ohif/i18n": "3.13.0-beta.80",
"@ohif/core": "workspace:*",
"@ohif/extension-cornerstone": "workspace:*",
"@ohif/extension-default": "workspace:*",
"@ohif/i18n": "workspace:*",
"prop-types": "15.8.1",
"react": "18.3.1",
"react-dom": "18.3.1",
@@ -45,6 +40,12 @@
"react-router-dom": "6.30.3"
},
"dependencies": {
"@babel/runtime": "7.28.2"
}
"@babel/runtime": "7.29.7"
},
"devDependencies": {
"cross-env": "7.0.3"
},
"keywords": [
"ohif-extension"
]
}
@@ -1,8 +1,8 @@
const webpack = require('webpack');
const webpack = require('@rspack/core');
const { merge } = require('webpack-merge');
const path = require('path');
const webpackCommon = require('./../../../.webpack/webpack.base.js');
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
const MiniCssExtractPlugin = webpack.CssExtractRspackPlugin;
const pkg = require('./../package.json');
@@ -35,9 +35,11 @@ module.exports = (env, argv) => {
sideEffects: true,
},
output: {
library: {
name: 'ohif-extension-cornerstone-dicom-seg',
type: 'umd',
},
path: ROOT_DIR,
library: 'ohif-extension-cornerstone-dicom-seg',
libraryTarget: 'umd',
filename: pkg.main,
},
externals: [/\b(vtk.js)/, /\b(dcmjs)/, /\b(gl-matrix)/, /^@ohif/, /^@cornerstonejs/],
@@ -3,6 +3,81 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
# [3.13.0-beta.89](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.88...v3.13.0-beta.89) (2026-06-10)
### Bug Fixes
* ohif tests to run with cornerstone 3d 5.0 ([#6043](https://github.com/OHIF/Viewers/issues/6043)) ([6dd150d](https://github.com/OHIF/Viewers/commit/6dd150d401ad73d60632a23378b7dfd4b5142690))
# [3.13.0-beta.88](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.87...v3.13.0-beta.88) (2026-06-05)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg
# [3.13.0-beta.87](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.86...v3.13.0-beta.87) (2026-05-29)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg
# [3.13.0-beta.86](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.85...v3.13.0-beta.86) (2026-05-28)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg
# [3.13.0-beta.85](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.84...v3.13.0-beta.85) (2026-05-28)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg
# [3.13.0-beta.84](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.83...v3.13.0-beta.84) (2026-05-27)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg
# [3.13.0-beta.83](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.82...v3.13.0-beta.83) (2026-05-26)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg
# [3.13.0-beta.82](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.81...v3.13.0-beta.82) (2026-05-22)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg
# [3.13.0-beta.81](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.80...v3.13.0-beta.81) (2026-05-21)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg
# [3.13.0-beta.80](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.79...v3.13.0-beta.80) (2026-05-20)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg
+26 -25
View File
@@ -1,42 +1,37 @@
{
"name": "@ohif/extension-cornerstone-dicom-seg",
"version": "3.13.0-beta.80",
"version": "3.13.0-beta.109",
"description": "DICOM SEG read workflow",
"author": "OHIF",
"license": "MIT",
"repository": "OHIF/Viewers",
"main": "dist/ohif-extension-cornerstone-dicom-seg.umd.js",
"module": "src/index.tsx",
"engines": {
"node": ">=24"
},
"files": [
"dist/**",
"public/**",
"README.md"
],
"repository": "OHIF/Viewers",
"keywords": [
"ohif-extension"
],
"publishConfig": {
"access": "public"
},
"engines": {
"node": ">=14",
"npm": ">=6",
"yarn": ">=1.18.0"
},
"scripts": {
"clean": "shx rm -rf dist",
"clean:deep": "yarn run clean && shx rm -rf node_modules",
"dev": "cross-env NODE_ENV=development webpack --config .webpack/webpack.dev.js --watch --output-pathinfo",
"dev:dicom-seg": "yarn run dev",
"build": "cross-env NODE_ENV=production webpack --config .webpack/webpack.prod.js",
"build:package-1": "yarn run build",
"start": "yarn run dev"
"clean:deep": "pnpm run clean && shx rm -rf node_modules",
"dev": "cross-env NODE_ENV=development rspack build --config .webpack/webpack.dev.js --watch",
"dev:dicom-seg": "pnpm run dev",
"build": "cross-env NODE_ENV=production rspack build --config .webpack/webpack.prod.js",
"build:package-1": "pnpm run build",
"start": "pnpm run dev"
},
"peerDependencies": {
"@ohif/core": "3.13.0-beta.80",
"@ohif/extension-cornerstone": "3.13.0-beta.80",
"@ohif/extension-default": "3.13.0-beta.80",
"@ohif/i18n": "3.13.0-beta.80",
"@ohif/core": "workspace:*",
"@ohif/extension-cornerstone": "workspace:*",
"@ohif/extension-default": "workspace:*",
"@ohif/i18n": "workspace:*",
"prop-types": "15.8.1",
"react": "18.3.1",
"react-dom": "18.3.1",
@@ -45,9 +40,15 @@
"react-router-dom": "6.30.3"
},
"dependencies": {
"@babel/runtime": "7.28.2",
"@cornerstonejs/adapters": "4.22.8",
"@cornerstonejs/core": "4.22.8",
"@kitware/vtk.js": "34.15.1"
}
"@babel/runtime": "7.29.7",
"@cornerstonejs/adapters": "5.0.2",
"@cornerstonejs/core": "5.0.2",
"@kitware/vtk.js": "35.5.3"
},
"devDependencies": {
"cross-env": "7.0.3"
},
"keywords": [
"ohif-extension"
]
}
@@ -1,8 +1,8 @@
const webpack = require('webpack');
const webpack = require('@rspack/core');
const { merge } = require('webpack-merge');
const path = require('path');
const webpackCommon = require('./../../../.webpack/webpack.base.js');
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
const MiniCssExtractPlugin = webpack.CssExtractRspackPlugin;
const pkg = require('./../package.json');
@@ -36,9 +36,11 @@ module.exports = (env, argv) => {
sideEffects: true,
},
output: {
library: {
name: 'ohif-extension-cornerstone-dicom-sr',
type: 'umd',
},
path: ROOT_DIR,
library: 'ohif-extension-cornerstone-dicom-sr',
libraryTarget: 'umd',
filename: pkg.main,
},
externals: [/\b(vtk.js)/, /\b(dcmjs)/, /\b(gl-matrix)/, /^@ohif/, /^@cornerstonejs/],
@@ -3,6 +3,81 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
# [3.13.0-beta.89](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.88...v3.13.0-beta.89) (2026-06-10)
### Bug Fixes
* ohif tests to run with cornerstone 3d 5.0 ([#6043](https://github.com/OHIF/Viewers/issues/6043)) ([6dd150d](https://github.com/OHIF/Viewers/commit/6dd150d401ad73d60632a23378b7dfd4b5142690))
# [3.13.0-beta.88](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.87...v3.13.0-beta.88) (2026-06-05)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr
# [3.13.0-beta.87](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.86...v3.13.0-beta.87) (2026-05-29)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr
# [3.13.0-beta.86](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.85...v3.13.0-beta.86) (2026-05-28)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr
# [3.13.0-beta.85](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.84...v3.13.0-beta.85) (2026-05-28)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr
# [3.13.0-beta.84](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.83...v3.13.0-beta.84) (2026-05-27)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr
# [3.13.0-beta.83](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.82...v3.13.0-beta.83) (2026-05-26)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr
# [3.13.0-beta.82](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.81...v3.13.0-beta.82) (2026-05-22)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr
# [3.13.0-beta.81](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.80...v3.13.0-beta.81) (2026-05-21)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr
# [3.13.0-beta.80](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.79...v3.13.0-beta.80) (2026-05-20)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr
+22 -22
View File
@@ -1,6 +1,6 @@
{
"name": "@ohif/extension-cornerstone-dicom-sr",
"version": "3.13.0-beta.80",
"version": "3.13.0-beta.109",
"description": "OHIF extension for an SR Cornerstone Viewport",
"author": "OHIF",
"license": "MIT",
@@ -8,9 +8,7 @@
"main": "dist/ohif-extension-cornerstone-dicom-sr.umd.js",
"module": "src/index.tsx",
"engines": {
"node": ">=14",
"npm": ">=6",
"yarn": ">=1.16.0"
"node": ">=24"
},
"files": [
"dist",
@@ -19,25 +17,21 @@
"publishConfig": {
"access": "public"
},
"keywords": [
"ohif-extension"
],
"scripts": {
"clean": "shx rm -rf dist",
"clean:deep": "yarn run clean && shx rm -rf node_modules",
"dev": "cross-env NODE_ENV=development webpack --config .webpack/webpack.dev.js --watch --output-pathinfo",
"dev:cornerstone": "yarn run dev",
"build": "cross-env NODE_ENV=production webpack --config .webpack/webpack.prod.js",
"build:package-1": "yarn run build",
"start": "yarn run dev",
"clean:deep": "pnpm run clean && shx rm -rf node_modules",
"dev": "cross-env NODE_ENV=development rspack build --config .webpack/webpack.dev.js --watch",
"dev:cornerstone": "pnpm run dev",
"build": "cross-env NODE_ENV=production rspack build --config .webpack/webpack.prod.js",
"build:package-1": "pnpm run build",
"start": "pnpm run dev",
"test:unit": "jest --watchAll",
"test:unit:ci": "jest --ci --runInBand --collectCoverage --passWithNoTests"
},
"peerDependencies": {
"@ohif/core": "3.13.0-beta.80",
"@ohif/extension-cornerstone": "3.13.0-beta.80",
"@ohif/extension-measurement-tracking": "3.13.0-beta.80",
"@ohif/ui": "3.13.0-beta.80",
"@ohif/core": "workspace:*",
"@ohif/extension-cornerstone": "workspace:*",
"@ohif/ui": "workspace:*",
"dcmjs": "0.49.4",
"dicom-parser": "1.8.21",
"hammerjs": "2.0.8",
@@ -45,10 +39,16 @@
"react": "18.3.1"
},
"dependencies": {
"@babel/runtime": "7.28.2",
"@cornerstonejs/adapters": "4.22.8",
"@cornerstonejs/core": "4.22.8",
"@cornerstonejs/tools": "4.22.8",
"@babel/runtime": "7.29.7",
"@cornerstonejs/adapters": "5.0.2",
"@cornerstonejs/core": "5.0.2",
"@cornerstonejs/tools": "5.0.2",
"classnames": "2.5.1"
}
},
"devDependencies": {
"cross-env": "7.0.3"
},
"keywords": [
"ohif-extension"
]
}
@@ -69,7 +69,12 @@ export default class DICOMSRDisplayTool extends AnnotationTool {
trackingUniqueIdentifiers.includes(annotation.data?.TrackingUniqueIdentifier)
);
if (!viewport._actors?.size) {
const hasActors =
typeof viewport.getActors === 'function'
? viewport.getActors().length > 0
: Boolean(viewport._actors?.size);
if (!hasActors) {
return;
}
@@ -1,8 +1,8 @@
const webpack = require('webpack');
const webpack = require('@rspack/core');
const { merge } = require('webpack-merge');
const path = require('path');
const webpackCommon = require('./../../../.webpack/webpack.base.js');
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
const MiniCssExtractPlugin = webpack.CssExtractRspackPlugin;
const pkg = require('./../package.json');
@@ -35,9 +35,11 @@ module.exports = (env, argv) => {
sideEffects: true,
},
output: {
library: {
name: 'ohif-extension-cornerstone',
type: 'umd',
},
path: ROOT_DIR,
library: 'ohif-extension-cornerstone',
libraryTarget: 'umd',
filename: pkg.main,
},
externals: [/\b(vtk.js)/, /\b(dcmjs)/, /\b(gl-matrix)/, /^@ohif/, /^@cornerstonejs/],
@@ -3,6 +3,81 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
# [3.13.0-beta.89](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.88...v3.13.0-beta.89) (2026-06-10)
### Bug Fixes
* ohif tests to run with cornerstone 3d 5.0 ([#6043](https://github.com/OHIF/Viewers/issues/6043)) ([6dd150d](https://github.com/OHIF/Viewers/commit/6dd150d401ad73d60632a23378b7dfd4b5142690))
# [3.13.0-beta.88](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.87...v3.13.0-beta.88) (2026-06-05)
**Note:** Version bump only for package @ohif/extension-cornerstone-dynamic-volume
# [3.13.0-beta.87](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.86...v3.13.0-beta.87) (2026-05-29)
**Note:** Version bump only for package @ohif/extension-cornerstone-dynamic-volume
# [3.13.0-beta.86](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.85...v3.13.0-beta.86) (2026-05-28)
**Note:** Version bump only for package @ohif/extension-cornerstone-dynamic-volume
# [3.13.0-beta.85](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.84...v3.13.0-beta.85) (2026-05-28)
**Note:** Version bump only for package @ohif/extension-cornerstone-dynamic-volume
# [3.13.0-beta.84](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.83...v3.13.0-beta.84) (2026-05-27)
**Note:** Version bump only for package @ohif/extension-cornerstone-dynamic-volume
# [3.13.0-beta.83](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.82...v3.13.0-beta.83) (2026-05-26)
**Note:** Version bump only for package @ohif/extension-cornerstone-dynamic-volume
# [3.13.0-beta.82](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.81...v3.13.0-beta.82) (2026-05-22)
**Note:** Version bump only for package @ohif/extension-cornerstone-dynamic-volume
# [3.13.0-beta.81](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.80...v3.13.0-beta.81) (2026-05-21)
**Note:** Version bump only for package @ohif/extension-cornerstone-dynamic-volume
# [3.13.0-beta.80](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.79...v3.13.0-beta.80) (2026-05-20)
**Note:** Version bump only for package @ohif/extension-cornerstone-dynamic-volume
@@ -1,12 +1,15 @@
{
"name": "@ohif/extension-cornerstone-dynamic-volume",
"version": "3.13.0-beta.80",
"version": "3.13.0-beta.109",
"description": "OHIF extension for 4D volumes data",
"author": "OHIF",
"license": "MIT",
"repository": "OHIF/Viewers",
"main": "dist/ohif-extension-cornerstone-dynamic-volume.umd.js",
"module": "src/index.ts",
"engines": {
"node": ">=24"
},
"exports": {
".": "./src/index.ts",
"./types": "./src/types/index.ts"
@@ -19,21 +22,21 @@
"access": "public"
},
"scripts": {
"dev": "cross-env NODE_ENV=development webpack --config .webpack/webpack.dev.js --watch --output-pathinfo",
"build": "cross-env NODE_ENV=production webpack --config .webpack/webpack.prod.js",
"build:package": "yarn run build",
"dev": "cross-env NODE_ENV=development rspack build --config .webpack/webpack.dev.js --watch",
"build": "cross-env NODE_ENV=production rspack build --config .webpack/webpack.prod.js",
"build:package": "pnpm run build",
"clean": "shx rm -rf dist",
"clean:deep": "yarn run clean && shx rm -rf node_modules",
"start": "yarn run dev",
"clean:deep": "pnpm run clean && shx rm -rf node_modules",
"start": "pnpm run dev",
"test:unit": "jest --watchAll",
"test:unit:ci": "jest --ci --runInBand --collectCoverage --passWithNoTests"
},
"peerDependencies": {
"@ohif/core": "3.13.0-beta.80",
"@ohif/extension-cornerstone": "3.13.0-beta.80",
"@ohif/extension-default": "3.13.0-beta.80",
"@ohif/i18n": "3.13.0-beta.80",
"@ohif/ui": "3.13.0-beta.80",
"@ohif/core": "workspace:*",
"@ohif/extension-cornerstone": "workspace:*",
"@ohif/extension-default": "workspace:*",
"@ohif/i18n": "workspace:*",
"@ohif/ui": "workspace:*",
"dcmjs": "0.49.4",
"dicom-parser": "1.8.21",
"hammerjs": "2.0.8",
@@ -41,9 +44,12 @@
"react": "18.3.1"
},
"dependencies": {
"@babel/runtime": "7.28.2",
"@cornerstonejs/core": "4.22.8",
"@cornerstonejs/tools": "4.22.8",
"@babel/runtime": "7.29.7",
"@cornerstonejs/core": "5.0.2",
"@cornerstonejs/tools": "5.0.2",
"classnames": "2.5.1"
},
"devDependencies": {
"cross-env": "7.0.3"
}
}
@@ -1,8 +1,8 @@
const webpack = require('webpack');
const webpack = require('@rspack/core');
const { merge } = require('webpack-merge');
const path = require('path');
const webpackCommon = require('./../../../.webpack/webpack.base.js');
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
const MiniCssExtractPlugin = webpack.CssExtractRspackPlugin;
const pkg = require('./../package.json');
@@ -35,9 +35,11 @@ module.exports = (env, argv) => {
sideEffects: true,
},
output: {
library: {
name: 'ohif-extension-cornerstone',
type: 'umd',
},
path: ROOT_DIR,
library: 'ohif-extension-cornerstone',
libraryTarget: 'umd',
filename: pkg.main,
},
externals: [/\b(vtk.js)/, /\b(dcmjs)/, /\b(gl-matrix)/, /^@ohif/, /^@cornerstonejs/],
+78
View File
@@ -3,6 +3,84 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
# [3.13.0-beta.89](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.88...v3.13.0-beta.89) (2026-06-10)
### Bug Fixes
* ohif tests to run with cornerstone 3d 5.0 ([#6043](https://github.com/OHIF/Viewers/issues/6043)) ([6dd150d](https://github.com/OHIF/Viewers/commit/6dd150d401ad73d60632a23378b7dfd4b5142690))
# [3.13.0-beta.88](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.87...v3.13.0-beta.88) (2026-06-05)
**Note:** Version bump only for package @ohif/extension-cornerstone
# [3.13.0-beta.87](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.86...v3.13.0-beta.87) (2026-05-29)
**Note:** Version bump only for package @ohif/extension-cornerstone
# [3.13.0-beta.86](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.85...v3.13.0-beta.86) (2026-05-28)
**Note:** Version bump only for package @ohif/extension-cornerstone
# [3.13.0-beta.85](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.84...v3.13.0-beta.85) (2026-05-28)
**Note:** Version bump only for package @ohif/extension-cornerstone
# [3.13.0-beta.84](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.83...v3.13.0-beta.84) (2026-05-27)
**Note:** Version bump only for package @ohif/extension-cornerstone
# [3.13.0-beta.83](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.82...v3.13.0-beta.83) (2026-05-26)
### Bug Fixes
* **segmentation:** restore navigation for duplicated contour segments ([#6038](https://github.com/OHIF/Viewers/issues/6038)) ([0c1ca6b](https://github.com/OHIF/Viewers/commit/0c1ca6b04a6c1aef478c7a7d9f360f9b9c50f188))
# [3.13.0-beta.82](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.81...v3.13.0-beta.82) (2026-05-22)
**Note:** Version bump only for package @ohif/extension-cornerstone
# [3.13.0-beta.81](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.80...v3.13.0-beta.81) (2026-05-21)
**Note:** Version bump only for package @ohif/extension-cornerstone
# [3.13.0-beta.80](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.79...v3.13.0-beta.80) (2026-05-20)
**Note:** Version bump only for package @ohif/extension-cornerstone
+24 -21
View File
@@ -1,6 +1,6 @@
{
"name": "@ohif/extension-cornerstone",
"version": "3.13.0-beta.80",
"version": "3.13.0-beta.109",
"description": "OHIF extension for Cornerstone",
"author": "OHIF",
"license": "MIT",
@@ -13,9 +13,7 @@
"./types": "./src/types/index.ts"
},
"engines": {
"node": ">=10",
"npm": ">=6",
"yarn": ">=1.16.0"
"node": ">=24"
},
"files": [
"dist",
@@ -26,12 +24,12 @@
},
"scripts": {
"clean": "shx rm -rf dist",
"clean:deep": "yarn run clean && shx rm -rf node_modules",
"dev": "cross-env NODE_ENV=development webpack --config .webpack/webpack.dev.js --watch --output-pathinfo",
"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",
"clean:deep": "pnpm run clean && shx rm -rf node_modules",
"dev": "cross-env NODE_ENV=development rspack build --config .webpack/webpack.dev.js --watch",
"dev:cornerstone": "pnpm run dev",
"build": "cross-env NODE_ENV=production rspack build --config .webpack/webpack.prod.js",
"build:package-1": "pnpm run build",
"start": "pnpm run dev",
"test:unit": "jest --watchAll",
"test:unit:ci": "jest --ci --runInBand --collectCoverage --passWithNoTests"
},
@@ -40,9 +38,10 @@
"@cornerstonejs/codec-libjpeg-turbo-8bit": "1.2.2",
"@cornerstonejs/codec-openjpeg": "1.3.0",
"@cornerstonejs/codec-openjph": "2.4.7",
"@cornerstonejs/dicom-image-loader": "4.22.8",
"@ohif/core": "3.13.0-beta.80",
"@ohif/ui": "3.13.0-beta.80",
"@cornerstonejs/dicom-image-loader": "5.0.2",
"@ohif/core": "workspace:*",
"@ohif/extension-default": "workspace:*",
"@ohif/ui": "workspace:*",
"dcmjs": "0.49.4",
"dicom-parser": "1.8.21",
"hammerjs": "2.0.8",
@@ -52,16 +51,17 @@
"react-resize-detector": "10.0.1"
},
"dependencies": {
"@babel/runtime": "7.28.2",
"@cornerstonejs/adapters": "4.22.8",
"@cornerstonejs/ai": "4.22.8",
"@cornerstonejs/core": "4.22.8",
"@cornerstonejs/labelmap-interpolation": "4.22.8",
"@cornerstonejs/polymorphic-segmentation": "4.22.8",
"@cornerstonejs/tools": "4.22.8",
"@babel/runtime": "7.29.7",
"@cornerstonejs/adapters": "5.0.2",
"@cornerstonejs/ai": "5.0.2",
"@cornerstonejs/core": "5.0.2",
"@cornerstonejs/labelmap-interpolation": "5.0.2",
"@cornerstonejs/metadata": "5.0.2",
"@cornerstonejs/polymorphic-segmentation": "5.0.2",
"@cornerstonejs/tools": "5.0.2",
"@icr/polyseg-wasm": "0.4.0",
"@itk-wasm/morphological-contour-interpolation": "1.1.0",
"@kitware/vtk.js": "34.15.1",
"@kitware/vtk.js": "35.5.3",
"html2canvas": "1.4.1",
"immutability-helper": "3.1.1",
"lodash.compact": "3.0.1",
@@ -70,5 +70,8 @@
"lodash.zip": "4.2.0",
"shader-loader": "1.3.1",
"worker-loader": "3.0.8"
},
"devDependencies": {
"cross-env": "7.0.3"
}
}
@@ -1,7 +1,8 @@
import React, { useEffect } from 'react';
import PropTypes from 'prop-types';
import { Enums, VolumeViewport3D, utilities as csUtils } from '@cornerstonejs/core';
import { Enums, utilities as csUtils } from '@cornerstonejs/core';
import { ImageScrollbar } from '@ohif/ui-next';
import { isVolume3DViewportType } from '../../utils/getLegacyViewportType';
function CornerstoneImageScrollbar({
viewportData,
@@ -40,7 +41,7 @@ function CornerstoneImageScrollbar({
const viewport = cornerstoneViewportService.getCornerstoneViewport(viewportId);
if (!viewport || viewport instanceof VolumeViewport3D) {
if (!viewport || isVolume3DViewportType(viewport)) {
return;
}
@@ -69,7 +70,7 @@ function CornerstoneImageScrollbar({
const updateIndex = event => {
const viewport = cornerstoneViewportService.getCornerstoneViewport(viewportId);
if (!viewport || viewport instanceof VolumeViewport3D) {
if (!viewport || isVolume3DViewportType(viewport)) {
return;
}
const { imageIndex, newImageIdIndex = imageIndex, imageIdIndex } = event.detail;
@@ -1,6 +1,7 @@
import React, { useMemo } from 'react';
import PropTypes from 'prop-types';
import { VolumeViewport3D, utilities as csUtils } from '@cornerstonejs/core';
import { utilities as csUtils } from '@cornerstonejs/core';
import { isVolume3DViewportType } from '../../../utils/getLegacyViewportType';
import {
SmartScrollbar,
SmartScrollbarTrack,
@@ -102,7 +103,7 @@ function ViewportSliceProgressScrollbar({
const onScrollbarValueChange = targetImageIndex => {
const viewport = cornerstoneViewportService.getCornerstoneViewport(viewportId);
if (!viewport || viewport instanceof VolumeViewport3D) {
if (!viewport || isVolume3DViewportType(viewport)) {
return;
}
@@ -1,5 +1,6 @@
import { Enums, VolumeViewport3D } from '@cornerstonejs/core';
import { Enums } from '@cornerstonejs/core';
import { ViewportData } from './types';
import { isVolume3DViewportType } from '../../../utils/getLegacyViewportType';
export function getImageIndexFromEvent(event): number | undefined {
const { imageIndex, newImageIdIndex = imageIndex, imageIdIndex } = event.detail;
@@ -19,7 +20,7 @@ export function getViewportImageIds(viewportData: ViewportData): string[] {
}
export function isProgressFullMode(viewportData: ViewportData, viewport): boolean {
if (!viewportData || !viewport || viewport instanceof VolumeViewport3D) {
if (!viewportData || !viewport || isVolume3DViewportType(viewport)) {
return false;
}
@@ -4,9 +4,9 @@ import {
Enums,
eventTarget,
utilities,
VolumeViewport3D,
} from '@cornerstonejs/core';
import { useByteArray } from '@ohif/ui-next';
import { isVolume3DViewportType } from '../../../utils/getLegacyViewportType';
import { getImageIdFromCacheEvent, getImageIndexFromEvent, isProgressFullMode } from './helpers';
import { ImageSliceData, ViewportData } from './types';
@@ -98,7 +98,7 @@ export function useViewportSliceSync({
}
const viewport = cornerstoneViewportService.getCornerstoneViewport(viewportId);
if (viewport && !(viewport instanceof VolumeViewport3D)) {
if (viewport && !isVolume3DViewportType(viewport)) {
try {
const currentImageIndex = viewport.getCurrentImageIdIndex();
const currentNumberOfSlices = viewport.getNumberOfSlices();
@@ -120,7 +120,7 @@ export function useViewportSliceSync({
const updateIndex = event => {
const viewport = cornerstoneViewportService.getCornerstoneViewport(viewportId);
if (!viewport || viewport instanceof VolumeViewport3D) {
if (!viewport || isVolume3DViewportType(viewport)) {
return;
}
+69 -15
View File
@@ -1,11 +1,7 @@
import {
getEnabledElement,
StackViewport,
VolumeViewport,
utilities as csUtils,
Enums as CoreEnums,
Types as CoreTypes,
BaseVolumeViewport,
getRenderingEngines,
} from '@cornerstonejs/core';
import {
@@ -15,6 +11,7 @@ import {
annotation,
Types as ToolTypes,
SplineContourSegmentationTool,
cancelActiveManipulations,
} from '@cornerstonejs/tools';
import {
SegmentInfo,
@@ -38,6 +35,12 @@ import { getFirstAnnotationSelected } from './utils/measurementServiceMappings/u
import { getViewportEnabledElement } from './utils/getViewportEnabledElement';
import getActiveViewportEnabledElement from './utils/getActiveViewportEnabledElement';
import toggleVOISliceSync from './utils/toggleVOISliceSync';
import {
isStackViewportType,
isOrthographicViewportType,
isVolume3DViewportType,
isVolumeViewportType,
} from './utils/getLegacyViewportType';
import {
usePositionPresentationStore,
useSegmentationPresentationStore,
@@ -304,6 +307,20 @@ function commandsModule({
commandsManager.run('setDisplaySetsForViewports', { viewportsToUpdate: updatedViewports });
},
/**
* Cancels any in-progress annotation manipulation (e.g. drawing a Spline,
* Livewire or PlanarFreehand contour) on the active viewport. Reached on
* Escape via the `cancelActiveOperation` command. `cancelActiveManipulations`
* invokes the `cancel` method of each active/passive tool that has an
* in-progress annotation, so it is a no-op when nothing is being drawn.
*/
cancelMeasurement: () => {
const element = _getActiveViewportEnabledElement()?.viewport?.element;
if (element) {
cancelActiveManipulations(element);
}
},
hydrateSecondaryDisplaySet: async ({ displaySet, viewportId }) => {
if (!displaySet) {
return;
@@ -324,7 +341,7 @@ function commandsModule({
// Todo: check if PMAP modality should be handled such as SEG
displaySet.Modality !== 'SEG'
? SegmentationRepresentations.Contour
: viewport.type === CoreEnums.ViewportType.VOLUME_3D
: isVolume3DViewportType(viewport)
? SegmentationRepresentations.Surface
: SegmentationRepresentations.Labelmap;
@@ -910,7 +927,7 @@ function commandsModule({
const { lower, upper } = csUtils.windowLevel.toLowHighRange(windowWidthNum, windowCenterNum);
if (viewport instanceof BaseVolumeViewport) {
if (isVolumeViewportType(viewport)) {
const volumeId = actions.getVolumeIdForDisplaySet({
viewportId,
displaySetInstanceUID,
@@ -986,8 +1003,12 @@ function commandsModule({
},
getVolumeIdForDisplaySet: ({ viewportId, displaySetInstanceUID }) => {
const viewport = cornerstoneViewportService.getCornerstoneViewport(viewportId);
if (viewport instanceof BaseVolumeViewport) {
const volumeIds = viewport.getAllVolumeIds();
// `instanceof BaseVolumeViewport` is false for GenericViewport compat adapters,
// so use the capability guard instead (cornerstone 5.x generic-viewport guide).
// A stack-mode adapter passes the guard but getAllVolumeIds() returns [], so the
// result is the same null as before on stacks.
if (csUtils.viewportSupportsVolumeId(viewport)) {
const volumeIds = (viewport as CoreTypes.IVolumeViewport).getAllVolumeIds();
const volumeId = volumeIds.find(id => id.includes(displaySetInstanceUID));
return volumeId;
}
@@ -1256,7 +1277,7 @@ function commandsModule({
}
const { viewport } = enabledElement;
if (viewport instanceof StackViewport) {
if (isStackViewportType(viewport)) {
if (direction) {
const { parallelScale } = viewport.getCamera();
viewport.setCamera({ parallelScale: parallelScale * scaleFactor });
@@ -1286,9 +1307,9 @@ function commandsModule({
// -> Copied from cornerstone3D jumpToSlice\_getImageSliceData()
let numberOfSlices = 0;
if (viewport instanceof StackViewport) {
if (isStackViewportType(viewport)) {
numberOfSlices = viewport.getImageIds().length;
} else if (viewport instanceof VolumeViewport) {
} else if (isOrthographicViewportType(viewport)) {
numberOfSlices = csUtils.getImageSliceDataForVolumeViewport(viewport).numberOfSlices;
} else {
throw new Error('Unsupported viewport type');
@@ -1344,11 +1365,11 @@ function commandsModule({
// HP takes priority over the default opacity
colormap = { ...colormap, opacity: hpOpacity || opacity };
if (viewport instanceof StackViewport) {
if (isStackViewportType(viewport)) {
viewport.setProperties({ colormap });
}
if (viewport instanceof VolumeViewport) {
if (isOrthographicViewportType(viewport)) {
if (!displaySetInstanceUID) {
const { viewports } = viewportGridService.getState();
displaySetInstanceUID = viewports.get(viewportId)?.displaySetInstanceUIDs[0];
@@ -2022,6 +2043,24 @@ function commandsModule({
rejectPreview: () => {
actions._handlePreviewAction('reject');
},
/**
* Generic Escape handler. A single Escape press should discard whatever the
* user has in progress, but that can be one of two unrelated things: a
* provisional segmentation preview, or an annotation being drawn. Rather
* than bind both `rejectPreview` and `cancelMeasurement` to `esc` (Mousetrap
* keeps only one handler per key, so the second silently shadows the first),
* this command orchestrates both single-purpose commands. Each is a no-op
* when its state is not active, so running both is safe and order-independent.
*/
cancelActiveOperation: () => {
try {
actions.rejectPreview();
} catch (error) {
console.debug('Error rejecting active preview', error);
} finally {
actions.cancelMeasurement();
}
},
clearMarkersForMarkerLabelmap: () => {
const { viewport } = _getActiveViewportEnabledElement();
const toolGroup = cornerstoneTools.ToolGroupManager.getToolGroupForViewport(viewport.id);
@@ -2145,7 +2184,7 @@ function commandsModule({
setViewportOrientation: ({ viewportId, orientation }) => {
const viewport = cornerstoneViewportService.getCornerstoneViewport(viewportId);
if (!viewport || viewport.type !== CoreEnums.ViewportType.ORTHOGRAPHIC) {
if (!viewport || !isOrthographicViewportType(viewport)) {
console.warn('Orientation can only be set on volume viewports');
return;
}
@@ -2227,7 +2266,7 @@ function commandsModule({
const { viewport } = enabledElement;
if (viewport instanceof BaseVolumeViewport) {
if (isVolumeViewportType(viewport)) {
const camera = viewport.getCamera();
const rotAngle = (rotation * Math.PI) / 180;
const rotMat = mat4.identity(new Float32Array(16));
@@ -2403,14 +2442,25 @@ function commandsModule({
targetSegmentInfo?: SegmentInfo;
}) => {
if (!targetSegmentInfo) {
const sourceSegmentation = segmentationService.getSegmentation(
sourceSegmentInfo.segmentationId
);
const sourceCachedStats =
sourceSegmentation?.segments?.[sourceSegmentInfo.segmentIndex]?.cachedStats;
targetSegmentInfo = {
segmentationId: sourceSegmentInfo.segmentationId,
segmentIndex: segmentationService.getNextAvailableSegmentIndex(
sourceSegmentInfo.segmentationId
),
};
// Copy source cachedStats so jump-to-segment navigation works on the duplicate segment.
segmentationService.addSegment(targetSegmentInfo.segmentationId, {
segmentIndex: targetSegmentInfo.segmentIndex,
...(sourceCachedStats && {
cachedStats: csUtils.deepClone(sourceCachedStats) as Record<string, unknown>,
}),
});
}
@@ -2533,6 +2583,9 @@ function commandsModule({
removeMeasurement: {
commandFn: actions.removeMeasurement,
},
cancelMeasurement: {
commandFn: actions.cancelMeasurement,
},
toggleLockMeasurement: {
commandFn: actions.toggleLockMeasurement,
},
@@ -2781,6 +2834,7 @@ function commandsModule({
toggleSegmentSelect: actions.toggleSegmentSelect,
acceptPreview: actions.acceptPreview,
rejectPreview: actions.rejectPreview,
cancelActiveOperation: actions.cancelActiveOperation,
toggleUseCenterSegmentIndex: actions.toggleUseCenterSegmentIndex,
toggleLabelmapAssist: actions.toggleLabelmapAssist,
interpolateScrollForMarkerLabelmap: actions.interpolateScrollForMarkerLabelmap,
@@ -60,11 +60,11 @@ const DicomUploadProgressItem = memo(
/>
);
case UploadStatus.InProgress:
return <Icons.ByName name="icon-transferring" />;
return <Icons.ByName name="icon-transferring" className="text-highlight" />;
case UploadStatus.Failed:
return <Icons.ByName name="icon-alert-small" />;
return <Icons.ByName name="icon-alert-small" className="text-destructive" />;
case UploadStatus.Cancelled:
return <Icons.ByName name="icon-alert-outline" />;
return <Icons.ByName name="icon-alert-outline" className="text-highlight" />;
default:
return <></>;
}
@@ -18,7 +18,7 @@ function TrackingStatus({ viewportId }: { viewportId: string }) {
<Tooltip>
<TooltipTrigger asChild>
<span>
<Icons.StatusTracking className="h-4 w-4" />
<Icons.StatusTracking className="h-4 w-4 text-highlight" />
</span>
</TooltipTrigger>
<TooltipContent side="bottom">
@@ -2,7 +2,7 @@ import React, { useEffect, useCallback, useState, ReactElement, useMemo } from '
import PropTypes from 'prop-types';
import debounce from 'lodash.debounce';
import { PanelSection, WindowLevel } from '@ohif/ui-next';
import { BaseVolumeViewport, Enums, eventTarget } from '@cornerstonejs/core';
import { Enums, eventTarget, utilities as csUtils, Types } from '@cornerstonejs/core';
import { useActiveViewportDisplaySets } from '@ohif/core';
import {
getNodeOpacity,
@@ -27,10 +27,16 @@ const ViewportWindowLevel = ({
const getViewportsWithVolumeIds = useCallback(
(volumeIds: string[]) => {
const renderingEngine = cornerstoneViewportService.getRenderingEngine();
const viewports = renderingEngine.getVolumeViewports();
// getVolumeViewports() was removed in the GenericViewport architecture
// (a PLANAR_NEXT viewport can be volume-capable without being a VolumeViewport).
// Official replacement: getViewports() + the viewportSupportsVolumeCompatibility
// capability guard (cornerstone codemod cornerstone3d/5/generic-viewport).
const viewports = renderingEngine
.getViewports()
.filter(csUtils.viewportSupportsVolumeCompatibility);
return viewports.filter(vp => {
const viewportVolumeIds = vp instanceof BaseVolumeViewport ? vp.getAllVolumeIds() : [];
const viewportVolumeIds = (vp as Types.IVolumeViewport).getAllVolumeIds();
return (
volumeIds.length === viewportVolumeIds.length &&
volumeIds.every(volumeId => viewportVolumeIds.includes(volumeId))
@@ -124,8 +130,9 @@ const ViewportWindowLevel = ({
return;
}
const viewportVolumeIds =
viewport instanceof BaseVolumeViewport ? viewport.getAllVolumeIds() : [];
const viewportVolumeIds = csUtils.viewportSupportsVolumeId(viewport)
? (viewport as Types.IVolumeViewport).getAllVolumeIds()
: [];
const viewports = getViewportsWithVolumeIds(viewportVolumeIds);
viewports.forEach(vp => {
@@ -1,7 +1,7 @@
import { useState, useEffect, useCallback } from 'react';
import { useSystem } from '@ohif/core/src';
import { useViewportDisplaySets } from './useViewportDisplaySets';
import { BaseVolumeViewport } from '@cornerstonejs/core';
import { isVolumeViewportType } from '../utils/getLegacyViewportType';
/**
* Hook that provides measurement tracking information for a viewport
@@ -54,7 +54,7 @@ export function useMeasurementTracking({ viewportId }: { viewportId: string }) {
const viewport = cornerstoneViewportService?.getCornerstoneViewport(viewportId);
const SeriesInstanceUID = backgroundDisplaySet.SeriesInstanceUID;
if (viewport instanceof BaseVolumeViewport) {
if (isVolumeViewportType(viewport)) {
const currentImageId = viewport?.getCurrentImageId();
if (!currentImageId) {
@@ -1,16 +1,13 @@
import React, { useCallback, useState, useEffect, useMemo } from 'react';
import { useSystem } from '@ohif/core';
import { useViewportDisplaySets } from './useViewportDisplaySets';
import {
StackViewport,
Types,
VolumeViewport3D,
utilities,
Enums,
BaseVolumeViewport,
cache,
} from '@cornerstonejs/core';
import { Types, utilities, Enums, cache } from '@cornerstonejs/core';
import { getDataIdForViewport } from '../utils/getDataIdForViewport';
import {
isStackViewportType,
isVolumeViewportType,
isVolume3DViewportType,
} from '../utils/getLegacyViewportType';
import { WindowLevelPreset } from '../types/WindowLevel';
import { ColorbarPositionType, ColorbarOptions, ColorbarProperties } from '../types/Colorbar';
import { VolumeRenderingConfig } from '../types/VolumeRenderingConfig';
@@ -130,7 +127,7 @@ export function useViewportRendering(
const [viewport, setViewport] = useState<Types.IViewport | null>(() =>
viewportId ? (cornerstoneViewportService.getCornerstoneViewport(viewportId) ?? null) : null
);
const [is3DVolume, setIs3DVolume] = useState(viewport instanceof VolumeViewport3D);
const [is3DVolume, setIs3DVolume] = useState(isVolume3DViewportType(viewport));
const [opacity, setOpacityState] = useState<number | undefined>();
const [opacityLinear, setOpacityLinearState] = useState<number | undefined>();
const [threshold, setThresholdState] = useState<number | undefined>();
@@ -219,7 +216,7 @@ export function useViewportRendering(
return;
}
if (!(viewport instanceof BaseVolumeViewport)) {
if (!isVolumeViewportType(viewport)) {
return;
}
@@ -264,7 +261,7 @@ export function useViewportRendering(
}, [allWindowLevelPresets, activeDisplaySetInstanceUID]);
useEffect(() => {
setIs3DVolume(viewport instanceof VolumeViewport3D);
setIs3DVolume(isVolume3DViewportType(viewport));
if (!viewport || !activeDisplaySetInstanceUID) {
return;
@@ -573,7 +570,7 @@ export function useViewportRendering(
const setOpacity = useCallback(
(opacityValue: number) => {
if (!viewport || !(viewport instanceof BaseVolumeViewport)) {
if (!viewport || !isVolumeViewportType(viewport)) {
return;
}
@@ -624,7 +621,7 @@ export function useViewportRendering(
const setThreshold = useCallback(
(thresholdValue: number) => {
if (!viewport || !(viewport instanceof BaseVolumeViewport)) {
if (!viewport || !isVolumeViewportType(viewport)) {
return;
}
@@ -665,7 +662,7 @@ export function useViewportRendering(
return null;
}
if (viewport instanceof StackViewport) {
if (isStackViewportType(viewport)) {
const { colormap } = viewport.getProperties();
if (!colormap) {
return (
+4
View File
@@ -74,6 +74,10 @@ export default async function init({
rendering: {
...cornerstone.getConfiguration().rendering,
strictZSpacingForVolumeViewport: appConfig.strictZSpacingForVolumeViewport,
// Opt-in: route legacy viewport types through the new GenericViewport render
// paths while keeping the legacy public API via compatibility adapters.
// No-op on cornerstone builds that predate the GenericViewport architecture.
useGenericViewport: Boolean(appConfig.useGenericViewport),
},
});
@@ -531,10 +531,8 @@ const connectMeasurementServiceToTools = ({
}
// Cancel any active tool manipulation (e.g., Spline/Livewire) to avoid leaving the tool
// in a drawing state after deleting a not completed measurement, which can block viewport interactivity.
const element = getActiveViewportEnabledElement(viewportGridService)?.viewport?.element;
if (element) {
cancelActiveManipulations(element);
}
commandsManager.run('cancelMeasurement');
const removedAnnotation = annotation.state.getAnnotation(removedMeasurementId);
removeAnnotation(removedMeasurementId);
// Ensure `removedAnnotation` is available before triggering the memo,
@@ -10,6 +10,10 @@ import {
metaData,
} from '@cornerstonejs/core';
import { ViewportType } from '@cornerstonejs/core/enums';
import {
isVolume3DViewportType,
isVolumeViewportType,
} from '../../utils/getLegacyViewportType';
import {
Enums as csToolsEnums,
@@ -312,7 +316,7 @@ class SegmentationService extends PubSubService {
let isConverted = false;
const defaultRepresentationType: csToolsEnums.SegmentationRepresentations =
csViewport.type === ViewportType.VOLUME_3D ? SURFACE : LABELMAP;
isVolume3DViewportType(csViewport) ? SURFACE : LABELMAP;
let representationTypeToUse = type || defaultRepresentationType;
if (representationTypeToUse === LABELMAP) {
@@ -896,6 +900,7 @@ class SegmentationService extends PubSubService {
active?: boolean;
color?: csTypes.Color; // Add color type
visibility?: boolean; // Add visibility option
cachedStats?: Record<string, unknown>;
} = {}
): void {
if (config?.segmentIndex === 0) {
@@ -1590,8 +1595,7 @@ class SegmentationService extends PubSubService {
}
private determineViewportAndSegmentationType(csViewport, segmentation) {
const isVolumeViewport =
csViewport.type === ViewportType.ORTHOGRAPHIC || csViewport.type === ViewportType.VOLUME_3D;
const isVolumeViewport = isVolumeViewportType(csViewport);
const isVolumeSegmentation = 'volumeId' in segmentation.representationData[LABELMAP];
return { isVolumeViewport, isVolumeSegmentation };
}
@@ -1622,7 +1626,7 @@ class SegmentationService extends PubSubService {
}
private async handleVolumeViewportCase(csViewport, segmentation, isVolumeSegmentation) {
if (csViewport.type === ViewportType.VOLUME_3D) {
if (isVolume3DViewportType(csViewport)) {
return {
representationTypeToUse: SURFACE,
isConverted: false,
@@ -7,6 +7,7 @@ import {
} from '@cornerstonejs/tools';
import { isAnyDisplaySetCommon } from '../../utils/isAnyDisplaySetCommon';
import { isVolume3DViewportType } from '../../utils/getLegacyViewportType';
const { createSynchronizer } = SynchronizerManager;
const { SEGMENTATION_REPRESENTATION_MODIFIED } = Enums.Events;
@@ -87,12 +88,17 @@ const segmentationRepresentationModifiedCallback = async (
return;
}
// Ensure the segmentation representation aligns with the target viewport type.
const type: Enums.SegmentationRepresentations =
viewport.type === CoreEnums.ViewportType.VOLUME_3D
? Enums.SegmentationRepresentations.Surface
: ((segmentationRepresentationType as Enums.SegmentationRepresentations) ??
Enums.SegmentationRepresentations.Labelmap);
// Ensure the segmentation representation aligns with the target viewport type
const is3D = isVolume3DViewportType(viewport);
const requestedRepresentation =
segmentationRepresentationType as Enums.SegmentationRepresentations;
const { Surface, Labelmap } = Enums.SegmentationRepresentations;
const type: Enums.SegmentationRepresentations = is3D
? Surface
: requestedRepresentation && requestedRepresentation !== Surface
? requestedRepresentation
: Labelmap;
await segmentationService.addSegmentationRepresentation(targetViewportId, {
segmentationId,
@@ -3,16 +3,11 @@ import { PubSubService } from '@ohif/core';
import { Types as OhifTypes } from '@ohif/core';
import {
RenderingEngine,
StackViewport,
Types,
getRenderingEngine,
utilities as csUtils,
VolumeViewport,
VolumeViewport3D,
ECGViewport,
cache,
Enums as csEnums,
BaseVolumeViewport,
} from '@cornerstonejs/core';
import { utilities as csToolsUtils, Enums as csToolsEnums } from '@cornerstonejs/tools';
@@ -39,6 +34,12 @@ import { usePositionPresentationStore } from '../../stores/usePositionPresentati
import { useSynchronizersStore } from '../../stores/useSynchronizersStore';
import { useSegmentationPresentationStore } from '../../stores/useSegmentationPresentationStore';
import getClosestOrientationFromIOP from '../../utils/isReferenceViewable';
import {
getLegacyViewportType,
isStackViewportType,
isVolume3DViewportType,
isVolumeViewportType,
} from '../../utils/getLegacyViewportType';
import { BlendModes } from '@cornerstonejs/core/enums';
const EVENTS = {
@@ -49,10 +50,19 @@ const EVENTS = {
const MIN_STACK_VIEWPORTS_TO_ENQUEUE_RESIZE = 12;
const MIN_VOLUME_VIEWPORTS_TO_ENQUEUE_RESIZE = 6;
// Actor class names that represent a primary volume binding in a viewport.
// Legacy ORTHOGRAPHIC / VOLUME_3D render the source volume as a 'vtkVolume'
// actor. Under the GenericViewport-backed compat path (useGenericViewport),
// the MPR slice render path emits 'vtkImageSlice' actors instead, so the
// volume-set-vs-add optimization below must recognize both. (3D still uses
// 'vtkVolume'.) Without this, the optimization silently no-ops and a full
// setVolumes() teardown blanks MPR during SEG hydration.
const VOLUME_ACTOR_CLASS_NAMES = new Set(['vtkVolume', 'vtkImageSlice']);
function getVolumeActorReferencedIds(viewport: Types.IVolumeViewport): string[] {
const actors = viewport.getActors?.() ?? [];
return actors
.filter(ac => ac.actor?.getClassName?.() === 'vtkVolume')
.filter(ac => VOLUME_ACTOR_CLASS_NAMES.has(ac.actor?.getClassName?.() ?? ''))
.map(ac => ac.referencedId)
.filter(Boolean) as string[];
}
@@ -81,7 +91,7 @@ function viewportMatchesDesiredVolumePresentation(
desiredViewportInfo: ViewportInfo
): boolean {
const desiredType = desiredViewportInfo.getViewportType();
if (viewport.type !== desiredType) {
if (getLegacyViewportType(viewport) !== desiredType) {
return false;
}
return (
@@ -370,7 +380,7 @@ class CornerstoneViewportService extends PubSubService implements IViewportServi
return {
viewportType: viewportInfo.getViewportType(),
viewReference: csViewport instanceof VolumeViewport3D ? null : csViewport.getViewReference(),
viewReference: isVolume3DViewportType(csViewport) ? null : csViewport.getViewReference(),
viewPresentation: csViewport.getViewPresentation({ pan: true, zoom: true }),
viewportId,
};
@@ -398,10 +408,9 @@ class CornerstoneViewportService extends PubSubService implements IViewportServi
return properties;
};
const properties =
csViewport instanceof BaseVolumeViewport
? new Map()
: cleanProperties(csViewport.getProperties());
const properties = isVolumeViewportType(csViewport)
? new Map()
: cleanProperties(csViewport.getProperties());
if (properties instanceof Map) {
const volumeIds = (csViewport as Types.IBaseVolumeViewport).getAllVolumeIds();
@@ -840,10 +849,14 @@ class CornerstoneViewportService extends PubSubService implements IViewportServi
_presentations: Presentations = {}
): Promise<void> {
const [displaySet] = viewportData.data;
return viewport.setDataIds(displaySet.imageIds, {
groupId: displaySet.displaySetInstanceUID,
viewReference: viewportInfo.getViewReference(),
});
// CS3D's "redo viewports" replaced setDataIds with the generic
// setDisplaySets({ displaySetId }) API; the legacy adapters key off
// imageIds[0] as the displaySetId, so do the same here.
await viewport.setDisplaySets({ displaySetId: displaySet.imageIds[0] });
const viewReference = viewportInfo.getViewReference();
if (viewReference) {
viewport.setViewReference(viewReference);
}
}
private async _setStackViewport(
@@ -1141,7 +1154,7 @@ class CornerstoneViewportService extends PubSubService implements IViewportServi
throw new Error('Background display set not found');
}
if (viewport.type === csEnums.ViewportType.VOLUME_3D) {
if (isVolume3DViewportType(viewport)) {
timeoutViewportCallback(() => {
viewportGridService.setDisplaySetsForViewport({
viewportId: viewport.id,
@@ -1311,7 +1324,7 @@ class CornerstoneViewportService extends PubSubService implements IViewportServi
let displaySetPromise;
if (viewport instanceof VolumeViewport || viewport instanceof VolumeViewport3D) {
if (isVolumeViewportType(viewport)) {
displaySetPromise = this._setVolumeViewport(viewport, viewportData, viewportInfo).then(() => {
if (keepCamera) {
viewport.setCamera(viewportCamera);
@@ -1320,7 +1333,7 @@ class CornerstoneViewportService extends PubSubService implements IViewportServi
});
}
if (viewport instanceof StackViewport) {
if (isStackViewportType(viewport)) {
displaySetPromise = this._setStackViewport(viewport, viewportData, viewportInfo);
}
@@ -1338,7 +1351,7 @@ class CornerstoneViewportService extends PubSubService implements IViewportServi
viewportInfo: ViewportInfo,
presentations: Presentations = {}
): Promise<void> {
if (viewport instanceof StackViewport) {
if (isStackViewportType(viewport)) {
return this._setStackViewport(
viewport,
viewportData as StackViewportData,
@@ -1347,7 +1360,7 @@ class CornerstoneViewportService extends PubSubService implements IViewportServi
);
}
if ([VolumeViewport, VolumeViewport3D].some(type => viewport instanceof type)) {
if (isVolumeViewportType(viewport)) {
return this._setVolumeViewport(
viewport as Types.IVolumeViewport,
viewportData as VolumeViewportData,
@@ -1356,7 +1369,7 @@ class CornerstoneViewportService extends PubSubService implements IViewportServi
);
}
if (viewport instanceof ECGViewport) {
if (getLegacyViewportType(viewport) === csEnums.ViewportType.ECG) {
return this._setEcgViewport(
viewport as unknown as Types.IECGViewport,
viewportData as StackViewportData
@@ -1508,7 +1521,7 @@ class CornerstoneViewportService extends PubSubService implements IViewportServi
}
const { properties } = lutPresentation;
if (viewport instanceof BaseVolumeViewport) {
if (isVolumeViewportType(viewport)) {
if (properties instanceof Map) {
properties.forEach((propertiesEntry, volumeId) => {
viewport.setProperties(propertiesEntry, volumeId);
@@ -1557,7 +1570,7 @@ class CornerstoneViewportService extends PubSubService implements IViewportServi
const { segmentationId, type, hydrated } = presentationItem;
const { Labelmap, Surface } = csToolsEnums.SegmentationRepresentations;
const isVolume3D = viewport.type === csEnums.ViewportType.VOLUME_3D;
const isVolume3D = isVolume3DViewportType(viewport);
// Determine the appropriate segmentation representation for the viewport.
// If the current type is Surface but the viewport is not 3D, fallback to Labelmap.
@@ -2,10 +2,10 @@ import {
Types,
Enums,
getEnabledElementByViewportId,
VolumeViewport,
utilities,
} from '@cornerstonejs/core';
import { StackViewportData, VolumeViewportData } from '../../types/CornerstoneCacheService';
import { isOrthographicViewportType } from '../../utils/getLegacyViewportType';
import getCornerstoneBlendMode from '../../utils/getCornerstoneBlendMode';
import getCornerstoneOrientation from '../../utils/getCornerstoneOrientation';
import getCornerstoneViewportType from '../../utils/getCornerstoneViewportType';
@@ -105,7 +105,7 @@ const dataContains = ({ data, displaySetUID, imageId, viewport }): boolean => {
return !!data.imageIds.find(dataId => dataId === imageId);
}
if (imageId && (data.volumeId || viewport instanceof VolumeViewport)) {
if (imageId && (data.volumeId || isOrthographicViewportType(viewport))) {
const isAcquisition = !!viewport.getCurrentImageId();
if (!isAcquisition) {
@@ -1,8 +1,9 @@
import { VolumeViewport, metaData, utilities } from '@cornerstonejs/core';
import { metaData, utilities } from '@cornerstonejs/core';
import { IStackViewport, IVolumeViewport } from '@cornerstonejs/core/types';
import { AnnotationDisplayTool, drawing } from '@cornerstonejs/tools';
import { guid, b64toBlob } from '@ohif/core/src/utils';
import OverlayPlaneModuleProvider from './OverlayPlaneModuleProvider';
import { isOrthographicViewportType } from '../utils/getLegacyViewportType';
interface CachedStat {
color: number[]; // [r, g, b, a]
@@ -49,7 +50,7 @@ class ImageOverlayViewerTool extends AnnotationDisplayTool {
onSetToolDisabled = (): void => {};
protected getReferencedImageId(viewport: IStackViewport | IVolumeViewport): string {
if (viewport instanceof VolumeViewport) {
if (isOrthographicViewportType(viewport)) {
return;
}
@@ -1,9 +1,10 @@
import { utils } from '@ohif/core';
import React, { useEffect, useState } from 'react';
import html2canvas from 'html2canvas';
import { getEnabledElement, StackViewport, BaseVolumeViewport } from '@cornerstonejs/core';
import { getEnabledElement } from '@cornerstonejs/core';
import { ToolGroupManager, segmentation, Enums } from '@cornerstonejs/tools';
import { getEnabledElement as OHIFgetEnabledElement } from '../state';
import { isStackViewportType, isVolumeViewportType } from './getLegacyViewportType';
import { useSystem } from '@ohif/core/src';
const { downloadUrl } = utils;
@@ -127,10 +128,10 @@ const CornerstoneViewportDownloadForm = ({
const viewPresentation = viewport.getViewPresentation?.();
const viewRef = viewport.getViewReference?.();
if (downloadViewport instanceof StackViewport) {
if (isStackViewportType(downloadViewport)) {
const imageId = viewport.getCurrentImageId();
await downloadViewport.setStack([imageId]);
} else if (downloadViewport instanceof BaseVolumeViewport) {
} else if (isVolumeViewportType(downloadViewport)) {
const volumeIds = viewport.getAllVolumeIds();
await downloadViewport.setVolumes([{ volumeId: volumeIds[0] }]);
}
@@ -36,7 +36,7 @@ type CreateSegmentationForViewportParams = {
export async function createSegmentationForViewport(
servicesManager: ServicesManager,
{ viewportId, options = {}, segmentationType }: CreateSegmentationForViewportParams
): Promise<string> {
): Promise<string | undefined> {
const { viewportGridService, displaySetService, segmentationService } = servicesManager.services;
const { viewports } = viewportGridService.getState();
const targetViewportId = viewportId;
@@ -44,7 +44,15 @@ export async function createSegmentationForViewport(
const viewport = viewports.get(targetViewportId);
// Todo: add support for multiple display sets
const displaySetInstanceUID = options.displaySetInstanceUID || viewport.displaySetInstanceUIDs[0];
// Guard against the grid not having this viewport yet (a transient race when
// "Add segmentation" runs before the viewport is registered in the grid). Bail
// gracefully instead of throwing, which would surface an uncaught error overlay.
const displaySetInstanceUID =
options.displaySetInstanceUID || viewport?.displaySetInstanceUIDs?.[0];
if (!displaySetInstanceUID) {
return undefined;
}
const segs = segmentationService.getSegmentations();
@@ -30,10 +30,14 @@ const mockDocument = {
},
};
Object.defineProperty(global, 'document', {
writable: true,
value: mockDocument,
});
// jsdom (jest 30) makes the global `document` non-configurable, so it can't be
// replaced wholesale. Route the methods the code under test uses onto the real
// jsdom document; assertions still target mockDocument's jest.fn() references.
const doc = document as unknown as typeof mockDocument;
doc.addEventListener = mockDocument.addEventListener;
doc.createElement = mockDocument.createElement;
document.body.appendChild = mockDocument.body.appendChild as typeof document.body.appendChild;
document.body.removeChild = mockDocument.body.removeChild as typeof document.body.removeChild;
describe('generateSegmentationCSVReport', () => {
const mockInfo = {
@@ -0,0 +1,66 @@
import { Enums as csEnums, Types as csTypes } from '@cornerstonejs/core';
const { ViewportType } = csEnums;
type ViewportLike = {
requestedType?: csEnums.ViewportType;
type?: csEnums.ViewportType;
};
/**
* Returns the legacy/requested viewport type, transparent across GenericViewport
* compatibility remapping.
*
* When `rendering.useGenericViewport` is enabled, a viewport requested as
* `STACK` or `ORTHOGRAPHIC` has runtime `viewport.type === PLANAR_NEXT`, and a
* `VOLUME_3D` request has `VOLUME_3D_NEXT`. The rendering engine records the
* original request on `viewport.requestedType`, which this reads. Falls back to
* `viewport.type` for legacy/non-remapped viewports (and older cornerstone builds
* that do not populate `requestedType`).
*
* Use this instead of `viewport instanceof StackViewport/VolumeViewport/...` or
* raw `viewport.type === ViewportType.X` when branching on the legacy viewport
* type. For "does this viewport support operation X" questions, prefer the
* cornerstone capability guards (`utilities.viewportSupports*`) instead.
*/
export function getLegacyViewportType(
viewport: unknown
): csEnums.ViewportType | undefined {
const vp = viewport as ViewportLike | null | undefined;
return vp?.requestedType ?? vp?.type;
}
/** Legacy STACK viewport (image stack). Replaces `instanceof StackViewport`. */
export function isStackViewportType(
viewport: unknown
): viewport is csTypes.IStackViewport {
return getLegacyViewportType(viewport) === ViewportType.STACK;
}
/** Legacy ORTHOGRAPHIC (MPR) viewport. Replaces `instanceof VolumeViewport`. */
export function isOrthographicViewportType(
viewport: unknown
): viewport is csTypes.IVolumeViewport {
return getLegacyViewportType(viewport) === ViewportType.ORTHOGRAPHIC;
}
/** Legacy VOLUME_3D viewport. Replaces `instanceof VolumeViewport3D`. */
export function isVolume3DViewportType(
viewport: unknown
): viewport is csTypes.IVolumeViewport {
return getLegacyViewportType(viewport) === ViewportType.VOLUME_3D;
}
/**
* Legacy ORTHOGRAPHIC or VOLUME_3D viewport (i.e. a `BaseVolumeViewport`).
* Replaces `instanceof BaseVolumeViewport`.
*/
export function isVolumeViewportType(
viewport: unknown
): viewport is csTypes.IVolumeViewport {
const legacyType = getLegacyViewportType(viewport);
return (
legacyType === ViewportType.ORTHOGRAPHIC ||
legacyType === ViewportType.VOLUME_3D
);
}
+6 -4
View File
@@ -1,8 +1,8 @@
const webpack = require('webpack');
const webpack = require('@rspack/core');
const { merge } = require('webpack-merge');
const path = require('path');
const webpackCommon = require('./../../../.webpack/webpack.base.js');
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
const MiniCssExtractPlugin = webpack.CssExtractRspackPlugin;
const pkg = require('./../package.json');
@@ -35,9 +35,11 @@ module.exports = (env, argv) => {
sideEffects: true,
},
output: {
library: {
name: 'ohif-extension-default',
type: 'umd',
},
path: ROOT_DIR,
library: 'ohif-extension-default',
libraryTarget: 'umd',
filename: pkg.main,
},
externals: [/\b(vtk.js)/, /\b(dcmjs)/, /\b(gl-matrix)/, /^@ohif/, /^@cornerstonejs/],
+75
View File
@@ -3,6 +3,81 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
# [3.13.0-beta.89](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.88...v3.13.0-beta.89) (2026-06-10)
**Note:** Version bump only for package @ohif/extension-default
# [3.13.0-beta.88](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.87...v3.13.0-beta.88) (2026-06-05)
**Note:** Version bump only for package @ohif/extension-default
# [3.13.0-beta.87](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.86...v3.13.0-beta.87) (2026-05-29)
**Note:** Version bump only for package @ohif/extension-default
# [3.13.0-beta.86](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.85...v3.13.0-beta.86) (2026-05-28)
**Note:** Version bump only for package @ohif/extension-default
# [3.13.0-beta.85](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.84...v3.13.0-beta.85) (2026-05-28)
### Features
* **WorkList:** New Study List (WorkList based on ui-next); old study list renamed to LegacyWorklist ([#6005](https://github.com/OHIF/Viewers/issues/6005)) ([daae4c1](https://github.com/OHIF/Viewers/commit/daae4c144e7cb80bbee7c05d9fceef8b332ed2a0))
# [3.13.0-beta.84](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.83...v3.13.0-beta.84) (2026-05-27)
**Note:** Version bump only for package @ohif/extension-default
# [3.13.0-beta.83](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.82...v3.13.0-beta.83) (2026-05-26)
**Note:** Version bump only for package @ohif/extension-default
# [3.13.0-beta.82](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.81...v3.13.0-beta.82) (2026-05-22)
**Note:** Version bump only for package @ohif/extension-default
# [3.13.0-beta.81](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.80...v3.13.0-beta.81) (2026-05-21)
**Note:** Version bump only for package @ohif/extension-default
# [3.13.0-beta.80](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.79...v3.13.0-beta.80) (2026-05-20)
**Note:** Version bump only for package @ohif/extension-default
+21 -21
View File
@@ -1,41 +1,36 @@
{
"name": "@ohif/extension-default",
"version": "3.13.0-beta.80",
"version": "3.13.0-beta.109",
"description": "Common/default features and functionality for basic image viewing",
"author": "OHIF Core Team",
"license": "MIT",
"repository": "OHIF/Viewers",
"main": "dist/ohif-extension-default.umd.js",
"module": "src/index.ts",
"publishConfig": {
"access": "public"
},
"engines": {
"node": ">=14",
"npm": ">=6",
"yarn": ">=1.18.0"
"node": ">=24"
},
"files": [
"dist",
"README.md"
],
"keywords": [
"ohif-extension"
],
"publishConfig": {
"access": "public"
},
"scripts": {
"clean": "shx rm -rf dist",
"clean:deep": "yarn run clean && shx rm -rf node_modules",
"dev": "cross-env NODE_ENV=development webpack --config .webpack/webpack.dev.js --watch --output-pathinfo",
"dev:dicom-pdf": "yarn run dev",
"build": "cross-env NODE_ENV=production webpack --config .webpack/webpack.prod.js",
"build:package-1": "yarn run build",
"start": "yarn run dev",
"clean:deep": "pnpm run clean && shx rm -rf node_modules",
"dev": "cross-env NODE_ENV=development rspack build --config .webpack/webpack.dev.js --watch",
"dev:dicom-pdf": "pnpm run dev",
"build": "cross-env NODE_ENV=production rspack build --config .webpack/webpack.prod.js",
"build:package-1": "pnpm run build",
"start": "pnpm run dev",
"test:unit": "jest --watchAll",
"test:unit:ci": "jest --ci --runInBand --collectCoverage --passWithNoTests"
},
"peerDependencies": {
"@ohif/core": "3.13.0-beta.80",
"@ohif/i18n": "3.13.0-beta.80",
"@ohif/core": "workspace:*",
"@ohif/i18n": "workspace:*",
"dcmjs": "0.49.4",
"dicomweb-client": "0.10.4",
"prop-types": "15.8.1",
@@ -43,14 +38,19 @@
"react-dom": "18.3.1",
"react-i18next": "12.3.1",
"react-window": "1.8.11",
"webpack": "5.105.0",
"webpack-merge": "5.10.0"
},
"dependencies": {
"@babel/runtime": "7.28.2",
"@babel/runtime": "7.29.7",
"@cornerstonejs/calculate-suv": "1.1.0",
"lodash.get": "4.4.2",
"lodash.uniqby": "4.7.0",
"react-color": "2.19.3"
}
},
"devDependencies": {
"webpack-merge": "5.10.0"
},
"keywords": [
"ohif-extension"
]
}
@@ -1,6 +1,17 @@
import React, { ReactElement, useCallback, useEffect, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { Icons, useModal } from '@ohif/ui-next';
import {
Icons,
useModal,
Button,
HoverCard,
HoverCardTrigger,
HoverCardContent,
Card,
CardHeader,
CardDescription,
CardContent,
} from '@ohif/ui-next';
import { Types } from '@ohif/core';
import DataSourceConfigurationModalComponent from './DataSourceConfigurationModalComponent';
@@ -18,6 +29,8 @@ function DataSourceConfigurationComponent({
const [configuredItems, setConfiguredItems] =
useState<Array<Types.BaseDataSourceConfigurationAPIItem>>();
const [itemLabels, setItemLabels] = useState<Array<string>>([]);
useEffect(() => {
let shouldUpdate = true;
@@ -38,6 +51,7 @@ function DataSourceConfigurationComponent({
const configAPI = configurationAPIFactory(activeDataSourceDef.sourceName);
setConfigurationAPI(configAPI);
setItemLabels(configAPI.getItemLabels());
// New configuration API means that the existing configured items must be cleared.
setConfiguredItems(null);
@@ -87,31 +101,42 @@ function DataSourceConfigurationComponent({
}, [configurationAPI, configuredItems, showConfigurationModal]);
return configuredItems ? (
<div className="text-muted-foreground flex items-center overflow-hidden">
<Icons.Settings
className="mr-2.5 h-3.5 w-3.5 shrink-0 cursor-pointer"
onClick={showConfigurationModal}
/>
{configuredItems.map((item, itemIndex) => {
return (
<div
key={itemIndex}
className="flex overflow-hidden"
>
<div
key={itemIndex}
className="overflow-hidden text-ellipsis whitespace-nowrap"
>
{item.name}
</div>
{itemIndex !== configuredItems.length - 1 && <div className="px-2.5">|</div>}
</div>
);
})}
</div>
) : (
<></>
);
<HoverCard>
<HoverCardTrigger asChild>
<Button
variant="ghost"
size="sm"
className="gap-1 text-sm"
onClick={showConfigurationModal}
>
<Icons.CloudSettings className="h-5 w-5" />
Source
</Button>
</HoverCardTrigger>
<HoverCardContent
align="center"
className="w-72 p-0"
>
<Card className="border-0 shadow-none">
<CardHeader className="p-3 pb-1">
<CardDescription className="text-sm">
<span className="text-foreground font-semibold">{t('Data Source')}:</span>{' '}
{t('Configure the server connection and storage settings')}
</CardDescription>
</CardHeader>
<CardContent className="grid grid-cols-[auto_1fr] gap-x-3 gap-y-1 p-3 pt-0 text-sm">
<div className="bg-input col-span-2 my-2 h-px" />
{itemLabels.map((label, index) => (
<React.Fragment key={label}>
<span className="text-muted-foreground">{t(label)}</span>
<span>{configuredItems[index]?.name ?? '—'}</span>
</React.Fragment>
))}
</CardContent>
</Card>
</HoverCardContent>
</HoverCard>
) : null;
}
export default DataSourceConfigurationComponent;
@@ -152,7 +152,7 @@ function DataSourceConfigurationModalComponent({
>
<div className="text- flex items-center gap-2">
{itemLabelIndex < selectedItems.length ? (
<Icons.ByName name="status-tracked" />
<Icons.ByName name="status-tracked" className="text-highlight" />
) : (
<Icons.ByName name="status-untracked" />
)}
@@ -12,8 +12,7 @@
.dicom-tag-browser-table tr {
padding-left: 10px;
padding-right: 10px;
color: #ffffff;
border-top: 1px solid #ddd;
color: hsl(var(--foreground));
white-space: nowrap;
}
@@ -43,7 +42,6 @@
padding-left: 10px;
padding-right: 10px;
text-align: center;
color: '#20A5D6';
}
.dicom-tag-browser-table th.dicom-tag-browser-table-left {
@@ -16,7 +16,8 @@ import { retrieveStudyMetadata, deleteStudyMetadataPromise } from './retrieveStu
import StaticWadoClient from './utils/StaticWadoClient';
import getDirectURL from '../utils/getDirectURL';
import { fixBulkDataURI } from './utils/fixBulkDataURI';
import {HeadersInterface} from '@ohif/core/src/types/RequestHeaders';
import { HeadersInterface } from '@ohif/core/src/types/RequestHeaders';
import { getGetThumbnailSrc, ThumbnailContext } from './retrieveThumbnail';
const { DicomMetaDictionary, DicomDict } = dcmjs.data;
@@ -47,6 +48,15 @@ export type DicomWebConfig = {
thumbnail - render using the thumbnail endpoint on wadors using bulkDataURI, passing authentication params to the url.
rendered - should use the rendered endpoint instead of the thumbnail endpoint
*/
thumbnailRequestStrategy?: 'bulkDataRetrieve' | 'fetch';
/**
* Thumbnail data request strategy when `thumbnailRendering` is `thumbnail`/`rendered`; ignored for `wadors`/`thumbnailDirect`.
*
* - `bulkDataRetrieve` (default): Uses the DICOMweb client's bulk data retrieve API (`retrieveBulkData`)
* - `fetch`: `GET` the WADO-RS thumbnail or rendered resource URL with auth headers and use the
* response body as a JPEG blob URL. For series-level context, if that `GET` fails, a single
* QIDO instances query (`limit=1`) is used to obtain `SOPInstanceUID` and the fetch is retried once.
*/
/** Whether the server supports reject calls (i.e. DCM4CHEE) */
supportsReject?: boolean;
/** indicates if the retrieves can fetch singlepart. Options are bulkdata, video, image, or true */
@@ -98,6 +108,7 @@ export type BulkDataURIConfig = {
relativeResolution?: 'studies' | 'series';
};
/**
* The header options are the options passed into the generateWadoHeader
* command. This takes an extensible set of attributes to allow future enhancements.
@@ -158,7 +169,7 @@ function createDicomWebApi(dicomWebConfig: DicomWebConfig, servicesManager) {
*/
generateWadoHeader = (options: HeaderOptions): HeadersInterface => {
const authorizationHeader = getAuthorizationHeader();
if (options?.includeTransferSyntax!==false) {
if (options?.includeTransferSyntax !== false) {
//Generate accept header depending on config params
const formattedAcceptHeader = utils.generateAcceptHeader(
dicomWebConfig.acceptHeader,
@@ -175,7 +186,7 @@ function createDicomWebApi(dicomWebConfig: DicomWebConfig, servicesManager) {
// which the server expects Accept: application/dicom+json will still include that in the
// header.
return {
...authorizationHeader
...authorizationHeader,
};
}
};
@@ -261,70 +272,16 @@ function createDicomWebApi(dicomWebConfig: DicomWebConfig, servicesManager) {
* or is already retrieved, or a promise to a URL for such use if a BulkDataURI
*/
getGetThumbnailSrc: function (instance, imageId) {
if (dicomWebConfig.thumbnailRendering === 'wadors') {
return function getThumbnailSrc(options) {
if (!imageId) {
return null;
}
if (!options?.getImageSrc) {
return null;
}
return options.getImageSrc(imageId);
};
}
if (dicomWebConfig.thumbnailRendering === 'thumbnailDirect') {
return function getThumbnailSrc() {
return this.directURL({
instance: instance,
defaultPath: '/thumbnail',
defaultType: 'image/jpeg',
singlepart: true,
tag: 'Absent',
});
}.bind(this);
}
if (dicomWebConfig.thumbnailRendering === 'thumbnail') {
return async function getThumbnailSrc() {
const { StudyInstanceUID, SeriesInstanceUID, SOPInstanceUID } = instance;
const bulkDataURI = `${dicomWebConfig.wadoRoot}/studies/${StudyInstanceUID}/series/${SeriesInstanceUID}/instances/${SOPInstanceUID}/thumbnail?accept=image/jpeg`;
return URL.createObjectURL(
new Blob(
[
await this.bulkDataURI({
BulkDataURI: bulkDataURI.replace('wadors:', ''),
defaultType: 'image/jpeg',
mediaTypes: ['image/jpeg'],
thumbnail: true,
}),
],
{ type: 'image/jpeg' }
)
);
}.bind(this);
}
if (dicomWebConfig.thumbnailRendering === 'rendered') {
return async function getThumbnailSrc() {
const { StudyInstanceUID, SeriesInstanceUID, SOPInstanceUID } = instance;
const bulkDataURI = `${dicomWebConfig.wadoRoot}/studies/${StudyInstanceUID}/series/${SeriesInstanceUID}/instances/${SOPInstanceUID}/rendered?accept=image/jpeg`;
return URL.createObjectURL(
new Blob(
[
await this.bulkDataURI({
BulkDataURI: bulkDataURI.replace('wadors:', ''),
defaultType: 'image/jpeg',
mediaTypes: ['image/jpeg'],
thumbnail: true,
}),
],
{ type: 'image/jpeg' }
)
);
}.bind(this);
}
getGetThumbnailSrc: function (thumbnailContext: ThumbnailContext, imageId) {
return getGetThumbnailSrc({
thumbnailContext,
imageId,
config: dicomWebConfig,
getAuthorizationHeader,
qidoDicomWebClient,
retrieve: this,
});
},
directURL: params => {
return getDirectURL(
{
@@ -55,6 +55,7 @@ function processResults(qidoStudies) {
instances: Number(getString(qidoStudy['00201208'])) || 0, // number
description: getString(qidoStudy['00081030']) || '',
modalities: getString(getModalities(qidoStudy['00080060'], qidoStudy['00080061'])) || '',
referringPhysicianName: utils.formatPN(getName(qidoStudy['00080090'])) || '', // Referring Physician's Name
})
);
@@ -151,6 +152,7 @@ function mapParams(params, options = {}) {
const commaSeparatedFields = [
'00081030', // Study Description
'00080060', // Modality
'00080090', // Referring Physician's Name
// Add more fields here if you want them in the result
].join(',');
@@ -0,0 +1,346 @@
import { HeadersInterface } from '@ohif/core/src/types/RequestHeaders';
export type ThumbnailContext = {
StudyInstanceUID?: string;
SeriesInstanceUID?: string;
SOPInstanceUID?: string;
};
type ThumbnailFetchRequestResult = {
url: string;
endpointPath: string;
headers: Record<string, string>;
};
type GetThumbnailSrcOptions = {
signal?: AbortSignal;
getImageSrc?: (imageId: unknown) => unknown;
};
type QidoClient = {
headers: HeadersInterface;
searchForInstances: (opts: unknown) => Promise<unknown[]>;
};
/**
* The subset of the data source's `retrieve` object the thumbnail strategies
* call back into for the directURL and bulkData-backed renderings.
*/
type RetrieveApi = {
directURL: (params: unknown) => unknown;
bulkDataURI: (params: unknown) => Promise<BlobPart>;
};
type ThumbnailConfig = {
thumbnailRendering?: string;
thumbnailRequestStrategy?: 'bulkDataRetrieve' | 'fetch';
wadoRoot?: string;
};
type GetGetThumbnailSrcDeps = {
thumbnailContext: ThumbnailContext;
imageId: unknown;
config: ThumbnailConfig;
getAuthorizationHeader: () => HeadersInterface;
qidoDicomWebClient: QidoClient;
retrieve: RetrieveApi;
};
/**
* Builds the `getThumbnailSrc` function for a given thumbnail context,
* selecting the strategy from `config.thumbnailRendering` /
* `config.thumbnailRequestStrategy`.
*
* Extracted from the DICOMweb data source so the thumbnail strategies live
* apart from the data source wiring. The `retrieve` dependency provides the
* `directURL` and `bulkDataURI` callbacks the data source used to reach via
* `this`.
*/
export function getGetThumbnailSrc({
thumbnailContext,
imageId,
config,
getAuthorizationHeader,
qidoDicomWebClient,
retrieve,
}: GetGetThumbnailSrcDeps) {
const thumbnailRendering = config.thumbnailRendering;
if (!thumbnailRendering) {
return function getThumbnailSrc() {
console.warn('thumbnailRendering is not configured; returning null thumbnail src.');
return null;
};
}
if (thumbnailRendering === 'wadors') {
return function getThumbnailSrc(options?: GetThumbnailSrcOptions) {
if (!imageId) {
return null;
}
if (!options?.getImageSrc) {
return null;
}
// Note: options.signal (Cornerstone-backed loadImageToCanvas via getImageSrc) does
// not currently expose an AbortSignal hook, so abort is not propagated to the
// underlying image load. We short-circuit only if already aborted at call time.
if (options?.signal?.aborted) {
return null;
}
return options.getImageSrc(imageId);
};
}
// thumbnailDirect is for plain <img src> URLs without auth headers; never use fetch here.
// No network call happens at this layer (the <img> element loads the URL later), so
// options.signal is not applicable here.
if (thumbnailRendering === 'thumbnailDirect') {
return function getThumbnailSrc() {
return retrieve.directURL({
instance: thumbnailContext,
defaultPath: '/thumbnail',
defaultType: 'image/jpeg',
singlepart: true,
tag: 'Absent',
});
};
}
const thumbnailRequestStrategy = config.thumbnailRequestStrategy || 'bulkDataRetrieve';
if (thumbnailRequestStrategy === 'fetch') {
return async function getThumbnailSrc(options?: { signal?: AbortSignal }) {
return fetchThumbnailWithQidoFallbackForSeries(
thumbnailContext,
thumbnailRendering,
config.wadoRoot,
getAuthorizationHeader,
qidoDicomWebClient,
options?.signal
);
};
}
if (thumbnailRendering === 'thumbnail') {
return async function getThumbnailSrc(options?: { signal?: AbortSignal }) {
// Note: this path goes through bulkDataURI -> dicomweb-client retrieveBulkData,
// which is XHR-based and does NOT honor AbortSignal. The underlying request will
// run to completion server-side even if signal aborts; we only short-circuit
// before kicking it off if already aborted.
if (options?.signal?.aborted) {
return null;
}
const endpoint = buildThumbnailEndpointPath(
thumbnailContext,
thumbnailRendering,
new URLSearchParams({ accept: 'image/jpeg' })
);
const bulkDataURI = `${config.wadoRoot}${endpoint}`;
return URL.createObjectURL(
new Blob(
[
await retrieve.bulkDataURI({
BulkDataURI: bulkDataURI.replace('wadors:', ''),
defaultType: 'image/jpeg',
mediaTypes: ['image/jpeg'],
thumbnail: true,
}),
],
{ type: 'image/jpeg' }
)
);
};
}
if (thumbnailRendering === 'rendered') {
return async function getThumbnailSrc(options?: { signal?: AbortSignal }) {
// Note: this path goes through bulkDataURI -> dicomweb-client retrieveBulkData,
// which is XHR-based and does NOT honor AbortSignal. The underlying request will
// run to completion server-side even if signal aborts; we only short-circuit
// before kicking it off if already aborted.
if (options?.signal?.aborted) {
return null;
}
const endpoint = buildThumbnailEndpointPath(
thumbnailContext,
thumbnailRendering,
new URLSearchParams({ accept: 'image/jpeg' })
);
const bulkDataURI = `${config.wadoRoot}${endpoint}`;
return URL.createObjectURL(
new Blob(
[
await retrieve.bulkDataURI({
BulkDataURI: bulkDataURI.replace('wadors:', ''),
defaultType: 'image/jpeg',
mediaTypes: ['image/jpeg'],
thumbnail: true,
}),
],
{ type: 'image/jpeg' }
)
);
};
}
return function getThumbnailSrc() {
console.warn(
`Unsupported thumbnailRendering "${thumbnailRendering}"; returning null thumbnail src.`
);
return null;
};
}
function buildThumbnailEndpointPath(
thumbnailContext: ThumbnailContext,
thumbnailRendering: string,
queryParams?: URLSearchParams
): string {
const { StudyInstanceUID, SeriesInstanceUID, SOPInstanceUID } = thumbnailContext;
const basePath =
SeriesInstanceUID && SOPInstanceUID
? `/studies/${StudyInstanceUID}/series/${SeriesInstanceUID}/instances/${SOPInstanceUID}/${thumbnailRendering}`
: SeriesInstanceUID
? `/studies/${StudyInstanceUID}/series/${SeriesInstanceUID}/${thumbnailRendering}`
: `/studies/${StudyInstanceUID}/${thumbnailRendering}`;
if (!queryParams) {
return basePath;
}
const queryString = queryParams.toString();
return queryString ? `${basePath}?${queryString}` : basePath;
}
function getThumbnailFetchRequest(
thumbnailContext: ThumbnailContext,
thumbnailRendering: string,
wadoRoot: string | undefined,
getAuthorizationHeader: () => HeadersInterface
): ThumbnailFetchRequestResult {
const endpointPath = buildThumbnailEndpointPath(
thumbnailContext,
thumbnailRendering,
// Thumbnails for some data source (e.g. dcm4chee) are pixelated by default, so we need to set the viewport to 256,256 to get a better thumbnail.
new URLSearchParams({ viewport: '256,256' })
);
const headers: Record<string, string> = {
...(getAuthorizationHeader() as Record<string, string>),
Accept: 'image/jpeg',
};
return {
url: `${wadoRoot}${endpointPath}`,
endpointPath,
headers,
};
}
async function fetchThumbnailObjectURL(
thumbnailContext: ThumbnailContext,
thumbnailRendering: string,
wadoRoot: string | undefined,
getAuthorizationHeader: () => HeadersInterface,
signal?: AbortSignal
): Promise<string | null> {
const fetchRequest = getThumbnailFetchRequest(
thumbnailContext,
thumbnailRendering,
wadoRoot,
getAuthorizationHeader
);
try {
const response = await fetch(fetchRequest.url, {
method: 'GET',
headers: fetchRequest.headers,
signal,
});
if (!response.ok) {
console.warn(
`thumbnail fetch failed with status ${response.status} for ${fetchRequest.endpointPath}`
);
return null;
}
const blob = await response.blob();
return URL.createObjectURL(blob);
} catch (error) {
if ((error as { name?: string })?.name === 'AbortError') {
return null;
}
console.warn('thumbnail fetch failed', error);
return null;
}
}
/**
* When thumbnailRequestStrategy is fetch: try WADO GET for the given context; if it fails and the
* context is series-level (no SOPInstanceUID), QIDO one instance and retry fetch once.
*/
async function fetchThumbnailWithQidoFallbackForSeries(
thumbnailContext: ThumbnailContext,
thumbnailRendering: string,
wadoRoot: string | undefined,
getAuthorizationHeader: () => HeadersInterface,
qidoClient: QidoClient,
signal?: AbortSignal
): Promise<string | null> {
const sopInstanceUidTag = '00080018';
const initialThumbnailUrl = await fetchThumbnailObjectURL(
thumbnailContext,
thumbnailRendering,
wadoRoot,
getAuthorizationHeader,
signal
);
if (initialThumbnailUrl) {
return initialThumbnailUrl;
}
if (signal?.aborted) {
return null;
}
if (thumbnailContext.SOPInstanceUID) {
return null;
}
if (!thumbnailContext.StudyInstanceUID || !thumbnailContext.SeriesInstanceUID) {
return null;
}
try {
qidoClient.headers = getAuthorizationHeader();
// Note: qidoClient.searchForInstances is XHR-based (dicomweb-client) and does not honor
// AbortSignal. If signal aborts mid-request the network call still completes; we just
// short-circuit before issuing a follow-up fetch below.
const instances = await qidoClient.searchForInstances({
studyInstanceUID: thumbnailContext.StudyInstanceUID,
seriesInstanceUID: thumbnailContext.SeriesInstanceUID,
queryParams: {
limit: 1,
includefield: sopInstanceUidTag,
},
});
if (signal?.aborted) {
return null;
}
const firstInstance = instances?.[0] as Record<string, unknown> | undefined;
const sopAttr = firstInstance?.[sopInstanceUidTag] as { Value?: string[] } | undefined;
const sopValues = sopAttr?.Value;
const SOPInstanceUID =
Array.isArray(sopValues) && sopValues.length ? String(sopValues[0]) : undefined;
if (!SOPInstanceUID) {
return null;
}
return fetchThumbnailObjectURL(
{ ...thumbnailContext, SOPInstanceUID },
thumbnailRendering,
wadoRoot,
getAuthorizationHeader,
signal
);
} catch (error) {
console.warn('thumbnail fetch QIDO fallback failed', error);
return null;
}
}
@@ -50,6 +50,7 @@ function createDicomWebProxyApi(dicomWebProxyConfig, servicesManager: AppTypes.S
},
},
retrieve: {
getGetThumbnailSrc: (...args) => dicomWebDelegate.retrieve.getGetThumbnailSrc(...args),
directURL: (...args) => dicomWebDelegate.retrieve.directURL(...args),
series: {
metadata: async (...args) => dicomWebDelegate.retrieve.series.metadata(...args),
@@ -219,6 +219,13 @@ function createMergeDataSourceApi(
},
},
retrieve: {
getGetThumbnailSrc: (...args: unknown[]) =>
callForDefaultDataSource({
path: 'retrieve.getGetThumbnailSrc',
args,
defaultDataSourceName,
extensionManager,
}),
bulkDataURI: (...args: unknown[]) =>
callForAllDataSourcesAsync({
mergeMap,
@@ -8,11 +8,10 @@ import { defaultActionIcons } from './constants';
import MoreDropdownMenu from '../../Components/MoreDropdownMenu';
import { CallbackCustomization } from 'platform/core/src/types';
import { type TabsProps } from '@ohif/core/src/utils/createStudyBrowserTabs';
import { thumbnailNoImageModalities } from '@ohif/core/src/utils/thumbnailNoImageModalities';
const { sortStudyInstances, formatDate, createStudyBrowserTabs } = utils;
const thumbnailNoImageModalities = ['SR', 'SEG', 'RTSTRUCT', 'RTPLAN', 'RTDOSE', 'DOC', 'PMAP'];
/**
* Study Browser component that displays and manages studies and their display sets
*/
@@ -43,6 +43,10 @@ function ViewerHeader({ appConfig }: withAppTypes<{ appConfig: AppTypes.Config }
'ohif.aboutModal'
) as Types.MenuComponentCustomization;
const AppearanceModal = customizationService.getCustomization(
'ohif.appearanceModal'
) as Types.MenuComponentCustomization;
const UserPreferencesModal = customizationService.getCustomization(
'ohif.userPreferencesModal'
) as Types.MenuComponentCustomization;
@@ -71,6 +75,19 @@ function ViewerHeader({ appConfig }: withAppTypes<{ appConfig: AppTypes.Config }
},
];
if (AppearanceModal) {
menuOptions.splice(1, 0, {
title: AppearanceModal.menuTitle ?? t('Header:Appearance'),
icon: 'ColorChange',
onClick: () =>
show({
content: AppearanceModal,
title: AppearanceModal.title ?? t('AppearanceModal:Appearance'),
containerClassName: AppearanceModal.containerClassName ?? 'max-w-md',
}),
});
}
if (appConfig.oidc) {
menuOptions.push({
title: t('Header:Logout'),
@@ -159,7 +159,7 @@ function ViewerLayout({
/>
<div
className="relative flex w-full flex-row flex-nowrap items-stretch overflow-hidden bg-background"
style={{ height: 'calc(100vh - 52px' }}
style={{ height: 'calc(100vh - 52px)' }}
>
<React.Fragment>
{showLoadingIndicator && <LoadingIndicatorProgress className="h-full w-full bg-background" />}
@@ -0,0 +1,143 @@
import React from 'react';
import {
AppearanceModal,
Select,
SelectTrigger,
SelectValue,
SelectContent,
SelectItem,
Button,
useActiveTheme,
themePresets,
} from '@ohif/ui-next';
import { useTranslation } from 'react-i18next';
function AppearanceModalDefault() {
const { activeTheme, setActiveTheme, customCss, applyCustomTheme, clearCustomTheme } =
useActiveTheme();
const { t } = useTranslation('AppearanceModal');
const [draftCss, setDraftCss] = React.useState(() => customCss);
const [isCustomOpen, setIsCustomOpen] = React.useState(() => activeTheme === 'custom');
const [parseFailed, setParseFailed] = React.useState(false);
const handleToggleCustom = () => {
setIsCustomOpen(!isCustomOpen);
};
const handleSave = () => {
setParseFailed(!applyCustomTheme(draftCss));
};
const handleClear = () => {
clearCustomTheme();
setDraftCss('');
setParseFailed(false);
setIsCustomOpen(false);
};
const handlePresetChange = (value: string) => {
if (value === 'custom') {
// Selecting "Custom" restores the saved custom theme; the draft is only
// committed through the explicit Apply button.
setIsCustomOpen(true);
if (customCss) {
applyCustomTheme(customCss);
}
} else {
setIsCustomOpen(false);
setActiveTheme(value);
}
};
const handleTextChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
setDraftCss(e.target.value);
setParseFailed(false);
};
return (
<AppearanceModal>
<AppearanceModal.Body>
<div className="grid grid-cols-[auto_1fr] items-center gap-x-6 gap-y-4">
<AppearanceModal.SectionLabel>{t('Theme')}</AppearanceModal.SectionLabel>
<div>
<Select
value={activeTheme}
onValueChange={handlePresetChange}
>
<SelectTrigger
className="w-[200px]"
aria-label={t('Theme')}
>
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="default">{t('Default Theme')}</SelectItem>
{themePresets.map(preset => (
<SelectItem
key={preset.name}
value={preset.name}
>
{preset.label}
</SelectItem>
))}
{(customCss || draftCss) && <SelectItem value="custom">{t('Custom')}</SelectItem>}
</SelectContent>
</Select>
</div>
<div className="col-start-2">
<Button
variant="ghost"
size="sm"
onClick={handleToggleCustom}
>
{isCustomOpen ? t('Hide') : t('Custom Theme')}
</Button>
</div>
</div>
{isCustomOpen && (
<div className="mt-4 flex flex-col space-y-2">
<textarea
value={draftCss}
onChange={handleTextChange}
placeholder={t('Paste your custom theme color tokens here')}
aria-label={t('Paste your custom theme color tokens here')}
rows={8}
className="bg-muted text-foreground border-input placeholder:text-muted-foreground focus:ring-ring rounded-md border px-3 py-2 font-mono text-sm focus:outline-none focus:ring-1 focus:ring-inset"
/>
{parseFailed && (
<span
className="text-destructive text-sm"
role="alert"
>
{t('No valid theme tokens found')}
</span>
)}
<div className="flex space-x-2">
<Button
variant="default"
size="sm"
onClick={handleSave}
>
{t('Apply')}
</Button>
<Button
variant="ghost"
size="sm"
onClick={handleClear}
>
{t('Clear')}
</Button>
</div>
</div>
)}
</AppearanceModal.Body>
</AppearanceModal>
);
}
export default {
'ohif.appearanceModal': AppearanceModalDefault,
};
@@ -0,0 +1,87 @@
import { StudyList } from '@ohif/ui-next';
/**
* Default customization values for the WorkList study-list route.
*
* - `workList.variant`: `'default' | 'legacy'` (default: `'default'`)
* Selects which study-list route is mounted at `/`.
* - `'default'`: the new ui-next WorkList.
* - `'legacy'`: the pre-3.13 WorkList (now `LegacyWorkList`). Useful as an
* opt-out while integrators migrate to the new study list.
*
* - `workList.previewSeriesView`: `'all' | 'thumbnails' | 'list'` (default: `'all'`)
* Controls which series views are available in the preview panel.
* - `'all'`: thumbnails/list toggle is visible; defaults to thumbnails.
* - `'thumbnails'`: toggle hidden; locked to thumbnails view.
* - `'list'`: toggle hidden; locked to list view.
* Note: the preview is forced to `'list'` when the active data source either:
* - declares `thumbnailRendering` as `'wadors'` or `'thumbnailDirect'`, or
* - declares `thumbnailRequestStrategy` as `'bulkDataRetrieve'` (default value).
* Currently only applies when `workList.variant` is `'default'`.
*
* - `workList.columns`: `ColumnDef[]` (default: `StudyList.defaultColumns`)
* The column set for the WorkList table, as a value (not a function). Because
* it is a plain array, override it with immutability-helper commands:
* - reorder / insert / remove: `$splice`
* - relabel / resize / reprioritize (all plain data in `meta`): `$set` / `$merge`
* - replace a renderer: `$set` a new `cell` / `header` function
* - `$apply: (cols) => ColumnDef[]`: receive the current columns and return the
* new array. Use it for anything the other commands don't express cleanly —
* moves, conditional inserts, or any edit keyed off a column's `id` rather
* than its position (e.g. `cols.find(c => c.id === 'modalities')`).
* Use `StudyList.textColumn(id, label, meta?)` to build a simple display-only
* column without writing the accessor/header/cell wiring.
*
* Gotchas / limitations:
* - A `ColumnDef`'s `accessorFn` / `cell` / `header` / `filterFn` / `sortingFn`
* are functions: `$set`/`$push` accept them, but they are not serializable,
* so columns that render anything beyond plain text still need code.
* - The trailing `actions` column should stay last for correct layout (its
* hover menu is right-aligned to sit at the row end) — this is cosmetic,
* not a hard requirement. Insert new columns *before* it with `$splice`
* (a `$push` lands after it, leaving the actions menu mid-row).
* - Index-based commands (e.g. `{ 2: { meta: { label: { $set: '…' } } } }`)
* are position-fragile; prefer `$apply` for id-based edits.
* If the merged value is not an array, WorkList falls back to the defaults.
* Currently only applies when `workList.variant` is `'default'`.
*
* - `workList.renderPreviewContent`: `(React, props) => ReactNode` (default: undefined)
* Render function for the preview panel content. Receives the host React and
* `{ study, series, seriesView, onThumbnailImageError }`:
* - `study`: the selected `StudyRow` (`null` when nothing is selected).
* - `series`: the study's series; each item has the raw data-source fields
* (`seriesInstanceUid`, `modality`, `description`, `seriesDate`,
* `seriesNumber`, `numSeriesInstances`, etc.) plus `thumbnailStatus` added
* by the shell, which is one of `{ status: 'loading' }`,
* `{ status: 'ready', src }`, `{ status: 'notAvailable' }`, or
* `{ status: 'notApplicable' }`. The `src` in the `'ready'` form is the
* URL to render in an `<img>`.
* - `seriesView`: `'all' | 'thumbnails' | 'list'`, resolved from
* `workList.previewSeriesView` with `'list'` forced for data sources that
* can't produce thumbnails. Honor it if your layout has both views.
* - `onThumbnailImageError(seriesUID)`: call when an `<img>` you render fails
* to load. The shell marks that series as `notAvailable` and revokes its
* blob URL if needed.
* Use this to change the preview layout while keeping the fetch/abort/thumbnail
* logic intact. When unset (or not a function), the built-in
* `<StudyList.PreviewContainer>` layout is used.
* Currently only applies when `workList.variant` is `'default'`.
*
* - `workList.settingsMenuItems`: `(defaults) => SettingsMenuItem[]` (default: identity)
* Builds the items in the WorkList settings popover. Receives the default
* items (`about`, `userPreferences`, and `logout` when OIDC is configured)
* and must return a
* `SettingsMenuItem[]`. Each item is `{ id, label, onClick }`. Use this to
* reorder, remove, or insert items without rebuilding the popover shell. If
* the returned value is not an array, WorkList falls back to the defaults.
* Currently only applies when `workList.variant` is `'default'`.
*/
export default function getWorkListCustomization() {
return {
'workList.variant': 'default',
'workList.previewSeriesView': 'all',
'workList.columns': StudyList.defaultColumns,
'workList.renderPreviewContent': undefined,
'workList.settingsMenuItems': (defaults: unknown) => defaults,
};
}
@@ -18,11 +18,13 @@ import progressLoadingBarCustomization from './customizations/progressLoadingBar
import labellingFlowCustomization from './customizations/labellingFlowCustomization';
import viewportNotificationCustomization from './customizations/notificationCustomization';
import aboutModalCustomization from './customizations/aboutModalCustomization';
import appearanceModalCustomization from './customizations/appearanceModalCustomization';
import userPreferencesCustomization from './customizations/userPreferencesCustomization';
import reportDialogCustomization from './customizations/reportDialogCustomization';
import hotkeyBindingsCustomization from './customizations/hotkeyBindingsCustomization';
import onboardingCustomization from './customizations/onboardingCustomization';
import instanceSortingCriteriaCustomization from './customizations/instanceSortingCriteriaCustomization';
import getWorkListCustomization from './customizations/workListCustomization';
/**
*
* Note: this is an example of how the customization module can be used
@@ -38,6 +40,10 @@ export default function getCustomizationModule({ servicesManager, extensionManag
name: 'helloPage',
value: helloPageCustomization,
},
{
name: 'theme',
value: appearanceModalCustomization,
},
{
name: 'datasources',
value: datasourcesCustomization,
@@ -71,6 +77,7 @@ export default function getCustomizationModule({ servicesManager, extensionManag
...hotkeyBindingsCustomization,
...onboardingCustomization,
...instanceSortingCriteriaCustomization,
...getWorkListCustomization(),
},
},
];
+12
View File
@@ -1,5 +1,6 @@
import { DicomMetadataStore, classes } from '@ohif/core';
import { calculateSUVScalingFactors } from '@cornerstonejs/calculate-suv';
import { ActiveThemeProvider } from '@ohif/ui-next';
import getPTImageIdInstanceMetadata from './getPTImageIdInstanceMetadata';
import { registerHangingProtocolAttributes } from './hangingprotocols';
@@ -16,7 +17,18 @@ export default function init({
servicesManager,
commandsManager,
hotkeysManager,
serviceProvidersManager,
appConfig,
}: withAppTypes): void {
const hasThemeModule =
Array.isArray(appConfig.customizationService) &&
appConfig.customizationService.some(
ref => typeof ref === 'string' && ref.includes('customizationModule.theme')
);
if (hasThemeModule) {
serviceProvidersManager.registerProvider('activeTheme', ActiveThemeProvider);
}
const { toolbarService, cineService, viewportGridService } = servicesManager.services;
toolbarService.registerEventForToolbarUpdate(cineService, [
@@ -1,3 +1,4 @@
.chrome-picker {
background: #090c29 !important;
background: transparent !important;
box-shadow: none !important;
}
@@ -12,7 +12,7 @@ function ColorPickerDialog({ value, hide, onSave }) {
};
return (
<div>
<div data-cy="color-picker-dialog">
<ChromePicker
color={color}
onChange={handleChange}
@@ -21,8 +21,14 @@ function ColorPickerDialog({ value, hide, onSave }) {
/>
<FooterAction>
<FooterAction.Right>
<FooterAction.Secondary onClick={hide}>Cancel</FooterAction.Secondary>
<FooterAction.Secondary
dataCY="color-picker-cancel-btn"
onClick={hide}
>
Cancel
</FooterAction.Secondary>
<FooterAction.Primary
dataCY="color-picker-save-btn"
onClick={() => {
hide();
onSave(color);
@@ -1,8 +1,8 @@
const webpack = require('webpack');
const webpack = require('@rspack/core');
const { merge } = require('webpack-merge');
const path = require('path');
const webpackCommon = require('./../../../.webpack/webpack.base.js');
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
const MiniCssExtractPlugin = webpack.CssExtractRspackPlugin;
const pkg = require('./../package.json');
@@ -35,9 +35,11 @@ module.exports = (env, argv) => {
sideEffects: true,
},
output: {
library: {
name: 'ohif-extension-dicom-microscopy',
type: 'umd',
},
path: ROOT_DIR,
library: 'ohif-extension-dicom-microscopy',
libraryTarget: 'umd',
filename: pkg.main,
},
externals: [
+72
View File
@@ -3,6 +3,78 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
# [3.13.0-beta.89](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.88...v3.13.0-beta.89) (2026-06-10)
**Note:** Version bump only for package @ohif/extension-dicom-microscopy
# [3.13.0-beta.88](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.87...v3.13.0-beta.88) (2026-06-05)
**Note:** Version bump only for package @ohif/extension-dicom-microscopy
# [3.13.0-beta.87](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.86...v3.13.0-beta.87) (2026-05-29)
**Note:** Version bump only for package @ohif/extension-dicom-microscopy
# [3.13.0-beta.86](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.85...v3.13.0-beta.86) (2026-05-28)
**Note:** Version bump only for package @ohif/extension-dicom-microscopy
# [3.13.0-beta.85](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.84...v3.13.0-beta.85) (2026-05-28)
**Note:** Version bump only for package @ohif/extension-dicom-microscopy
# [3.13.0-beta.84](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.83...v3.13.0-beta.84) (2026-05-27)
**Note:** Version bump only for package @ohif/extension-dicom-microscopy
# [3.13.0-beta.83](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.82...v3.13.0-beta.83) (2026-05-26)
**Note:** Version bump only for package @ohif/extension-dicom-microscopy
# [3.13.0-beta.82](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.81...v3.13.0-beta.82) (2026-05-22)
**Note:** Version bump only for package @ohif/extension-dicom-microscopy
# [3.13.0-beta.81](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.80...v3.13.0-beta.81) (2026-05-21)
**Note:** Version bump only for package @ohif/extension-dicom-microscopy
# [3.13.0-beta.80](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.79...v3.13.0-beta.80) (2026-05-20)
**Note:** Version bump only for package @ohif/extension-dicom-microscopy
+25 -23
View File
@@ -1,39 +1,34 @@
{
"name": "@ohif/extension-dicom-microscopy",
"version": "3.13.0-beta.80",
"version": "3.13.0-beta.109",
"description": "OHIF extension for DICOM microscopy",
"author": "Bill Wallace, md-prog",
"license": "MIT",
"repository": "OHIF/Viewers",
"main": "dist/ohif-extension-dicom-microscopy.umd.js",
"module": "src/index.tsx",
"engines": {
"node": ">=24"
},
"files": [
"dist/**",
"public/**",
"README.md"
],
"repository": "OHIF/Viewers",
"keywords": [
"ohif-extension"
],
"module": "src/index.tsx",
"engines": {
"node": ">=14",
"npm": ">=6",
"yarn": ">=1.18.0"
},
"scripts": {
"clean": "shx rm -rf dist",
"clean:deep": "yarn run clean && shx rm -rf node_modules",
"dev": "cross-env NODE_ENV=development webpack --config .webpack/webpack.dev.js --watch --output-pathinfo",
"dev:dicom-pdf": "yarn run dev",
"build": "cross-env NODE_ENV=production webpack --config .webpack/webpack.prod.js",
"build:package-1": "yarn run build",
"start": "yarn run dev"
"clean:deep": "pnpm run clean && shx rm -rf node_modules",
"dev": "cross-env NODE_ENV=development rspack build --config .webpack/webpack.dev.js --watch",
"dev:dicom-pdf": "pnpm run dev",
"build": "cross-env NODE_ENV=production rspack build --config .webpack/webpack.prod.js",
"build:package-1": "pnpm run build",
"start": "pnpm run dev"
},
"peerDependencies": {
"@ohif/core": "3.13.0-beta.80",
"@ohif/extension-default": "3.13.0-beta.80",
"@ohif/i18n": "3.13.0-beta.80",
"@ohif/ui": "3.13.0-beta.80",
"@ohif/core": "workspace:*",
"@ohif/extension-default": "workspace:*",
"@ohif/i18n": "workspace:*",
"@ohif/ui": "workspace:*",
"prop-types": "15.8.1",
"react": "18.3.1",
"react-dom": "18.3.1",
@@ -42,12 +37,19 @@
"react-router-dom": "6.30.3"
},
"dependencies": {
"@babel/runtime": "7.28.2",
"@babel/runtime": "7.29.7",
"@cornerstonejs/codec-charls": "1.2.3",
"@cornerstonejs/codec-libjpeg-turbo-8bit": "1.2.2",
"@cornerstonejs/codec-openjpeg": "1.3.0",
"colormap": "2.3.2",
"dicom-microscopy-viewer": "0.48.6",
"lodash.debounce": "4.0.8",
"mathjs": "12.4.3"
}
},
"devDependencies": {
"cross-env": "7.0.3"
},
"keywords": [
"ohif-extension"
]
}
@@ -1,9 +1,9 @@
const webpack = require('webpack');
const webpack = require('@rspack/core');
const { merge } = require('webpack-merge');
const path = require('path');
const webpackCommon = require('./../../../.webpack/webpack.base.js');
const pkg = require('./../package.json');
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
const MiniCssExtractPlugin = webpack.CssExtractRspackPlugin;
const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;
@@ -37,8 +37,10 @@ module.exports = (env, argv) => {
},
output: {
path: ROOT_DIR,
library: 'ohif-extension-dicom-pdf',
libraryTarget: 'umd',
library: {
name: 'ohif-extension-dicom-pdf',
type: 'umd',
},
filename: `${pkg.main}`,
},
externals: [/\b(vtk.js)/, /\b(dcmjs)/, /\b(gl-matrix)/, /^@ohif/, /^@cornerstonejs/],
Loaded 100 of 629 files, more files were not shown because too many files have changed in this diff. Show more