diff --git a/.circleci/config.yml b/.circleci/config.yml index 10b61cf55ed..a11e00b022c 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -24,7 +24,7 @@ executors: chrome-and-pacs: docker: # Primary container image where all steps run. - - image: 'cypress/browsers:node14.15.0-chrome86-ff82' + - image: 'cypress/browsers:node14.17.0-chrome88-ff89' defaults: &defaults docker: @@ -181,7 +181,7 @@ jobs: DEPLOY_TO_DEV: docker: - - image: circleci/node:14.15.0 + - image: circleci/node:14.17.0 environment: TERM: xterm NETLIFY_SITE_ID: 32708787-c9b0-4634-b50f-7ca41952da77 @@ -196,7 +196,7 @@ jobs: DEPLOY_TO_STAGING: docker: - - image: circleci/node:14.15.0 + - image: circleci/node:14.17.0 environment: TERM: xterm NETLIFY_SITE_ID: c7502ae3-b150-493c-8422-05701e44a969 @@ -211,7 +211,7 @@ jobs: DEPLOY_TO_PRODUCTION: docker: - - image: circleci/node:14.15.0 + - image: circleci/node:14.17.0 environment: TERM: xterm NETLIFY_SITE_ID: 79c4a5da-5c95-4dc9-84f7-45fd9dfe21b0 @@ -263,7 +263,7 @@ jobs: echo "//registry.npmjs.org/:_authToken=$NPM_TOKEN" > ~/repo/.npmrc - run: npx lerna version - run: yarn run build:package-all - - run: npx lerna publish from-package + - run: yarn run lerna:publish - run: npx lerna run ci:generateSuccessVersion --stream - persist_to_workspace: root: ~/repo @@ -367,12 +367,13 @@ workflows: executor: cypress/browsers-chrome76 browser: chrome pre-steps: - - run: 'rm -rf ~/.yarn && yarn -v && yarn global - add wait-on' + - run: 'rm -rf ~/.yarn && yarn -v && yarn global add wait-on' yarn: true store_artifacts: false working_directory: platform/viewer - build: yarn test:data && npx cross-env QUICK_BUILD=true APP_CONFIG=config/dicomweb-server.js yarn run build + build: + yarn test:data && npx cross-env QUICK_BUILD=true + APP_CONFIG=config/dicomweb-server.js yarn run build # start server --> verify running --> percy + chrome + cypress command: yarn run test:e2e:dist cache-key: 'yarn-packages-{{ checksum "yarn.lock" }}' @@ -434,7 +435,9 @@ workflows: yarn: true store_artifacts: false working_directory: platform/viewer - build: npx cross-env QUICK_BUILD=true APP_CONFIG=config/e2e.js yarn run build + build: + npx cross-env QUICK_BUILD=true APP_CONFIG=config/e2e.js yarn run + build # start server --> verify running --> percy + chrome + cypress command: yarn run test:e2e:dist cache-key: 'yarn-packages-{{ checksum "yarn.lock" }}' diff --git a/.eslintignore b/.eslintignore new file mode 100644 index 00000000000..b722f6a0365 --- /dev/null +++ b/.eslintignore @@ -0,0 +1,4 @@ +config/** +docs/** +img/** +node_modules diff --git a/.eslintrc.json b/.eslintrc.json index c543ada9f1c..55db8c49169 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -12,12 +12,7 @@ "settings": { "react": { "version": "detect" - - }, - "rules": { - "react/jsx-props-no-spreading": "error", - "react-hooks/exhaustive-deps": "false", - "import/no-unused-modules": [1, {"unusedExports": true}] + } }, "globals": { "cy": true, diff --git a/.gitignore b/.gitignore index 706f5ead678..45d0fb3c8d1 100644 --- a/.gitignore +++ b/.gitignore @@ -36,3 +36,6 @@ screenshots/ # Locize settings .locize + +# autogenerated files +platform/viewer/src/pluginImports.js diff --git a/.netlify/package.json b/.netlify/package.json index 36223752451..ca122efa12f 100644 --- a/.netlify/package.json +++ b/.netlify/package.json @@ -2,7 +2,7 @@ "name": "root", "private": true, "engines": { - "node": ">=10", + "node": ">=14", "npm": ">=6", "yarn": ">=1.16.0" }, diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 00000000000..dd449725e18 --- /dev/null +++ b/.prettierignore @@ -0,0 +1 @@ +*.md diff --git a/.webpack/rules/cssToJavaScript.js b/.webpack/rules/cssToJavaScript.js index 24d44e80380..aa2aafafc32 100644 --- a/.webpack/rules/cssToJavaScript.js +++ b/.webpack/rules/cssToJavaScript.js @@ -1,7 +1,9 @@ const autoprefixer = require('autoprefixer'); const path = require('path'); const tailwindcss = require('tailwindcss'); -const tailwindConfigPath = path.resolve('tailwind.config.js'); +const tailwindConfigPath = path.resolve( + '../../platform/viewer/tailwind.config.js' +); const MiniCssExtractPlugin = require('mini-css-extract-plugin'); const devMode = process.env.NODE_ENV !== 'production'; diff --git a/.webpack/webpack.base.js b/.webpack/webpack.base.js index 40249f39b6e..d5cd08f49a8 100644 --- a/.webpack/webpack.base.js +++ b/.webpack/webpack.base.js @@ -3,17 +3,20 @@ const dotenv = require('dotenv'); // const path = require('path'); const webpack = require('webpack'); + +// ~~ PLUGINS +const BundleAnalyzerPlugin = require('webpack-bundle-analyzer') + .BundleAnalyzerPlugin; +const TerserJSPlugin = require('terser-webpack-plugin'); +const CopyPlugin = require('copy-webpack-plugin'); + +// ~~ PackageJSON const PACKAGE = require('../platform/viewer/package.json'); // ~~ RULES const loadShadersRule = require('./rules/loadShaders.js'); const loadWebWorkersRule = require('./rules/loadWebWorkers.js'); const transpileJavaScriptRule = require('./rules/transpileJavaScript.js'); const cssToJavaScript = require('./rules/cssToJavaScript.js'); -// ~~ PLUGINS -const BundleAnalyzerPlugin = require('webpack-bundle-analyzer') - .BundleAnalyzerPlugin; -const TerserJSPlugin = require('terser-webpack-plugin'); -const CopyPlugin = require('copy-webpack-plugin'); // ~~ ENV VARS const NODE_ENV = process.env.NODE_ENV; @@ -39,16 +42,16 @@ module.exports = (env, argv, { SRC_DIR, DIST_DIR }) => { app: `${SRC_DIR}/index.js`, }, optimization: { - splitChunks: { - // include all types of chunks - chunks: 'all', - }, + // splitChunks: { + // // include all types of chunks + // chunks: 'all', + // }, //runtimeChunk: 'single', minimize: isProdBuild, sideEffects: true, }, output: { - clean: true, + // clean: true, publicPath: '/', }, context: SRC_DIR, @@ -98,6 +101,8 @@ module.exports = (env, argv, { SRC_DIR, DIST_DIR }) => { path.resolve(__dirname, '../node_modules'), // Hoisted Yarn Workspace Modules path.resolve(__dirname, '../../../node_modules'), + path.resolve(__dirname, '../platform/viewer/node_modules'), + path.resolve(__dirname, '../platform/ui/node_modules'), SRC_DIR, ], // Attempt to resolve these extensions in order. @@ -131,15 +136,6 @@ module.exports = (env, argv, { SRC_DIR, DIST_DIR }) => { }), // Uncomment to generate bundle analyzer // new BundleAnalyzerPlugin(), - new CopyPlugin({ - patterns: [ - { - from: - '../../../node_modules/cornerstone-wado-image-loader/dist/dynamic-import', - to: DIST_DIR, - }, - ], - }), ], }; diff --git a/extensions/_example/src/index.js b/extensions/_example/src/index.js index 2ff6b785ffe..c4fdb9eb1cd 100644 --- a/extensions/_example/src/index.js +++ b/extensions/_example/src/index.js @@ -5,7 +5,7 @@ import { IWebApiDataSource } from '@ohif/core'; * */ export default { - id: 'org.ohif.*', + id: '@ohif/extension-*', /** * LIFECYCLE HOOKS diff --git a/extensions/cornerstone/.webpack/webpack.dev.js b/extensions/cornerstone/.webpack/webpack.dev.js index 1ae30844802..db7c206b134 100644 --- a/extensions/cornerstone/.webpack/webpack.dev.js +++ b/extensions/cornerstone/.webpack/webpack.dev.js @@ -1,5 +1,5 @@ const path = require('path'); -const webpackCommon = require('./../../../.webpack/webpack.commonjs.js'); +const webpackCommon = require('./../../../.webpack/webpack.base.js'); const SRC_DIR = path.join(__dirname, '../src'); const DIST_DIR = path.join(__dirname, '../dist'); diff --git a/extensions/cornerstone/.webpack/webpack.prod.js b/extensions/cornerstone/.webpack/webpack.prod.js index ca612cd37e1..a45f2b86be4 100644 --- a/extensions/cornerstone/.webpack/webpack.prod.js +++ b/extensions/cornerstone/.webpack/webpack.prod.js @@ -1,8 +1,9 @@ const webpack = require('webpack'); -const merge = require('webpack-merge'); +const { merge } = require('webpack-merge'); const path = require('path'); -const webpackCommon = require('./../../../.webpack/webpack.commonjs.js'); +const webpackCommon = require('./../../../.webpack/webpack.base.js'); const pkg = require('./../package.json'); +const MiniCssExtractPlugin = require('mini-css-extract-plugin'); const ROOT_DIR = path.join(__dirname, './..'); const SRC_DIR = path.join(__dirname, '../src'); @@ -39,6 +40,10 @@ module.exports = (env, argv) => { new webpack.optimize.LimitChunkCountPlugin({ maxChunks: 1, }), + new MiniCssExtractPlugin({ + filename: './dist/[name].css', + chunkFilename: './dist/[id].css', + }), ], }); }; diff --git a/extensions/cornerstone/package.json b/extensions/cornerstone/package.json index f982b00b839..918561a15de 100644 --- a/extensions/cornerstone/package.json +++ b/extensions/cornerstone/package.json @@ -8,7 +8,7 @@ "main": "dist/index.umd.js", "module": "src/index.js", "engines": { - "node": ">=10", + "node": ">=14", "npm": ">=6", "yarn": ">=1.16.0" }, @@ -16,6 +16,9 @@ "dist", "README.md" ], + "keywords": [ + "ohif-extension" + ], "publishConfig": { "access": "public" }, @@ -29,8 +32,8 @@ "test:unit:ci": "jest --ci --runInBand --collectCoverage" }, "peerDependencies": { - "@ohif/core": "^0.50.0", - "@ohif/ui": "^0.50.0", + "@ohif/core": "^3.0.0", + "@ohif/ui": "^2.0.0", "cornerstone-core": "2.6.0", "cornerstone-math": "0.1.9", "cornerstone-tools": "6.0.2", diff --git a/extensions/cornerstone/src/id.js b/extensions/cornerstone/src/id.js index 6721cf5c414..ebe5acd98ae 100644 --- a/extensions/cornerstone/src/id.js +++ b/extensions/cornerstone/src/id.js @@ -1,3 +1,5 @@ -const id = 'org.ohif.cornerstone'; +import packageJson from '../package.json'; -export default id; +const id = packageJson.name; + +export { id }; diff --git a/extensions/cornerstone/src/index.js b/extensions/cornerstone/src/index.js index c3345756e55..58b0e181dde 100644 --- a/extensions/cornerstone/src/index.js +++ b/extensions/cornerstone/src/index.js @@ -1,7 +1,8 @@ import React from 'react'; import init from './init.js'; import commandsModule from './commandsModule.js'; -import CornerstoneViewportDownloadForm from './CornerstoneViewportDownloadForm'; +import { id } from './id.js'; +// import CornerstoneViewportDownloadForm from './CornerstoneViewportDownloadForm'; const Component = React.lazy(() => { return import(/* webpackPrefetch: true */ './OHIFCornerstoneViewport'); @@ -22,8 +23,7 @@ export default { /** * Only required property. Should be a unique value across all extensions. */ - id: 'org.ohif.cornerstone', - + id, /** * * @@ -59,5 +59,3 @@ export default { return commandsModule({ servicesManager, commandsManager }); }, }; - -export { CornerstoneViewportDownloadForm }; diff --git a/extensions/default/.webpack/webpack.dev.js b/extensions/default/.webpack/webpack.dev.js index 1ae30844802..db7c206b134 100644 --- a/extensions/default/.webpack/webpack.dev.js +++ b/extensions/default/.webpack/webpack.dev.js @@ -1,5 +1,5 @@ const path = require('path'); -const webpackCommon = require('./../../../.webpack/webpack.commonjs.js'); +const webpackCommon = require('./../../../.webpack/webpack.base.js'); const SRC_DIR = path.join(__dirname, '../src'); const DIST_DIR = path.join(__dirname, '../dist'); diff --git a/extensions/default/.webpack/webpack.prod.js b/extensions/default/.webpack/webpack.prod.js index f5103890276..e509d1853ba 100644 --- a/extensions/default/.webpack/webpack.prod.js +++ b/extensions/default/.webpack/webpack.prod.js @@ -1,10 +1,11 @@ const webpack = require('webpack'); -const merge = require('webpack-merge'); +const { merge } = require('webpack-merge'); const path = require('path'); -const webpackCommon = require('./../../../.webpack/webpack.commonjs.js'); -const pkg = require('./../package.json'); +const webpackCommon = require('./../../../.webpack/webpack.base.js'); +const MiniCssExtractPlugin = require('mini-css-extract-plugin'); -const ROOT_DIR = path.join(__dirname, './..'); +const pkg = require('./../package.json'); +const ROOT_DIR = path.join(__dirname, './../'); const SRC_DIR = path.join(__dirname, '../src'); const DIST_DIR = path.join(__dirname, '../dist'); @@ -30,7 +31,7 @@ module.exports = (env, argv) => { }, output: { path: ROOT_DIR, - library: 'OHIFExtDefault', + library: 'OHIFExtCornerstone', libraryTarget: 'umd', libraryExport: 'default', filename: pkg.main, @@ -39,6 +40,10 @@ module.exports = (env, argv) => { new webpack.optimize.LimitChunkCountPlugin({ maxChunks: 1, }), + new MiniCssExtractPlugin({ + filename: './dist/[name].css', + chunkFilename: './dist/[id].css', + }), ], }); }; diff --git a/extensions/default/package.json b/extensions/default/package.json index ab404853f47..ce02c79c460 100644 --- a/extensions/default/package.json +++ b/extensions/default/package.json @@ -1,6 +1,6 @@ { "name": "@ohif/extension-default", - "version": "1.0.1", + "version": "3.0.0", "description": "Common/default features and functionality for basic image viewing", "author": "OHIF Core Team", "license": "MIT", @@ -11,7 +11,7 @@ "access": "public" }, "engines": { - "node": ">=10", + "node": ">=14", "npm": ">=6", "yarn": ">=1.18.0" }, @@ -19,6 +19,9 @@ "dist", "README.md" ], + "keywords": [ + "ohif-extension" + ], "scripts": { "dev": "cross-env NODE_ENV=development webpack --config .webpack/webpack.dev.js --watch --debug --output-pathinfo", "dev:dicom-pdf": "yarn run dev", @@ -27,16 +30,14 @@ "start": "yarn run dev" }, "peerDependencies": { - "@ohif/core": "^0.50.0", - "@ohif/i18n": "^0.52.8", + "@ohif/core": "^3.0.0", + "@ohif/i18n": "^1.0.0", "dcmjs": "0.16.1", "dicomweb-client": "^0.6.0", "prop-types": "^15.6.2", - "react": "^16.13.1", - "react-dom": "^16.13.1", + "react": "^17.0.2", + "react-dom": "^17.0.2", "react-i18next": "^10.11.0", - "react-router": "next", - "react-router-dom": "next", "webpack": "^5.50.0", "webpack-merge": "^5.7.3" }, diff --git a/extensions/default/src/Panels/PanelStudyBrowser.jsx b/extensions/default/src/Panels/PanelStudyBrowser.jsx index aaac606d035..d375237c8e1 100644 --- a/extensions/default/src/Panels/PanelStudyBrowser.jsx +++ b/extensions/default/src/Panels/PanelStudyBrowser.jsx @@ -165,18 +165,22 @@ function PanelStudyBrowser({ ); const updatedExpandedStudyInstanceUIDs = shouldCollapseStudy ? // eslint-disable-next-line prettier/prettier - [ - ...expandedStudyInstanceUIDs.filter( - stdyUid => stdyUid !== StudyInstanceUID - ), - ] + [ + ...expandedStudyInstanceUIDs.filter( + stdyUid => stdyUid !== StudyInstanceUID + ), + ] : [...expandedStudyInstanceUIDs, StudyInstanceUID]; setExpandedStudyInstanceUIDs(updatedExpandedStudyInstanceUIDs); if (!shouldCollapseStudy) { - const madeInClient = true - requestDisplaySetCreationForStudy(DisplaySetService, StudyInstanceUID, madeInClient); + const madeInClient = true; + requestDisplaySetCreationForStudy( + DisplaySetService, + StudyInstanceUID, + madeInClient + ); } } diff --git a/extensions/default/src/ViewerLayout/index.jsx b/extensions/default/src/ViewerLayout/index.jsx index c57f8761515..d377f688de2 100644 --- a/extensions/default/src/ViewerLayout/index.jsx +++ b/extensions/default/src/ViewerLayout/index.jsx @@ -1,5 +1,6 @@ import React, { useEffect, useState } from 'react'; import PropTypes from 'prop-types'; +import { useNavigate } from 'react-router-dom'; import { useTranslation } from 'react-i18next'; import { SidePanel, @@ -12,12 +13,10 @@ import { import i18n from '@ohif/i18n'; import { hotkeys } from '@ohif/core'; -import { useNavigate } from 'react-router-dom'; +import { useAppConfig } from '@state'; const { availableLanguages, defaultLanguage, currentLanguage } = i18n; -import { useAppConfig } from '@state'; - function Toolbar({ servicesManager }) { const { ToolBarService } = servicesManager.services; const [toolbarButtons, setToolbarButtons] = useState([]); @@ -94,7 +93,7 @@ function ViewerLayout({ const navigate = useNavigate(); const onClickReturnButton = () => { - navigate('/') + navigate('/'); }; const { t } = useTranslation(); @@ -108,7 +107,12 @@ function ViewerLayout({ { title: t('Header:About'), icon: 'info', - onClick: () => show({ content: AboutModal, title: 'About OHIF Viewer', contentProps: { versionNumber, buildNumber } }), + onClick: () => + show({ + content: AboutModal, + title: 'About OHIF Viewer', + contentProps: { versionNumber, buildNumber }, + }), }, { title: t('Header:Preferences'), @@ -136,7 +140,7 @@ function ViewerLayout({ hide(); }, onReset: () => hotkeysManager.restoreDefaultBindings(), - hotkeysModule: hotkeys + hotkeysModule: hotkeys, }, }), }, @@ -185,7 +189,11 @@ function ViewerLayout({ return (
- extensions + extensionDependencies | extensions needed by the mode | @@ -180,7 +195,8 @@ developers write their extensions to create re-usable functionalities that later can be used by `modes`. Now, it is time to describe how the registered extensions will get utilized for a workflow mode via its `id`. -To use a module element you can use the +Each `mode` has a list of its `extensions dependencies` which are the +the `extension` name and version number. In addition, to use a module element you can use the `${extensionId}.${moduleType}.${element.name}` schema. For instance, if a mode requires the left panel with name of `AIPanel` that is added by the `myAIExtension` via the following `getPanelModule` code, it should address it as @@ -217,47 +233,37 @@ function getPanelModule({ } ``` -Now, let's look at `longitudinal` mode which consumes various functionalities -from different extensions. Note that, you don't need to have -`org.ohif.extensionName`, this is a pattern we chose to name our -[OHIF-maintained](../extensions/index.md#ohif-maintained-extensions) extensions, -you can simply have `extensionName` as the `id` for yours and refer to it inside -your modes. +Now, let's look at a simplified code of the `basic viewer` mode which consumes various functionalities +from different extensions. ```js -export default function mode({ modeConfiguration }) { + +const extensionDependencies = { + '@ohif/extension-default': '^3.0.0', + '@ohif/extension-cornerstone': '^3.0.0', + '@ohif/extension-measurement-tracking': '^3.0.0', +}; + +const id = 'viewer'; +const version = '3.0.0'; + +function modeFactory({ modeConfiguration }) { return { - /* - ... - */ + id, + // ... routes: [ { - /* - ... - */ + // ... layoutTemplate: ({ location, servicesManager }) => { return { id: ohif.layout, props: { - leftPanels: [ - 'org.ohif.measurement-tracking.panelModule.seriesList', - ], - rightPanels: [ - 'org.ohif.measurement-tracking.panelModule.trackedMeasurements', - ], + leftPanels: ['@ohif/extension-measurement-tracking.panelModule.seriesList'], + rightPanels: ['@ohif/extension-measurement-tracking.panelModule.trackedMeasurements'], viewports: [ { - namespace: - 'org.ohif.measurement-tracking.viewportModule.cornerstone-tracked', - displaySetsToDisplay: [ - 'org.ohif.default.sopClassHandlerModule.stack', - ], - }, - { - namespace: 'org.ohif.dicom-sr.viewportModule.dicom-sr', - displaySetsToDisplay: [ - 'org.ohif.dicom-sr.sopClassHandlerModule.dicom-sr', - ], + namespace: '@ohif/extension-measurement-tracking.viewportModule.cornerstone-tracked', + displaySetsToDisplay: ['@ohif/extension-default.sopClassHandlerModule.stack'], }, ], }, @@ -265,20 +271,20 @@ export default function mode({ modeConfiguration }) { }, }, ], - extensions: [ - 'org.ohif.default', - 'org.ohif.cornerstone', - 'org.ohif.measurement-tracking', - 'org.ohif.dicom-sr', - ], - hangingProtocols: ['org.ohif.default.hangingProtocolModule.petCT'], - sopClassHandlers: [ - 'org.ohif.default.sopClassHandlerModule.stack', - 'org.ohif.dicom-sr.sopClassHandlerModule.dicom-sr', - ], - /*...*/ + extensions: extensionDependencies, + hangingProtocols: ['@ohif/extension-default.hangingProtocolModule.petCT'], + sopClassHandlers: ['@ohif/extension-default.sopClassHandlerModule.stack'], + // ... }; } + +const mode = { + id, + modeFactory, + extensionDependencies, +} + +export default mode ``` ### Routes @@ -288,56 +294,19 @@ of the viewer at the designated route is defined by the `layoutTemplate` and `init` functions for the route. We will learn more about each of the above properties inside the [route documentation](./routes.md) -### Extensions - -Currently `extensions` property in the mode config is used to add -_contextModule_ of the mentioned extensions to the list of contexts and provide -them through out the app. Since extensions are registered by the -ExtensionManager, modes have access to them even if they have not been -referred in the mode config file inside _extensions_ property. -[Read more about extension registration](../extensions/index.md#registering-an-extension) - -```js title="platform/viewer/src/routes/Mode/Mode.jsx" -const { extensions } = mode; - -extensions.forEach(extensionId => { - const allRegisteredModuleIds = Object.keys(extensionManager.modulesMap); - const moduleIds = allRegisteredModuleIds.filter(id => - id.includes(`${extensionId}.contextModule.`) - ); - - const modules = moduleIds.map(extensionManager.getModuleEntry); - contextModules = contextModules.concat(modules); -}); -``` ### HangingProtocols Currently, you can pass your defined hanging protocols inside the -`hangingProtocols` property of the mode's config. This will get used inside the -`Mode.jsx` to configure the `HangingProtocolService`. - -```js title="platform/viewer/src/routes/Mode/Mode.jsx" -const { hangingProtocols } = mode; - -hangingProtocols.forEach(extentionProtocols => { - const { protocols } = extensionManager.getModuleEntry(extentionProtocols); - HangingProtocolService.addProtocols(protocols); -}); -``` +`hangingProtocols` property of the mode's config. This will get registered +inside `HangingProtocolService`. ### SopClassHandlers Mode's configuration also accepts the `sopClassHandler` modules that have been -added by the extensions. This information will get used inside the `Mode.jsx` to -initialize the `DisplaySetService` with the provided SOPClass modules which +added by the extensions. This information will get used to initialize `DisplaySetService` with the provided SOPClass modules which handles creation of the displaySets. -```js title="platform/viewer/src/routes/Mode/Mode.jsx" -const { sopClassHandlers } = mode; - -DisplaySetService.init(extensionManager, sopClassHandlers); -``` ### Hotkeys @@ -366,8 +335,9 @@ const myHotkeys = [ }, ] -export default function mode() { +function modeFactory() { return { + id: '', id: '', displayName: '', /* @@ -376,70 +346,38 @@ export default function mode() { hotkeys: [..hotkeys.defaults.hotkeyBindings, ...myHotkeys], } } -``` -```js title="platform/viewer/src/routes/Mode/Mode.jsx" -hotkeysManager.setDefaultHotKeys(hotkeys); -hotkeysManager.setHotkeys(hotkeys); +// exports ``` ## Registration -Upon release modes will also be plugged into the app via configuration, but this -is still an area which is under development/discussion, and they are currently -pulled from the `window` in beta. +Similar to extension registration, `viewer` will look inside the `pluginConfig.json` to +find the `modes` to register. -```js title="modes/longitudinal/src/index.js" -export default function mode() { - return { - id: 'viewer', - displayName: 'Basic Viewer', - onModeEnter: () => { - /**...**/ - }, - onModeExit: () => { - /**...**/ - }, - validationTags: { - /**...**/ - }, - isValidMode: () => { - /**...**/ + +```js title=platform/viewer/pluginConfig.json +// Simplified version of the `pluginConfig.json` file +{ + "extensions": [ + { + "packageName": "@ohif/extension-cornerstone", + "version": "3.0.0" }, - routes: [ - { - path: 'longitudinal', - init: () => { - /**...**/ - }, - layoutTemplate: () => { - /**...**/ - }, - }, - ], - extensions: [ - /**...**/ - ], - hangingProtocols: [ - /**...**/ - ], - sopClassHandlers: [ - /**...**/ - ], - hotkeys: [ - /**...**/ - ], - }; + // ... + ], + "modes": [ + { + "packageName": "@ohif/mode-longitudinal", + "version": "0.0.1" + } + ] } - -window.longitudinalMode = mode({}); ``` -and inside `@ohif/viewer` we have: +:::note Important +You SHOULD NOT directly register modes in the `pluginConfig.json` file. +Use the provided `cli` to add/remove/install/uninstall modes. Read more [here](../../development/ohif-cli.md) +::: -```js title="platform/viewer/src/appInit.js" -if (!appConfig.modes.length) { - appConfig.modes.push(window.longitudinalMode); - // appConfig.modes.push(window.segmentationMode); -} -``` +The final registration and import of the modes happen inside a non-tracked file `pluginImport.js` (this file is also for internal use only). diff --git a/platform/docs/docs/platform/modes/installation.md b/platform/docs/docs/platform/modes/installation.md index 74a62756486..08c456d29e1 100644 --- a/platform/docs/docs/platform/modes/installation.md +++ b/platform/docs/docs/platform/modes/installation.md @@ -3,206 +3,10 @@ sidebar_position: 5 sidebar_label: Installation --- -# Mode: Installation +# Modes: Installation -OHIF-v3 provides the ability to utilize external modes and extensions. In this -document we will describe how to add/install external modes. +OHIF-v3 provides the ability to utilize external modes. -> Our long-term plan is to make OHIF-v3 capable of installing modes at runtime, -> however in the meantime, please use the instructions below to manually install -> modes and their extensions. -## Installing a Mode - -### 1) Mode Files Copy - -We use a [Template Mode](https://github.com/OHIF/mode-template) repository to -demonstrate how to install an external mode. This repository also includes all -the files required to create a new mode. You can use this repository as a -starting point to create your own mode. - -As you can see in the Template mode -[code base](https://github.com/OHIF/mode-template), folders structure are -similar to the OHIF-maintained modes. Let's have more detailed look at the -structure of the `Template mode`: - -- `src/index.js`: The most important file in any mode. This file is where modes' - authors hav defined the mode configurations such as: - - The layout and the panels for left and right side. - - LifeCycle hooks such as `onModeEnter` and `onModeExit` -- Other files/folders/configs: .webpack, LICENSE, README.md, babel.config.js - -Note: It is highly recommended to use the `Template Mode` as a starting point -for your own mode. This way, you can easily reuse the necessary files and -folders. - -#### Package.json - -Mode name is defined in the `package.json` file. The `package.json` file is a -JSON file that defines the mode name, version, and dependencies. For instance -for the Template mode, the `package.json` file looks like this: - -```js {2} title="templateMode/package.json" -{ - "name": "@ohif/mode-template", - "version": "0.0.1", - "description": "A template mode for installation demonstration", - ... -} -``` - -Note 1: We will use the `@ohif/mode-template` inside OHIF to let OHIF know about -existence of this mode. - -Note 2: You don't need to use the `@ohif` scope for your modes/extensions. You -can use any scope you want or none at all. - -Note 3: Although folders names are not important and the `package.json` file -contains the mode name, we recommend to use the same name for the folder name. - -![Template Mode](../../assets/img/template-mode-files.png) - -### 2) Configuring OHIF - -In order to install/register the mode, we must make changes to a few areas -inside OHIF. The OHIF should be updated using the following lines of code: - -#### Viewer's package.json - -```js {6} titl="platform/viewer/package.json" -/* ... */ -"dependencies": { - /* ... */ - "@ohif/i18n": "^0.52.8", - "@ohif/mode-longitudinal": "^0.0.1", - "@ohif/mode-template": "^0.0.1", - "@ohif/ui": "^2.0.0", - "@types/react": "^16.0.0", - /* ... */ -} -``` - -#### App.jsx - -```js {3} title="platform/viewer/src/App.jsx" -/* ... */ -import '@ohif/mode-longitudinal'; -import '@ohif/mode-template'; -/* ... */ -``` - -#### appInit.js - -```js {4} title="platform/viewer/src/appInit.js" -/* ... */ -if (!appConfig.modes.length) { - appConfig.modes.push(window.longitudinalMode); - appConfig.modes.push(window.templateMode); -} -/* ... */ -``` - -Note that we are assigning mode configuration objects from the `window` object; -therefore, we should use the reference to the name of the mode which were -defined in the last line of `src/index.js` file in mode configuration - -```js {8} title="templateMode/src/index.js" -/* ... */ -export default function mode({ modeConfiguration }) { - return { - /** */ - }; -} - -window.templateMode = mode({}); -``` - -### Required Extensions for a Mode - -Some modes require external extensions to be installed. For instance, the -`@ohif/mode-longitudinal` mode requires the `@ohif/cornerstone` extension to be -registered/installed in OHIF which is available in the OHIF-v3 repository. - -How do we know that a mode requires an extension? (Until we have a more proper -dependency management for modes and extensions) you can take a look inside the -mode itself. Mode is a configuration file that defines the layout -(layoutModule), panels (panelModule), viewport (viewportModule), and tools -(commands) that are used to create an application at a given route. By looking -inside the mode configuration file (`index.js`), you can see which extensions -are required by the mode in the `extensions` property. - -```js {12-16} title="platform/viewer/src/appInit.js" -export default function mode({ modeConfiguration }) { - return { - id: 'template', - displayName: 'Template Mode', - /** ... */ - - routes: [ - { - /** ... */ - }, - ], - extensions: [ - 'extension.template', - 'org.ohif.default', - 'org.ohif.cornerstone', - ], - /** ... */ - }; -} -``` - -As seen, our `Template Mode` requires the `org.ohif.default`, -`org.ohif.cornerstone` and `extension.template` extensions. - -> Note: Currently extensions dependencies are not handled by OHIF from the -> `extensions` property. We will be adding this feature in the future. - -In addition to the `extensions` property, the `mode` configuration object also -has the reference for each module that is used. For instance, the `index.js` -file in the `@ohif/mode-template` mode looks like this: - -```js {10} title="clockMode/src/index.js" -// .... -routes: [ - { - path: "template", - layoutTemplate: ({ location, servicesManager }) => { - return { - id: ohif.layout, - props: { - leftPanels: [], - rightPanels: ["extension.template.panelModule.clockPanel"], - viewports: [ - { - namespace: "org.ohif.cornerstone.viewportModule.cornerstone", - displaySetsToDisplay: ["org.ohif.default.sopClassHandlerModule.stack"], - }, - ], - }, - }; - }, - }, -], -// .... -``` - -As seen, the right panel is defined as -`"extension.template.panelModule.clockPanel"` which means that the -`@ohif/mode-template` mode requires the `extension.template`. You can read more -about installing extensions in the -[Extension Installation](../extensions/installation.md) - -> Additionally you can check the commands that the toolbar buttons will execute -> in the `toolbarButtons` and see if any of them requires an extension. - -After you installed the extension, you need to run `yarn install` in the root -folder of the OHIF repository to install the registered extension and modes. - -Running `yarn dev` will then start the application with the installed mode, by -navigating to the `/template` route (e.g., -http://localhost:3000/template?StudyInstanceUIDs=1.3.6.1.4.1.14519.5.2.1.2744.7002.150059977302243314164020079415) -you can see the clock panel. Congrats! 🎉 - -![](../../assets/img/template-mode-ui.png) +You can use ohif `cli` tool to install both local and publicly published +modes on NPM. You can read more [here](../../development/ohif-cli.md) diff --git a/platform/docs/docs/platform/modes/lifecycle.md b/platform/docs/docs/platform/modes/lifecycle.md index eb3f7a0242b..0e5ef9955bc 100644 --- a/platform/docs/docs/platform/modes/lifecycle.md +++ b/platform/docs/docs/platform/modes/lifecycle.md @@ -23,9 +23,10 @@ For instance, in `longitudinal` mode we are using this hook to initialize the buttons to the toolbar. ```js -export default function mode() { +function modeFactory() { return { id: '', + version: '', displayName: '', onModeEnter: ({ servicesManager, extensionManager }) => { const { ToolBarService } = servicesManager.services; @@ -68,7 +69,7 @@ For instance, it can be used to reset the `ToolbarService` which reset the toggled buttons. ```js -export default function mode() { +function modeFactory() { return { id: '', displayName: '', diff --git a/platform/docs/docs/platform/modes/routes.md b/platform/docs/docs/platform/modes/routes.md index aa646579e3c..1a149decf68 100644 --- a/platform/docs/docs/platform/modes/routes.md +++ b/platform/docs/docs/platform/modes/routes.md @@ -28,9 +28,10 @@ configuration: route (panels, viewports) ```js -export default function mode() { +function modeFactory() { return { id: 'viewer', + version: '3.0.0', displayName: '', routes: [ { @@ -43,23 +44,23 @@ export default function mode() { id: ohif.layout, props: { leftPanels: [ - 'org.ohif.measurement-tracking.panelModule.seriesList', + '@ohif/extension-measurement-tracking.panelModule.seriesList', ], rightPanels: [ - 'org.ohif.measurement-tracking.panelModule.trackedMeasurements', + '@ohif/extension-measurement-tracking.panelModule.trackedMeasurements', ], viewports: [ { namespace: - 'org.ohif.measurement-tracking.viewportModule.cornerstone-tracked', + '@ohif/extension-measurement-tracking.viewportModule.cornerstone-tracked', displaySetsToDisplay: [ - 'org.ohif.default.sopClassHandlerModule.stack', + '@ohif/extension-default.sopClassHandlerModule.stack', ], }, { - namespace: 'org.ohif.dicom-sr.viewportModule.dicom-sr', + namespace: '@ohif/extension-dicom-sr.viewportModule.dicom-sr', displaySetsToDisplay: [ - 'org.ohif.dicom-sr.sopClassHandlerModule.dicom-sr', + '@ohif/extension-dicom-sr.sopClassHandlerModule.dicom-sr', ], }, ], @@ -251,7 +252,7 @@ the extension, and any mode that is interested in using `layoutTemplate-2` */ layoutTemplate: ({ location, servicesManager }) => { return { - id: 'org.ohif.default.layoutTemplateModule.viewerLayout', + id: '@ohif/extension-default.layoutTemplateModule.viewerLayout', props: { leftPanels: [ 'myExtension.panelModule.leftPanel1', @@ -291,7 +292,7 @@ component you have written for that route. `Mode` handle showing the correct component for the specified route. ```js -export default function mode() { +function modeFactory() { return { id: 'viewer', displayName: '', diff --git a/platform/docs/docs/platform/modes/validity.md b/platform/docs/docs/platform/modes/validity.md index 44835652a5f..e3f70af7668 100644 --- a/platform/docs/docs/platform/modes/validity.md +++ b/platform/docs/docs/platform/modes/validity.md @@ -13,8 +13,6 @@ There are two mechanism for checking the validity of a mode for a study. - - ## isValidMode This hook can be used to define a function that return a `boolean` which decided the validity of the mode based on `StudyInstanceUID` and `modalities` that are in the study. @@ -22,7 +20,7 @@ validity of the mode based on `StudyInstanceUID` and `modalities` that are in th For instance, for pet-ct mode, both `PT` and 'CT' modalities should be available inside the study. ```js -export default function mode() { +function modeFactory() { return { id: '', displayName: '', diff --git a/platform/docs/docs/platform/services/config-point-service.md b/platform/docs/docs/platform/services/config-point-service.md deleted file mode 100644 index 3c94da5fd73..00000000000 --- a/platform/docs/docs/platform/services/config-point-service.md +++ /dev/null @@ -1,224 +0,0 @@ -# Config Point Service -The Config Point service is based on the external library -[config-point](https://github.com/OHIF/config-point). -It is a service that allows exposing internal "static" configuration data -for modification by sites at load time by defining "theme" files. For -information on the configuration side of things, see [theme-configuration](../../configuration/theme-conffiguration.md). - -The service isn't a traditional OHIF service available in the services -deployment, but is rather a service which exposes static declarations of -data as configurable data. For example, suppose a list of modalities -was required for the search constraints. The core code might decide to -supply such a list by default, but sites may want to customize it to -only list the actual modalities they use. Further, they might want to -change the name of some of these. The core code could declare the modalities -list in a file like this: -```js -export default const { ModalitiesList } = ConfigPoint.register({ - ModalitiesList: [ - "CR", - {id: "MR", description: "Magnetic Resonance Imaging"}, - {id: "CT", name: "Computed Tomography"}, - ] -}) -``` -The CR modality is just a plain name, whereas MR includes a description, -and CT includes an alternate name. That list is used exactly as a straight -list for display, so the code needs to understand both simple strings and -the simple object definitions with id and/or name and description. - -Now, a site might also happen to have an Ultrasound modality, so they would -want to extend the list. They could then follow the instructions in the -theme-configuration area to add the "US" device. One way of doing that is -by editing the `platform/viewer/public/theme/theme.json5` file, and adding -the following element to it: -```js -{ - ... - ModalitiesList: { - US: {id: 'US', name: 'Ultrasound'}, - }, -} -``` - -The intent of the config point service is to expose a configuration point -that can be further modified. The exposed point needs to be basically -a constant/static definition. This may involve reworking some of the -code design to extract the configuration value from the dynamic code. For -example, in the above modalities list, the original declaration is: -```js - inputProps: { - options: [ - { value: 'AR', label: 'AR' }, - { value: 'ASMT', label: 'ASMT' }, - ... -``` -so the constant should be extracted to its own file, but it is a fairly -simple list, so extracting it that way is fairly easy. A more complex example -might be the columns displayed in the search page. These are directly -referenced in the WorkList as bits of code that have both the configuration -and the ReactJs functionality. To allow configuring this, the change would -need to extract what should be displayed into a config point, from the actual -rendering logic to render a table. - -A table rendering component might be defined externally, something like: -```js -import {patientInfoFilter} from './filtersMeta.js'; - -export default const {WorkListColumns} = ConfigPoint.register({ - WorkListColumns: [ - { // This one has custom row and query rendering - id: 'PatientInfo', - rowRender: (props) => {... function to render a row }, - queryRender: patientInfoFilter, - }, - { // This one defaults the row and query to fetching StudyDescription - id: 'Description', - rowData: 'StudyDescription', - } - ... rest of columns - ], -}) -``` -Note how this version includes functions as static data, as well as -the basic data structure. It would then need to be rendered by iterating -over the elements of the WorkListColumns. - -Always the basic idea is that the code extracts a literal declaration of -data, defaulting to the base behaviour of the application, allowing it to -be exposed later to enhancements in a declarative fashion. The remaining -sections below simply expand on the idea with some more advanced concepts. - -## Automatic Sorting of Data -Sometimes, data needs to be sorted in the provided order. The above -examples show how that can be done, but in other cases, it is desirable -to allow sorting the data, either with the initially provided sort order, -or with additional sort constraints. This can be done by adding a -configOperation to the literal value. The operation is applied at the -time the value is first retrieved, and then is cached (unless the value is -further updated, in which case it is re-calculated). - -In the WorkListColumns example, this would look like: -```js -WorkListColumns: { - configOperation: 'sort', - sortKey: 'priority', - usePositionAsSortKey: true, - value: [ ... definition above of original columns] - }, -``` -This would sort the value in the original order it was defined in, and then -any news items would get sorted into position. - -## Transform Data -It is possible that you want a transformation of the data. For example, -there might be a simple declarative form for a value, but you want a custom -version to be used for display. For example, setting default values for -missing attributes, or for turning a simple string into a full definition of -a name. Here is a possible example: -```js -// Declared as part of the base configuration, which is always included -// first -configBase: { - ModalitiesList: { - configOperation: 'reference', - transform: list => list.map(str => ({id:str, name:str, value:str})), - }, -}, -// Then, for this example, the same name is referenced to assign the value, -ModalitiesList: ["CR", "CT"], -``` -This type of transform needs to be declared in code because of the need -to reference a function, but allows for having a uniform format, but -including an easy declarative form. - -## Reference Data -Sometimes you want to separate long declarations into their own configuration -item. You can do this via the reference operation, for example: -```js -export default const { HangingProtocols } = ConfigPoint.register({ - MGHangingProtocol: { - rightOnLeft: // Definition for MG Hanging Protocol right on left, - leftOnLeft: // Definition for MG hanging protocol left on left - }, - CRHangingProtocol: // Defn for CR hanging protocol - ... - HangingProtocols: { - default: // Definition of default HP - protocols: [ - // Reference the child element rightOnLeft of MGHangingProtocols - {configOperation: 'reference', source: "MGHangingProtocols", reference: "rightOnLeft"}, - // Just reference the entire object CRHangingProtocol - {configOperation: 'reference', source: 'CRHangingProtocol',}, - // Reference default within the current config point instance - {configOperation: 'reference', reference: 'default',} -``` - -## Theme Load Timing -The theme values are intended to be constants for a given instance of OHIF. -However, because the actual theme files are loaded at startup time, if an initial -render is performed before all the theme files have been fully loaded, it may -be necessary to re-render after all the theme loads have completed. There is -a listener service which can be used to listen for load events, and then to -re-render the display. It works like: -```js - if (!this._listenThemeProtocols) { - this._listenThemeProtocols = this.listenThemeProtocols.bind(this); - } - ConfigPoint.addLoadListener(ThemeProtocols, this._listenThemeProtocols); -``` - -This is NOT intended for listening for programmatic changes to the configuration, -but is intended only for load time updates. Again, the idea is that the -configuration points are constants loaded from configuration files. - -## Mode Changes to Configuration Point -If the config points are "constants", then one might ask how different modes -can end up apply different configuration values. The answer to that is to -add a new configuration point specific to that mode, and to change the referenced -name of which configuration gets used. For example, suppose a "Mammo Mode" wanted -to specify a bunch of customizations to configuration such as the set of -hanging protocols to apply. One way to do that is to have the mode specify -the name of the configuration, and then to use the initial/default configuration -as a base, and extend it, something like this: -```js -const {MGHangingProtocols} = ConfigPoint.register({ - MGHangingProtocols: { - // The config base says to START with the value from the named - // config point as the base values. - configBase: 'HangingProtocols', - // Now, just extend the protocols list with the mg protocols. - protocols: { - mgProtocol1: ... - mgProtocol2: ... - } - -``` - -As an alternative to extending the protocols, they can be replaced via: -```js -protocols: { - configOperation: 'replace', - value: [ - // mgProtocol1, - // mgProtocol2 - ] -} -``` -The replace operation is an immediate operation, and allows the protocols -list to be extended in the normal fashion. - -The MGHangingProtocols value would then just be set as the mode hanging -protocols object, and it would then be used directly. It could also have -been set by name instead of value, depending on the desired context. - -The site can then extend the MGHangingProtocols in the usual way, by creating -custom theme files. - -# Concluding Remarks -Use the configuration point service to extract things that might need to be -configured by some sites by extracting the data into a constant declaration, -and then register it with config point to expose it. Then, document -your configuration points in the [theme-configuration](../../configuration/theme-configuration.md) -guide. That will allow sites to make declarative configuration changes to -the values. diff --git a/platform/docs/docs/platform/services/data/HangingProtocolService.md b/platform/docs/docs/platform/services/data/HangingProtocolService.md index 7613b50b2e4..269e409c2eb 100644 --- a/platform/docs/docs/platform/services/data/HangingProtocolService.md +++ b/platform/docs/docs/platform/services/data/HangingProtocolService.md @@ -241,7 +241,7 @@ const getTimePointUID = (metaData) => { return myBackEndAPI(metaData) } -export default function mode() { +function modeFactory() { return { id: 'myMode', /** .. **/ diff --git a/platform/docs/netlify.toml b/platform/docs/netlify.toml index b9b50acfbe2..2e73378ea62 100644 --- a/platform/docs/netlify.toml +++ b/platform/docs/netlify.toml @@ -16,7 +16,7 @@ [build.environment] # If 'production', `yarn install` does not install devDependencies NODE_ENV = "development" - NODE_VERSION = "12.13.0" + NODE_VERSION = "14.19.1" YARN_VERSION = "1.22.0" RUBY_VERSION = "2.6.2" YARN_FLAGS = "--no-ignore-optional --pure-lockfile" diff --git a/platform/docs/package.json b/platform/docs/package.json index f352b2a7b52..0d6fdb33703 100644 --- a/platform/docs/package.json +++ b/platform/docs/package.json @@ -1,6 +1,6 @@ { "name": "ohif-docs", - "version": "0.0.0", + "version": "0.0.1", "private": true, "workspaces": { "nohoist": [ @@ -59,6 +59,6 @@ }, "devDependencies": { "postcss-import": "^14.0.2", - "postcss-preset-env": "^6.7.0" + "postcss-preset-env": "^7.4.3" } } diff --git a/platform/i18n/.webpack/webpack.dev.js b/platform/i18n/.webpack/webpack.dev.js index 1ae30844802..db7c206b134 100644 --- a/platform/i18n/.webpack/webpack.dev.js +++ b/platform/i18n/.webpack/webpack.dev.js @@ -1,5 +1,5 @@ const path = require('path'); -const webpackCommon = require('./../../../.webpack/webpack.commonjs.js'); +const webpackCommon = require('./../../../.webpack/webpack.base.js'); const SRC_DIR = path.join(__dirname, '../src'); const DIST_DIR = path.join(__dirname, '../dist'); diff --git a/platform/i18n/.webpack/webpack.prod.js b/platform/i18n/.webpack/webpack.prod.js index 9865b26c8ff..fd164230cf6 100644 --- a/platform/i18n/.webpack/webpack.prod.js +++ b/platform/i18n/.webpack/webpack.prod.js @@ -1,6 +1,7 @@ -const merge = require('webpack-merge'); +const { merge } = require('webpack-merge'); const path = require('path'); -const webpackCommon = require('./../../../.webpack/webpack.commonjs.js'); + +const webpackCommon = require('./../../../.webpack/webpack.base.js'); const pkg = require('./../package.json'); const ROOT_DIR = path.join(__dirname, './..'); diff --git a/platform/i18n/package.json b/platform/i18n/package.json index 53df5f4090c..3a03b04e854 100644 --- a/platform/i18n/package.json +++ b/platform/i18n/package.json @@ -1,6 +1,6 @@ { "name": "@ohif/i18n", - "version": "0.52.8", + "version": "1.0.0", "description": "Internationalization library for The OHIF Viewer", "author": "OHIF", "license": "MIT", @@ -8,7 +8,7 @@ "main": "dist/index.umd.js", "module": "src/index.js", "engines": { - "node": ">=10", + "node": ">=14", "npm": ">=6", "yarn": ">=1.16.0" }, diff --git a/platform/ui/.webpack/webpack.dev.js b/platform/ui/.webpack/webpack.dev.js index 1ae30844802..db7c206b134 100644 --- a/platform/ui/.webpack/webpack.dev.js +++ b/platform/ui/.webpack/webpack.dev.js @@ -1,5 +1,5 @@ const path = require('path'); -const webpackCommon = require('./../../../.webpack/webpack.commonjs.js'); +const webpackCommon = require('./../../../.webpack/webpack.base.js'); const SRC_DIR = path.join(__dirname, '../src'); const DIST_DIR = path.join(__dirname, '../dist'); diff --git a/platform/ui/.webpack/webpack.prod.js b/platform/ui/.webpack/webpack.prod.js index 38172746802..016f5da281a 100644 --- a/platform/ui/.webpack/webpack.prod.js +++ b/platform/ui/.webpack/webpack.prod.js @@ -1,5 +1,7 @@ const { merge } = require('webpack-merge'); const path = require('path'); +const MiniCssExtractPlugin = require('mini-css-extract-plugin'); + const webpackCommon = require('./../../../.webpack/webpack.base.js'); const pkg = require('./../package.json'); @@ -37,5 +39,11 @@ module.exports = (env, argv) => { react: 'React', 'react-dom': 'ReactDOM', }, + plugins: [ + new MiniCssExtractPlugin({ + filename: `./dist/[name].css`, + chunkFilename: `./dist/[id].css`, + }), + ], }); }; diff --git a/platform/ui/package.json b/platform/ui/package.json index 8e7d8999139..b5f701f0260 100644 --- a/platform/ui/package.json +++ b/platform/ui/package.json @@ -10,7 +10,7 @@ "access": "public" }, "engines": { - "node": ">=10", + "node": ">=14", "npm": ">=6", "yarn": ">=1.16.0" }, diff --git a/platform/viewer/.webpack/webpack.pwa.js b/platform/viewer/.webpack/webpack.pwa.js index 409d8d767af..aad4acab1c6 100644 --- a/platform/viewer/.webpack/webpack.pwa.js +++ b/platform/viewer/.webpack/webpack.pwa.js @@ -10,6 +10,7 @@ const CopyWebpackPlugin = require('copy-webpack-plugin'); const HtmlWebpackPlugin = require('html-webpack-plugin'); const { InjectManifest } = require('workbox-webpack-plugin'); const MiniCssExtractPlugin = require('mini-css-extract-plugin'); +const CopyPlugin = require('copy-webpack-plugin'); // ~~ Directories const SRC_DIR = path.join(__dirname, '../src'); const DIST_DIR = path.join(__dirname, '../dist'); @@ -22,6 +23,9 @@ const PROXY_TARGET = process.env.PROXY_TARGET; const PROXY_DOMAIN = process.env.PROXY_DOMAIN; const ENTRY_TARGET = process.env.ENTRY_TARGET || `${SRC_DIR}/index.js`; const Dotenv = require('dotenv-webpack'); +const writePluginImportFile = require('./writePluginImportsFile.js'); + +writePluginImportFile(SRC_DIR); const setHeaders = (res, path) => { if (path.indexOf('.gz') !== -1) { @@ -112,6 +116,15 @@ module.exports = (env, argv) => { // Increase the limit to 4mb: maximumFileSizeToCacheInBytes: 4 * 1024 * 1024, }), + new CopyPlugin({ + patterns: [ + { + from: + '../../../node_modules/cornerstone-wado-image-loader/dist/dynamic-import', + to: DIST_DIR, + }, + ], + }), ], // https://webpack.js.org/configuration/dev-server/ devServer: { diff --git a/platform/viewer/.webpack/writePluginImportsFile.js b/platform/viewer/.webpack/writePluginImportsFile.js new file mode 100644 index 00000000000..2590b5b6c70 --- /dev/null +++ b/platform/viewer/.webpack/writePluginImportsFile.js @@ -0,0 +1,82 @@ +const pluginConfig = require('../pluginConfig.json'); +const fs = require('fs'); + +const autogenerationDisclaimer = ` +// THIS FILE IS AUTOGENERATED AS PART OF THE EXTENSION AND MODE PLUGIN PROCESS. +// IT SHOULD NOT BE MODIFIED MANUALLY \n`; + +function constructLines(input, categoryName) { + let pluginCount = 0; + + const lines = { + importLines: [], + addToWindowLines: [], + }; + + input.forEach(entry => { + const packageName = entry.packageName; + + const defaultImportName = `${categoryName}${pluginCount}`; + + lines.importLines.push( + `import ${defaultImportName} from '${packageName}';\n` + ); + lines.addToWindowLines.push( + `window.${categoryName}.push(${defaultImportName});\n` + ); + + pluginCount++; + }); + + return lines; +} + +function getFormattedImportBlock(importLines) { + let content = ''; + // Imports + importLines.forEach(importLine => { + content += importLine; + }); + + return content; +} + +function getFormattedWindowBlock(addToWindowLines) { + let content = `window.extensions = [];\nwindow.modes = [];\n\n`; + + addToWindowLines.forEach(addToWindowLine => { + content += addToWindowLine; + }); + + return content; +} + +function writePluginImportsFile(SRC_DIR) { + let pluginImportsJsContent = autogenerationDisclaimer; + + const extensionLines = constructLines(pluginConfig.extensions, 'extensions'); + const modeLines = constructLines(pluginConfig.modes, 'modes'); + + pluginImportsJsContent += getFormattedImportBlock([ + ...extensionLines.importLines, + ...modeLines.importLines, + ]); + pluginImportsJsContent += getFormattedWindowBlock([ + ...extensionLines.addToWindowLines, + ...modeLines.addToWindowLines, + ]); + + fs.writeFileSync( + `${SRC_DIR}/pluginImports.js`, + pluginImportsJsContent, + { flag: 'w+' }, + err => { + if (err) { + console.error(err); + return; + } + } + ); +} + +module.exports = writePluginImportsFile; diff --git a/platform/viewer/netlify.toml b/platform/viewer/netlify.toml index 53176467486..6949be0913c 100644 --- a/platform/viewer/netlify.toml +++ b/platform/viewer/netlify.toml @@ -20,7 +20,7 @@ [build.environment] # If 'production', `yarn install` does not install devDependencies NODE_ENV = "development" - NODE_VERSION = "12.13.0" + NODE_VERSION = "14.19.1" YARN_VERSION = "1.22.0" RUBY_VERSION = "2.6.2" YARN_FLAGS = "--no-ignore-optional --pure-lockfile" diff --git a/platform/viewer/package.json b/platform/viewer/package.json index 188ae2c3386..f55e304457c 100644 --- a/platform/viewer/package.json +++ b/platform/viewer/package.json @@ -1,6 +1,6 @@ { "name": "@ohif/viewer", - "version": "4.0.0", + "version": "5.0.0", "productVersion": "3.0.8", "description": "OHIF Viewer", "author": "OHIF Contributors", @@ -12,7 +12,7 @@ "access": "public" }, "engines": { - "node": ">=10", + "node": ">=14", "npm": ">=6", "yarn": ">=1.16.0" }, @@ -49,11 +49,13 @@ }, "dependencies": { "@babel/runtime": "7.16.3", - "@ohif/core": "^2.5.1", - "@ohif/extension-cornerstone": "^2.4.0", - "@ohif/extension-measurement-tracking": "^0.0.1", - "@ohif/i18n": "^0.52.8", - "@ohif/mode-longitudinal": "^0.0.1", + "@ohif/core": "^3.0.0", + "@ohif/extension-cornerstone": "^3.0.0", + "@ohif/extension-default": "^3.0.0", + "@ohif/extension-dicom-sr": "^3.0.0", + "@ohif/extension-measurement-tracking": "^3.0.0", + "@ohif/i18n": "^1.0.0", + "@ohif/mode-longitudinal": "^3.0.0", "@ohif/ui": "^2.0.0", "@types/react": "^16.0.0", "classnames": "^2.2.6", @@ -65,7 +67,7 @@ "dicom-parser": "^1.8.9", "dotenv-webpack": "^1.7.0", "hammerjs": "^2.0.8", - "history": "5.0.0", + "history": "^5.2.0", "i18next": "^17.0.3", "i18next-browser-languagedetector": "^3.0.1", "lodash.isequal": "4.5.0", @@ -78,8 +80,8 @@ "react-dropzone": "^10.1.7", "react-i18next": "^10.11.0", "react-resize-detector": "^4.2.0", - "react-router": "next", - "react-router-dom": "next" + "react-router": "^6.2.1", + "react-router-dom": "^6.2.1" }, "devDependencies": { "@percy/cypress": "^2.3.0", diff --git a/platform/viewer/pluginConfig.json b/platform/viewer/pluginConfig.json new file mode 100644 index 00000000000..a5caf24bdc6 --- /dev/null +++ b/platform/viewer/pluginConfig.json @@ -0,0 +1,34 @@ +{ + "extensions": [ + { + "packageName": "@ohif/extension-cornerstone", + "version": "3.0.0" + }, + { + "packageName": "@ohif/extension-measurement-tracking", + "version": "3.0.0" + }, + { + "packageName": "@ohif/extension-dicom-sr", + "version": "3.0.0" + }, + { + "packageName": "@ohif/extension-default", + "version": "3.0.0" + }, + { + "packageName": "@ohif/extension-dicom-pdf", + "version": "3.0.1" + }, + { + "packageName": "@ohif/extension-dicom-video", + "version": "3.0.1" + } + ], + "modes": [ + { + "packageName": "@ohif/mode-longitudinal", + "version": "3.0.0" + } + ] +} diff --git a/platform/viewer/public/config/aws.js b/platform/viewer/public/config/aws.js index ccdb7949aea..d209de51fe9 100644 --- a/platform/viewer/public/config/aws.js +++ b/platform/viewer/public/config/aws.js @@ -8,7 +8,7 @@ window.config = { dataSources: [ { friendlyName: 'dcmjs DICOMWeb Server', - namespace: 'org.ohif.default.dataSourcesModule.dicomweb', + namespace: '@ohif/extension-default.dataSourcesModule.dicomweb', sourceName: 'dicomweb', configuration: { name: 'DCM4CHEE', @@ -28,7 +28,7 @@ window.config = { }, { friendlyName: 'dicom json', - namespace: 'org.ohif.default.dataSourcesModule.dicomjson', + namespace: '@ohif/extension-default.dataSourcesModule.dicomjson', sourceName: 'dicomjson', configuration: { name: 'json', @@ -36,7 +36,7 @@ window.config = { }, { friendlyName: 'dicom local', - namespace: 'org.ohif.default.dataSourcesModule.dicomlocal', + namespace: '@ohif/extension-default.dataSourcesModule.dicomlocal', sourceName: 'dicomlocal', configuration: {}, }, diff --git a/platform/viewer/public/config/default.js b/platform/viewer/public/config/default.js index fa6358a54ec..ccad643070f 100644 --- a/platform/viewer/public/config/default.js +++ b/platform/viewer/public/config/default.js @@ -8,7 +8,7 @@ window.config = { dataSources: [ { friendlyName: 'dcmjs DICOMWeb Server', - namespace: 'org.ohif.default.dataSourcesModule.dicomweb', + namespace: '@ohif/extension-default.dataSourcesModule.dicomweb', sourceName: 'dicomweb', configuration: { name: 'DCM4CHEE', @@ -26,7 +26,7 @@ window.config = { }, { friendlyName: 'dicom json', - namespace: 'org.ohif.default.dataSourcesModule.dicomjson', + namespace: '@ohif/extension-default.dataSourcesModule.dicomjson', sourceName: 'dicomjson', configuration: { name: 'json', @@ -34,7 +34,7 @@ window.config = { }, { friendlyName: 'dicom local', - namespace: 'org.ohif.default.dataSourcesModule.dicomlocal', + namespace: '@ohif/extension-default.dataSourcesModule.dicomlocal', sourceName: 'dicomlocal', configuration: {}, }, diff --git a/platform/viewer/public/config/dicomweb-server.js b/platform/viewer/public/config/dicomweb-server.js index 6cb8b2e1aac..81d11db144b 100644 --- a/platform/viewer/public/config/dicomweb-server.js +++ b/platform/viewer/public/config/dicomweb-server.js @@ -8,7 +8,7 @@ window.config = { dataSources: [ { friendlyName: 'dcmjs DICOMWeb Server', - namespace: 'org.ohif.default.dataSourcesModule.dicomweb', + namespace: '@ohif/extension-default.dataSourcesModule.dicomweb', sourceName: 'dicomweb', configuration: { name: 'DCM4CHEE', diff --git a/platform/viewer/public/config/docker_nginx-orthanc.js b/platform/viewer/public/config/docker_nginx-orthanc.js index bc552947d13..281b7f92047 100644 --- a/platform/viewer/public/config/docker_nginx-orthanc.js +++ b/platform/viewer/public/config/docker_nginx-orthanc.js @@ -6,7 +6,7 @@ window.config = { dataSources: [ { friendlyName: 'Orthanc Server', - namespace: 'org.ohif.default.dataSourcesModule.dicomweb', + namespace: '@ohif/extension-default.dataSourcesModule.dicomweb', sourceName: 'dicomweb', configuration: { name: 'Orthanc', diff --git a/platform/viewer/public/config/docker_openresty-orthanc.js b/platform/viewer/public/config/docker_openresty-orthanc.js index 2a60ba2fe36..7f768e27995 100644 --- a/platform/viewer/public/config/docker_openresty-orthanc.js +++ b/platform/viewer/public/config/docker_openresty-orthanc.js @@ -6,7 +6,7 @@ window.config = { dataSources: [ { friendlyName: 'Orthanc Server', - namespace: 'org.ohif.default.dataSourcesModule.dicomweb', + namespace: '@ohif/extension-default.dataSourcesModule.dicomweb', sourceName: 'dicomweb', configuration: { name: 'Orthanc', diff --git a/platform/viewer/public/config/e2e.js b/platform/viewer/public/config/e2e.js index d6fc7e9dd4f..c3c2cb49fbf 100644 --- a/platform/viewer/public/config/e2e.js +++ b/platform/viewer/public/config/e2e.js @@ -8,7 +8,7 @@ window.config = { dataSources: [ { friendlyName: 'StaticWado test data', - namespace: 'org.ohif.default.dataSourcesModule.dicomweb', + namespace: '@ohif/extension-default.dataSourcesModule.dicomweb', sourceName: 'dicomweb', configuration: { name: 'DCM4CHEE', @@ -23,12 +23,12 @@ window.config = { supportsFuzzyMatching: false, supportsWildcard: true, staticWado: true, - singlepart: "video,thumbnail,pdf", + singlepart: 'video,thumbnail,pdf', }, }, // { // friendlyName: 'StaticWado default data', - // namespace: 'org.ohif.default.dataSourcesModule.dicomweb', + // namespace: '@ohif/extension-default.dataSourcesModule.dicomweb', // sourceName: 'dicomweb', // configuration: { // name: 'DCM4CHEE', @@ -47,7 +47,7 @@ window.config = { // }, { friendlyName: 'dicom json', - namespace: 'org.ohif.default.dataSourcesModule.dicomjson', + namespace: '@ohif/extension-default.dataSourcesModule.dicomjson', sourceName: 'dicomjson', configuration: { name: 'json', @@ -55,7 +55,7 @@ window.config = { }, { friendlyName: 'dicom local', - namespace: 'org.ohif.default.dataSourcesModule.dicomlocal', + namespace: '@ohif/extension-default.dataSourcesModule.dicomlocal', sourceName: 'dicomlocal', configuration: {}, }, diff --git a/platform/viewer/public/config/google.js b/platform/viewer/public/config/google.js index 6de99fe2b8a..fbd9d28c125 100644 --- a/platform/viewer/public/config/google.js +++ b/platform/viewer/public/config/google.js @@ -28,7 +28,7 @@ window.config = { dataSources: [ { friendlyName: 'dcmjs DICOMWeb Server', - namespace: 'org.ohif.default.dataSourcesModule.dicomweb', + namespace: '@ohif/extension-default.dataSourcesModule.dicomweb', sourceName: 'dicomweb', configuration: { name: 'GCP', @@ -48,7 +48,7 @@ window.config = { }, { friendlyName: 'dicom json', - namespace: 'org.ohif.default.dataSourcesModule.dicomjson', + namespace: '@ohif/extension-default.dataSourcesModule.dicomjson', sourceName: 'dicomjson', configuration: { name: 'json', @@ -56,7 +56,7 @@ window.config = { }, { friendlyName: 'dicom local', - namespace: 'org.ohif.default.dataSourcesModule.dicomlocal', + namespace: '@ohif/extension-default.dataSourcesModule.dicomlocal', sourceName: 'dicomlocal', configuration: {}, }, diff --git a/platform/viewer/public/config/local_dcm4chee.js b/platform/viewer/public/config/local_dcm4chee.js index 09de4c2d14c..561c582f604 100644 --- a/platform/viewer/public/config/local_dcm4chee.js +++ b/platform/viewer/public/config/local_dcm4chee.js @@ -6,7 +6,7 @@ window.config = { dataSources: [ { friendlyName: 'DCM4CHEE Server', - namespace: 'org.ohif.default.dataSourcesModule.dicomweb', + namespace: '@ohif/extension-default.dataSourcesModule.dicomweb', sourceName: 'dicomweb', configuration: { name: 'DCM4CHEE', diff --git a/platform/viewer/public/config/local_static.js b/platform/viewer/public/config/local_static.js index b31eca387c7..bd998fc0e61 100644 --- a/platform/viewer/public/config/local_static.js +++ b/platform/viewer/public/config/local_static.js @@ -8,7 +8,7 @@ window.config = { dataSources: [ { friendlyName: 'Static WADO Local Data', - namespace: 'org.ohif.default.dataSourcesModule.dicomweb', + namespace: '@ohif/extension-default.dataSourcesModule.dicomweb', sourceName: 'dicomweb', configuration: { name: 'DCM4CHEE', @@ -23,12 +23,12 @@ window.config = { supportsFuzzyMatching: false, supportsWildcard: true, staticWado: true, - singlepart: "bulkdata,video,pdf", + singlepart: 'bulkdata,video,pdf', }, }, { friendlyName: 'dicom json', - namespace: 'org.ohif.default.dataSourcesModule.dicomjson', + namespace: '@ohif/extension-default.dataSourcesModule.dicomjson', sourceName: 'dicomjson', configuration: { name: 'json', @@ -36,7 +36,7 @@ window.config = { }, { friendlyName: 'dicom local', - namespace: 'org.ohif.default.dataSourcesModule.dicomlocal', + namespace: '@ohif/extension-default.dataSourcesModule.dicomlocal', sourceName: 'dicomlocal', configuration: {}, }, diff --git a/platform/viewer/public/config/netlify.js b/platform/viewer/public/config/netlify.js index fa6358a54ec..ccad643070f 100644 --- a/platform/viewer/public/config/netlify.js +++ b/platform/viewer/public/config/netlify.js @@ -8,7 +8,7 @@ window.config = { dataSources: [ { friendlyName: 'dcmjs DICOMWeb Server', - namespace: 'org.ohif.default.dataSourcesModule.dicomweb', + namespace: '@ohif/extension-default.dataSourcesModule.dicomweb', sourceName: 'dicomweb', configuration: { name: 'DCM4CHEE', @@ -26,7 +26,7 @@ window.config = { }, { friendlyName: 'dicom json', - namespace: 'org.ohif.default.dataSourcesModule.dicomjson', + namespace: '@ohif/extension-default.dataSourcesModule.dicomjson', sourceName: 'dicomjson', configuration: { name: 'json', @@ -34,7 +34,7 @@ window.config = { }, { friendlyName: 'dicom local', - namespace: 'org.ohif.default.dataSourcesModule.dicomlocal', + namespace: '@ohif/extension-default.dataSourcesModule.dicomlocal', sourceName: 'dicomlocal', configuration: {}, }, diff --git a/platform/viewer/src/App.jsx b/platform/viewer/src/App.jsx index 9b9470306a5..6979035938f 100644 --- a/platform/viewer/src/App.jsx +++ b/platform/viewer/src/App.jsx @@ -24,13 +24,10 @@ import createRoutes from './routes'; import appInit from './appInit.js'; import OpenIdConnectRoutes from './utils/OpenIdConnectRoutes.jsx'; -// TODO: Temporarily for testing -import '@ohif/mode-longitudinal'; - let commandsManager, extensionManager, servicesManager, hotkeysManager; -function App({ config, defaultExtensions }) { - const init = appInit(config, defaultExtensions); +function App({ config, defaultExtensions, defaultModes }) { + const init = appInit(config, defaultExtensions, defaultModes); // Set above for named export commandsManager = init.commandsManager; @@ -63,27 +60,29 @@ function App({ config, defaultExtensions }) { const providers = [ [AppConfigProvider, { value: appConfigState }], - [UserAuthenticationProvider, { service: UserAuthenticationService}], + [UserAuthenticationProvider, { service: UserAuthenticationService }], [I18nextProvider, { i18n }], [ThemeWrapper], - [ViewportGridProvider, {service: ViewportGridService}], - [ViewportDialogProvider, {service: UIViewportDialogService}], - [CineProvider, {service: CineService}], - [SnackbarProvider, {service: UINotificationService}], - [DialogProvider, {service: UIDialogService}], - [ModalProvider, {service: UIModalService, modal: Modal}], - ] + [ViewportGridProvider, { service: ViewportGridService }], + [ViewportDialogProvider, { service: UIViewportDialogService }], + [CineProvider, { service: CineService }], + [SnackbarProvider, { service: UINotificationService }], + [DialogProvider, { service: UIDialogService }], + [ModalProvider, { service: UIModalService, modal: Modal }], + ]; const CombinedProviders = ({ children }) => Compose({ components: providers, children }); let authRoutes = null; if (oidc) { - authRoutes = (