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 (
-
+
@@ -193,7 +201,7 @@ function ViewerLayout({
{/* LEFT SIDEPANELS */} diff --git a/extensions/default/src/getSopClassHandlerModule.js b/extensions/default/src/getSopClassHandlerModule.js index 41a261a44d9..528a18aa153 100644 --- a/extensions/default/src/getSopClassHandlerModule.js +++ b/extensions/default/src/getSopClassHandlerModule.js @@ -1,7 +1,7 @@ import { isImage } from '@ohif/core/src/utils/isImage'; import ImageSet from '@ohif/core/src/classes/ImageSet'; import isDisplaySetReconstructable from '@ohif/core/src/utils/isDisplaySetReconstructable'; -import id from './id'; +import { id } from './id'; const sopClassHandlerName = 'stack'; diff --git a/extensions/default/src/id.js b/extensions/default/src/id.js index d98b866c843..ebe5acd98ae 100644 --- a/extensions/default/src/id.js +++ b/extensions/default/src/id.js @@ -1,3 +1,5 @@ -const id = 'org.ohif.default'; +import packageJson from '../package.json'; -export default id; +const id = packageJson.name; + +export { id }; diff --git a/extensions/default/src/index.js b/extensions/default/src/index.js index 502addf6931..90937672c69 100644 --- a/extensions/default/src/index.js +++ b/extensions/default/src/index.js @@ -5,7 +5,7 @@ import getSopClassHandlerModule from './getSopClassHandlerModule.js'; import getHangingProtocolModule from './getHangingProtocolModule.js'; import getToolbarModule from './getToolbarModule.js'; import commandsModule from './commandsModule'; -import id from './id'; +import { id } from './id.js'; export default { /** diff --git a/extensions/dicom-pdf/package.json b/extensions/dicom-pdf/package.json index 17ea43990d1..cb5bfa0d1ff 100644 --- a/extensions/dicom-pdf/package.json +++ b/extensions/dicom-pdf/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" }, @@ -29,8 +29,8 @@ "test:unit:ci": "jest --ci --runInBand --collectCoverage --passWithNoTests" }, "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/dicom-pdf/src/getSopClassHandlerModule.js b/extensions/dicom-pdf/src/getSopClassHandlerModule.js index fc34fcf8385..ac19112a900 100644 --- a/extensions/dicom-pdf/src/getSopClassHandlerModule.js +++ b/extensions/dicom-pdf/src/getSopClassHandlerModule.js @@ -1,4 +1,4 @@ -import { Name, SOPClassHandlerId } from './id'; +import { SOPClassHandlerId } from './id'; import { utils, classes } from '@ohif/core'; const { ImageSet } = classes; @@ -9,49 +9,67 @@ const SOP_CLASS_UIDS = { const sopClassUids = Object.values(SOP_CLASS_UIDS); - -const _getDisplaySetsFromSeries = (instances, servicesManager, extensionManager) => { +const _getDisplaySetsFromSeries = ( + instances, + servicesManager, + extensionManager +) => { const dataSource = extensionManager.getActiveDataSource()[0]; - return instances - .map(instance => { - const { Modality, SOPInstanceUID, EncapsulatedDocument } = instance; - const { SeriesDescription = "PDF", MIMETypeOfEncapsulatedDocument, } = instance; - const { SeriesNumber, SeriesDate, SeriesInstanceUID, StudyInstanceUID, } = instance; - const pdfUrl = dataSource.retrieve.directURL({ - instance, - tag: 'EncapsulatedDocument', - defaultType: MIMETypeOfEncapsulatedDocument || "application/pdf", - singlepart: "pdf", - }); + return instances.map(instance => { + const { Modality, SOPInstanceUID, EncapsulatedDocument } = instance; + const { + SeriesDescription = 'PDF', + MIMETypeOfEncapsulatedDocument, + } = instance; + const { + SeriesNumber, + SeriesDate, + SeriesInstanceUID, + StudyInstanceUID, + } = instance; + const pdfUrl = dataSource.retrieve.directURL({ + instance, + tag: 'EncapsulatedDocument', + defaultType: MIMETypeOfEncapsulatedDocument || 'application/pdf', + singlepart: 'pdf', + }); - const displaySet = { - //plugin: id, - Modality, - displaySetInstanceUID: utils.guid(), - SeriesDescription, - SeriesNumber, - SeriesDate, - SOPInstanceUID, - SeriesInstanceUID, - StudyInstanceUID, - SOPClassHandlerId, - referencedImages: null, - measurements: null, - pdfUrl, - others: [instance], - thumbnailSrc: dataSource.retrieve.directURL({ instance, defaultPath: "/thumbnail", defaultType: "image/jpeg", tag: "Absent" }), - isDerivedDisplaySet: true, - isLoaded: false, - sopClassUids, - numImageFrames: 0, - numInstances: 1, + const displaySet = { + //plugin: id, + Modality, + displaySetInstanceUID: utils.guid(), + SeriesDescription, + SeriesNumber, + SeriesDate, + SOPInstanceUID, + SeriesInstanceUID, + StudyInstanceUID, + SOPClassHandlerId, + referencedImages: null, + measurements: null, + pdfUrl, + others: [instance], + thumbnailSrc: dataSource.retrieve.directURL({ instance, - }; - return displaySet; - }); + defaultPath: '/thumbnail', + defaultType: 'image/jpeg', + tag: 'Absent', + }), + isDerivedDisplaySet: true, + isLoaded: false, + sopClassUids, + numImageFrames: 0, + numInstances: 1, + instance, + }; + return displaySet; + }); }; -export default function getSopClassHandlerModule({ servicesManager, extensionManager }) { +export default function getSopClassHandlerModule({ + servicesManager, + extensionManager, +}) { const getDisplaySetsFromSeries = instances => { return _getDisplaySetsFromSeries( instances, @@ -62,7 +80,7 @@ export default function getSopClassHandlerModule({ servicesManager, extensionMan return [ { - name: Name, + name: 'dicom-pdf', sopClassUids, getDisplaySetsFromSeries, }, diff --git a/extensions/dicom-pdf/src/id.js b/extensions/dicom-pdf/src/id.js index 6f5978f4059..22e153e1a23 100644 --- a/extensions/dicom-pdf/src/id.js +++ b/extensions/dicom-pdf/src/id.js @@ -1,8 +1,6 @@ -const Name = 'dicom-pdf'; -const id = `org.ohif.${Name}`; +import packageJson from '../package.json'; -export default id; +const id = packageJson.name; +const SOPClassHandlerId = `${id}.sopClassHandlerModule.dicom-pdf`; -const SOPClassHandlerId = `${id}.sopClassHandlerModule.${Name}`; - -export { Name, SOPClassHandlerId, }; +export { id, SOPClassHandlerId }; diff --git a/extensions/dicom-pdf/src/index.js b/extensions/dicom-pdf/src/index.js index b40fc2de8aa..94b8f13493a 100644 --- a/extensions/dicom-pdf/src/index.js +++ b/extensions/dicom-pdf/src/index.js @@ -1,6 +1,6 @@ import React from 'react'; import getSopClassHandlerModule from './getSopClassHandlerModule'; -import id from './id.js'; +import { id } from './id.js'; const Component = React.lazy(() => { return import( @@ -24,23 +24,6 @@ export default { * Only required property. Should be a unique value across all extensions. */ id, - dependencies: [ - // TODO -> This isn't used anywhere yet, but we do have a hard dependency, and need to check for these in the future. - // OHIF-229 - { - id: 'org.ohif.cornerstone', - version: '3.0.0', - }, - { - id: 'org.ohif.measurement-tracking', - version: '^0.0.1', - }, - ], - - preRegistration({ servicesManager, configuration = {} }) { - // No-op for now - }, - /** * * @@ -58,7 +41,9 @@ export default { ); }; - return [{ name: 'dicom-pdf', component: ExtendedOHIFCornerstonePdfViewport }]; + return [ + { name: 'dicom-pdf', component: ExtendedOHIFCornerstonePdfViewport }, + ]; }, getCommandsModule({ servicesManager }) { return { diff --git a/extensions/dicom-sr/.webpack/webpack.dev.js b/extensions/dicom-sr/.webpack/webpack.dev.js index 1ae30844802..db7c206b134 100644 --- a/extensions/dicom-sr/.webpack/webpack.dev.js +++ b/extensions/dicom-sr/.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/dicom-sr/.webpack/webpack.prod.js b/extensions/dicom-sr/.webpack/webpack.prod.js index 61802e5cc8e..08b25fc133d 100644 --- a/extensions/dicom-sr/.webpack/webpack.prod.js +++ b/extensions/dicom-sr/.webpack/webpack.prod.js @@ -1,10 +1,12 @@ 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 MiniCssExtractPlugin = require('mini-css-extract-plugin'); + const pkg = require('./../package.json'); +const webpackCommon = require('./../../../.webpack/webpack.base.js'); -const ROOT_DIR = path.join(__dirname, './..'); +const ROOT_DIR = path.join(__dirname, './../'); const SRC_DIR = path.join(__dirname, '../src'); const DIST_DIR = path.join(__dirname, '../dist'); @@ -30,7 +32,7 @@ module.exports = (env, argv) => { }, output: { path: ROOT_DIR, - library: 'OHIFExtDICOMSR', + library: 'OHIFExtCornerstone', libraryTarget: 'umd', libraryExport: 'default', filename: pkg.main, @@ -39,6 +41,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/dicom-sr/package.json b/extensions/dicom-sr/package.json index c2a03a60642..17914702be0 100644 --- a/extensions/dicom-sr/package.json +++ b/extensions/dicom-sr/package.json @@ -1,6 +1,6 @@ { "name": "@ohif/extension-dicom-sr", - "version": "0.0.1", + "version": "3.0.0", "description": "OHIF extension for an SR Cornerstone Viewport", "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" }, @@ -19,6 +19,9 @@ "publishConfig": { "access": "public" }, + "keywords": [ + "ohif-extension" + ], "scripts": { "dev": "cross-env NODE_ENV=development webpack --config .webpack/webpack.dev.js --watch --debug --output-pathinfo", "dev:cornerstone": "yarn run dev", @@ -29,8 +32,8 @@ "test:unit:ci": "jest --ci --runInBand --collectCoverage --passWithNoTests" }, "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", @@ -40,7 +43,9 @@ "hammerjs": "^2.0.8", "prop-types": "^15.6.2", "react": "^17.0.2", - "react-cornerstone-viewport": "4.1.2" + "react-cornerstone-viewport": "4.1.2", + "@ohif/extension-cornerstone": "^3.0.0", + "@ohif/extension-measurement-tracking": "^3.0.0" }, "dependencies": { "@babel/runtime": "7.16.3", diff --git a/extensions/dicom-sr/src/id.js b/extensions/dicom-sr/src/id.js index 623f69a9080..66191e8cd9c 100644 --- a/extensions/dicom-sr/src/id.js +++ b/extensions/dicom-sr/src/id.js @@ -1,7 +1,8 @@ -const id = 'org.ohif.dicom-sr'; +import packageJson from '../package.json'; -export default id; +const id = packageJson.name; const SOPClassHandlerName = 'dicom-sr'; const SOPClassHandlerId = `${id}.sopClassHandlerModule.${SOPClassHandlerName}`; -export { SOPClassHandlerName, SOPClassHandlerId }; + +export { SOPClassHandlerName, SOPClassHandlerId, id }; diff --git a/extensions/dicom-sr/src/index.js b/extensions/dicom-sr/src/index.js index f366f8b34b1..61fa0a6a19a 100644 --- a/extensions/dicom-sr/src/index.js +++ b/extensions/dicom-sr/src/index.js @@ -1,8 +1,8 @@ import React from 'react'; import getSopClassHandlerModule from './getSopClassHandlerModule'; import onModeEnter from './onModeEnter'; -import id from './id.js'; import init from './init'; +import { id } from './id.js'; const Component = React.lazy(() => { return import( @@ -26,18 +26,6 @@ export default { * Only required property. Should be a unique value across all extensions. */ id, - dependencies: [ - // TODO -> This isn't used anywhere yet, but we do have a hard dependency, and need to check for these in the future. - // OHIF-229 - { - id: 'org.ohif.cornerstone', - version: '3.0.0', - }, - { - id: 'org.ohif.measurement-tracking', - version: '^0.0.1', - }, - ], preRegistration({ servicesManager, configuration = {} }) { init({ servicesManager, configuration }); diff --git a/extensions/dicom-sr/src/init.js b/extensions/dicom-sr/src/init.js index d11b2088de5..6e73d82da2e 100644 --- a/extensions/dicom-sr/src/init.js +++ b/extensions/dicom-sr/src/init.js @@ -1,6 +1,6 @@ import cornerstoneTools from 'cornerstone-tools'; import dicomSRModule from './tools/modules/dicomSRModule'; -import id from './id'; +import { id } from './id'; import TOOL_NAMES from './constants/toolNames'; @@ -14,9 +14,9 @@ const defaultConfig = { * @param {object} configuration */ export default function init({ configuration = {} }) { - const conifg = Object.assign({}, defaultConfig, configuration); + const config = Object.assign({}, defaultConfig, configuration); - TOOL_NAMES.DICOM_SR_DISPLAY_TOOL = conifg.TOOL_NAMES.DICOM_SR_DISPLAY_TOOL; + TOOL_NAMES.DICOM_SR_DISPLAY_TOOL = config.TOOL_NAMES.DICOM_SR_DISPLAY_TOOL; cornerstoneTools.register('module', id, dicomSRModule); } diff --git a/extensions/dicom-sr/src/tools/DICOMSRDisplayTool.js b/extensions/dicom-sr/src/tools/DICOMSRDisplayTool.js index 6e6a19c08a7..6a35250e099 100644 --- a/extensions/dicom-sr/src/tools/DICOMSRDisplayTool.js +++ b/extensions/dicom-sr/src/tools/DICOMSRDisplayTool.js @@ -3,7 +3,7 @@ import { pixelToCanvas } from 'cornerstone-core'; import TOOL_NAMES from '../constants/toolNames'; import SCOORD_TYPES from '../constants/scoordTypes'; -import id from '../id'; +import { id } from '../id'; // Cornerstone 3rd party dev kit imports const draw = importInternal('drawing/draw'); diff --git a/extensions/dicom-sr/src/viewports/OHIFCornerstoneSRViewport.js b/extensions/dicom-sr/src/viewports/OHIFCornerstoneSRViewport.js index de7c8278540..56242fcf669 100644 --- a/extensions/dicom-sr/src/viewports/OHIFCornerstoneSRViewport.js +++ b/extensions/dicom-sr/src/viewports/OHIFCornerstoneSRViewport.js @@ -14,7 +14,7 @@ import { } from '@ohif/ui'; import TOOL_NAMES from './../constants/toolNames'; import { adapters } from 'dcmjs'; -import id from './../id'; +import { id } from './../id'; const { formatDate } = utils; const scrollToIndex = cornerstoneTools.importInternal('util/scrollToIndex'); @@ -23,7 +23,8 @@ const globalImageIdSpecificToolStateManager = const { StackManager, guid } = OHIF.utils; -const MEASUREMENT_TRACKING_EXTENSION_ID = 'org.ohif.measurement-tracking'; +const MEASUREMENT_TRACKING_EXTENSION_ID = + '@ohif/extension-measurement-tracking'; function OHIFCornerstoneSRViewport({ children, @@ -81,7 +82,7 @@ function OHIFCornerstoneSRViewport({ ) ) { const contextModule = extensionManager.getModuleEntry( - 'org.ohif.measurement-tracking.contextModule.TrackedMeasurementsContext' + '@ohif/extension-measurement-tracking.contextModule.TrackedMeasurementsContext' ); const useTrackedMeasurements = () => useContext(contextModule.context); diff --git a/extensions/dicom-video/package.json b/extensions/dicom-video/package.json index 471c91678a5..fe394cb9c4b 100644 --- a/extensions/dicom-video/package.json +++ b/extensions/dicom-video/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" }, @@ -30,7 +30,7 @@ }, "peerDependencies": { "@ohif/core": "^0.50.0", - "@ohif/ui": "^0.50.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/dicom-video/src/getSopClassHandlerModule.js b/extensions/dicom-video/src/getSopClassHandlerModule.js index f1d93a2ef7b..31aebed9b13 100644 --- a/extensions/dicom-video/src/getSopClassHandlerModule.js +++ b/extensions/dicom-video/src/getSopClassHandlerModule.js @@ -1,5 +1,5 @@ -import { Name, SOPClassHandlerId } from './id'; -import { utils, } from '@ohif/core'; +import { SOPClassHandlerId } from './id'; +import { utils } from '@ohif/core'; const SOP_CLASS_UIDS = { VIDEO_MICROSCOPIC_IMAGE_STORAGE: '1.2.840.10008.5.1.4.1.1.77.1.2.1', @@ -25,17 +25,33 @@ const SupportedTransferSyntaxes = { const supportedTransferSyntaxUIDs = Object.values(SupportedTransferSyntaxes); -const _getDisplaySetsFromSeries = (instances, servicesManager, extensionManager) => { +const _getDisplaySetsFromSeries = ( + instances, + servicesManager, + extensionManager +) => { const dataSource = extensionManager.getActiveDataSource()[0]; return instances .filter(metadata => { - const tsuid = metadata.AvailableTransferSyntaxUID || - metadata.TransferSyntaxUID || metadata['00083002']; + const tsuid = + metadata.AvailableTransferSyntaxUID || + metadata.TransferSyntaxUID || + metadata['00083002']; return supportedTransferSyntaxUIDs.includes(tsuid); }) .map(instance => { - const { Modality, SOPInstanceUID, SeriesDescription = "VIDEO" } = instance; - const { SeriesNumber, SeriesDate, SeriesInstanceUID, StudyInstanceUID, NumberOfFrames } = instance; + const { + Modality, + SOPInstanceUID, + SeriesDescription = 'VIDEO', + } = instance; + const { + SeriesNumber, + SeriesDate, + SeriesInstanceUID, + StudyInstanceUID, + NumberOfFrames, + } = instance; const displaySet = { //plugin: id, Modality, @@ -49,9 +65,18 @@ const _getDisplaySetsFromSeries = (instances, servicesManager, extensionManager) SOPClassHandlerId, referencedImages: null, measurements: null, - videoUrl: dataSource.retrieve.directURL({ instance, singlepart: "video", tag: "PixelData", }), + videoUrl: dataSource.retrieve.directURL({ + instance, + singlepart: 'video', + tag: 'PixelData', + }), others: [instance], - thumbnailSrc: dataSource.retrieve.directURL({ instance, defaultPath: "/thumbnail", defaultType: "image/jpeg", tag: "Absent" }), + thumbnailSrc: dataSource.retrieve.directURL({ + instance, + defaultPath: '/thumbnail', + defaultType: 'image/jpeg', + tag: 'Absent', + }), isDerivedDisplaySet: true, isLoaded: false, sopClassUids, @@ -62,7 +87,10 @@ const _getDisplaySetsFromSeries = (instances, servicesManager, extensionManager) }); }; -export default function getSopClassHandlerModule({ servicesManager, extensionManager }) { +export default function getSopClassHandlerModule({ + servicesManager, + extensionManager, +}) { const getDisplaySetsFromSeries = instances => { return _getDisplaySetsFromSeries( instances, @@ -73,7 +101,7 @@ export default function getSopClassHandlerModule({ servicesManager, extensionMan return [ { - name: Name, + name: 'dicom-video', sopClassUids, getDisplaySetsFromSeries, }, diff --git a/extensions/dicom-video/src/id.js b/extensions/dicom-video/src/id.js index 6a36a5e4f95..ef30eaf79c0 100644 --- a/extensions/dicom-video/src/id.js +++ b/extensions/dicom-video/src/id.js @@ -1,8 +1,6 @@ -const Name = 'dicom-video'; -const id = `org.ohif.${Name}`; +import packageJson from '../package.json'; -export default id; +const id = packageJson.name; +const SOPClassHandlerId = `${id}.sopClassHandlerModule.dicom-video`; -const SOPClassHandlerId = `${id}.sopClassHandlerModule.${Name}`; - -export { Name, SOPClassHandlerId, }; +export { SOPClassHandlerId, id }; diff --git a/extensions/dicom-video/src/index.js b/extensions/dicom-video/src/index.js index fb563ab409e..fab345e7655 100644 --- a/extensions/dicom-video/src/index.js +++ b/extensions/dicom-video/src/index.js @@ -1,6 +1,6 @@ import React from 'react'; import getSopClassHandlerModule from './getSopClassHandlerModule'; -import id from './id.js'; +import { id } from './id'; const Component = React.lazy(() => { return import( @@ -24,22 +24,6 @@ export default { * Only required property. Should be a unique value across all extensions. */ id, - dependencies: [ - // TODO -> This isn't used anywhere yet, but we do have a hard dependency, and need to check for these in the future. - // OHIF-229 - { - id: 'org.ohif.cornerstone', - version: '3.0.0', - }, - { - id: 'org.ohif.measurement-tracking', - version: '^0.0.1', - }, - ], - - preRegistration({ servicesManager, configuration = {} }) { - // No-op for now - }, /** * @@ -58,7 +42,9 @@ export default { ); }; - return [{ name: 'dicom-video', component: ExtendedOHIFCornerstoneVideoViewport }]; + return [ + { name: 'dicom-video', component: ExtendedOHIFCornerstoneVideoViewport }, + ]; }, getCommandsModule({ servicesManager }) { return { diff --git a/extensions/measurement-tracking/.webpack/webpack.dev.js b/extensions/measurement-tracking/.webpack/webpack.dev.js index 1ae30844802..db7c206b134 100644 --- a/extensions/measurement-tracking/.webpack/webpack.dev.js +++ b/extensions/measurement-tracking/.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/measurement-tracking/.webpack/webpack.prod.js b/extensions/measurement-tracking/.webpack/webpack.prod.js index f5103890276..08b25fc133d 100644 --- a/extensions/measurement-tracking/.webpack/webpack.prod.js +++ b/extensions/measurement-tracking/.webpack/webpack.prod.js @@ -1,10 +1,12 @@ 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 MiniCssExtractPlugin = require('mini-css-extract-plugin'); + const pkg = require('./../package.json'); +const webpackCommon = require('./../../../.webpack/webpack.base.js'); -const ROOT_DIR = path.join(__dirname, './..'); +const ROOT_DIR = path.join(__dirname, './../'); const SRC_DIR = path.join(__dirname, '../src'); const DIST_DIR = path.join(__dirname, '../dist'); @@ -30,7 +32,7 @@ module.exports = (env, argv) => { }, output: { path: ROOT_DIR, - library: 'OHIFExtDefault', + library: 'OHIFExtCornerstone', libraryTarget: 'umd', libraryExport: 'default', filename: pkg.main, @@ -39,6 +41,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/measurement-tracking/package.json b/extensions/measurement-tracking/package.json index ef447e0df80..e070d0141fd 100644 --- a/extensions/measurement-tracking/package.json +++ b/extensions/measurement-tracking/package.json @@ -1,6 +1,6 @@ { "name": "@ohif/extension-measurement-tracking", - "version": "0.0.1", + "version": "3.0.0", "description": "Tracking 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,21 +30,22 @@ "start": "yarn run dev" }, "peerDependencies": { - "@ohif/core": "^0.50.0", + "@ohif/core": "^3.0.0", "classnames": "^2.2.6", "cornerstone-core": "^2.6.0", "cornerstone-tools": "6.0.2", "dcmjs": "0.16.1", "prop-types": "^15.6.2", - "react": "^16.13.1", + "react": "^17.0.2", + "react-dom": "^17.0.2", "react-cornerstone-viewport": "^4.1.2", - "react-dom": "^16.13.1", "webpack": "^5.50.0", - "webpack-merge": "^5.7.3" + "webpack-merge": "^5.7.3", + "@ohif/ui": "^2.0.0" }, "dependencies": { "@babel/runtime": "7.16.3", - "@ohif/ui": "^1.8.2", + "@ohif/ui": "^2.0.0", "@xstate/react": "^0.8.1", "xstate": "^4.10.0" } diff --git a/extensions/measurement-tracking/src/contexts/TrackedMeasurementsContext/TrackedMeasurementsContext.jsx b/extensions/measurement-tracking/src/contexts/TrackedMeasurementsContext/TrackedMeasurementsContext.jsx index b9edb42799f..5e4e40a993f 100644 --- a/extensions/measurement-tracking/src/contexts/TrackedMeasurementsContext/TrackedMeasurementsContext.jsx +++ b/extensions/measurement-tracking/src/contexts/TrackedMeasurementsContext/TrackedMeasurementsContext.jsx @@ -17,7 +17,8 @@ const TrackedMeasurementsContext = React.createContext(); TrackedMeasurementsContext.displayName = 'TrackedMeasurementsContext'; const useTrackedMeasurements = () => useContext(TrackedMeasurementsContext); -const SR_SOPCLASSHANDLERID = "org.ohif.dicom-sr.sopClassHandlerModule.dicom-sr"; +const SR_SOPCLASSHANDLERID = + '@ohif/extension-dicom-sr.sopClassHandlerModule.dicom-sr'; /** * @@ -33,7 +34,10 @@ function TrackedMeasurementsContextProvider( const machineOptions = Object.assign({}, defaultOptions); machineOptions.actions = Object.assign({}, machineOptions.actions, { jumpToFirstMeasurementInActiveViewport: (ctx, evt) => { - const { DisplaySetService, MeasurementService } = servicesManager.services; + const { + DisplaySetService, + MeasurementService, + } = servicesManager.services; const { trackedStudy, trackedSeries } = ctx; const measurements = MeasurementService.getMeasurements(); const trackedMeasurements = measurements.filter( @@ -44,7 +48,10 @@ function TrackedMeasurementsContextProvider( const id = trackedMeasurements[0].id; - MeasurementService.jumpToMeasurement(viewportGrid.activeViewportIndex, id); + MeasurementService.jumpToMeasurement( + viewportGrid.activeViewportIndex, + id + ); }, showStructuredReportDisplaySetInActiveViewport: (ctx, evt) => { if (evt.data.createdDisplaySetInstanceUIDs.length > 0) { @@ -98,7 +105,7 @@ function TrackedMeasurementsContextProvider( }), promptHydrateStructuredReport: promptHydrateStructuredReport.bind(null, { servicesManager, - extensionManager + extensionManager, }), }); @@ -149,9 +156,11 @@ function TrackedMeasurementsContextProvider( // The issue here is that this handler in TrackedMeasurementsContext // ends up occurring before the Viewport is created, so the displaySet // is not loaded yet, and isRehydratable is undefined unless we call load(). - if (displaySet.SOPClassHandlerId === SR_SOPCLASSHANDLERID && - !displaySet.isLoaded && - displaySet.load) { + if ( + displaySet.SOPClassHandlerId === SR_SOPCLASSHANDLERID && + !displaySet.isLoaded && + displaySet.load + ) { displaySet.load(); } diff --git a/extensions/measurement-tracking/src/id.js b/extensions/measurement-tracking/src/id.js new file mode 100644 index 00000000000..ebe5acd98ae --- /dev/null +++ b/extensions/measurement-tracking/src/id.js @@ -0,0 +1,5 @@ +import packageJson from '../package.json'; + +const id = packageJson.name; + +export { id }; diff --git a/extensions/measurement-tracking/src/index.js b/extensions/measurement-tracking/src/index.js index b184ed64d29..4c1a4dc9be4 100644 --- a/extensions/measurement-tracking/src/index.js +++ b/extensions/measurement-tracking/src/index.js @@ -1,12 +1,14 @@ import getContextModule from './getContextModule.js'; import getPanelModule from './getPanelModule.js'; import getViewportModule from './getViewportModule.js'; +import { id } from './id.js'; export default { /** * Only required property. Should be a unique value across all extensions. */ - id: 'org.ohif.measurement-tracking', + id, + getContextModule, getPanelModule, getViewportModule, diff --git a/extensions/measurement-tracking/src/viewports/TrackedCornerstoneViewport.js b/extensions/measurement-tracking/src/viewports/TrackedCornerstoneViewport.js index e0bec512414..08b40e802c6 100644 --- a/extensions/measurement-tracking/src/viewports/TrackedCornerstoneViewport.js +++ b/extensions/measurement-tracking/src/viewports/TrackedCornerstoneViewport.js @@ -228,7 +228,7 @@ function TrackedCornerstoneViewport(props) { const renderViewport = () => { const { component: Component } = extensionManager.getModuleEntry( - 'org.ohif.cornerstone.viewportModule.cornerstone' + '@ohif/extension-cornerstone.viewportModule.cornerstone' ); return ( { const commonConfig = webpackCommon(env, argv, { SRC_DIR, DIST_DIR }); @@ -30,7 +33,7 @@ module.exports = (env, argv) => { }, output: { path: ROOT_DIR, - library: 'OHIFModeLongitudinal', + library: 'OHIFExtCornerstone', libraryTarget: 'umd', libraryExport: 'default', filename: pkg.main, @@ -39,6 +42,10 @@ module.exports = (env, argv) => { new webpack.optimize.LimitChunkCountPlugin({ maxChunks: 1, }), + new MiniCssExtractPlugin({ + filename: './dist/[name].css', + chunkFilename: './dist/[id].css', + }), ], }); }; diff --git a/modes/longitudinal/package.json b/modes/longitudinal/package.json index 98759cecf01..be63edf5527 100644 --- a/modes/longitudinal/package.json +++ b/modes/longitudinal/package.json @@ -1,6 +1,6 @@ { "name": "@ohif/mode-longitudinal", - "version": "0.0.1", + "version": "3.0.0", "description": "Longitudinal Workflow", "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" }, @@ -19,6 +19,9 @@ "publishConfig": { "access": "public" }, + "keywords": [ + "ohif-mode" + ], "scripts": { "dev": "cross-env NODE_ENV=development webpack --config .webpack/webpack.dev.js --watch --debug --output-pathinfo", "dev:cornerstone": "yarn run dev", @@ -29,7 +32,13 @@ "test:unit:ci": "jest --ci --runInBand --collectCoverage --passWithNoTests" }, "peerDependencies": { - "@ohif/core": "^2.12.3" + "@ohif/core": "^3.0.0", + "@ohif/extension-default": "^3.0.0", + "@ohif/extension-cornerstone": "^3.0.0", + "@ohif/extension-dicom-sr": "^3.0.0", + "@ohif/extension-dicom-pdf": "^3.0.1", + "@ohif/extension-dicom-video": "^3.0.1", + "@ohif/extension-measurement-tracking": "^3.0.0" }, "dependencies": { "@babel/runtime": "7.16.3" diff --git a/modes/longitudinal/src/id.js b/modes/longitudinal/src/id.js new file mode 100644 index 00000000000..ebe5acd98ae --- /dev/null +++ b/modes/longitudinal/src/id.js @@ -0,0 +1,5 @@ +import packageJson from '../package.json'; + +const id = packageJson.name; + +export { id }; diff --git a/modes/longitudinal/src/index.js b/modes/longitudinal/src/index.js index 3936db7f626..3158bd4f4d3 100644 --- a/modes/longitudinal/src/index.js +++ b/modes/longitudinal/src/index.js @@ -1,38 +1,53 @@ -import toolbarButtons from './toolbarButtons.js'; import { hotkeys } from '@ohif/core'; +import toolbarButtons from './toolbarButtons.js'; +import { id } from './id.js'; const ohif = { - layout: 'org.ohif.default.layoutTemplateModule.viewerLayout', - sopClassHandler: 'org.ohif.default.sopClassHandlerModule.stack', - hangingProtocols: 'org.ohif.default.hangingProtocolModule.default', + layout: '@ohif/extension-default.layoutTemplateModule.viewerLayout', + sopClassHandler: '@ohif/extension-default.sopClassHandlerModule.stack', + hangingProtocols: '@ohif/extension-default.hangingProtocolModule.default', }; const tracked = { - measurements: 'org.ohif.measurement-tracking.panelModule.trackedMeasurements', - thumbnailList: 'org.ohif.measurement-tracking.panelModule.seriesList', - viewport: 'org.ohif.measurement-tracking.viewportModule.cornerstone-tracked', + measurements: + '@ohif/extension-measurement-tracking.panelModule.trackedMeasurements', + thumbnailList: '@ohif/extension-measurement-tracking.panelModule.seriesList', + viewport: + '@ohif/extension-measurement-tracking.viewportModule.cornerstone-tracked', }; const dicomsr = { - sopClassHandler: 'org.ohif.dicom-sr.sopClassHandlerModule.dicom-sr', - viewport: 'org.ohif.dicom-sr.viewportModule.dicom-sr', + sopClassHandler: '@ohif/extension-dicom-sr.sopClassHandlerModule.dicom-sr', + viewport: '@ohif/extension-dicom-sr.viewportModule.dicom-sr', }; const dicomvideo = { - sopClassHandler: 'org.ohif.dicom-video.sopClassHandlerModule.dicom-video', - viewport: 'org.ohif.dicom-video.viewportModule.dicom-video', -} + sopClassHandler: + '@ohif/extension-dicom-video.sopClassHandlerModule.dicom-video', + viewport: '@ohif/extension-dicom-video.viewportModule.dicom-video', +}; const dicompdf = { - sopClassHandler: 'org.ohif.dicom-pdf.sopClassHandlerModule.dicom-pdf', - viewport: 'org.ohif.dicom-pdf.viewportModule.dicom-pdf', -} + sopClassHandler: '@ohif/extension-dicom-pdf.sopClassHandlerModule.dicom-pdf', + viewport: '@ohif/extension-dicom-pdf.viewportModule.dicom-pdf', +}; -export default function mode({ modeConfiguration }) { +const extensionDependencies = { + // Can derive the versions at least process.env.from npm_package_version + '@ohif/extension-default': '^3.0.0', + '@ohif/extension-cornerstone': '^3.0.0', + '@ohif/extension-measurement-tracking': '^3.0.0', + '@ohif/extension-dicom-sr': '^3.0.0', + '@ohif/extension-dicom-pdf': '^3.0.1', + '@ohif/extension-dicom-video': '^3.0.1', +}; + +function modeFactory({ modeConfiguration }) { return { // TODO: We're using this as a route segment // We should not be. - id: 'viewer', + id, + routeName: 'viewer', displayName: 'Basic Viewer', /** * Lifecycle hooks @@ -73,7 +88,7 @@ export default function mode({ modeConfiguration }) { const modalities_list = modalities.split('\\'); // Slide Microscopy modality not supported by basic mode yet - return !modalities_list.includes('SM') + return !modalities_list.includes('SM'); }, routes: [ { @@ -111,26 +126,26 @@ export default function mode({ modeConfiguration }) { }, }, ], - extensions: [ - 'org.ohif.default', - 'org.ohif.cornerstone', - 'org.ohif.measurement-tracking', - 'org.ohif.dicom-sr', - 'org.ohif.dicom-video', - 'org.ohif.dicom-pdf', - ], + extensions: extensionDependencies, hangingProtocols: [ohif.hangingProtocols], // Order is important in sop class handlers when two handlers both use // the same sop class under different situations. In that case, the more - // general handler needs to come last. For this case, the dicomvideo msut + // general handler needs to come last. For this case, the dicomvideo must // come first to remove video transfer syntax before ohif uses images sopClassHandlers: [ dicomvideo.sopClassHandler, ohif.sopClassHandler, dicompdf.sopClassHandler, - dicomsr.sopClassHandler,], + dicomsr.sopClassHandler, + ], hotkeys: [...hotkeys.defaults.hotkeyBindings], }; } -window.longitudinalMode = mode({}); +const mode = { + id, + modeFactory, + extensionDependencies, +}; + +export default mode; diff --git a/package.json b/package.json index 66f267736ec..40c2557d90e 100644 --- a/package.json +++ b/package.json @@ -1,5 +1,5 @@ { - "name": "root", + "name": "ohif-monorepo-root", "private": true, "workspaces": { "packages": [ @@ -12,7 +12,7 @@ ] }, "engines": { - "node": ">=10", + "node": ">=14", "npm": ">=6", "yarn": ">=1.16.0" }, @@ -22,8 +22,10 @@ "build:dev": "lerna run build:dev --stream", "build:ci": "lerna run build:viewer:ci --stream", "build:qa": "lerna run build:viewer:qa --stream", + "cli": "node ./platform/cli/src/index.js", "build:ui:deploy-preview": "lerna run build:ui:deploy-preview --stream", "build:demo": "lerna run build:viewer:demo --stream", + "build:package-all": "lerna run build:package --parallel --stream", "dev": "lerna run dev:viewer --stream", "dev:project": ".scripts/dev.sh", "dev:orthanc": "lerna run dev:orthanc --stream", @@ -48,7 +50,7 @@ "lerna:cache": "./netlify-lerna-cache.sh", "lerna:restore": "./netlify-lerna-restore.sh", "lerna:version": "npx lerna version prerelease --force-publish", - "lerna:publish": "lerna publish from-package --canary --dist-tag canary", + "lerna:publish": "lerna publish from-package --dist-tag next", "link-list": "npm ls --depth=0 --link=true" }, "dependencies": { @@ -62,18 +64,18 @@ "react-dom": "17.0.2" }, "devDependencies": { - "@babel/core": "^7.5.0", - "@babel/plugin-proposal-class-properties": "^7.5.0", - "@babel/plugin-proposal-object-rest-spread": "^7.5.5", - "@babel/plugin-syntax-dynamic-import": "^7.2.0", - "@babel/plugin-transform-arrow-functions": "^7.2.0", - "@babel/plugin-transform-regenerator": "^7.4.5", - "@babel/plugin-transform-runtime": "^7.5.0", - "@babel/preset-env": "^7.5.0", - "@babel/preset-react": "^7.0.0", - "autoprefixer": "10.2.4", + "@babel/core": "^7.17.8", + "@babel/plugin-proposal-class-properties": "^7.16.7", + "@babel/plugin-proposal-object-rest-spread": "^7.17.3", + "@babel/plugin-syntax-dynamic-import": "^7.8.3", + "@babel/plugin-transform-arrow-functions": "^7.16.7", + "@babel/plugin-transform-regenerator": "^7.16.7", + "@babel/plugin-transform-runtime": "^7.17.0", + "@babel/preset-env": "^7.16.11", + "@babel/preset-react": "^7.16.7", + "autoprefixer": "10.4.4", "babel-eslint": "9.x", - "babel-loader": "^8.0.6", + "babel-loader": "^8.2.4", "babel-plugin-inline-react-svg": "1.1.0", "babel-plugin-module-resolver": "^3.2.0", "clean-webpack-plugin": "^3.0.0", @@ -81,17 +83,17 @@ "cross-env": "^5.2.0", "css-loader": "^3.2.0", "dotenv": "^8.1.0", - "eslint": "6.8.0", - "eslint-config-prettier": "^6.4.0", - "eslint-config-react-app": "^5.2.0", - "eslint-plugin-flowtype": "2.x", - "eslint-plugin-import": "2.x", - "eslint-plugin-jsx-a11y": "6.x", - "eslint-plugin-node": "^9.1.0", - "eslint-plugin-prettier": "^3.1.1", - "eslint-plugin-promise": "^4.2.1", - "eslint-plugin-react": "7.x", - "eslint-plugin-react-hooks": "4.2.0", + "eslint": "^7.32.0", + "eslint-config-prettier": "^7.2.0", + "eslint-config-react-app": "^6.0.0", + "eslint-plugin-flowtype": "^7.0.0", + "eslint-plugin-import": "^2.26.0", + "eslint-plugin-jsx-a11y": "^6.5.1", + "eslint-plugin-node": "^11.1.0", + "eslint-plugin-prettier": "^4.0.0", + "eslint-plugin-promise": "^5.2.0", + "eslint-plugin-react": "^7.29.4", + "eslint-plugin-react-hooks": "^4.4.0", "html-webpack-plugin": "^5.3.2", "husky": "^3.0.0", "jest": "^24.8.0", @@ -104,7 +106,7 @@ "postcss": "^8.3.5", "postcss-import": "^14.0.2", "postcss-loader": "^6.1.1", - "postcss-preset-env": "^6.7.0", + "postcss-preset-env": "^7.4.3", "prettier": "^1.18.2", "react-hot-loader": "^4.13.0", "serve": "^11.1.0", diff --git a/platform/cli/package.json b/platform/cli/package.json new file mode 100644 index 00000000000..b13709ecce7 --- /dev/null +++ b/platform/cli/package.json @@ -0,0 +1,43 @@ +{ + "name": "@ohif/cli", + "version": "2.0.7", + "description": "A CLI to bootstrap new OHIF extension or mode", + "type": "module", + "main": "src/index.js", + "private": true, + "bin": { + "ohif-cli": "src/index.js" + }, + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "keywords": [ + "cli", + "ohif" + ], + "author": "OHIF Contributors", + "license": "MIT", + "dependencies": { + "@babel/core": "^7.13.10", + "chalk": "^5.0.0", + "commander": "^8.3.0", + "axios": "^0.26.1", + "execa": "^6.0.0", + "gitignore": "^0.7.0", + "inquirer": "^8.2.0", + "listr": "^0.14.3", + "mustache": "^4.2.0", + "ncp": "^2.0.0", + "node-fetch": "^3.1.1", + "pkg-install": "^1.0.0", + "registry-url": "^6.0.0", + "spdx-license-list": "^6.4.0", + "util": "^0.12.4", + "yarn-programmatic": "^0.1.2" + }, + "files": [ + "bin/", + "src/", + "templates/" + ] +} diff --git a/platform/cli/src/commands/addExtension.js b/platform/cli/src/commands/addExtension.js new file mode 100644 index 00000000000..62ca566efc6 --- /dev/null +++ b/platform/cli/src/commands/addExtension.js @@ -0,0 +1,55 @@ +import Listr from 'listr'; +import chalk from 'chalk'; + +import { + installNPMPackage, + getYarnInfo, + validateExtension, + getVersionedPackageName, + addExtensionToConfig, +} from './utils/index.js'; + +export default async function addExtension(packageName, version) { + console.log(chalk.green.bold(`Adding ohif-extension ${packageName}...`)); + + const versionedPackageName = getVersionedPackageName(packageName, version); + + const tasks = new Listr( + [ + { + title: `Searching for extension: ${versionedPackageName}`, + task: async () => await validateExtension(packageName, version), + }, + { + title: `Installing npm package: ${versionedPackageName}`, + task: async () => await installNPMPackage(packageName, version), + }, + { + title: 'Adding ohif-extension to the configuration file', + task: async ctx => { + const yarnInfo = await getYarnInfo(packageName); + + addExtensionToConfig(packageName, yarnInfo); + + ctx.yarnInfo = yarnInfo; + }, + }, + ], + { + exitOnError: true, + } + ); + + await tasks + .run() + .then(ctx => { + console.log( + `${chalk.green.bold( + `Added ohif-extension ${packageName}@${ctx.yarnInfo.version}` + )} ` + ); + }) + .catch(error => { + console.log(error.message); + }); +} diff --git a/platform/cli/src/commands/addExtensions.js b/platform/cli/src/commands/addExtensions.js new file mode 100644 index 00000000000..5da1d071077 --- /dev/null +++ b/platform/cli/src/commands/addExtensions.js @@ -0,0 +1,38 @@ +import Listr from 'listr'; +import chalk from 'chalk'; +import addExtension from './addExtension.js'; + +export default async function addExtensions(ohifExtensions) { + // Auto generate Listr tasks... + const taskEntries = []; + + ohifExtensions.forEach(({ packageName, version }) => { + const title = `Adding ohif-extension ${packageName}`; + + taskEntries.push({ + title, + task: async () => await addExtension(packageName, version), + }); + }); + + const tasks = new Listr(taskEntries, { + exitOnError: true, + }); + + await tasks + .run() + .then(() => { + let extensonsString = ''; + + ohifExtensions.forEach(({ packageName, version }) => { + extensonsString += ` ${packageName}@${version}`; + }); + + console.log( + `${chalk.green.bold(`Extensions added:${extensonsString}`)} ` + ); + }) + .catch(error => { + console.log(error.message); + }); +} diff --git a/platform/cli/src/commands/addMode.js b/platform/cli/src/commands/addMode.js new file mode 100644 index 00000000000..6769261389c --- /dev/null +++ b/platform/cli/src/commands/addMode.js @@ -0,0 +1,72 @@ +import Listr from 'listr'; +import chalk from 'chalk'; + +import { + installNPMPackage, + getYarnInfo, + getVersionedPackageName, + validateMode, + addModeToConfig, + findRequiredOhifExtensionsForMode, +} from './utils/index.js'; +import addExtensions from './addExtensions.js'; + +export default async function addMode(packageName, version) { + console.log(chalk.green.bold(`Adding ohif-mode ${packageName}...`)); + + const versionedPackageName = getVersionedPackageName(packageName, version); + + const tasks = new Listr( + [ + { + title: `Searching for mode: ${versionedPackageName}`, + task: async () => await validateMode(packageName, version), + }, + { + title: `Installing npm package: ${versionedPackageName}`, + task: async () => await installNPMPackage(packageName, version), + }, + { + title: 'Adding ohif-mode to the configuration file', + task: async ctx => { + const yarnInfo = await getYarnInfo(packageName); + + addModeToConfig(packageName, yarnInfo); + + ctx.yarnInfo = yarnInfo; + }, + }, + { + title: 'Detecting required ohif-extensions...', + task: async ctx => { + ctx.ohifExtensions = await findRequiredOhifExtensionsForMode( + ctx.yarnInfo + ); + }, + }, + ], + { + exitOnError: true, + } + ); + + await tasks + .run() + .then(async ctx => { + console.log( + `${chalk.green.bold( + `Added ohif-mode ${packageName}@${ctx.yarnInfo.version}` + )} ` + ); + + const ohifExtensions = ctx.ohifExtensions; + + if (ohifExtensions.length) { + console.log(`${chalk.green.bold(`Installing dependent extensions`)} `); + await addExtensions(ohifExtensions); + } + }) + .catch(error => { + console.log(error.message); + }); +} diff --git a/platform/cli/src/commands/constants/notFound.js b/platform/cli/src/commands/constants/notFound.js new file mode 100644 index 00000000000..30bb85e14d8 --- /dev/null +++ b/platform/cli/src/commands/constants/notFound.js @@ -0,0 +1 @@ +export default 'Not found'; diff --git a/platform/cli/src/commands/createPackage.js b/platform/cli/src/commands/createPackage.js new file mode 100644 index 00000000000..84fa7d45492 --- /dev/null +++ b/platform/cli/src/commands/createPackage.js @@ -0,0 +1,97 @@ +import Listr from 'listr'; +import chalk from 'chalk'; +import fs from 'fs'; + +import { + createDirectoryContents, + editPackageJson, + createLicense, + createReadme, + initGit, +} from './utils/index.js'; + +const createPackage = async (options) => { + const { packageType } = options; // extension or mode + + if (fs.existsSync(options.targetDir)) { + console.error( + `%s ${packageType} with the same name already exists in this directory, either delete it or choose a different name`, + chalk.red.bold('ERROR') + ); + process.exit(1); + } + + fs.mkdirSync(options.targetDir); + + const tasks = new Listr( + [ + { + title: 'Copying template files', + task: () => + createDirectoryContents( + options.templateDir, + options.targetDir, + options.prettier + ), + }, + { + title: 'Editing Package.json with provided information', + task: () => editPackageJson(options), + }, + { + title: 'Creating a License file', + task: () => createLicense(options), + }, + { + title: 'Creating a Readme file', + task: () => createReadme(options), + }, + { + title: 'Initializing a Git Repository', + enabled: () => options.gitRepository, + task: () => initGit(options), + }, + ], + { + exitOnError: true, + } + ); + + await tasks.run(); + console.log(); + console.log( + chalk.green(`Done: ${packageType} is ready at`, options.targetDir) + ); + console.log(); + + console.log( + chalk.green(`NOTE: In order to use this ${packageType} for development,`) + ); + console.log( + chalk.green( + `run the following command inside the root of the OHIF monorepo` + ) + ); + + console.log(); + console.log( + chalk.green.bold( + ` yarn run cli link-${packageType} ${options.targetDir}` + ) + ); + console.log(); + console.log( + chalk.yellow( + "and when you don't need it anymore, run the following command to unlink it" + ) + ); + console.log(); + console.log( + chalk.yellow(` yarn run cli unlink-${packageType} ${options.name}`) + ); + console.log(); + + return true; +}; + +export default createPackage; diff --git a/platform/cli/src/commands/enums/colors.js b/platform/cli/src/commands/enums/colors.js new file mode 100644 index 00000000000..566da789d7c --- /dev/null +++ b/platform/cli/src/commands/enums/colors.js @@ -0,0 +1,8 @@ +const colors = { + LIGHT: '#5acce6', + MAIN: '#0944b3', + DARK: '#090c29', + ACTIVE: '#348cfd', +}; + +export default colors; diff --git a/platform/cli/src/commands/enums/endPoints.js b/platform/cli/src/commands/enums/endPoints.js new file mode 100644 index 00000000000..edc73492802 --- /dev/null +++ b/platform/cli/src/commands/enums/endPoints.js @@ -0,0 +1,5 @@ +const endPoints = { + NPM_KEYWORD: 'https://registry.npmjs.com/-/v1/search?text=keywords:', +}; + +export default endPoints; diff --git a/platform/cli/src/commands/enums/index.js b/platform/cli/src/commands/enums/index.js new file mode 100644 index 00000000000..b58628997f7 --- /dev/null +++ b/platform/cli/src/commands/enums/index.js @@ -0,0 +1,5 @@ +import keywords from './keywords.js'; +import colors from './colors.js'; +import endPoints from './endPoints.js'; + +export { keywords, colors, endPoints }; diff --git a/platform/cli/src/commands/enums/keywords.js b/platform/cli/src/commands/enums/keywords.js new file mode 100644 index 00000000000..56df4807b58 --- /dev/null +++ b/platform/cli/src/commands/enums/keywords.js @@ -0,0 +1,6 @@ +const keywords = { + MODE: 'ohif-mode', + EXTENSION: 'ohif-extension', +}; + +export default keywords; diff --git a/platform/cli/src/commands/index.js b/platform/cli/src/commands/index.js new file mode 100644 index 00000000000..4101a492448 --- /dev/null +++ b/platform/cli/src/commands/index.js @@ -0,0 +1,23 @@ +import createPackage from './createPackage.js'; +import addExtension from './addExtension.js'; +import removeExtension from './removeExtension.js'; +import addMode from './addMode.js'; +import removeMode from './removeMode.js'; +import listPlugins from './listPlugins.js'; +import searchPlugins from './searchPlugins.js'; +import { linkExtension, linkMode } from './linkPackage.js'; +import { unlinkExtension, unlinkMode } from './unlinkPackage.js'; + +export { + createPackage, + addExtension, + removeExtension, + addMode, + removeMode, + listPlugins, + searchPlugins, + linkExtension, + linkMode, + unlinkExtension, + unlinkMode, +}; diff --git a/platform/cli/src/commands/linkPackage.js b/platform/cli/src/commands/linkPackage.js new file mode 100644 index 00000000000..34a3b553a20 --- /dev/null +++ b/platform/cli/src/commands/linkPackage.js @@ -0,0 +1,56 @@ +import fs from 'fs'; +import path from 'path'; +import { execa } from 'execa'; +import { keywords } from './enums/index.js'; +import { + validateYarn, + addExtensionToConfig, + addModeToConfig, +} from './utils/index.js'; + +async function linkPackage(packageDir, options, addToConfig, keyword) { + const { viewerDirectory } = options; + + // read package.json from packageDir + const file = fs.readFileSync(path.join(packageDir, 'package.json'), 'utf8'); + + // name of the package + const packageJSON = JSON.parse(file); + const packageName = packageJSON.name; + const packageKeywords = packageJSON.keywords; + + // check if package is an extension or a mode + if (!packageKeywords.includes(keyword)) { + throw new Error(`${packageName} is not ${keyword}`); + } + + const version = packageJSON.version; + + // make sure yarn is installed + await validateYarn(); + + // change directory to packageDir and execute yarn link + process.chdir(packageDir); + + let results; + results = await execa(`yarn`, ['link']); + + // change directory to OHIF Platform root and execute yarn link + process.chdir(viewerDirectory); + + results = await execa(`yarn`, ['link', packageName]); + console.log(results.stdout); + addToConfig(packageName, { version }); +} + +function linkExtension(packageDir, options) { + const keyword = keywords.EXTENSION; + linkPackage(packageDir, options, addExtensionToConfig, keyword); +} + +function linkMode(packageDir, options) { + const keyword = keywords.MODE; + linkPackage(packageDir, options, addModeToConfig, keyword); +} + +export { linkExtension, linkMode }; diff --git a/platform/cli/src/commands/listPlugins.js b/platform/cli/src/commands/listPlugins.js new file mode 100644 index 00000000000..039f2ad485c --- /dev/null +++ b/platform/cli/src/commands/listPlugins.js @@ -0,0 +1,23 @@ +import fs from 'fs'; +import { prettyPrint } from './utils/index.js'; +import { colors } from './enums/index.js'; + +const listPlugins = async configPath => { + const pluginConfig = JSON.parse(fs.readFileSync(configPath, 'utf8')); + + const { extensions, modes } = pluginConfig; + + const titleOptions = { color: colors.LIGHT, bold: true }; + const itemsOptions = { color: colors.ACTIVE, bold: true }; + + const extensionsItems = extensions.map( + extension => `${extension.packageName} @ ${extension.version}` + ); + + const modesItems = modes.map(mode => `${mode.packageName} @ ${mode.version}`); + + prettyPrint('Extensions', titleOptions, extensionsItems, itemsOptions); + prettyPrint('Modes', titleOptions, modesItems, itemsOptions); +}; + +export default listPlugins; diff --git a/platform/cli/src/commands/removeExtension.js b/platform/cli/src/commands/removeExtension.js new file mode 100644 index 00000000000..e29ec135e84 --- /dev/null +++ b/platform/cli/src/commands/removeExtension.js @@ -0,0 +1,49 @@ +import chalk from 'chalk'; +import Listr from 'listr'; + +import { + uninstallNPMPackage, + throwIfExtensionUsedByInstalledMode, + removeExtensionFromConfig, + validateExtensionYarnInfo, +} from './utils/index.js'; + +export default async function removeExtension(packageName) { + console.log(chalk.green.bold(`Removing ohif-extension ${packageName}...`)); + + const tasks = new Listr( + [ + { + title: `Searching for installed extension: ${packageName}`, + task: async () => await validateExtensionYarnInfo(packageName), + }, + { + title: `Checking if ${packageName} is in use by an installed mode`, + task: async () => + await throwIfExtensionUsedByInstalledMode(packageName), + }, + { + title: `Uninstalling npm package: ${packageName}`, + task: async () => await uninstallNPMPackage(packageName), + }, + { + title: 'Removing ohif-extension from the configuration file', + task: async () => removeExtensionFromConfig(packageName), + }, + ], + { + exitOnError: true, + } + ); + + await tasks + .run() + .then(() => { + console.log( + `${chalk.green.bold(`Removed ohif-extension ${packageName}`)} ` + ); + }) + .catch(error => { + console.log(error.message); + }); +} diff --git a/platform/cli/src/commands/removeExtensions.js b/platform/cli/src/commands/removeExtensions.js new file mode 100644 index 00000000000..2b29e1be802 --- /dev/null +++ b/platform/cli/src/commands/removeExtensions.js @@ -0,0 +1,38 @@ +import Listr from 'listr'; +import chalk from 'chalk'; +import removeExtension from './removeExtension.js'; + +export default async function removeExtensions(ohifExtensionsToRemove) { + // Auto generate Listr tasks... + const taskEntries = []; + + ohifExtensionsToRemove.forEach(packageName => { + const title = `Removing ohif-extension ${packageName}`; + + taskEntries.push({ + title, + task: async () => await removeExtension(packageName), + }); + }); + + const tasks = new Listr(taskEntries, { + exitOnError: true, + }); + + await tasks + .run() + .then(() => { + let extensonsString = ''; + + ohifExtensionsToRemove.forEach(packageName => { + extensonsString += ` ${packageName}`; + }); + + console.log( + `${chalk.green.bold(`Extensions removed:${extensonsString}`)} ` + ); + }) + .catch(error => { + console.log(error.message); + }); +} diff --git a/platform/cli/src/commands/removeMode.js b/platform/cli/src/commands/removeMode.js new file mode 100644 index 00000000000..75455834330 --- /dev/null +++ b/platform/cli/src/commands/removeMode.js @@ -0,0 +1,68 @@ +import Listr from 'listr'; +import chalk from 'chalk'; + +import { + uninstallNPMPackage, + findOhifExtensionsToRemoveAfterRemovingMode, + removeModeFromConfig, + validateModeYarnInfo, + getYarnInfo, +} from './utils/index.js'; +import removeExtensions from './removeExtensions.js'; + +export default async function removeMode(packageName) { + console.log(chalk.green.bold(`Removing ohif-mode ${packageName}...`)); + + const tasks = new Listr( + [ + { + title: `Searching for installed mode: ${packageName}`, + task: async ctx => { + ctx.yarnInfo = await getYarnInfo(packageName); + await validateModeYarnInfo(packageName); + }, + }, + { + title: `Uninstalling npm package: ${packageName}`, + task: async () => await uninstallNPMPackage(packageName), + }, + { + title: 'Removing ohif-mode from the configuration file', + task: async () => await removeModeFromConfig(packageName), + }, + { + title: 'Detecting extensions that can be removed...', + task: async ctx => { + ctx.ohifExtensionsToRemove = await findOhifExtensionsToRemoveAfterRemovingMode( + ctx.yarnInfo + ); + }, + }, + ], + { + exitOnError: true, + } + ); + + await tasks + .run() + .then(async ctx => { + // Remove extensions if they aren't used by any other mode. + console.log(`${chalk.green.bold(`Removed ohif-mode ${packageName}`)} `); + + const ohifExtensionsToRemove = ctx.ohifExtensionsToRemove; + + if (ohifExtensionsToRemove.length) { + console.log( + `${chalk.green.bold( + `Removing ${ohifExtensionsToRemove.length} extensions no longer used by any installed mode` + )}` + ); + + await removeExtensions(ohifExtensionsToRemove); + } + }) + .catch(error => { + console.log(error.message); + }); +} diff --git a/platform/cli/src/commands/searchPlugins.js b/platform/cli/src/commands/searchPlugins.js new file mode 100644 index 00000000000..ef5e3af53a8 --- /dev/null +++ b/platform/cli/src/commands/searchPlugins.js @@ -0,0 +1,57 @@ +import axios from 'axios'; + +import { prettyPrint } from './utils/index.js'; +import { keywords, colors, endPoints } from './enums/index.js'; + +async function searchRegistry(keyword) { + const url = `${endPoints.NPM_KEYWORD}${keyword}`; + + try { + const response = await axios.get(url); + const { objects } = response.data; + return objects; + } catch (error) { + console.log(error); + } +} + +async function searchPlugins(options) { + const { verbose } = options; + + const extensions = await searchRegistry(keywords.EXTENSION); + const modes = await searchRegistry(keywords.MODE); + + const titleOptions = { color: colors.LIGHT, bold: true }; + const itemsOptions = {}; + + const extensionsItems = extensions.map(extension => { + const item = [ + `${extension.package.name} @ ${extension.package.version}`, + [`Description: ${extension.package.description}`], + ]; + + if (verbose) { + item[1].push(`Repository: ${extension.package.links.repository}`); + } + + return item; + }); + + const modesItems = modes.map(mode => { + const item = [ + `${mode.package.name} @ ${mode.package.version}`, + [`Description: ${mode.package.description}`], + ]; + + if (verbose) { + item[1].push(`Repository: ${mode.package.links.repository}`); + } + + return item; + }); + + prettyPrint('Extensions', titleOptions, extensionsItems, itemsOptions); + prettyPrint('Modes', titleOptions, modesItems, itemsOptions); +} + +export default searchPlugins; diff --git a/platform/cli/src/commands/unlinkPackage.js b/platform/cli/src/commands/unlinkPackage.js new file mode 100644 index 00000000000..e192714bac4 --- /dev/null +++ b/platform/cli/src/commands/unlinkPackage.js @@ -0,0 +1,32 @@ +import { execa } from 'execa'; +import { + validateYarn, + removeExtensionFromConfig, + removeModeFromConfig, +} from './utils/index.js'; + +const linkPackage = async (packageName, options, removeFromConfig) => { + const { viewerDirectory } = options; + + // make sure yarn is installed + await validateYarn(); + + // change directory to OHIF Platform root and execute yarn link + process.chdir(viewerDirectory); + + const results = await execa(`yarn`, ['unlink', packageName]); + console.log(results.stdout); + + //update the plugin.json file + removeFromConfig(packageName); +}; + +function unlinkExtension(extensionName, options) { + linkPackage(extensionName, options, removeExtensionFromConfig); +} + +function unlinkMode(modeName, options) { + linkPackage(modeName, options, removeModeFromConfig); +} + +export { unlinkExtension, unlinkMode }; diff --git a/platform/cli/src/commands/utils/addToConfig.js b/platform/cli/src/commands/utils/addToConfig.js new file mode 100644 index 00000000000..45b4f348213 --- /dev/null +++ b/platform/cli/src/commands/utils/addToConfig.js @@ -0,0 +1,34 @@ +import { + addExtensionToConfigJson, + addModeToConfigJson, + readPluginConfigFile, + writePluginConfigFile, +} from './private/index.js'; + +function addToAndOverwriteConfig(packageName, options, augmentConfigFunction) { + const installedVersion = options.version; + let pluginConfig = readPluginConfigFile(); + + if (!pluginConfig) { + pluginConfig = { + extensions: [], + modes: [], + }; + } + + augmentConfigFunction(pluginConfig, { + packageName, + version: installedVersion, + }); + writePluginConfigFile(pluginConfig); +} + +function addExtensionToConfig(packageName, options) { + addToAndOverwriteConfig(packageName, options, addExtensionToConfigJson); +} + +function addModeToConfig(packageName, options) { + addToAndOverwriteConfig(packageName, options, addModeToConfigJson); +} + +export { addExtensionToConfig, addModeToConfig }; diff --git a/platform/cli/src/commands/utils/createDirectoryContents.js b/platform/cli/src/commands/utils/createDirectoryContents.js new file mode 100644 index 00000000000..90a9329c64a --- /dev/null +++ b/platform/cli/src/commands/utils/createDirectoryContents.js @@ -0,0 +1,42 @@ +import fs from 'fs'; + +// https://github.dev/leoroese/template-cli/blob/628dd24db7df399ebb520edd0bc301bc7b5e8b66/index.js#L19 +const createDirectoryContents = ( + templatePath, + targetDirPath, + copyPrettierRules +) => { + const filesToCreate = fs.readdirSync(templatePath); + + filesToCreate.forEach(file => { + if (!copyPrettierRules && file === '.prettierrc') { + return; + } + + const origFilePath = `${templatePath}/${file}`; + + // get stats about the current file + const stats = fs.statSync(origFilePath); + + if (stats.isFile()) { + const contents = fs.readFileSync(origFilePath, 'utf8'); + + // Rename + if (file === '.npmignore') file = '.gitignore'; + + const writePath = `${targetDirPath}/${file}`; + fs.writeFileSync(writePath, contents, 'utf8'); + } else if (stats.isDirectory()) { + fs.mkdirSync(`${targetDirPath}/${file}`); + + // recursive call + createDirectoryContents( + `${templatePath}/${file}`, + `${targetDirPath}/${file}`, + copyPrettierRules + ); + } + }); +}; + +export default createDirectoryContents; diff --git a/platform/cli/src/commands/utils/createLicense.js b/platform/cli/src/commands/utils/createLicense.js new file mode 100644 index 00000000000..b48bde869bc --- /dev/null +++ b/platform/cli/src/commands/utils/createLicense.js @@ -0,0 +1,31 @@ +import chalk from 'chalk'; +import fs from 'fs'; +import path from 'path'; +import { promisify } from 'util'; +import spdxLicenseList from 'spdx-license-list/full.js'; + +const writeFile = promisify(fs.writeFile); + +async function createLicense(options) { + const { targetDir, name, email } = options; + const targetPath = path.join(targetDir, 'LICENSE'); + + let license; + try { + license = spdxLicenseList[options.license]; + } catch (err) { + console.error( + '%s License %s not found in the list of licenses', + chalk.red.bold('ERROR'), + options.license + ); + process.exit(1); + } + + const licenseContent = license.licenseText + .replace('', new Date().getFullYear()) + .replace('', `${name} (${email})`); + return writeFile(targetPath, licenseContent, 'utf8'); +} + +export default createLicense; diff --git a/platform/cli/src/commands/utils/createReadme.js b/platform/cli/src/commands/utils/createReadme.js new file mode 100644 index 00000000000..ab4a72de196 --- /dev/null +++ b/platform/cli/src/commands/utils/createReadme.js @@ -0,0 +1,23 @@ +import fs from 'fs'; +import path from 'path'; +import { promisify } from 'util'; +import mustache from 'mustache'; + +const writeFile = promisify(fs.writeFile); + +async function createReadme(options) { + let template = `# {{name}} \n## Description \n{{description}} \n## Author \n{{author}} \n## License \n{{license}}`; + const { name, description, author, license, targetDir } = options; + const targetPath = path.join(targetDir, 'README.md'); + + const readmeContent = mustache.render(template, { + name, + description, + author, + license, + }); + + return writeFile(targetPath, readmeContent, 'utf8'); +} + +export default createReadme; diff --git a/platform/cli/src/commands/utils/editPackageJson.js b/platform/cli/src/commands/utils/editPackageJson.js new file mode 100644 index 00000000000..ce7e44eb6e4 --- /dev/null +++ b/platform/cli/src/commands/utils/editPackageJson.js @@ -0,0 +1,33 @@ +import fs from 'fs'; +import path from 'path'; + +async function editPackageJson(options) { + const { name, version, description, author, license, targetDir } = options; + + // read package.json from targetDir + const dependenciesPath = path.join(targetDir, 'dependencies.json'); + const rawData = fs.readFileSync(dependenciesPath, 'utf8'); + const packageJson = JSON.parse(rawData); + + // edit package.json + const mergedObj = Object.assign( + { + name, + version, + description, + author, + license, + files: ['dist', 'README.md'], + }, + packageJson + ); + + // write package.json back to targetDir + const writePath = path.join(targetDir, 'package.json'); + fs.writeFileSync(writePath, JSON.stringify(mergedObj, null, 2)); + + // remove the dependencies.json file + fs.unlinkSync(dependenciesPath); +} + +export default editPackageJson; diff --git a/platform/cli/src/commands/utils/findOhifExtensionsToRemoveAfterRemovingMode.js b/platform/cli/src/commands/utils/findOhifExtensionsToRemoveAfterRemovingMode.js new file mode 100644 index 00000000000..be69cc6d142 --- /dev/null +++ b/platform/cli/src/commands/utils/findOhifExtensionsToRemoveAfterRemovingMode.js @@ -0,0 +1,67 @@ +import { readPluginConfigFile } from './private/index.js'; +import getYarnInfo from './getYarnInfo.js'; + +export default async function findOhifExtensionsToRemoveAfterRemovingMode( + removedModeYarnInfo +) { + const pluginConfig = readPluginConfigFile(); + + if (!pluginConfig) { + // No other modes or extensions, no action item. + return []; + } + + const { modes, extensions } = pluginConfig; + + const registeredExtensions = extensions.map( + extension => extension.packageName + ); + // TODO this is not a function + const ohifExtensionsOfMode = Object.keys( + removedModeYarnInfo.peerDependencies + ).filter(peerDependency => registeredExtensions.includes(peerDependency)); + + const ohifExtensionsUsedInOtherModes = ohifExtensionsOfMode.map( + packageName => { + return { + packageName, + used: false, + }; + } + ); + + // Check if other modes use each extension used by this mode + const otherModes = modes.filter( + mode => mode.packageName !== removedModeYarnInfo.name + ); + + for (let i = 0; i < otherModes.length; i++) { + const mode = otherModes[i]; + const yarnInfo = await getYarnInfo(mode.packageName); + + const peerDependencies = yarnInfo.peerDependencies; + + if (!peerDependencies) { + continue; + } + + for (let j = 0; j < ohifExtensionsUsedInOtherModes.length; j++) { + const ohifExtension = ohifExtensionsUsedInOtherModes[j]; + if (ohifExtension.used) { + // Already accounted that we can't delete this, so don't waste effort + return; + } + + if (Object.keys(peerDependencies).includes(ohifExtension.packageName)) { + ohifExtension.used = true; + } + } + } + + // Return list of now unused extensions + const ohifExtensionsToRemove = ohifExtensionsUsedInOtherModes + .filter(ohifExtension => !ohifExtension.used) + .map(ohifExtension => ohifExtension.packageName); + + return ohifExtensionsToRemove; +} diff --git a/platform/cli/src/commands/utils/findRequiredOhifExtensionsForMode.js b/platform/cli/src/commands/utils/findRequiredOhifExtensionsForMode.js new file mode 100644 index 00000000000..c907a56b0e5 --- /dev/null +++ b/platform/cli/src/commands/utils/findRequiredOhifExtensionsForMode.js @@ -0,0 +1,41 @@ +import { validateExtension } from './validate.js'; + +export default async function findRequiredOhifExtensionsForMode(yarnInfo) { + // Get yarn info file and get peer dependencies + if (!yarnInfo.peerDependencies) { + // No ohif-extension dependencies + return; + } + + const peerDependencies = yarnInfo.peerDependencies; + const dependencies = []; + const ohifExtensions = []; + + Object.keys(peerDependencies).forEach((packageName) => { + dependencies.push({ + packageName, + version: peerDependencies[packageName], + }); + }); + + const promises = []; + + // Fetch each npm json and check which are ohif extensions + for (let i = 0; i < dependencies.length; i++) { + const dependency = dependencies[i]; + const { packageName, version } = dependency; + const promise = validateExtension(packageName, version) + .then(() => { + ohifExtensions.push({ packageName, version }); + }) + .catch(() => {}); + + promises.push(promise); + } + + // Await all the extensions // TODO -> Improve so we async install each + // extension and await all of those promises instead. + await Promise.all(promises); + + return ohifExtensions; +} diff --git a/platform/cli/src/commands/utils/getVersionedPackageName.js b/platform/cli/src/commands/utils/getVersionedPackageName.js new file mode 100644 index 00000000000..900d7aa2c2f --- /dev/null +++ b/platform/cli/src/commands/utils/getVersionedPackageName.js @@ -0,0 +1,3 @@ +export default function getVersionedPackageName(packageName, version) { + return version === undefined ? packageName : `${packageName}@${version}`; +} diff --git a/platform/cli/src/commands/utils/getYarnInfo.js b/platform/cli/src/commands/utils/getYarnInfo.js new file mode 100644 index 00000000000..d5f3ce572dd --- /dev/null +++ b/platform/cli/src/commands/utils/getYarnInfo.js @@ -0,0 +1,5 @@ +import { info } from 'yarn-programmatic'; + +export default async function getYarnInfo(packageName) { + return await info(packageName); +} diff --git a/platform/cli/src/commands/utils/index.js b/platform/cli/src/commands/utils/index.js new file mode 100644 index 00000000000..b053c73c932 --- /dev/null +++ b/platform/cli/src/commands/utils/index.js @@ -0,0 +1,50 @@ +import getVersionedPackageName from './getVersionedPackageName.js'; +import installNPMPackage from './installNPMPackage.js'; +import uninstallNPMPackage from './uninstallNPMPackage.js'; +import { + validateMode, + validateExtension, + validateModeYarnInfo, + validateExtensionYarnInfo, +} from './validate.js'; +import getYarnInfo from './getYarnInfo.js'; +import { addExtensionToConfig, addModeToConfig } from './addToConfig.js'; +import findRequiredOhifExtensionsForMode from './findRequiredOhifExtensionsForMode.js'; +import { + removeExtensionFromConfig, + removeModeFromConfig, +} from './removeFromConfig.js'; +import throwIfExtensionUsedByInstalledMode from './throwIfExtensionUsedByInstalledMode.js'; +import findOhifExtensionsToRemoveAfterRemovingMode from './findOhifExtensionsToRemoveAfterRemovingMode.js'; +import initGit from './initGit.js'; +import createDirectoryContents from './createDirectoryContents.js'; +import editPackageJson from './editPackageJson.js'; +import createLicense from './createLicense.js'; +import createReadme from './createReadme.js'; +import prettyPrint from './prettyPrint.js'; +import validateYarn from './validateYarn.js'; + +export { + getYarnInfo, + getVersionedPackageName, + installNPMPackage, + uninstallNPMPackage, + validateMode, + validateExtension, + validateModeYarnInfo, + validateExtensionYarnInfo, + addExtensionToConfig, + addModeToConfig, + findRequiredOhifExtensionsForMode, + removeExtensionFromConfig, + throwIfExtensionUsedByInstalledMode, + removeModeFromConfig, + findOhifExtensionsToRemoveAfterRemovingMode, + initGit, + createDirectoryContents, + editPackageJson, + createLicense, + createReadme, + prettyPrint, + validateYarn, +}; diff --git a/platform/cli/src/commands/utils/initGit.js b/platform/cli/src/commands/utils/initGit.js new file mode 100644 index 00000000000..88686a5e334 --- /dev/null +++ b/platform/cli/src/commands/utils/initGit.js @@ -0,0 +1,35 @@ +import chalk from 'chalk'; +import fs from 'fs'; +import path from 'path'; +import { promisify } from 'util'; +import { execa } from 'execa'; + +const exists = promisify(fs.exists); + +async function initGit(options) { + const { targetDir } = options; + const targetPath = path.join(targetDir, '.git'); + + // Check if git is installed + try { + await execa('git', ['--version']); + } catch (err) { + console.error( + '%s Git is not installed. Please install git and try again.', + chalk.red.bold('ERROR') + ); + process.exit(1); + } + + if (!(await exists(targetPath))) { + try { + await execa('git', ['init'], { cwd: targetDir }); + } catch (err) { + console.error('%s Failed to initialize git', chalk.red.bold('ERROR')); + console.error(err); + process.exit(1); + } + } +} + +export default initGit; diff --git a/platform/cli/src/commands/utils/installNPMPackage.js b/platform/cli/src/commands/utils/installNPMPackage.js new file mode 100644 index 00000000000..fd125a19085 --- /dev/null +++ b/platform/cli/src/commands/utils/installNPMPackage.js @@ -0,0 +1,14 @@ +import { install } from 'pkg-install'; + +const installNPMPackage = async (packageName, version) => { + let installObject = {}; + + installObject[packageName] = version; + + await install(installObject, { + prefer: 'yarn', + cwd: process.cwd(), + }); +}; + +export default installNPMPackage; diff --git a/platform/cli/src/commands/utils/prettyPrint.js b/platform/cli/src/commands/utils/prettyPrint.js new file mode 100644 index 00000000000..ad7d9a1b075 --- /dev/null +++ b/platform/cli/src/commands/utils/prettyPrint.js @@ -0,0 +1,79 @@ +import chalk from 'chalk'; +import { colors } from '../enums/index.js'; + +function getStyle({ color, bold }) { + return bold ? chalk.hex(color).bold : chalk.hex(color); +} + +function levelOnePrint(items) { + let output = ''; + if (Array.isArray(items)) { + items.forEach(item => { + output += ` |- ${item}\n`; + }); + return output; + } + + return ` |- ${items}\n`; +} + +function levelTwoPrint(items) { + let output = ''; + items.forEach(item => { + output += ` | |- ${item}\n`; + }); + return output; +} + +/** + * + * @param {string} title Title of the section + * @param {object} titleOptions Options for the title includes color and bold + * @param { [] | [][] } items Array of items to display, OR a list of lists + * @param {object} itemOptions Options for the items includes color and bold + * + * + * items= ['Mode-A', 'Mode-B', 'Mode-C'] + * + * |- Mode-A + * |- Mode-B + * |- Mode-C + * + * items = [['Mode-A', ['Description-A', 'Authors-A', 'Repository-A]], ['Mode-B', ['Description-B', 'Authors-B', 'Repository-B]], ['Mode-C', ['Description-C', 'Authors-C', 'Repository-C]]] + * + * |- Mode-A + * | |- Description-A + * | |- Authors-A + * | |- Repository-A + * | + * |- Mode-B + * | |- Description-B + * | |- Authors-B + * | |- Repository-B + * + * + */ +function prettyPrint( + title, + titleOptions = { color: colors.MAIN, bold: true }, + itemsArray = [[]], + itemOptions = {} +) { + console.log(''); + console.log(getStyle(titleOptions)(title)); + + let output = ''; + itemsArray.forEach(items => { + if (!Array.isArray(items)) { + output += levelOnePrint(items); + } else { + output += levelOnePrint(items[0]); + output += levelTwoPrint(items[1]); + } + }); + + const itmeStyle = itemOptions.color ? getStyle(itemOptions)(output) : output; + console.log(itmeStyle); +} + +export default prettyPrint; diff --git a/platform/cli/src/commands/utils/private/getPackageNameAndScope.js b/platform/cli/src/commands/utils/private/getPackageNameAndScope.js new file mode 100644 index 00000000000..14c05f6833c --- /dev/null +++ b/platform/cli/src/commands/utils/private/getPackageNameAndScope.js @@ -0,0 +1,15 @@ +export default function getPackageNameAndScope(packageName) { + let scope; + let packageNameLessScope; + + if (packageName.includes('@')) { + [scope, packageNameLessScope] = packageName.split('/'); + } else { + packageNameLessScope = packageName; + } + + return { + scope, + packageNameLessScope, + }; +} diff --git a/platform/cli/src/commands/utils/private/index.js b/platform/cli/src/commands/utils/private/index.js new file mode 100644 index 00000000000..907418e1d65 --- /dev/null +++ b/platform/cli/src/commands/utils/private/index.js @@ -0,0 +1,19 @@ +import getPackageNameAndScope from './getPackageNameAndScope.js'; +import { + addExtensionToConfigJson, + removeExtensionFromConfigJson, + addModeToConfigJson, + removeModeFromConfigJson, +} from './manipulatePluginConfigFile.js'; +import writePluginConfigFile from './writePluginConfigFile.js'; +import readPluginConfigFile from './readPluginConfigFile.js'; + +export { + getPackageNameAndScope, + addExtensionToConfigJson, + removeExtensionFromConfigJson, + addModeToConfigJson, + removeModeFromConfigJson, + readPluginConfigFile, + writePluginConfigFile, +}; diff --git a/platform/cli/src/commands/utils/private/manipulatePluginConfigFile.js b/platform/cli/src/commands/utils/private/manipulatePluginConfigFile.js new file mode 100644 index 00000000000..37afbda00cc --- /dev/null +++ b/platform/cli/src/commands/utils/private/manipulatePluginConfigFile.js @@ -0,0 +1,40 @@ +function addExtensionToConfigJson(pluginConfig, { packageName, version }) { + addToList('extensions', pluginConfig, { packageName, version }); +} + +function addModeToConfigJson(pluginConfig, { packageName, version }) { + addToList('modes', pluginConfig, { packageName, version }); +} + +function removeExtensionFromConfigJson(pluginConfig, { packageName }) { + removeFromList('extensions', pluginConfig, { packageName }); +} + +function removeModeFromConfigJson(pluginConfig, { packageName }) { + removeFromList('modes', pluginConfig, { packageName }); +} + +function removeFromList(listName, pluginConfig, { packageName }) { + const list = pluginConfig[listName]; + + const indexOfExistingEntry = list.findIndex( + entry => entry.packageName === packageName + ); + + if (indexOfExistingEntry !== -1) { + pluginConfig[listName].splice(indexOfExistingEntry, 1); + } +} + +function addToList(listName, pluginConfig, { packageName, version }) { + removeFromList(listName, pluginConfig, { packageName }); + + pluginConfig[listName].push({ packageName, version }); +} + +export { + addExtensionToConfigJson, + addModeToConfigJson, + removeExtensionFromConfigJson, + removeModeFromConfigJson, +}; diff --git a/platform/cli/src/commands/utils/private/readPluginConfigFile.js b/platform/cli/src/commands/utils/private/readPluginConfigFile.js new file mode 100644 index 00000000000..07193fe7658 --- /dev/null +++ b/platform/cli/src/commands/utils/private/readPluginConfigFile.js @@ -0,0 +1,17 @@ +import fs from 'fs'; + +export default function readPluginConfigFile() { + let fileContents; + + try { + fileContents = fs.readFileSync('./pluginConfig.json', { flag: 'r' }); + } catch (err) { + return; // File doesn't exist yet. + } + + if (fileContents) { + fileContents = JSON.parse(fileContents); + } + + return fileContents; +} diff --git a/platform/cli/src/commands/utils/private/writePluginConfigFile.js b/platform/cli/src/commands/utils/private/writePluginConfigFile.js new file mode 100644 index 00000000000..eaef5234364 --- /dev/null +++ b/platform/cli/src/commands/utils/private/writePluginConfigFile.js @@ -0,0 +1,18 @@ +import fs from 'fs'; + +export default function writePluginConfigFile(pluginConfig) { + // Note: Second 2 arguments are to pretty print the JSON so its human readable. + const jsonStringOfFileContents = JSON.stringify(pluginConfig, null, 2); + + fs.writeFileSync( + `./pluginConfig.json`, + jsonStringOfFileContents, + { flag: 'w+' }, + (err) => { + if (err) { + console.error(err); + return; + } + } + ); +} diff --git a/platform/cli/src/commands/utils/removeFromConfig.js b/platform/cli/src/commands/utils/removeFromConfig.js new file mode 100644 index 00000000000..820cd3ee9ae --- /dev/null +++ b/platform/cli/src/commands/utils/removeFromConfig.js @@ -0,0 +1,26 @@ +import { + removeExtensionFromConfigJson, + removeModeFromConfigJson, + writePluginConfigFile, + readPluginConfigFile, +} from './private/index.js'; + +function removeFromAndOverwriteConfig(packageName, augmentConfigFunction) { + const pluginConfig = readPluginConfigFile(); + + // Note: if file is not found, nothing to remove. + if (pluginConfig) { + augmentConfigFunction(pluginConfig, { packageName }); + writePluginConfigFile(pluginConfig); + } +} + +function removeExtensionFromConfig(packageName) { + removeFromAndOverwriteConfig(packageName, removeExtensionFromConfigJson); +} + +function removeModeFromConfig(packageName) { + removeFromAndOverwriteConfig(packageName, removeModeFromConfigJson); +} + +export { removeExtensionFromConfig, removeModeFromConfig }; diff --git a/platform/cli/src/commands/utils/throwIfExtensionUsedByInstalledMode.js b/platform/cli/src/commands/utils/throwIfExtensionUsedByInstalledMode.js new file mode 100644 index 00000000000..63ce1ba3df6 --- /dev/null +++ b/platform/cli/src/commands/utils/throwIfExtensionUsedByInstalledMode.js @@ -0,0 +1,48 @@ +import { readPluginConfigFile } from './private/index.js'; +import getYarnInfo from './getYarnInfo.js'; +import chalk from 'chalk'; + +export default async function throwIfExtensionUsedByInstalledMode(packageName) { + const pluginConfig = readPluginConfigFile(); + + if (!pluginConfig) { + // No other modes, not in use + return false; + } + + const { modes } = pluginConfig; + + const modesUsingExtension = []; + + for (let i = 0; i < modes.length; i++) { + const mode = modes[i]; + const modePackageName = mode.packageName; + const yarnInfo = await getYarnInfo(modePackageName); + + const peerDependencies = yarnInfo.peerDependencies; + + if (!peerDependencies) { + continue; + } + + if (Object.keys(peerDependencies).includes(packageName)) { + modesUsingExtension.push(modePackageName); + } + } + + if (modesUsingExtension.length > 0) { + let modesString = ''; + + modesUsingExtension.forEach(packageName => { + modesString += ` ${packageName}`; + }); + + const error = new Error( + `${chalk.yellow.red( + 'Error' + )} ohif-extension ${packageName} used by installed modes:${modesString}` + ); + + throw error; + } +} diff --git a/platform/cli/src/commands/utils/uninstallNPMPackage.js b/platform/cli/src/commands/utils/uninstallNPMPackage.js new file mode 100644 index 00000000000..e8b32719a4b --- /dev/null +++ b/platform/cli/src/commands/utils/uninstallNPMPackage.js @@ -0,0 +1,12 @@ +import { remove } from 'yarn-programmatic'; + +const uninstallNPMPackage = async packageName => { + // TODO - Anoyingly pkg-install doesn't seem to have uninstall. + // So since we are using yarn we will just use yarn here, but the tool + // is certainly less generic. But its a super minor issue. + await remove(packageName).catch(err => { + console.log(err); + }); +}; + +export default uninstallNPMPackage; diff --git a/platform/cli/src/commands/utils/validate.js b/platform/cli/src/commands/utils/validate.js new file mode 100644 index 00000000000..89ccb589179 --- /dev/null +++ b/platform/cli/src/commands/utils/validate.js @@ -0,0 +1,160 @@ +import registryUrl from 'registry-url'; +import keywords from '../enums/keywords.js'; +import { getPackageNameAndScope } from './private/index.js'; +import chalk from 'chalk'; +import fetch from 'node-fetch'; +import getYarnInfo from './getYarnInfo.js'; +import NOT_FOUND from '../constants/notFound.js'; + +async function validateMode(packageName, version) { + return validate(packageName, version, keywords.MODE); +} + +async function validateExtension(packageName, version) { + return validate(packageName, version, keywords.EXTENSION); +} + +async function validateModeYarnInfo(packageName) { + return validateYarnInfo(packageName, keywords.MODE); +} + +async function validateExtensionYarnInfo(packageName) { + return validateYarnInfo(packageName, keywords.EXTENSION); +} + +function validateYarnInfo(packageName, keyword) { + return new Promise(async (resolve, reject) => { + function rejectIfNotFound() { + const error = new Error( + `${chalk.red.bold('Error')} extension ${packageName} not installed` + ); + reject(error); + } + + const packageInfo = await getYarnInfo(packageName).catch(() => { + rejectIfNotFound(); + }); + + if (!packageInfo) { + rejectIfNotFound(); + return; + } + + const { keywords } = packageInfo; + const isValid = keywords && keywords.includes(keyword); + + if (isValid) { + resolve(true); + } else { + const error = new Error( + `${chalk.red.bold('Error')} package ${packageName} is not an ${keyword}` + ); + reject(error); + } + }); +} + +function getVersion(json, version) { + const versions = Object.keys(json.versions); + // if no version is defined get the latest + if (version === undefined) { + return json['dist-tags'].latest; + } + + // Get and validate version if it is explicitly defined + const allowMinorVersionUpgrade = version.startsWith('^'); + if (!allowMinorVersionUpgrade) { + const isValidVersion = versions.includes(version); + + if (!isValidVersion) { + return; + } + + return version; + } + + // Choose version based on the newer minor/patch versions + const [majorVersion] = version + .split('^')[1] + .split('.') + .map((v) => parseInt(v)); + + // Find the version that matches the major version, but is the latest minor version + versions + .filter((version) => parseInt(version.split('.')[0]) === majorVersion) + .sort((a, b) => { + const [majorA, minorA, patchA] = a.split('.').map((v) => parseInt(v)); + const [majorB, minorB, patchB] = b.split('.').map((v) => parseInt(v)); + + if (majorA === majorB) { + if (minorA === minorB) { + return patchB - patchA; + } + + return minorB - minorA; + } + + return majorB - majorA; + }); + + if (versions.length === 0) { + return; + } + + return versions[0]; +} + +function validate(packageName, version, keyword) { + return new Promise(async (resolve, reject) => { + const { scope } = getPackageNameAndScope(packageName); + + // Gets the registry of the package. Scoped packages may not be using the global default. + const registryUrlOfPackage = registryUrl(scope); + + const response = await fetch(`${registryUrlOfPackage}${packageName}`); + const json = await response.json(); + + if (json.error && json.error === NOT_FOUND) { + const error = new Error( + `${chalk.red.bold('Error')} package ${packageName} not found` + ); + reject(error); + return; + } + + const packageVersion = getVersion(json, version); + + if (packageVersion) { + const versionedJson = json.versions[packageVersion]; + const keywords = versionedJson.keywords; + + const isValid = keywords && keywords.includes(keyword); + + if (isValid) { + resolve(true); + } else { + const error = new Error( + `${chalk.red.bold( + 'Error' + )} package ${packageName} is not an ${keyword}` + ); + reject(error); + } + } else { + // Particular version undefined + const error = new Error( + `${chalk.red.bold( + 'Error' + )} version ${packageVersion} of package ${packageName} not found` + ); + reject(error); + } + }); +} + +export { + validateMode, + validateExtension, + validateModeYarnInfo, + validateExtensionYarnInfo, +}; diff --git a/platform/cli/src/commands/utils/validateYarn.js b/platform/cli/src/commands/utils/validateYarn.js new file mode 100644 index 00000000000..8a74b83c08b --- /dev/null +++ b/platform/cli/src/commands/utils/validateYarn.js @@ -0,0 +1,14 @@ +import chalk from 'chalk'; +import { execa } from 'execa'; + +export default async function validateYarn() { + try { + await execa('yarn', ['--version']); + } catch (err) { + console.log( + '%s Yarn is not installed, please install it before linking your extension', + chalk.red.bold('ERROR') + ); + process.exit(1); + } +} diff --git a/platform/cli/src/index.js b/platform/cli/src/index.js new file mode 100755 index 00000000000..ffdf5cb7707 --- /dev/null +++ b/platform/cli/src/index.js @@ -0,0 +1,210 @@ +#!/usr/bin/env node + +import program from 'commander'; +import inquirer from 'inquirer'; +import path from 'path'; +import fs from 'fs'; +import { fileURLToPath } from 'url'; + +import { getPathQuestions, getRepoQuestions } from './questions.js'; +import { + createPackage, + addExtension, + removeExtension, + addMode, + removeMode, + listPlugins, + searchPlugins, + linkExtension, + linkMode, + unlinkExtension, + unlinkMode, +} from './commands/index.js'; +import chalk from 'chalk'; + +const runningDirectory = process.cwd(); +const viewerDirectory = path.resolve(runningDirectory, 'platform/viewer'); +const __dirname = path.dirname(fileURLToPath(import.meta.url)); + +const packageJsonPath = path.join(runningDirectory, 'package.json'); + +try { + const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8')); + if (packageJson.name !== 'ohif-monorepo-root') { + console.log(packageJson); + console.log( + chalk.red('ohif-cli must run from the root of the OHIF platform') + ); + process.exit(1); + } +} catch (error) { + console.log( + chalk.red('ohif-cli must run from the root of the OHIF platform') + ); + process.exit(1); +} + +function _createPackage(packageType) { + const pathQuestions = getPathQuestions(packageType); + const repoQuestions = getRepoQuestions(packageType); + + let pathAnswers; + + const askPathQuestions = () => { + inquirer.prompt(pathQuestions).then((answers) => { + pathAnswers = answers; + if (pathAnswers.confirm) { + askRepoQuestions(answers.baseDir, answers.name); + } else { + askPathQuestions(); + } + }); + }; + + const askRepoQuestions = () => { + inquirer.prompt(repoQuestions).then((repoAnswers) => { + const answers = { + ...pathAnswers, + ...repoAnswers, + }; + + const templateDir = path.join(__dirname, `../templates/${packageType}`); + answers.templateDir = templateDir; + answers.targetDir = path.join(answers.baseDir); + answers.packageType = packageType; + + createPackage(answers); + }); + }; + + askPathQuestions(); +} + +// Todo: inject with webpack +program.version('2.0.7').description('OHIF CLI'); + +program + .command('create-extension') + .description('Create a new template extension') + .action(() => { + _createPackage('extension'); + }); + +program + .command('create-mode') + .description('Create a new template Mode') + .action(() => { + _createPackage('mode'); + }); + +program + .command('add-extension [version]') + .description('Adds an ohif extension') + .action((packageName, version) => { + // change directory to viewer + process.chdir(viewerDirectory); + addExtension(packageName, version); + }); + +program + .command('remove-extension ') + .description('removes an ohif extension') + .action((packageName) => { + // change directory to viewer + process.chdir(viewerDirectory); + removeExtension(packageName); + }); + +program + .command('add-mode [version]') + .description('Removes an ohif mode') + .action((packageName, version) => { + // change directory to viewer + process.chdir(viewerDirectory); + addMode(packageName, version); + }); + +program + .command('remove-mode ') + .description('Removes an ohif mode') + .action((packageName) => { + // change directory to viewer + process.chdir(viewerDirectory); + removeMode(packageName); + }); + +program + .command('link-extension ') + .description( + 'Links a local OHIF extension to the Viewer to be used for development' + ) + .action((packageDir) => { + if (!fs.existsSync(packageDir)) { + console.log( + chalk.red( + 'The extension directory does not exist, please provide a valid directory' + ) + ); + process.exit(1); + } + linkExtension(packageDir, { viewerDirectory }); + }); + +program + .command('unlink-extension ') + .description('Unlinks a local OHIF extension from the Viewer') + .action((extensionName) => { + unlinkExtension(extensionName, { viewerDirectory }); + console.log( + chalk.green( + `Successfully unlinked extension ${extensionName} from the Viewer, don't forget to run yarn install --force` + ) + ); + }); + +program + .command('link-mode ') + .description( + 'Links a local OHIF mode to the Viewer to be used for development' + ) + .action((packageDir) => { + if (!fs.existsSync(packageDir)) { + console.log( + chalk.red( + 'The mode directory does not exist, please provide a valid directory' + ) + ); + process.exit(1); + } + linkMode(packageDir, { viewerDirectory }); + }); + +program + .command('unlink-mode ') + .description('Unlinks a local OHIF mode from the Viewer') + .action((modeName) => { + unlinkMode(modeName, { viewerDirectory }); + console.log( + chalk.green( + `Successfully unlinked mode ${modeName} from the Viewer, don't forget to run yarn install --force` + ) + ); + }); + +program + .command('list') + .description('List Added Extensions and Modes') + .action(() => { + const configPath = path.resolve(viewerDirectory, './pluginConfig.json'); + listPlugins(configPath); + }); + +program + .command('search') + .option('-v, --verbose', 'Verbose output') + .description('Search NPM for the list of Modes and Extensions') + .action((options) => { + searchPlugins(options); + }); + +program.parse(process.argv); diff --git a/platform/cli/src/questions.js b/platform/cli/src/questions.js new file mode 100644 index 00000000000..c640e2e1c5c --- /dev/null +++ b/platform/cli/src/questions.js @@ -0,0 +1,85 @@ +import path from 'path'; + +function getPathQuestions(packageType) { + return [ + { + type: 'input', + name: 'name', + message: `What is the name of your ${packageType}?`, + validate: (input) => { + if (!input) { + return 'Please enter a name'; + } + return true; + }, + default: `my-${packageType}`, + }, + { + type: 'input', + name: 'baseDir', + message: `What is the target path to create your ${packageType} (we recommend you do not use the OHIF ${packageType} folder (./${packageType}s) unless you are developing a core ${packageType}):`, + validate: (input) => { + if (!input) { + console.log('Please provide a valid target directory path'); + return; + } + return true; + }, + filter: (input, answers) => { + return path.resolve(input, answers.name); + }, + }, + { + type: 'confirm', + name: 'confirm', + message: `Please confirm the above path for generating the ${packageType} folder:`, + }, + ]; +} + +function getRepoQuestions(packageType) { + return [ + { + type: 'confirm', + name: 'gitRepository', + message: 'Should it be a git repository?', + }, + { + type: 'confirm', + name: 'prettier', + message: 'Should it follow same prettier rules as OHIF?', + }, + { + type: 'input', + name: 'version', + message: `What is the version of your ${packageType}?`, + default: '0.0.1', + }, + { + type: 'input', + name: 'description', + message: `What is the description of your ${packageType}?`, + default: '', + }, + { + type: 'input', + name: 'author', + message: `Who is the author of your ${packageType}?`, + default: '', + }, + { + type: 'input', + name: 'email', + message: 'What is your email address?', + default: '', + }, + { + type: 'input', + name: 'license', + message: `What is the license of your ${packageType}?`, + default: 'MIT', + }, + ]; +} + +export { getPathQuestions, getRepoQuestions }; diff --git a/platform/cli/templates/extension/.gitignore b/platform/cli/templates/extension/.gitignore new file mode 100644 index 00000000000..67045665db2 --- /dev/null +++ b/platform/cli/templates/extension/.gitignore @@ -0,0 +1,104 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +lerna-debug.log* + +# Diagnostic reports (https://nodejs.org/api/report.html) +report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json + +# Runtime data +pids +*.pid +*.seed +*.pid.lock + +# Directory for instrumented libs generated by jscoverage/JSCover +lib-cov + +# Coverage directory used by tools like istanbul +coverage +*.lcov + +# nyc test coverage +.nyc_output + +# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) +.grunt + +# Bower dependency directory (https://bower.io/) +bower_components + +# node-waf configuration +.lock-wscript + +# Compiled binary addons (https://nodejs.org/api/addons.html) +build/Release + +# Dependency directories +node_modules/ +jspm_packages/ + +# TypeScript v1 declaration files +typings/ + +# TypeScript cache +*.tsbuildinfo + +# Optional npm cache directory +.npm + +# Optional eslint cache +.eslintcache + +# Microbundle cache +.rpt2_cache/ +.rts2_cache_cjs/ +.rts2_cache_es/ +.rts2_cache_umd/ + +# Optional REPL history +.node_repl_history + +# Output of 'npm pack' +*.tgz + +# Yarn Integrity file +.yarn-integrity + +# dotenv environment variables file +.env +.env.test + +# parcel-bundler cache (https://parceljs.org/) +.cache + +# Next.js build output +.next + +# Nuxt.js build / generate output +.nuxt +dist + +# Gatsby files +.cache/ +# Comment in the public line in if your project uses Gatsby and *not* Next.js +# https://nextjs.org/blog/next-9-1#public-directory-support +# public + +# vuepress build output +.vuepress/dist + +# Serverless directories +.serverless/ + +# FuseBox cache +.fusebox/ + +# DynamoDB Local files +.dynamodb/ + +# TernJS port file +.tern-port diff --git a/platform/cli/templates/extension/.prettierrc b/platform/cli/templates/extension/.prettierrc new file mode 100644 index 00000000000..b80ec6b3474 --- /dev/null +++ b/platform/cli/templates/extension/.prettierrc @@ -0,0 +1,8 @@ +{ + "trailingComma": "es5", + "printWidth": 80, + "proseWrap": "always", + "tabWidth": 2, + "semi": true, + "singleQuote": true +} diff --git a/platform/cli/templates/extension/.webpack/webpack.prod.js b/platform/cli/templates/extension/.webpack/webpack.prod.js new file mode 100644 index 00000000000..11ba8eee5c3 --- /dev/null +++ b/platform/cli/templates/extension/.webpack/webpack.prod.js @@ -0,0 +1,48 @@ +const path = require('path'); +const pkg = require('../package.json'); + +const outputFile = 'index.umd.js'; +const rootDir = path.resolve(__dirname, '../'); +const outputFolder = path.join(__dirname, '../dist'); + +const config = { + mode: 'production', + entry: rootDir + '/' + pkg.module, + devtool: 'inline-source-map', + output: { + path: outputFolder, + filename: outputFile, + library: pkg.name, + libraryTarget: 'umd', + umdNamedDefine: true, + globalObject: "typeof self !== 'undefined' ? self : this", + }, + externals: [ + { + react: { + root: 'React', + commonjs2: 'react', + commonjs: 'react', + amd: 'react', + }, + }, + ], + module: { + rules: [ + { + test: /(\.jsx|\.js)$/, + loader: 'babel-loader', + exclude: /(node_modules|bower_components)/, + resolve: { + extensions: ['.js', '.jsx'], + }, + }, + ], + }, + resolve: { + modules: [path.resolve('./node_modules'), path.resolve('./src')], + extensions: ['.json', '.js', '.jsx'], + }, +}; + +module.exports = config; diff --git a/platform/cli/templates/extension/babel.config.js b/platform/cli/templates/extension/babel.config.js new file mode 100644 index 00000000000..4afa952b0c1 --- /dev/null +++ b/platform/cli/templates/extension/babel.config.js @@ -0,0 +1,41 @@ +module.exports = { + plugins: ['inline-react-svg', '@babel/plugin-proposal-class-properties'], + env: { + test: { + presets: [ + [ + // TODO: https://babeljs.io/blog/2019/03/19/7.4.0#migration-from-core-js-2 + '@babel/preset-env', + { + modules: 'commonjs', + debug: false, + }, + ], + '@babel/preset-react', + ], + plugins: [ + '@babel/plugin-proposal-object-rest-spread', + '@babel/plugin-syntax-dynamic-import', + '@babel/plugin-transform-regenerator', + '@babel/plugin-transform-runtime', + ], + }, + production: { + presets: [ + // WebPack handles ES6 --> Target Syntax + ['@babel/preset-env', { modules: false }], + '@babel/preset-react', + ], + ignore: ['**/*.test.jsx', '**/*.test.js', '__snapshots__', '__tests__'], + }, + development: { + presets: [ + // WebPack handles ES6 --> Target Syntax + ['@babel/preset-env', { modules: false }], + '@babel/preset-react', + ], + plugins: ['react-hot-loader/babel'], + ignore: ['**/*.test.jsx', '**/*.test.js', '__snapshots__', '__tests__'], + }, + }, +}; diff --git a/platform/cli/templates/extension/dependencies.json b/platform/cli/templates/extension/dependencies.json new file mode 100644 index 00000000000..471d842603c --- /dev/null +++ b/platform/cli/templates/extension/dependencies.json @@ -0,0 +1,58 @@ +{ + "repository": "OHIF/Viewers", + "keywords": ["ohif-extension"], + "main": "dist/index.umd.js", + "module": "src/index.js", + "engines": { + "node": ">=14", + "npm": ">=6", + "yarn": ">=1.18.0" + }, + "scripts": { + "dev": "cross-env NODE_ENV=development webpack --config .webpack/webpack.dev.js --watch --debug --output-pathinfo", + "dev:dicom-pdf": "yarn run dev", + "build": "cross-env NODE_ENV=production webpack --config .webpack/webpack.prod.js", + "build:package": "yarn run build", + "start": "yarn run dev" + }, + "peerDependencies": { + "@ohif/core": "^3.0.0", + "@ohif/extension-default": "^1.0.1", + "@ohif/extension-cornerstone": "^3.0.0", + "@ohif/i18n": "^1.0.0", + "prop-types": "^15.6.2", + "react": "^17.0.2", + "react-dom": "^17.0.2", + "react-i18next": "^10.11.0", + "react-router": "^6.2.1", + "react-router-dom": "^6.2.1", + "webpack": "^5.50.0", + "webpack-merge": "^5.7.3" + }, + "dependencies": { + "@babel/runtime": "7.7.6" + }, + "devDependencies": { + "@babel/core": "^7.5.0", + "@babel/plugin-proposal-class-properties": "^7.5.0", + "@babel/plugin-proposal-object-rest-spread": "^7.5.5", + "@babel/plugin-syntax-dynamic-import": "^7.2.0", + "@babel/plugin-transform-arrow-functions": "^7.2.0", + "@babel/plugin-transform-regenerator": "^7.4.5", + "@babel/plugin-transform-runtime": "^7.5.0", + "babel-plugin-inline-react-svg": "^2.0.1", + "@babel/preset-env": "^7.5.0", + "@babel/preset-react": "^7.0.0", + "babel-eslint": "^8.0.3", + "babel-loader": "^8.0.0-beta.4", + "clean-webpack-plugin": "^4.0.0", + "copy-webpack-plugin": "^10.2.0", + "cross-env": "^7.0.3", + "dotenv": "^14.1.0", + "eslint": "^5.0.1", + "eslint-loader": "^2.0.0", + "uglifyjs-webpack-plugin": "^1.2.7", + "webpack": "^4.12.2", + "webpack-cli": "^3.0.8" + } +} diff --git a/platform/cli/templates/extension/src/id.js b/platform/cli/templates/extension/src/id.js new file mode 100644 index 00000000000..ebe5acd98ae --- /dev/null +++ b/platform/cli/templates/extension/src/id.js @@ -0,0 +1,5 @@ +import packageJson from '../package.json'; + +const id = packageJson.name; + +export { id }; diff --git a/platform/cli/templates/extension/src/index.js b/platform/cli/templates/extension/src/index.js new file mode 100644 index 00000000000..fa659bced10 --- /dev/null +++ b/platform/cli/templates/extension/src/index.js @@ -0,0 +1,126 @@ +import { id } from './id'; + +/** + * You can remove any of the following modules if you don't need them. + */ +export default { + /** + * Only required property. Should be a unique value across all extensions. + * You ID can be anything you want, but it should be unique. + */ + id, + + /** + * Perform any pre-registration tasks here. This is called before the extension + * is registered. Usually we run tasks such as: configuring the libraries + * (e.g. cornerstone, cornerstoneTools, ...) or registering any services that + * this extension is providing. + */ + preRegistration: ({ + servicesManager, + commandsManager, + configuration = {}, + }) => {}, + /** + * PanelModule should provide a list of panels that will be available in OHIF + * for Modes to consume and render. Each panel is defined by a {name, + * iconName, iconLabel, label, component} object. Example of a panel module + * is the StudyBrowserPanel that is provided by the default extension in OHIF. + */ + getPanelModule: ({ + servicesManager, + commandsManager, + extensionManager, + }) => {}, + /** + * ViewportModule should provide a list of viewports that will be available in OHIF + * for Modes to consume and use in the viewports. Each viewport is defined by + * {name, component} object. Example of a viewport module is the CornerstoneViewport + * that is provided by the Cornerstone extension in OHIF. + */ + getViewportModule: ({ + servicesManager, + commandsManager, + extensionManager, + }) => {}, + /** + * ToolbarModule should provide a list of tool buttons that will be available in OHIF + * for Modes to consume and use in the toolbar. Each tool button is defined by + * {name, defaultComponent, clickHandler }. Examples include radioGroupIcons and + * splitButton toolButton that the default extension is providing. + */ + getToolbarModule: ({ + servicesManager, + commandsManager, + extensionManager, + }) => {}, + /** + * LayoutTemplateMOdule should provide a list of layout templates that will be + * available in OHIF for Modes to consume and use to layout the viewer. + * Each layout template is defined by a { name, id, component}. Examples include + * the default layout template provided by the default extension which renders + * a Header, left and right sidebars, and a viewport section in the middle + * of the viewer. + */ + getLayoutTemplateModule: ({ + servicesManager, + commandsManager, + extensionManager, + }) => {}, + /** + * SopClassHandlerModule should provide a list of sop class handlers that will be + * available in OHIF for Modes to consume and use to create displaySets from Series. + * Each sop class handler is defined by a { name, sopClassUids, getDisplaySetsFromSeries}. + * Examples include the default sop class handler provided by the default extension + */ + getSopClassHandlerModule: ({ + servicesManager, + commandsManager, + extensionManager, + }) => {}, + /** + * HangingProtocolModule should provide a list of hanging protocols that will be + * available in OHIF for Modes to use to decide on the structure of the viewports + * and also the series that hung in the viewports. Each hanging protocol is defined by + * { name, protocols}. Examples include the default hanging protocol provided by + * the default extension that shows 2x2 viewports. + */ + getHangingProtocolModule: ({ + servicesManager, + commandsManager, + extensionManager, + }) => {}, + /** + * CommandsModule should provide a list of commands that will be available in OHIF + * for Modes to consume and use in the viewports. Each command is defined by + * an object of { actions, definitions, defaultContext } where actions is an + * object of functions, definitions is an object of available commands, their + * options, and defaultContext is the default context for the command to run against. + */ + getCommandsModule: ({ + servicesManager, + commandsManager, + extensionManager, + }) => {}, + /** + * ContextModule should provide a list of context that will be available in OHIF + * and will be provided to the Modes. A context is a state that is shared OHIF. + * Context is defined by an object of { name, context, provider }. Examples include + * the measurementTracking context provided by the measurementTracking extension. + */ + getContextModule: ({ + servicesManager, + commandsManager, + extensionManager, + }) => {}, + /** + * DataSourceModule should provide a list of data sources to be used in OHIF. + * DataSources can be used to map the external data formats to the OHIF's + * native format. DataSources are defined by an object of { name, type, createDataSource }. + */ + getDataSourcesModule: ({ + servicesManager, + commandsManager, + extensionManager, + }) => {}, +}; diff --git a/platform/cli/templates/mode/.gitignore b/platform/cli/templates/mode/.gitignore new file mode 100644 index 00000000000..67045665db2 --- /dev/null +++ b/platform/cli/templates/mode/.gitignore @@ -0,0 +1,104 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +lerna-debug.log* + +# Diagnostic reports (https://nodejs.org/api/report.html) +report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json + +# Runtime data +pids +*.pid +*.seed +*.pid.lock + +# Directory for instrumented libs generated by jscoverage/JSCover +lib-cov + +# Coverage directory used by tools like istanbul +coverage +*.lcov + +# nyc test coverage +.nyc_output + +# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) +.grunt + +# Bower dependency directory (https://bower.io/) +bower_components + +# node-waf configuration +.lock-wscript + +# Compiled binary addons (https://nodejs.org/api/addons.html) +build/Release + +# Dependency directories +node_modules/ +jspm_packages/ + +# TypeScript v1 declaration files +typings/ + +# TypeScript cache +*.tsbuildinfo + +# Optional npm cache directory +.npm + +# Optional eslint cache +.eslintcache + +# Microbundle cache +.rpt2_cache/ +.rts2_cache_cjs/ +.rts2_cache_es/ +.rts2_cache_umd/ + +# Optional REPL history +.node_repl_history + +# Output of 'npm pack' +*.tgz + +# Yarn Integrity file +.yarn-integrity + +# dotenv environment variables file +.env +.env.test + +# parcel-bundler cache (https://parceljs.org/) +.cache + +# Next.js build output +.next + +# Nuxt.js build / generate output +.nuxt +dist + +# Gatsby files +.cache/ +# Comment in the public line in if your project uses Gatsby and *not* Next.js +# https://nextjs.org/blog/next-9-1#public-directory-support +# public + +# vuepress build output +.vuepress/dist + +# Serverless directories +.serverless/ + +# FuseBox cache +.fusebox/ + +# DynamoDB Local files +.dynamodb/ + +# TernJS port file +.tern-port diff --git a/platform/cli/templates/mode/.prettierrc b/platform/cli/templates/mode/.prettierrc new file mode 100644 index 00000000000..b80ec6b3474 --- /dev/null +++ b/platform/cli/templates/mode/.prettierrc @@ -0,0 +1,8 @@ +{ + "trailingComma": "es5", + "printWidth": 80, + "proseWrap": "always", + "tabWidth": 2, + "semi": true, + "singleQuote": true +} diff --git a/platform/cli/templates/mode/.webpack/webpack.prod.js b/platform/cli/templates/mode/.webpack/webpack.prod.js new file mode 100644 index 00000000000..11ba8eee5c3 --- /dev/null +++ b/platform/cli/templates/mode/.webpack/webpack.prod.js @@ -0,0 +1,48 @@ +const path = require('path'); +const pkg = require('../package.json'); + +const outputFile = 'index.umd.js'; +const rootDir = path.resolve(__dirname, '../'); +const outputFolder = path.join(__dirname, '../dist'); + +const config = { + mode: 'production', + entry: rootDir + '/' + pkg.module, + devtool: 'inline-source-map', + output: { + path: outputFolder, + filename: outputFile, + library: pkg.name, + libraryTarget: 'umd', + umdNamedDefine: true, + globalObject: "typeof self !== 'undefined' ? self : this", + }, + externals: [ + { + react: { + root: 'React', + commonjs2: 'react', + commonjs: 'react', + amd: 'react', + }, + }, + ], + module: { + rules: [ + { + test: /(\.jsx|\.js)$/, + loader: 'babel-loader', + exclude: /(node_modules|bower_components)/, + resolve: { + extensions: ['.js', '.jsx'], + }, + }, + ], + }, + resolve: { + modules: [path.resolve('./node_modules'), path.resolve('./src')], + extensions: ['.json', '.js', '.jsx'], + }, +}; + +module.exports = config; diff --git a/platform/cli/templates/mode/babel.config.js b/platform/cli/templates/mode/babel.config.js new file mode 100644 index 00000000000..4afa952b0c1 --- /dev/null +++ b/platform/cli/templates/mode/babel.config.js @@ -0,0 +1,41 @@ +module.exports = { + plugins: ['inline-react-svg', '@babel/plugin-proposal-class-properties'], + env: { + test: { + presets: [ + [ + // TODO: https://babeljs.io/blog/2019/03/19/7.4.0#migration-from-core-js-2 + '@babel/preset-env', + { + modules: 'commonjs', + debug: false, + }, + ], + '@babel/preset-react', + ], + plugins: [ + '@babel/plugin-proposal-object-rest-spread', + '@babel/plugin-syntax-dynamic-import', + '@babel/plugin-transform-regenerator', + '@babel/plugin-transform-runtime', + ], + }, + production: { + presets: [ + // WebPack handles ES6 --> Target Syntax + ['@babel/preset-env', { modules: false }], + '@babel/preset-react', + ], + ignore: ['**/*.test.jsx', '**/*.test.js', '__snapshots__', '__tests__'], + }, + development: { + presets: [ + // WebPack handles ES6 --> Target Syntax + ['@babel/preset-env', { modules: false }], + '@babel/preset-react', + ], + plugins: ['react-hot-loader/babel'], + ignore: ['**/*.test.jsx', '**/*.test.js', '__snapshots__', '__tests__'], + }, + }, +}; diff --git a/platform/cli/templates/mode/dependencies.json b/platform/cli/templates/mode/dependencies.json new file mode 100644 index 00000000000..a671e67df5b --- /dev/null +++ b/platform/cli/templates/mode/dependencies.json @@ -0,0 +1,49 @@ +{ + "repository": "OHIF/Viewers", + "keywords": ["ohif-mode"], + "main": "dist/index.umd.js", + "module": "src/index.js", + "engines": { + "node": ">=14", + "npm": ">=6", + "yarn": ">=1.16.0" + }, + "scripts": { + "dev": "cross-env NODE_ENV=development webpack --config .webpack/webpack.dev.js --watch --debug --output-pathinfo", + "dev:cornerstone": "yarn run dev", + "build": "cross-env NODE_ENV=production webpack --config .webpack/webpack.prod.js", + "build:package": "yarn run build", + "start": "yarn run dev", + "test:unit": "jest --watchAll", + "test:unit:ci": "jest --ci --runInBand --collectCoverage --passWithNoTests" + }, + "peerDependencies": { + "@ohif/core": "^3.0.0" + }, + "dependencies": { + "@babel/runtime": "7.7.6" + }, + "devDependencies": { + "@babel/core": "^7.5.0", + "@babel/plugin-proposal-class-properties": "^7.5.0", + "@babel/plugin-proposal-object-rest-spread": "^7.5.5", + "@babel/plugin-syntax-dynamic-import": "^7.2.0", + "@babel/plugin-transform-arrow-functions": "^7.2.0", + "@babel/plugin-transform-regenerator": "^7.4.5", + "@babel/plugin-transform-runtime": "^7.5.0", + "babel-plugin-inline-react-svg": "^2.0.1", + "@babel/preset-env": "^7.5.0", + "@babel/preset-react": "^7.0.0", + "babel-eslint": "^8.0.3", + "babel-loader": "^8.0.0-beta.4", + "clean-webpack-plugin": "^4.0.0", + "copy-webpack-plugin": "^10.2.0", + "cross-env": "^7.0.3", + "dotenv": "^14.1.0", + "eslint": "^5.0.1", + "eslint-loader": "^2.0.0", + "uglifyjs-webpack-plugin": "^1.2.7", + "webpack": "^4.12.2", + "webpack-cli": "^3.0.8" + } +} diff --git a/platform/cli/templates/mode/src/id.js b/platform/cli/templates/mode/src/id.js new file mode 100644 index 00000000000..ebe5acd98ae --- /dev/null +++ b/platform/cli/templates/mode/src/id.js @@ -0,0 +1,5 @@ +import packageJson from '../package.json'; + +const id = packageJson.name; + +export { id }; diff --git a/platform/cli/templates/mode/src/index.js b/platform/cli/templates/mode/src/index.js new file mode 100644 index 00000000000..5574f373048 --- /dev/null +++ b/platform/cli/templates/mode/src/index.js @@ -0,0 +1,106 @@ +import { id } from './id'; + +const ohif = { + layout: '@ohif/extension-default.layoutTemplateModule.viewerLayout', + sopClassHandler: '@ohif/extension-default.sopClassHandlerModule.stack', + hangingProtocols: '@ohif/extension-default.hangingProtocolModule.default', + leftPanel: '@ohif/extension-default.panelModule.seriesList', + rightPanel: '@ohif/extension-default.panelModule.measure', +}; + +const cornerstone = { + viewport: '@ohif/extension-cornerstone.viewportModule.cornerstone', +}; + +/** + * Just two dependencies to be able to render a viewport with panels in order + * to make sure that the mode is working. + */ +const extensionDependencies = { + '@ohif/extension-default': '^3.0.0', + '@ohif/extension-cornerstone': '^3.0.0', +}; + +function modeFactory({ modeConfiguration }) { + return { + /** + * Mode ID, which should be unique among modes used by the viewer. This ID + * is used to identify the mode in the viewer's state. + */ + id, + routeName: 'template', + /** + * Mode name, which is displayed in the viewer's UI in the workList, for the + * user to select the mode. + */ + displayName: 'Template Mode', + /** + * Runs when the Mode Route is mounted to the DOM. Usually used to initialize + * Services and other resources. + */ + onModeEnter: ({ servicesManager, extensionManager }) => {}, + /** + * Runs when the Mode Route is unmounted from the DOM. Usually used to clean + * up resources and states + */ + onModeExit: () => {}, + /** */ + validationTags: { + study: [], + series: [], + }, + /** + * A boolean return value that indicates whether the mode is valid for the + * modalities of the selected studies. For instance a PET/CT mode should be + */ + isValidMode: ({ modalities }) => true, + /** + * Mode Routes are used to define the mode's behavior. A list of Mode Route + * that includes the mode's path and the layout to be used. The layout will + * include the components that are used in the layout. For instance, if the + * default layoutTemplate is used (id: '@ohif/extension-default.layoutTemplateModule.viewerLayout') + * it will include the leftPanels, rightPanels, and viewports. However, if + * you define another layoutTemplate that includes a Footer for instance, + * you should provide the Footer component here too. Note: We use Strings + * to reference the component's ID as they are registered in the internal + * ExtensionManager. The template for the string is: + * `${extensionId}.{moduleType}.${componentId}`. + */ + routes: [ + { + path: 'template', + layoutTemplate: ({ location, servicesManager }) => { + return { + id: ohif.layout, + props: { + leftPanels: [ohif.leftPanel], + rightPanels: [ohif.rightPanel], + viewports: [ + { + namespace: cornerstone.viewport, + displaySetsToDisplay: [ohif.sopClassHandler], + }, + ], + }, + }; + }, + }, + ], + /** List of extensions that are used by the mode */ + extensions: extensionDependencies, + /** HangingProtocols used by the mode */ + hangingProtocols: [''], + /** SopClassHandlers used by the mode */ + sopClassHandlers: [ohif.sopClassHandler], + /** hotkeys for mode */ + hotkeys: [''], + }; +} + +const mode = { + id, + modeFactory, + extensionDependencies, +}; + +export default mode; diff --git a/platform/core/.webpack/webpack.dev.js b/platform/core/.webpack/webpack.dev.js index 1ae30844802..db7c206b134 100644 --- a/platform/core/.webpack/webpack.dev.js +++ b/platform/core/.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/core/.webpack/webpack.prod.js b/platform/core/.webpack/webpack.prod.js index 890e6d935b4..6ddd016979e 100644 --- a/platform/core/.webpack/webpack.prod.js +++ b/platform/core/.webpack/webpack.prod.js @@ -1,9 +1,9 @@ -const merge = require('webpack-merge'); +const { merge } = require('webpack-merge'); const path = require('path'); const webpackCommon = require('./../../../.webpack/webpack.base.js'); -const pkg = require('./../package.json'); -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'); diff --git a/platform/core/package.json b/platform/core/package.json index 164d560a900..70d28744494 100644 --- a/platform/core/package.json +++ b/platform/core/package.json @@ -1,6 +1,6 @@ { "name": "@ohif/core", - "version": "2.9.6", + "version": "3.0.0", "description": "Generic business logic for web-based medical imaging applications", "author": "OHIF Core Team", "license": "MIT", @@ -16,7 +16,7 @@ "README.md" ], "engines": { - "node": ">=10", + "node": ">=14", "npm": ">=6", "yarn": ">=1.16.0" }, @@ -35,7 +35,7 @@ "cornerstone-math": "0.1.9", "cornerstone-wado-image-loader": "4.0.4", "dicom-parser": "^1.8.9", - "@ohif/ui": "^1.8.2" + "@ohif/ui": "^2.0.0" }, "dependencies": { "@babel/runtime": "7.16.3", diff --git a/platform/core/src/utils/index.js b/platform/core/src/utils/index.js index baed6ace70f..6cee230c54b 100644 --- a/platform/core/src/utils/index.js +++ b/platform/core/src/utils/index.js @@ -20,6 +20,8 @@ import resolveObjectPath from './resolveObjectPath'; import hierarchicalListUtils from './hierarchicalListUtils'; import progressTrackingUtils from './progressTrackingUtils'; import isLowPriorityModality from './isLowPriorityModality'; +import { isImage } from './isImage'; +import isDisplaySetReconstructable from './isDisplaySetReconstructable'; // Commented out unused functionality. // Need to implement new mechanism for dervived displaySets using the displaySetManager. @@ -47,6 +49,8 @@ const utils = { hierarchicalListUtils, progressTrackingUtils, isLowPriorityModality, + isImage, + isDisplaySetReconstructable, }; export { @@ -70,6 +74,8 @@ export { hierarchicalListUtils, progressTrackingUtils, isLowPriorityModality, + isImage, + isDisplaySetReconstructable, }; export default utils; diff --git a/platform/core/src/utils/index.test.js b/platform/core/src/utils/index.test.js index 791681acdc4..c75a84e64c0 100644 --- a/platform/core/src/utils/index.test.js +++ b/platform/core/src/utils/index.test.js @@ -15,6 +15,8 @@ describe('Top level exports', () => { 'formatDate', 'formatPN', //'loadAndCacheDerivedDisplaySets', + 'isDisplaySetReconstructable', + 'isImage', 'DicomLoaderService', 'urlUtil', 'makeDeferred', diff --git a/platform/docs/docs/assets/img/add-extension.png b/platform/docs/docs/assets/img/add-extension.png new file mode 100644 index 00000000000..bb4955e97fb Binary files /dev/null and b/platform/docs/docs/assets/img/add-extension.png differ diff --git a/platform/docs/docs/assets/img/add-mode.png b/platform/docs/docs/assets/img/add-mode.png new file mode 100644 index 00000000000..6f1a16280db Binary files /dev/null and b/platform/docs/docs/assets/img/add-mode.png differ diff --git a/platform/docs/docs/assets/img/cli-search-no-verbose.png b/platform/docs/docs/assets/img/cli-search-no-verbose.png new file mode 100644 index 00000000000..40b511309b8 Binary files /dev/null and b/platform/docs/docs/assets/img/cli-search-no-verbose.png differ diff --git a/platform/docs/docs/assets/img/cli-search-with-verbose.png b/platform/docs/docs/assets/img/cli-search-with-verbose.png new file mode 100644 index 00000000000..b15713b3985 Binary files /dev/null and b/platform/docs/docs/assets/img/cli-search-with-verbose.png differ diff --git a/platform/docs/docs/assets/img/clock-mode.png b/platform/docs/docs/assets/img/clock-mode.png new file mode 100644 index 00000000000..68ea6dc01a7 Binary files /dev/null and b/platform/docs/docs/assets/img/clock-mode.png differ diff --git a/platform/docs/docs/assets/img/clock-mode1.png b/platform/docs/docs/assets/img/clock-mode1.png new file mode 100644 index 00000000000..7b0375dcc39 Binary files /dev/null and b/platform/docs/docs/assets/img/clock-mode1.png differ diff --git a/platform/docs/docs/assets/img/create-extension.png b/platform/docs/docs/assets/img/create-extension.png new file mode 100644 index 00000000000..a68264936a8 Binary files /dev/null and b/platform/docs/docs/assets/img/create-extension.png differ diff --git a/platform/docs/docs/assets/img/create-mode.png b/platform/docs/docs/assets/img/create-mode.png new file mode 100644 index 00000000000..3ca4e26b006 Binary files /dev/null and b/platform/docs/docs/assets/img/create-mode.png differ diff --git a/platform/docs/docs/assets/img/mode-clock.png b/platform/docs/docs/assets/img/mode-clock.png new file mode 100644 index 00000000000..d855edbca75 Binary files /dev/null and b/platform/docs/docs/assets/img/mode-clock.png differ diff --git a/platform/docs/docs/assets/img/mode-template.png b/platform/docs/docs/assets/img/mode-template.png new file mode 100644 index 00000000000..9442411b5e0 Binary files /dev/null and b/platform/docs/docs/assets/img/mode-template.png differ diff --git a/platform/docs/docs/assets/img/ohif-cli-list.png b/platform/docs/docs/assets/img/ohif-cli-list.png new file mode 100644 index 00000000000..a992f0169b2 Binary files /dev/null and b/platform/docs/docs/assets/img/ohif-cli-list.png differ diff --git a/platform/docs/docs/configuration/configuration.md b/platform/docs/docs/configuration/configuration.md index cfc50f97ec1..cea7ce6fabd 100644 --- a/platform/docs/docs/configuration/configuration.md +++ b/platform/docs/docs/configuration/configuration.md @@ -53,7 +53,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', @@ -73,39 +73,6 @@ window.config = { }; ``` -## Run Time Configuration (Config-Point) -There is a library [config-point](https://github.com/OHIF/config-point) -used to allow loading of configuration values dynamically, -that is, at load time rather than being built into the runtime configuration. -A user of OHIF can specify a dynamic configuration by adding one or more theme -parameters, for example: -``` -https://ohif.hospital.org/?theme=mgHP&theme=euroKeyboard -``` -to load two hypothetical theme settings files mgHP and euroKeyboard to add -mammographic hanging protocols and European keyboard settings. - -A site can add such settings by creating custom files in the deployment -directory (which is wherever the deployed OHIF is located.) For a deployment -running off a straight build of OHIF, this would be: -``` -...Viewers/platform/viewer/dist/theme/mgHP.json5 -...Viewers/platform/viewer/dist/theme/euroKeyboard.json5 -``` -A site might build such different themes to support various user preferences -or site differences between users, such as themes to support specific clinics -or differences in user groups such as left on right mammography viewing versus -right on left mammography viewing. - -The decision to use the JSON5 parser for this was primarily aimed at allowing -comments in the configuration files, an important consideration for sites -wanting to document their settings. - -See [theme-configuration](theme-configuration.md) for more details on the -specific configuration settings which can be applied. - -See [config-point-service](../platform/services/config-point-service.md) for -information on how to add your own config-point based extensions to the code. diff --git a/platform/docs/docs/platform/extensions/extension.md b/platform/docs/docs/platform/extensions/extension.md index 70d2407f76e..148b82a9faf 100644 --- a/platform/docs/docs/platform/extensions/extension.md +++ b/platform/docs/docs/platform/extensions/extension.md @@ -6,6 +6,7 @@ sidebar_label: Extension Manager # Extension Manager ## Overview + The `ExtensionManager` is a class made available to us via the `@ohif/core` project (platform/core). Our application instantiates a single instance of it, and provides a `ServicesManager` and `CommandsManager` along with the @@ -28,16 +29,15 @@ The `ExtensionManager` only has a few public members: - `getActiveDataSource` - Returns the currently active data source - `getModuleEntry` - Returns the module entry by the give id. - ## Accessing Modules -We use `getModuleEntry` in our `ViewerLayout` logic to find the panels based on the -provided IDs in the mode's configuration. - - -For instance: `extensionManager.getModuleEntry("org.ohif.measurement-tracking.panelModule.seriesList")` -accesses the `seriesList` panel from `panelModule` of the `org.ohif.measurement-tracking` extension. +We use `getModuleEntry` in our `ViewerLayout` logic to find the panels based on +the provided IDs in the mode's configuration. +For instance: +`extensionManager.getModuleEntry("@ohif/extension-measurement-tracking.panelModule.seriesList")` +accesses the `seriesList` panel from `panelModule` of the +`@ohif/extension-measurement-tracking` extension. ```js const getPanelData = id => { diff --git a/platform/docs/docs/platform/extensions/index.md b/platform/docs/docs/platform/extensions/index.md index 7fb0f72e9d0..b1920b52104 100644 --- a/platform/docs/docs/platform/extensions/index.md +++ b/platform/docs/docs/platform/extensions/index.md @@ -40,16 +40,16 @@ Practical examples of extensions include: ## Extension Skeleton -An extension is a plain JavaScript object that has an `id` property, and one or +An extension is a plain JavaScript object that has `id` and `version` properties, and one or more [modules](#modules) and/or [lifecycle hooks](#lifecycle-hooks). ```js // prettier-ignore export default { /** - * Only required property. Should be a unique value across all extensions. + * Required properties. Should be a unique value across all extensions. */ - id: 'example-extension', + id, // Lifecyle preRegistration() { /* */ }, @@ -136,85 +136,47 @@ the top level [`extensions/`][ext-source] directory. -## Registering an Extension +## Registering of Extensions + +`viewer` starts by registering all the extensions specified inside the +`pluginConfig.json`, by default we register all extensions in the repo. + + +```js title=platform/viewer/pluginConfig.json +// Simplified version of the `pluginConfig.json` file +{ + "extensions": [ + { + "packageName": "@ohif/extension-cornerstone", + "version": "3.0.0" + }, + { + "packageName": "@ohif/extension-measurement-tracking", + "version": "3.0.0" + }, + // ... + ], + "modes": [ + { + "packageName": "@ohif/mode-longitudinal", + "version": "0.0.1" + } + ] +} +``` + +:::note Important +You SHOULD NOT directly register extensions in the `pluginConfig.json` file. +Use the provided `cli` to add/remove/install/uninstall extensions. Read more [here](../../development/ohif-cli.md) +::: -Extensions are building blocks that need to be registered. There are two -different ways to register and configure extensions: At -[runtime](#registering-at-runtime) and at -[build time](#registering-at-build-time). You can leverage one or both -strategies. Which one(s) you choose depends on your application's requirements. +The final registration and import of the extensions happen inside a non-tracked file `pluginImport.js` (this file is also for internal use only). -Each [module](#modules) defined by the extension becomes available to the modes +After an extension gets registered withing the `viewer`, +each [module](#modules) defined by the extension becomes available to the modes via the `ExtensionManager` by requesting it via its id. [Read more about Extension Manager](#extension-manager) -### Registering at Runtime - -The `@ohif/viewer` uses a [configuration file](../../configuration/index.md) at -startup. The schema for that file includes an `extensions` key that supports an -array of extensions to register. - -```js -import MyFirstExtension from '@ohif/extension-first'; -import MySecondExtension from '@ohif/extension-second'; - -const extensionConfig = { - /* extension configuration */ -}; - -const config = { - routerBasename: '/', - extensions: [MyFirstExtension, [MySecondExtension, extensionConfig]], - modes: [ - /* modes */ - ], - showStudyList: true, - dataSources: [ - /* data source config */ - ], -}; -``` - -Then, behind the scene, the runtime-added extensions will get merged with the -default app extensions (note: default app extensions include: -`OHIFDefaultExtension`, `OHIFCornerstoneExtension`, `OHIFDICOMSRExtension`, -`OHIFMeasurementTrackingExtension`) - -### Registering at Build Time - -The `@ohif/viewer` works best when built as a "Progressive Web Application" -(PWA). If you know the extensions your application will need, you can specify -them at "build time" to leverage advantages afforded to us by modern tooling: - -- Code Splitting (dynamic imports) -- Tree Shaking -- Dependency deduplication - -You can update the list of bundled extensions by: - -1. Having your `@ohif/viewer` project depend on the extension -2. Importing and adding it to the list of extensions in the entrypoint: - -```js title="/platform/src/index.js" -import OHIFDefaultExtension from '@ohif/extension-default'; -import OHIFCornerstoneExtension from '@ohif/extension-cornerstone'; -import OHIFMeasurementTrackingExtension from '@ohif/extension-measurement-tracking'; -import OHIFDICOMSRExtension from '@ohif/extension-dicom-sr'; -import MyFirstExtension from '@ohif/extension-first'; - -/** Combine our appConfiguration and "baked-in" extensions */ -const appProps = { - config: window ? window.config : {}, - defaultExtensions: [ - OHIFDefaultExtension, - OHIFCornerstoneExtension, - OHIFMeasurementTrackingExtension, - OHIFDICOMSRExtension, - MyFirstExtension, - ], -}; -``` - ## Lifecycle Hooks Currently, there are three lifecycle hook for extensions: diff --git a/platform/docs/docs/platform/extensions/installation.md b/platform/docs/docs/platform/extensions/installation.md index 8a1988b8625..2e5fb81c2a1 100644 --- a/platform/docs/docs/platform/extensions/installation.md +++ b/platform/docs/docs/platform/extensions/installation.md @@ -5,117 +5,8 @@ sidebar_label: Installation # Extension: Installation -OHIF-v3 provides the ability to utilize external extensions. In this document we -will describe how to add/install external extensions. +OHIF-v3 provides the ability to utilize external extensions. -> Our long-term plan is to make OHIF-v3 capable of installing extensions from -> `npm` with a command line. Until then, please use the instructions below to -> manually install extensions. -## Installing an Extension - -### 1) Extension Files Copy - -We use a [Template Extension](https://github.com/OHIF/extension-template) -repository to describe the necessary steps to use a new extension. You can use -this repository as a starting point to create your own extension. - -As you can see in the extension code base, folders structure are similar to the -OHIF-maintained extensions. Let's look at our `Template Extension`: - -- `src/index.js`: The most important file in any extension. This file is where - extensions' authors hav defined the extension modules, lifecycle hooks, and - other configurations. - -For instance the `Template Extension` has the following modules which will be -registered in OHIF by [Extension Manager](./extension.md) - -Each extension has an ID which is used to register the extension in OHIF. For -instance, for the `Template Extension`, the extension ID is -`extension.template`. - -```js {2} title="templateExtension/src/index.js" -export default { - id: 'extension.template', - getPanelModule, - getCommandsModule, -}; -``` - -#### Package.json - -Extension package name is defined in the `package.json` file. The `package.json` -file is a JSON file that defines the extension name, version, and dependencies. -For instance for the Template extension, the `package.json` file looks like -this: - -```js {2} title="templateExtension/package.json" -{ - "name": "@ohif/extension-template", - "version": "1.0.0", - "description": "A template extension to show extension installation", - ... -} -``` - -Note 1: We will use the `@ohif/extension-template` inside OHIF to let OHIF know -about existence of this extension. - -Note 2: You don't need to use the `@ohif` scope for your extensions. You can use -any scope you want. - -Note 3: Although folders names are not important and the `package.json` file -contains the mode name, we recommend using the same name as the folder name. - -![Template Extension](../../assets/img/template-extension-files.png) - -### 2) Configuring OHIF - -There are a couple of places inside OHIF which we need to modify in order to add -the extension. The following lines of code should be added to the OHIF: - -#### Viewer's package.json - -```js {8} titl="platform/viewer/package.json" -/* ... */ -"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/extension-template": "^0.0.1", - /* ... */ -} -``` - -#### index.js - -```js {4,13} title="platform/viewer/src/index.js" -/* ... */ -import OHIFMeasurementTrackingExtension from '@ohif/extension-measurement-tracking'; -import OHIFDICOMSRExtension from '@ohif/extension-dicom-sr'; -import OHIFTemplateExtension from '@ohif/extension-template'; - -const appProps = { - config: window ? window.config : {}, - defaultExtensions: [ - OHIFDefaultExtension, - OHIFCornerstoneExtension, - OHIFMeasurementTrackingExtension, - OHIFDICOMSRExtension, - OHIFTemplateExtension, - ], -}; -/* ... */ -``` - -After you followed the above steps, you should run `yarn install` in the root -folder of the OHIF repository to install the registered extension. - -Now you have added the extension to the OHIF, and its modules (layout, commands, -panels, toolbars, hangingProtocols, etc.) are made available to the OHIF -`modes`. Read more on how to consume extensions -[here](../modes/index.md#consuming-extensions) - -Congrats! 🎉 +You can use ohif `cli` tool to install both local and publicly published +extensions on NPM. You can read more [here](../../development/ohif-cli.md) diff --git a/platform/docs/docs/platform/extensions/lifecycle.md b/platform/docs/docs/platform/extensions/lifecycle.md index 87c77800d35..223386bc633 100644 --- a/platform/docs/docs/platform/extensions/lifecycle.md +++ b/platform/docs/docs/platform/extensions/lifecycle.md @@ -52,7 +52,7 @@ and import MyNewService from './MyNewService' export default { - id: 'MyExampleExtension', + id, /** * @param {object} params @@ -90,7 +90,7 @@ _Example `onModeEnter` hook implementation_ ```js export default { - id: 'org.ohif.dicom-sr', + id: '@ohif/extension-dicom-sr', onModeEnter({ servicesManager }) { const { DisplaySetService } = servicesManager.services; diff --git a/platform/docs/docs/platform/extensions/modules/panel.md b/platform/docs/docs/platform/extensions/modules/panel.md index 185fbe6bbba..55e1e82be10 100644 --- a/platform/docs/docs/platform/extensions/modules/panel.md +++ b/platform/docs/docs/platform/extensions/modules/panel.md @@ -67,9 +67,20 @@ using the mode configuration. As seen below, the `leftPanels` and `rightPanels` accept an `Array` of the `IDs`. ```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', + '@ohif/extension-dicom-sr': '^3.0.0', +}; + +const id = 'viewer' +const version = '3.0.0 + +function modeFactory({ modeConfiguration }) { return { - id: 'viewer', + id, routes: [ { path: 'longitudinal', @@ -78,10 +89,10 @@ export default function mode({ modeConfiguration }) { id, 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, }, @@ -89,12 +100,16 @@ export default function mode({ modeConfiguration }) { }, }, ], - extensions: [ - 'org.ohif.default', - 'org.ohif.cornerstone', - 'org.ohif.measurement-tracking', - 'org.ohif.dicom-sr', - ], + extensions: extensionDependencies }; } + +const mode = { + id, + modeFactory, + extensionDependencies, +}; + +export default mode; + ``` diff --git a/platform/docs/docs/platform/extensions/modules/toolbar.md b/platform/docs/docs/platform/extensions/modules/toolbar.md index 02fdd95d569..f340e0c4e2c 100644 --- a/platform/docs/docs/platform/extensions/modules/toolbar.md +++ b/platform/docs/docs/platform/extensions/modules/toolbar.md @@ -6,12 +6,11 @@ sidebar_label: Toolbar # Module: Toolbar An extension can register a Toolbar Module by defining a `getToolbarModule` -method. `OHIF-v3`'s `default` extension (`"ohif.org.default"`) provides 5 main +method. `OHIF-v3`'s `default` extension (`"@ohif/extension-default"`) provides 5 main toolbar button types: ![toolbarModule](../../../assets/img/toolbar-module.png) - ## Example Toolbar Module The Toolbar Module should return an array of `objects`. There are currently a @@ -57,7 +56,7 @@ a mode can add buttons to the toolbar by calling `toolDefinitions` which we will learn next. ```js -export default function mode({ modeConfiguration }) { +function modeFactory({ modeConfiguration }) { return { id: 'viewer', displayName: 'Basic Viewer', @@ -78,12 +77,6 @@ export default function mode({ modeConfiguration }) { }, }, ], - extensions: [ - 'org.ohif.default', - 'org.ohif.cornerstone', - 'org.ohif.measurement-tracking', - 'org.ohif.dicom-sr', - ], }; } ``` diff --git a/platform/docs/docs/platform/extensions/modules/viewport.md b/platform/docs/docs/platform/extensions/modules/viewport.md index c3d3d60e194..b49b33f6661 100644 --- a/platform/docs/docs/platform/extensions/modules/viewport.md +++ b/platform/docs/docs/platform/extensions/modules/viewport.md @@ -58,7 +58,7 @@ function TrackedCornerstoneViewport({ }) { const renderViewport = () => { const { component: Component } = extensionManager.getModuleEntry( - 'org.ohif.cornerstone.viewportModule.cornerstone' + '@ohif/extension-cornerstone.viewportModule.cornerstone' ); return ( { diff --git a/platform/docs/docs/platform/modes/index.md b/platform/docs/docs/platform/modes/index.md index ad717caca68..95421bfae96 100644 --- a/platform/docs/docs/platform/modes/index.md +++ b/platform/docs/docs/platform/modes/index.md @@ -50,13 +50,20 @@ The mode configuration specifies which `extensions` the mode requires, which template this defines which `side panels` will be available, as well as what `viewports` and which `displaySets` they may hang. -Mode's config is actually a function that return a config object with certain +Mode's config is composed of three elements: +- `id`: the mode `id` +- `modeFactory`: the function that returns the mode specific configuration +- `extensionDependencies`: the list of extensions that the mode requires + + +that return a config object with certain properties, the high-level view of this config object is: ```js title="modes/example/src/index.js" -export default function mode() { +function modeFactory() { return { id: '', + version: '', displayName: '', onModeEnter: () => {}, onModeExit: () => {}, @@ -69,12 +76,20 @@ export default function mode() { layoutTemplate: () => {}, }, ], - extensions: [], + extensions: extensionDependencies, hangingProtocols: [], sopClassHandlers: [], hotkeys: [], }; } + +const mode = { + id, + modeFactory, + extensionDependencies, +}; + +export default mode; ``` @@ -140,7 +155,7 @@ export default function 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 = () + authRoutes = ( + + ); } return ( diff --git a/platform/viewer/src/appInit.js b/platform/viewer/src/appInit.js index 8015ca3eb84..360893c8cdb 100644 --- a/platform/viewer/src/appInit.js +++ b/platform/viewer/src/appInit.js @@ -14,7 +14,7 @@ import { HangingProtocolService, CineService, UserAuthenticationService, - errorHandler + errorHandler, // utils, } from '@ohif/core'; @@ -22,7 +22,7 @@ import { * @param {object|func} appConfigOrFunc - application configuration, or a function that returns application configuration * @param {object[]} defaultExtensions - array of extension objects */ -function appInit(appConfigOrFunc, defaultExtensions) { +function appInit(appConfigOrFunc, defaultExtensions, defaultModes) { const appConfig = { ...(typeof appConfigOrFunc === 'function' ? appConfigOrFunc({ servicesManager }) @@ -86,10 +86,18 @@ function appInit(appConfigOrFunc, defaultExtensions) { throw new Error('No modes are defined! Check your app-config.js'); } - // TODO: Remove this - if (!appConfig.modes.length) { - appConfig.modes.push(window.longitudinalMode); - // appConfig.modes.push(window.segmentationMode); + for (let i = 0; i < defaultModes.length; i++) { + const { modeFactory, id } = defaultModes[i]; + + // If the appConfig contains configuration for this mode, use it. + const modeConfig = + appConfig.modeConfig && appConfig.modeConfig[i] + ? appConfig.modeConfig[id] + : {}; + + const mode = modeFactory(modeConfig); + + appConfig.modes.push(mode); } return { diff --git a/platform/viewer/src/index.js b/platform/viewer/src/index.js index 74d480d2bb3..f0ca01053e0 100644 --- a/platform/viewer/src/index.js +++ b/platform/viewer/src/index.js @@ -7,36 +7,24 @@ import React from 'react'; import ReactDOM from 'react-dom'; /** - * EXTENSIONS + * EXTENSIONS AND MODES * ================= + * pluginImports.js is dynamically generated from extension and mode + * configuration at build time. * - * Importing and modifying the extensions our app uses HERE allows us to leverage - * tree shaking and a few other niceties. However, by including them here they become - * "baked in" to the published application. - * - * Depending on your use case/needs, you may want to consider not adding any extensions - * by default HERE, and instead provide them via the extensions configuration key or - * by using the exported `App` component, and passing in your extensions as props using - * the defaultExtensions property. + * pluginImports.js imports all of the modes and extensions and adds them + * to the window for processing. */ -import OHIFDefaultExtension from '@ohif/extension-default'; -import OHIFCornerstoneExtension from '@ohif/extension-cornerstone'; -import OHIFMeasurementTrackingExtension from '@ohif/extension-measurement-tracking'; -import OHIFDICOMSRExtension from '@ohif/extension-dicom-sr'; -import OHIFDICOMVIDEOExtension from '@ohif/extension-dicom-video'; -import OHIFDICOMPDFExtension from '@ohif/extension-dicom-pdf'; +import './pluginImports.js'; -/** Combine our appConfiguration and "baked-in" extensions */ +/** + * Combine our appConfiguration with installed extensions and modes. + * In the future appConfiguration may contain modes added at runtime. + * */ const appProps = { config: window ? window.config : {}, - defaultExtensions: [ - OHIFDefaultExtension, - OHIFCornerstoneExtension, - OHIFMeasurementTrackingExtension, - OHIFDICOMSRExtension, - OHIFDICOMVIDEOExtension, - OHIFDICOMPDFExtension, - ], + defaultExtensions: window.extensions, + defaultModes: window.modes, }; /** Create App */ diff --git a/platform/viewer/src/routes/Mode/Mode.jsx b/platform/viewer/src/routes/Mode/Mode.jsx index 4f0bc49153e..15be6a48449 100644 --- a/platform/viewer/src/routes/Mode/Mode.jsx +++ b/platform/viewer/src/routes/Mode/Mode.jsx @@ -1,5 +1,6 @@ import React, { useEffect, useState, useRef } from 'react'; import { useParams, useLocation } from 'react-router'; + import PropTypes from 'prop-types'; // TODO: DicomMetadataStore should be injected? import { DicomMetadataStore } from '@ohif/core'; @@ -82,7 +83,6 @@ export default function ModeRoute({ const { DisplaySetService, HangingProtocolService, - UserAuthenticationService, } = servicesManager.services; const { extensions, sopClassHandlers, hotkeys, hangingProtocols } = mode; @@ -100,16 +100,11 @@ export default function ModeRoute({ // Only handling one route per mode for now const route = mode.routes[0]; - const layoutTemplateRouteData = route.layoutTemplate({ location }); - const layoutTemplateModuleEntry = extensionManager.getModuleEntry( - layoutTemplateRouteData.id - ); - const LayoutComponent = layoutTemplateModuleEntry.component; - // For each extension, look up their context modules // TODO: move to extension manager. let contextModules = []; - extensions.forEach(extensionId => { + + Object.keys(extensions).forEach(extensionId => { const allRegisteredModuleIds = Object.keys(extensionManager.modulesMap); const moduleIds = allRegisteredModuleIds.filter(id => id.includes(`${extensionId}.contextModule.`) @@ -215,9 +210,9 @@ export default function ModeRoute({ // Adding hanging protocols of extensions after onModeEnter since // it will reset the protocols - hangingProtocols.forEach(extentionProtocols => { + hangingProtocols.forEach(extensionProtocols => { const hangingProtocolModule = extensionManager.getModuleEntry( - extentionProtocols + extensionProtocols ); if (hangingProtocolModule?.protocols) { HangingProtocolService.addProtocols(hangingProtocolModule.protocols); diff --git a/platform/viewer/src/routes/PrivateRoute.jsx b/platform/viewer/src/routes/PrivateRoute.jsx index 849fee748eb..34bd3955b7e 100644 --- a/platform/viewer/src/routes/PrivateRoute.jsx +++ b/platform/viewer/src/routes/PrivateRoute.jsx @@ -1,15 +1,14 @@ -import React from "react"; -import { Route } from "react-router-dom"; -import { useUserAuthentication } from "@ohif/ui"; +import React from 'react'; +import { useUserAuthentication } from '@ohif/ui'; -export const PrivateRoute = ({ ...rest }) => { - const [{ user, enabled }, userAuthenticationService] = useUserAuthentication(); +export const PrivateRoute = ({ children, handleUnauthenticated }) => { + const [{ user, enabled }] = useUserAuthentication(); - if (enabled && !user) { - return userAuthenticationService.handleUnauthenticated(); - } + if (enabled && !user) { + return handleUnauthenticated(); + } - return ; -} + return children; +}; export default PrivateRoute; diff --git a/platform/viewer/src/routes/WorkList/WorkList.jsx b/platform/viewer/src/routes/WorkList/WorkList.jsx index c80aa2d3ea6..5350493c287 100644 --- a/platform/viewer/src/routes/WorkList/WorkList.jsx +++ b/platform/viewer/src/routes/WorkList/WorkList.jsx @@ -194,10 +194,7 @@ function WorkList({ const fetchSeries = async studyInstanceUid => { try { const series = await dataSource.query.series.search(studyInstanceUid); - seriesInStudiesMap.set( - studyInstanceUid, - sortBySeriesDate(series) - ); + seriesInStudiesMap.set(studyInstanceUid, sortBySeriesDate(series)); setStudiesWithSeriesData([...studiesWithSeriesData, studyInstanceUid]); } catch (ex) { // TODO: UI Notification Service @@ -336,22 +333,23 @@ function WorkList({ {appConfig.modes.map((mode, i) => { const isFirst = i === 0; - const isValidMode = mode.isValidMode({ modalities }) + const isValidMode = mode.isValidMode({ modalities }); + // TODO: Modes need a default/target route? We mostly support a single one for now. // We should also be using the route path, but currently are not - // mode.id + // mode.routeName // mode.routes[x].path // Don't specify default data source, and it should just be picked up... (this may not currently be the case) // How do we know which params to pass? Today, it's just StudyInstanceUIDs return (
- extensions + extensionDependencies extensions needed by the mode