feat(cli): Ohif cli for modes and extensions modification (#2696)
* feat: Add initial cli tool structure * feat: add copying template files * feat: Add mode template and command * feat: Add readme template generation * feat: Add documentation to extension template * feat: Enhance documentation of the template mode * fix: cli module type * feat: Add config-based mode and extension registration (#2660) * feat: Add ohif cli add/remove extension/mode (#2661) * Basic working CLI for add-extension and remove-extension * Basic cli for add/remove extension/mode, lots more to do. * Cleanup and harden] * feat: Add list of tasks to add-mode Co-authored-by: Alireza <ar.sedghi@gmail.com> * feat: Add git initialization for the mode or extension template (#2662) * fix: package json file to include templates * feat: Add git initialization for the mode or extension template * feat: Add more checks of git and target dir * feat: refactore library utilities * feat: Add the list command to print extensions and modes (#2664) * feat: Add the list command to print extensions and modes * Add todo * Feat/ohif cli validation + auto install (#2671) * WIP * Working mode keyword verification * Validation * auto install extensions based on modes * WIP remove unused extensions on removeMove * Working add-mode, remove-mode automatic extension management. * If extension is in used by a mode, don't allow the CLI to uninstall it * Cleanup addExtension * cleanup removeExtension and addMode * Cleanup removeMode * Update existing extensions with the needed keywords/peer deps * Fix broken config * Feat/cli search (#2677) * feat: refactor pretty print for console * feat: add search for modes and extensions * fix: ugly colors * Feat/ohif cli error handling publishing (#2679) * WIP * fix: webpack imports * wip * fix: react router dom private routes * from last commit * wip * fix: webpack prod builds * WIP * Working regsitration with new IDs * Stable Co-authored-by: Alireza <ar.sedghi@gmail.com> * verify extensions when constructing modes. (#2681) * verify extensions when constructing modes. * Add version to unit tests so it conforms to schema * Update ohif utils exposed via @ohif/core * Fix import * fix tests * feat: ohif-cli link local modes/extensions for development (#2682) * feat: enable cli to work with project root * feat: add initial link package * feat: add link and unlink extension * feat: add link and unlink mode * erro handling for link-package * feat: add comment on ohif-cli linking for development (#2686) * Docs/ohif cli (#2687) * feat: Add documentation for templates * feat: Add more documentation * Fix/core publish (#2685) * versions * wip * remove webpack clean output * fix publish * use next as dist tag for v3 for now * fix webpack pro recipe for output * fix: lerna publish next * fix(cli): fix issues when trying to link an extension or a mode (#2725) The generated package.json doesn't contain keywords property which is required by the linkPackage function. The module apth wasn't correclty handled too, and when there is no pluginOptions, it fails while reading the file or while generating a default configuration. * make dicom pdf and video work after cli merge * add axios dependency * comment out the chdir for now * create id and version based on user inputs * customizable path for extension and modes * fix template to make the template mode load * fix the questions to loop if path is not desirable * fix templates * correct package json order * unify the package creation for extension and mode * bump versions for each package * bump extension versions to 3.0 * add gitignore to templates * fix version requirements when ^ * update docs * update docs and fix tests * try to fix the tests * bump node version * remove the version from extensions * remove the version from modes * remove version from extensionManager * fix eslint * revert husky version * fix eslint * fix node version for new eslint * fix documentatoin removing version * fix cicle ci image version * fix circle ci node image * fix circle ci node image * add back the video and pdf Co-authored-by: Matthis Duclos <matthis.duclos@gmail.com> Co-authored-by: James A. Petts <jamesapetts@gmail.com>
No files matched your search
@@ -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/"
|
||||
]
|
||||
}
|
||||
@@ -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);
|
||||
});
|
||||
}
|
||||
@@ -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);
|
||||
});
|
||||
}
|
||||
@@ -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);
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export default 'Not found';
|
||||
@@ -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;
|
||||
@@ -0,0 +1,8 @@
|
||||
const colors = {
|
||||
LIGHT: '#5acce6',
|
||||
MAIN: '#0944b3',
|
||||
DARK: '#090c29',
|
||||
ACTIVE: '#348cfd',
|
||||
};
|
||||
|
||||
export default colors;
|
||||
@@ -0,0 +1,5 @@
|
||||
const endPoints = {
|
||||
NPM_KEYWORD: 'https://registry.npmjs.com/-/v1/search?text=keywords:',
|
||||
};
|
||||
|
||||
export default endPoints;
|
||||
@@ -0,0 +1,5 @@
|
||||
import keywords from './keywords.js';
|
||||
import colors from './colors.js';
|
||||
import endPoints from './endPoints.js';
|
||||
|
||||
export { keywords, colors, endPoints };
|
||||
@@ -0,0 +1,6 @@
|
||||
const keywords = {
|
||||
MODE: 'ohif-mode',
|
||||
EXTENSION: 'ohif-extension',
|
||||
};
|
||||
|
||||
export default keywords;
|
||||
@@ -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,
|
||||
};
|
||||
@@ -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 };
|
||||
@@ -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;
|
||||
@@ -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);
|
||||
});
|
||||
}
|
||||
@@ -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);
|
||||
});
|
||||
}
|
||||
@@ -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);
|
||||
});
|
||||
}
|
||||
@@ -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;
|
||||
@@ -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 };
|
||||
@@ -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 };
|
||||
@@ -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;
|
||||
@@ -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('<year>', new Date().getFullYear())
|
||||
.replace('<copyright holders>', `${name} (${email})`);
|
||||
return writeFile(targetPath, licenseContent, 'utf8');
|
||||
}
|
||||
|
||||
export default createLicense;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
export default function getVersionedPackageName(packageName, version) {
|
||||
return version === undefined ? packageName : `${packageName}@${version}`;
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
import { info } from 'yarn-programmatic';
|
||||
|
||||
export default async function getYarnInfo(packageName) {
|
||||
return await info(packageName);
|
||||
}
|
||||
@@ -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,
|
||||
};
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
@@ -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,
|
||||
};
|
||||
@@ -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,
|
||||
};
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
@@ -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 };
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
@@ -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,
|
||||
};
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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 <packageName> [version]')
|
||||
.description('Adds an ohif extension')
|
||||
.action((packageName, version) => {
|
||||
// change directory to viewer
|
||||
process.chdir(viewerDirectory);
|
||||
addExtension(packageName, version);
|
||||
});
|
||||
|
||||
program
|
||||
.command('remove-extension <packageName>')
|
||||
.description('removes an ohif extension')
|
||||
.action((packageName) => {
|
||||
// change directory to viewer
|
||||
process.chdir(viewerDirectory);
|
||||
removeExtension(packageName);
|
||||
});
|
||||
|
||||
program
|
||||
.command('add-mode <packageName> [version]')
|
||||
.description('Removes an ohif mode')
|
||||
.action((packageName, version) => {
|
||||
// change directory to viewer
|
||||
process.chdir(viewerDirectory);
|
||||
addMode(packageName, version);
|
||||
});
|
||||
|
||||
program
|
||||
.command('remove-mode <packageName>')
|
||||
.description('Removes an ohif mode')
|
||||
.action((packageName) => {
|
||||
// change directory to viewer
|
||||
process.chdir(viewerDirectory);
|
||||
removeMode(packageName);
|
||||
});
|
||||
|
||||
program
|
||||
.command('link-extension <packageDir>')
|
||||
.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 <extensionName>')
|
||||
.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 <packageDir>')
|
||||
.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 <modeName>')
|
||||
.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);
|
||||
@@ -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 };
|
||||
@@ -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
|
||||
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"trailingComma": "es5",
|
||||
"printWidth": 80,
|
||||
"proseWrap": "always",
|
||||
"tabWidth": 2,
|
||||
"semi": true,
|
||||
"singleQuote": true
|
||||
}
|
||||
@@ -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;
|
||||
@@ -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__'],
|
||||
},
|
||||
},
|
||||
};
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
import packageJson from '../package.json';
|
||||
|
||||
const id = packageJson.name;
|
||||
|
||||
export { id };
|
||||
@@ -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,
|
||||
}) => {},
|
||||
};
|
||||
@@ -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
|
||||
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"trailingComma": "es5",
|
||||
"printWidth": 80,
|
||||
"proseWrap": "always",
|
||||
"tabWidth": 2,
|
||||
"semi": true,
|
||||
"singleQuote": true
|
||||
}
|
||||
@@ -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;
|
||||
@@ -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__'],
|
||||
},
|
||||
},
|
||||
};
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
import packageJson from '../package.json';
|
||||
|
||||
const id = packageJson.name;
|
||||
|
||||
export { id };
|
||||
@@ -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;
|
||||
@@ -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');
|
||||
|
||||
|
||||
@@ -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');
|
||||
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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;
|
||||
@@ -15,6 +15,8 @@ describe('Top level exports', () => {
|
||||
'formatDate',
|
||||
'formatPN',
|
||||
//'loadAndCacheDerivedDisplaySets',
|
||||
'isDisplaySetReconstructable',
|
||||
'isImage',
|
||||
'DicomLoaderService',
|
||||
'urlUtil',
|
||||
'makeDeferred',
|
||||
|
||||
|
After Width: | Height: | Size: 95 KiB |
|
After Width: | Height: | Size: 99 KiB |
|
After Width: | Height: | Size: 71 KiB |
|
After Width: | Height: | Size: 116 KiB |
|
After Width: | Height: | Size: 97 KiB |
|
After Width: | Height: | Size: 422 KiB |
|
After Width: | Height: | Size: 140 KiB |
|
After Width: | Height: | Size: 160 KiB |
|
After Width: | Height: | Size: 88 KiB |
|
After Width: | Height: | Size: 21 KiB |
|
After Width: | Height: | Size: 67 KiB |
@@ -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.
|
||||
|
||||
<!--
|
||||
LINKS
|
||||
|
||||
@@ -132,7 +132,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',
|
||||
|
||||
@@ -34,7 +34,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',
|
||||
@@ -83,7 +83,7 @@ window.config = ({ servicesManager } = {}) => {
|
||||
dataSources: [
|
||||
{
|
||||
friendlyName: 'dcmjs DICOMWeb Server',
|
||||
namespace: 'org.ohif.default.dataSourcesModule.dicomweb',
|
||||
namespace: '@ohif/extension-default.dataSourcesModule.dicomweb',
|
||||
sourceName: 'dicomweb',
|
||||
configuration: {
|
||||
name: 'DCM4CHEE',
|
||||
|
||||
@@ -1,287 +0,0 @@
|
||||
# Theme Configuration
|
||||
When adding new theme extendible configuration items, please document
|
||||
them here. See [Theme Configuration with Config Point](#configPoint) on how to modify certain types of
|
||||
configuration values using the config-point definitions.
|
||||
|
||||
## Hanging Protocols
|
||||
It is possible to customize the available hanging protocols by defining them
|
||||
in a new theme file OR by defining the hanging protocols in a custom mode.
|
||||
If done correctly, the mode defined hanging protocols are automatically
|
||||
applied when using a particular view mode, allowing for further customization
|
||||
by theme files.
|
||||
|
||||
The default hanging protocols for the cornerstone mode is defined in
|
||||
themeProtocolProvider.js, in a configuration point named
|
||||
`ThemeProtocols.protocols` To define a new
|
||||
hanging protocol, it is possible to simply extend the protocols list with
|
||||
a new definition, for example, in the file mgHP.json5, the following definition
|
||||
would add a new hanging protocol:
|
||||
```js
|
||||
{
|
||||
ThemeProtocols: {
|
||||
protocols: {
|
||||
MG: {
|
||||
// This is a working HP, but isn't MG specific...
|
||||
id: 'MG',
|
||||
locked: true,
|
||||
hasUpdatedPriorsInformation: false,
|
||||
name: '1x2',
|
||||
createdDate: '2021-11-01T18:32:42.849Z',
|
||||
modifiedDate: '2021-11-01T18:32:42.849Z',
|
||||
availableTo: {},
|
||||
editableBy: {},
|
||||
protocolMatchingRules: [
|
||||
{
|
||||
id: 'NumberOfStudyRelatedSeries>1',
|
||||
weight: 10,
|
||||
attribute: 'NumberOfStudyRelatedSeries',
|
||||
constraint: {
|
||||
greaterThan: {
|
||||
value: 1,
|
||||
},
|
||||
},
|
||||
required: true,
|
||||
},
|
||||
],
|
||||
stages: [
|
||||
{
|
||||
name: 'OneByTwo',
|
||||
viewportStructure: {
|
||||
type: 'grid',
|
||||
properties: {
|
||||
rows: 1,
|
||||
columns: 2,
|
||||
},
|
||||
},
|
||||
viewport: {},
|
||||
},
|
||||
],
|
||||
numberOfPriorsReferenced: 0,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
```
|
||||
The MG protocol doesn't initially exist, so this would add a new hanging
|
||||
protocol, which would be defined in the normal hanging protocol definition.
|
||||
|
||||
## Query List
|
||||
One of the suggested areas for customization is the columns in the query table.
|
||||
TODO
|
||||
|
||||
## Demographics Overlay
|
||||
Another recommended change is to configure the demographics overlay using
|
||||
themes to allow site or mode specific demographics overlays.
|
||||
This should be done at several levels. An overall level, defining the system
|
||||
defaults, and then an overlay for each mode type, to allow mode specific information
|
||||
to be added to the general model. Note how that allows customizing at two
|
||||
or more levels to specify only the required change (aspect oriented programming).
|
||||
|
||||
# <a name="configPoint" />Theme Configuration with Config Point
|
||||
This section explains the syntax used for declaring various types of theme
|
||||
configurations, as well as where to place theme files.
|
||||
|
||||
The configuration schema is based on the
|
||||
[config-point](https://github.com/OHIF/config-point)
|
||||
library. This library is design to allow developers to create configuration
|
||||
points for their code by declaring static values, which can be modified externally.
|
||||
See [config-point-service](../platform/services/config-point-service.md) for
|
||||
internal details and development documentation.
|
||||
|
||||
## Theme Files
|
||||
Theme files are simple json5 definition files,
|
||||
available from the endpoint `https://ohif/theme/`
|
||||
for the site deployment. How the files get there is the responsibility of the
|
||||
site deployment. JSON5 is an extension of JSON,
|
||||
for which the primary addition here is the ability to use comments within the
|
||||
JSON structure. The attribute definition can also be an unquoted string when
|
||||
it is a simple attribute value.
|
||||
|
||||
The default example themes are located in `Viewers/platform/viewer/public/theme/`,
|
||||
for example, there is a theme there named 'theme.json5'. These files are
|
||||
automatically included in a default distribution.
|
||||
|
||||
Their content looks like a static object declaration in JavaScript, where
|
||||
the object has one or more attributes declared. The name of the attribute
|
||||
matches the configuration point for the given configuration item. For example:
|
||||
```js
|
||||
{
|
||||
// This extension modifies the ThemeProtocols
|
||||
// by adding a new hanging protocol for MG and modifying the existing
|
||||
// 2x2 layout to make it not preferred.
|
||||
ThemeProtocols: {
|
||||
// The original protocols declaration was a list,
|
||||
// doing this as an object matches by the id value specified here.
|
||||
// With no ID being found, creates a new entry.
|
||||
protocols: {
|
||||
// MG is just a referencable key, that matches the MG name here
|
||||
// In this case, it does not match any existing id, so it is net new.
|
||||
MG: {
|
||||
id: 'MG',
|
||||
// ... rest of definition of hanging protocol
|
||||
},
|
||||
// 2x2 is an existing hanging protocol, matching by id
|
||||
// Thus, it modifies values rather than replacing/updating it
|
||||
'2x2': {
|
||||
protocolMatchingRules: {
|
||||
// The actual change is weight:5 instead of weight:20, to make this
|
||||
// a non-preferred hanging protocol according to the HP definitions.
|
||||
'NumberOfStudyRelatedSeries>2': { weight: 5 },
|
||||
},
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
```
|
||||
that modifies the hanging protocols, both by adding and updating
|
||||
elements. Note how this is a deep modification to a value. The intent is
|
||||
to allow modifying configuration values which are heavily nested and/or list
|
||||
based.
|
||||
|
||||
## Working with Objects
|
||||
The object tree is matched by the simple attribute name. The attribute tree
|
||||
is then merged with the existing objects. That is, suppose we have:
|
||||
```js
|
||||
// base object definition
|
||||
baseObject: {
|
||||
value1: 5,
|
||||
value2: { subValue1: true, leftAlone: "value to be unchanged', },
|
||||
}
|
||||
```
|
||||
|
||||
then value1 and subValue1 can be changed by the following theme configuration:
|
||||
```js
|
||||
// ... base object extension:
|
||||
baseObject: {
|
||||
value1: 7,
|
||||
value2: {subValue1: false, subValue2: 'new value', },
|
||||
}
|
||||
```
|
||||
changing value1 to 7 and subValue1 to false, and adding subValue2.
|
||||
|
||||
This is the basic modification for all changes - match the path and replace one
|
||||
or more left (primitive) values.
|
||||
|
||||
## Working with Arrays
|
||||
Arrays in JSON and JSON5 only allow natural plus zero indices, and it isn't easy
|
||||
to specify sparse array values or "next" values. To address this, a base
|
||||
array, declared exactly as a normal array value can be extended with an
|
||||
object where the key of the object matches the extension value in some way.
|
||||
For example, the base array:
|
||||
```js
|
||||
array: ['value1', {id: 'value2', ...}, 'value3']
|
||||
// can be extended with the changes in an array, modifying only
|
||||
// the array[1] and adding an array[3] element.
|
||||
array: [null, {...extensions for value2},null,{id:'new array element'}]
|
||||
// Or, this can be re-written as:
|
||||
array: {
|
||||
'value1': 'new-string-for-value1',
|
||||
'value2': {...extensions for value2},
|
||||
'value4': {id:'new array element',...},
|
||||
}
|
||||
```
|
||||
|
||||
matches value1 by simple comparison, getting `array[0]` as the value to change.
|
||||
Then matching 'value2' to `array[1]` by `array[1].id==='value2'` and finally not
|
||||
matching any value with `id==='new array element'` so adding it to the end
|
||||
(warning, adding to the end MAY be replaced by adding in the middle of the
|
||||
array such that the id is between an id smaller than the new id and larger
|
||||
than the new id, still TBD along with one or two other small enhancements).
|
||||
|
||||
## Custom Mappings (configOperation)
|
||||
There are a number of default custom mappings available for config-point.
|
||||
The general format is:
|
||||
```js
|
||||
valueName: {configOperation: 'opName', value?: 'default-value', reference?: 'named-reference',
|
||||
source?: 'name-of-config-point-for-reference', ...}
|
||||
```
|
||||
which defines an operation to perform instead of using the value literally.
|
||||
The value provided can then be later extended/updated in the usual way, for
|
||||
example:
|
||||
```js
|
||||
valueName: 'alternateStringValue for valueName',
|
||||
```
|
||||
would replace valueName that the config operation acts on.
|
||||
|
||||
### Immediate operations
|
||||
Immediate operations perform the action immediately, and can thus be
|
||||
extended further. They support the additional keys:
|
||||
* position to modify something at a given position
|
||||
|
||||
The immediate operations are:
|
||||
* replace, insert, delete
|
||||
|
||||
For example, supposing there was a configuration point ModalityList, then
|
||||
the following extensions could be applied:
|
||||
```js
|
||||
// Replace the entire list
|
||||
ModalityList: {configOperation: 'replace', value: [
|
||||
'CT', 'MR', 'CR',
|
||||
]}
|
||||
|
||||
// Replace an item at position 3
|
||||
ModalityList: [ {configOperation: 'replace', position: 3, value: {id: 'MRI', description:'Magnetic resonance imaging'}}]
|
||||
|
||||
// Delete an item with value or id 'MR'
|
||||
ModalityList: {
|
||||
'MR': {configOperation: 'delete'},
|
||||
|
||||
// Insert before the item with id 'MG'
|
||||
ModalityList: {
|
||||
'MG': {configOperation: 'insert', value: 'MGTomo'},
|
||||
```
|
||||
|
||||
### Getter Operations
|
||||
Getter operations are performed at the time the attribute is accessed, and then
|
||||
are stored. These attributes typically have parameters:
|
||||
* reference to get a value relative to the current context or the source value.
|
||||
* source to get a value for reference from another ConfigPoint root
|
||||
* value to get a literal, immediate value
|
||||
* transform to modify the value(s)
|
||||
|
||||
The default getter operations are:
|
||||
* sort to generate a sorted list
|
||||
* reference to get another value from elsewhere
|
||||
but the configurations below may also list new getter operations.
|
||||
|
||||
An example for sort might be:
|
||||
```js
|
||||
// Define a basic list:
|
||||
list: ['CT', {id:'MR', name: 'Magnetic Resonance Imaging'}, 'CR']
|
||||
// Declared to be sorted like this (can be done before/after the base declaration)
|
||||
list: {configOperation: 'sort', sortKey: 'priority', usePositionAsSortKey: true},
|
||||
// Then extended via:
|
||||
list: {
|
||||
// Change the priority and add a name to the CT value
|
||||
// This makes it an object instead of a string
|
||||
'CT': {id:'CT', name:'Computed Tomography', priority: 3},
|
||||
}
|
||||
```
|
||||
|
||||
and example for reference could be:
|
||||
```js
|
||||
// Base definition:
|
||||
MGHangingProtocol: // ... full definition of MG HP here
|
||||
// Reference to it
|
||||
ThemeProtocols: {
|
||||
protocols: {
|
||||
MGHangingProtocol: {configOperation:reference, source:"MGHangingProtocol"},
|
||||
}
|
||||
}
|
||||
|
||||
// Or, a bit of definition for a table element combining two values to render:
|
||||
StudyInstancesColumn: {
|
||||
id: 'StudyInstancesColumn',
|
||||
title: '# of Series and Instances',
|
||||
value: {
|
||||
configOperation: 'reference',
|
||||
value: '`Se: ${study.NumberOfStudyRelatedSeries} Obj: ${study.NumberOfStudyRelatedInstances}`',
|
||||
transform: ${
|
||||
configOperation: "reference",
|
||||
source: 'ConfigPointOperation',
|
||||
reference: 'safeFunction'}
|
||||
```
|
||||
Note how in the last example, the transform itself contains a reference. This
|
||||
is a function that generates a javascript function taking props, where the
|
||||
props are available directly. Thus, these props would need `study` containing
|
||||
the appropriate child objects.
|
||||
@@ -1,5 +1,5 @@
|
||||
---
|
||||
sidebar_position: 6
|
||||
sidebar_position: 7
|
||||
sidebar_label: Continous Integration
|
||||
---
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
---
|
||||
sidebar_position: 3
|
||||
sidebar_position: 4
|
||||
sidebar_label: Contributing
|
||||
---
|
||||
|
||||
|
||||
@@ -0,0 +1,291 @@
|
||||
---
|
||||
sidebar_position: 3
|
||||
sidebar_label: OHIF CLI
|
||||
---
|
||||
|
||||
# OHIF Command Line Interface
|
||||
|
||||
OHIF-v3 architecture has been re-designed to enable building applications that
|
||||
are easily extensible to various use cases (Modes) that behind the scene would
|
||||
utilize desired functionalities (Extensions) to reach the goal of the use case.
|
||||
Now, the question is _how to create/remove/install/uninstall an extension and/or
|
||||
mode?_
|
||||
|
||||
You can use the `cli` script that comes with the OHIF monorepo to achieve these
|
||||
goals.
|
||||
|
||||
:::note Info
|
||||
In the long-term, we envision our `cli` tool to be a separate installable
|
||||
package that you can invoke anywhere on your local system to achieve the same
|
||||
goals. In the meantime, `cli` will remain as part of the OHIF monorepo and needs
|
||||
to be invoked using the `yarn` command.
|
||||
:::
|
||||
|
||||
|
||||
## CLI Installation
|
||||
|
||||
You don't need to install the `cli` currently. You can use `yarn` to invoke its
|
||||
commands.
|
||||
|
||||
## Commands
|
||||
|
||||
:::note Important
|
||||
All commands should run from the root of the monorepo.
|
||||
:::
|
||||
|
||||
|
||||
There are various commands that can be used to interact with the OHIF-v3 CLI. If
|
||||
you run the following command, you will see a list of available commands.
|
||||
|
||||
```
|
||||
yarn run cli --help
|
||||
```
|
||||
|
||||
which will output
|
||||
|
||||
```
|
||||
OHIF CLI
|
||||
|
||||
Options:
|
||||
-V, --version output the version number
|
||||
-h, --help display help for command
|
||||
|
||||
Commands:
|
||||
create-extension Create a new template extension
|
||||
create-mode Create a new template Mode
|
||||
add-extension <packageName> [version] Adds an ohif extension
|
||||
remove-extension <packageName> removes an ohif extension
|
||||
add-mode <packageName> [version] Removes an ohif mode
|
||||
remove-mode <packageName> Removes an ohif mode
|
||||
link-extension <packageDir> Links a local OHIF extension to the Viewer to be used for development
|
||||
unlink-extension <extensionName> Unlinks a local OHIF extension from the Viewer
|
||||
link-mode <packageDir> Links a local OHIF mode to the Viewer to be used for development
|
||||
unlink-mode <extensionName> Unlinks a local OHIF mode from the Viewer
|
||||
list List Added Extensions and Modes
|
||||
search [options] Search NPM for the list of Modes and Extensions
|
||||
help [command] display help for command
|
||||
```
|
||||
|
||||
As seen there are commands for you such as: `create-extension`, `create-mode`,
|
||||
`add-extension`, `remove-extension`, `add-mode`, `remove-mode`,
|
||||
`link-extension`, `unlink-extension`, `link-mode`, `unlink-mode`, `list`,
|
||||
`search`, and `help`. Here we will go through each of the commands and describe
|
||||
them.
|
||||
|
||||
### create-mode
|
||||
|
||||
If you need to create a new mode, you can use the `create-mode` command. This
|
||||
command will create a new mode template in the directory that you specify.
|
||||
The command will ask you couple of information/questions in order
|
||||
to properly create the mode metadata in the `package.json` file.
|
||||
|
||||
```bash
|
||||
yarn run cli create-mode
|
||||
```
|
||||
|
||||
<div style={{textAlign: 'center',}}>
|
||||
|
||||

|
||||
|
||||
|
||||
</div>
|
||||
|
||||
Note 1: Some questions have a default answer, which is indicated inside the
|
||||
parenthesis. If you don't want to answer the question, just hit enter. It will
|
||||
use the default answer.
|
||||
|
||||
Note 2: As you see in the questions, you can initiate a git repository for the
|
||||
new mode right away by answering `Y` (default) to the question.
|
||||
|
||||
Note 3: Finally, as indicated by the green lines at the end, `create-mode` command only
|
||||
create the mode template. You will need to link the mode to the Viewer in order
|
||||
to use it. See the [`link-mode`](#link-mode) command.
|
||||
|
||||
If we take a look at the directory that we created, we will see the following
|
||||
files:
|
||||
|
||||
<div style={{maxWidth:"500px"}}>
|
||||
|
||||

|
||||
|
||||
</div>
|
||||
|
||||
|
||||
### create-extension
|
||||
|
||||
Similar to the `create-extension` command, you can use the `create-extension`
|
||||
command to create a new extension template. This command will create a new
|
||||
extension template in the directory that you specify the path.
|
||||
|
||||
```bash
|
||||
yarn run cli create-extension
|
||||
```
|
||||
|
||||
|
||||
Note: again similar to the `create-extension` command, you need to manually link
|
||||
the extension to the Viewer in order to use it. See the
|
||||
[`link-mode`](#link-mode) command.
|
||||
|
||||
|
||||
### link-extension
|
||||
|
||||
`link-extension` command will link a local OHIF extension to the Viewer. This
|
||||
command will utilize `yarn link` to achieve so.
|
||||
|
||||
```bash
|
||||
yarn run cli link-extension <extensionDir>
|
||||
```
|
||||
|
||||
### unlink-extension
|
||||
|
||||
There might be situations where you want to unlink an extension from the Viewer
|
||||
after some developments. `unlink-extension` command will do so.
|
||||
|
||||
```bash
|
||||
ohif-cli unlink-extension <extensionName>
|
||||
```
|
||||
|
||||
|
||||
|
||||
### link-mode
|
||||
|
||||
Similar to the `link-extension` command, `link-mode` command will link a local
|
||||
OHIF mode to the Viewer.
|
||||
|
||||
```bash
|
||||
yarn run cli link-mode <modeDir>
|
||||
```
|
||||
|
||||
### unlink-mode
|
||||
|
||||
Similar to the `unlink-extension` command, `unlink-mode` command will unlink a
|
||||
local OHIF mode from the Viewer.
|
||||
|
||||
```bash
|
||||
ohif-cli unlink-mode <modeName>
|
||||
```
|
||||
|
||||
### add-mode
|
||||
|
||||
OHIF is a modular viewer. This means that you can install (add) different modes
|
||||
to the viewer if they are published online . `add-mode` command will add a new mode to
|
||||
the viewer. It will look for the mode in the NPM registry and installs it. This
|
||||
command will also add the extension dependencies that the mode relies on to the
|
||||
Viewer (if specified in the peerDependencies section of the package.json).
|
||||
|
||||
:::note Important
|
||||
`cli` will validate the npm package before adding it to the Viewer. An OHIF mode
|
||||
should have `ohif-mode` as one of its keywords.
|
||||
:::
|
||||
|
||||
Note: If you don't specify the version, the latest version will be used.
|
||||
|
||||
```bash
|
||||
yarn run cli add-mode <packageName> [version]
|
||||
```
|
||||
|
||||
For instance `@ohif-test/mode-clock` is an example OHIF mode that we have
|
||||
published to NPM. This mode basically has a panel that shows the clock :)
|
||||
|
||||
We can add this mode to the Viewer by running the following command:
|
||||
|
||||
```bash
|
||||
yarn run cli add-mode @ohif-test/mode-clock
|
||||
```
|
||||
|
||||
After installation, the Viewer has a new mode!
|
||||
|
||||
|
||||

|
||||
|
||||
|
||||
Note: If the mode has an extension peerDependency (in this case @ohif-test/extension-clock),
|
||||
`cli` will automatically add the extension to the Viewer too.
|
||||
|
||||
The result
|
||||
|
||||

|
||||

|
||||
|
||||
### add-extension
|
||||
|
||||
This command will add an OHIF extension to the Viewer. It will look for the
|
||||
extension in the NPM registry and install it.
|
||||
|
||||
```bash
|
||||
yarn run cli add-extension <packageName> [version]
|
||||
```
|
||||
|
||||
|
||||
### remove-mode
|
||||
|
||||
This command will remove the mode from the Viewer and also remove the extension
|
||||
dependencies that the mode relies on from the Viewer.
|
||||
|
||||
```bash
|
||||
yarn run cli remove-mode <packageName>
|
||||
```
|
||||
|
||||
|
||||
### remove-extension
|
||||
|
||||
Similar to the `remove-mode` command, this command will remove the extension
|
||||
from the Viewer.
|
||||
|
||||
```bash
|
||||
yarn run cli remove-extension <packageName>
|
||||
```
|
||||
|
||||
### list
|
||||
|
||||
`list` command will list all the installed extensions and modes in
|
||||
the Viewer. It uses the `PluginConfig.json` file to list the installed
|
||||
extensions and modes.
|
||||
|
||||
```bash
|
||||
yarn run cli list
|
||||
```
|
||||
|
||||
an output would look like this:
|
||||
|
||||
<div style={{maxWidth:"500px"}}>
|
||||
|
||||

|
||||
|
||||
</div>
|
||||
|
||||
### search
|
||||
|
||||
Using `search` command, you can search for OHIF extensions and modes
|
||||
in the NPM registry. This tool can accept a `--verbose` flag to show more
|
||||
information about the results.
|
||||
|
||||
```bash
|
||||
yarn run cli search [--verbose]
|
||||
```
|
||||
|
||||
<div style={{maxWidth:"600px"}}>
|
||||
|
||||

|
||||
|
||||
</div>
|
||||
|
||||
with the verbose flag `ohif-cli search --verbose` you will achieve the following
|
||||
output:
|
||||
|
||||
<div style={{maxWidth:"600px"}}>
|
||||
|
||||

|
||||
|
||||
</div>
|
||||
|
||||
|
||||
## PluginConfig.json
|
||||
|
||||
To make all the above commands work, we have created a new file called `PluginConfig.json` which contains the
|
||||
information needed to run the commands. You **don't need to (and should not)**
|
||||
edit/update/modify this file as it is automatically generated by the CLI. You
|
||||
can take a look at what this file contains by going to
|
||||
`platform/viewer/PluginConfig.json` in your project's root directory. In short,
|
||||
this file tracks and stores all the extensions/modes and the their version that
|
||||
are currently being used by the viewer.
|
||||
@@ -1,5 +1,5 @@
|
||||
---
|
||||
sidebar_position: 4
|
||||
sidebar_position: 5
|
||||
sidebar_label: Issue & PR Triage Process
|
||||
---
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
---
|
||||
sidebar_position: 5
|
||||
sidebar_position: 6
|
||||
sidebar_label: Testing
|
||||
---
|
||||
|
||||
@@ -26,11 +26,19 @@ unsuccessful tests with detailed explanations.
|
||||
## End-to-end test
|
||||
For running the OHIF e2e test you need to run the following steps:
|
||||
|
||||
- Open a new terminal, navigate to the OHIF project, and run OHIF with the
|
||||
e2e config. The test:data only needs to be run once and checks the data out.
|
||||
- Open a new terminal, and from the root of the OHIF mono repo, run the following command:
|
||||
|
||||
```bash
|
||||
yarn test:data
|
||||
```
|
||||
|
||||
This will download the required data to run the e2e tests (it might take a while).
|
||||
The `test:data` only needs to be run once and checks the data out. Read more about
|
||||
test data [below](#test-data).
|
||||
|
||||
- Run the viewer with e2e config
|
||||
|
||||
```bash
|
||||
APP_CONFIG=config/e2e.js yarn start
|
||||
```
|
||||
|
||||
@@ -158,7 +166,6 @@ We write integration tests to gain confidence that several units work together.
|
||||
Generally, we want to mock as little as possible for these tests. In practice,
|
||||
this means only mocking network requests.
|
||||
|
||||
|
||||
### End-to-End Tests
|
||||
|
||||
These are the most expensive tests to write and maintain. Largely because, when
|
||||
@@ -186,8 +193,6 @@ feature `X` or scenario `Y`? Open an issue and let's discuss.
|
||||
- [Best Practices](https://docs.cypress.io/guides/references/best-practices.html)
|
||||
- [Example Recipes](https://docs.cypress.io/examples/examples/recipes.html)
|
||||
|
||||
|
||||
|
||||
<!--
|
||||
Links
|
||||
-->
|
||||
|
||||
@@ -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 => {
|
||||
|
||||
@@ -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.
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
## Registering an Extension
|
||||
## Registering of Extensions
|
||||
|
||||
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.
|
||||
`viewer` starts by registering all the extensions specified inside the
|
||||
`pluginConfig.json`, by default we register all extensions in the repo.
|
||||
|
||||
Each [module](#modules) defined by the extension becomes available to the modes
|
||||
|
||||
```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)
|
||||
:::
|
||||
|
||||
The final registration and import of the extensions happen inside a non-tracked file `pluginImport.js` (this file is also for internal use only).
|
||||
|
||||
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="<repo-root>/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:
|
||||
|
||||
@@ -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.
|
||||
|
||||

|
||||
|
||||
### 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)
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
```
|
||||
@@ -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:
|
||||
|
||||

|
||||
|
||||
|
||||
## 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',
|
||||
],
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
@@ -58,7 +58,7 @@ function TrackedCornerstoneViewport({
|
||||
}) {
|
||||
const renderViewport = () => {
|
||||
const { component: Component } = extensionManager.getModuleEntry(
|
||||
'org.ohif.cornerstone.viewportModule.cornerstone'
|
||||
'@ohif/extension-cornerstone.viewportModule.cornerstone'
|
||||
);
|
||||
return (
|
||||
<Component
|
||||
|
||||
@@ -2,9 +2,11 @@
|
||||
sidebar_position: 2
|
||||
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
|
||||
@@ -27,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 => {
|
||||
|
||||
@@ -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;
|
||||
```
|
||||
|
||||
<table>
|
||||
@@ -140,7 +155,7 @@ export default function mode() {
|
||||
<tr>
|
||||
<td align="left">
|
||||
<a href="./index#consuming-extensions">
|
||||
extensions
|
||||
extensionDependencies
|
||||
</a>
|
||||
</td>
|
||||
<td align="left">extensions needed by the mode</td>
|
||||
@@ -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: () => {
|
||||
/**...**/
|
||||
},
|
||||
routes: [
|
||||
{
|
||||
path: 'longitudinal',
|
||||
init: () => {
|
||||
/**...**/
|
||||
},
|
||||
layoutTemplate: () => {
|
||||
/**...**/
|
||||
},
|
||||
},
|
||||
],
|
||||
extensions: [
|
||||
/**...**/
|
||||
],
|
||||
hangingProtocols: [
|
||||
/**...**/
|
||||
],
|
||||
sopClassHandlers: [
|
||||
/**...**/
|
||||
],
|
||||
hotkeys: [
|
||||
/**...**/
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
window.longitudinalMode = mode({});
|
||||
```
|
||||
|
||||
and inside `@ohif/viewer` we have:
|
||||
|
||||
```js title="platform/viewer/src/appInit.js"
|
||||
if (!appConfig.modes.length) {
|
||||
appConfig.modes.push(window.longitudinalMode);
|
||||
// appConfig.modes.push(window.segmentationMode);
|
||||
```js title=platform/viewer/pluginConfig.json
|
||||
// Simplified version of the `pluginConfig.json` file
|
||||
{
|
||||
"extensions": [
|
||||
{
|
||||
"packageName": "@ohif/extension-cornerstone",
|
||||
"version": "3.0.0"
|
||||
},
|
||||
// ...
|
||||
],
|
||||
"modes": [
|
||||
{
|
||||
"packageName": "@ohif/mode-longitudinal",
|
||||
"version": "0.0.1"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
:::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)
|
||||
:::
|
||||
|
||||
The final registration and import of the modes happen inside a non-tracked file `pluginImport.js` (this file is also for internal use only).
|
||||
@@ -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.
|
||||
|
||||

|
||||
|
||||
### 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! 🎉
|
||||
|
||||

|
||||
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)
|
||||
@@ -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: '',
|
||||
|
||||
@@ -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: '',
|
||||
|
||||
@@ -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: '',
|
||||
|
||||
@@ -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.
|
||||
@@ -241,7 +241,7 @@ const getTimePointUID = (metaData) => {
|
||||
return myBackEndAPI(metaData)
|
||||
}
|
||||
|
||||
export default function mode() {
|
||||
function modeFactory() {
|
||||
return {
|
||||
id: 'myMode',
|
||||
/** .. **/
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -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');
|
||||
|
||||
|
||||