feat(segmentation mode): Add create, and export SEG with Brushes (#3632)

This commit is contained in:
Alireza authored and GitHub committed 2023-09-22 10:23:44 -04:00
1 parent 896866e9d0
commit 48bbd6281a
186 files changed
+6801 -2883

No files matched your search

+1 -1
View File
@@ -30,7 +30,7 @@ export default defineConfig({
responseTimeout: 10000,
specPattern: 'cypress/integration/**/*.spec.[jt]s',
projectId: '4oe38f',
video: false,
video: true,
reporter: 'junit',
reporterOptions: {
mochaFile: 'cypress/results/test-output.xml',
@@ -7,6 +7,7 @@ describe('OHIF Multi Study', () => {
cy.expectMinimumThumbnails(4);
cy.initCornerstoneToolsAliases();
cy.initCommonElementsAliases();
cy.waitDicomImage();
};
it('Should display 2 comparison up', () => {
@@ -1,8 +1,6 @@
describe('OHIF PDF Display', function () {
beforeEach(function () {
cy.openStudyInViewer('2.25.317377619501274872606137091638706705333');
cy.resetViewport().wait(50);
});
it('checks if series thumbnails are being displayed', function () {
@@ -1,7 +1,6 @@
describe('OHIF Video Display', function () {
beforeEach(function () {
cy.openStudyInViewer('2.25.96975534054447904995905761963464388233');
cy.resetViewport().wait(50);
});
it('checks if series thumbnails are being displayed', function () {
@@ -1,5 +1,5 @@
describe('OHIF HP', () => {
const beforeSetup = () => {
beforeEach(() => {
cy.checkStudyRouteInViewer(
'1.3.6.1.4.1.25403.345050719074.3824.20170125113417.1',
'&hangingProtocolId=@ohif/mnGrid'
@@ -7,17 +7,14 @@ describe('OHIF HP', () => {
cy.expectMinimumThumbnails(3);
cy.initCornerstoneToolsAliases();
cy.initCommonElementsAliases();
};
cy.waitDicomImage();
});
it('Should display 3 up', () => {
beforeSetup();
cy.get('[data-cy="viewport-pane"]').its('length').should('be.eq', 3);
});
it('Should navigate next/previous stage', () => {
beforeSetup();
cy.get('body').type(',');
cy.wait(250);
cy.get('[data-cy="viewport-pane"]').its('length').should('be.eq', 4);
@@ -5,7 +5,7 @@ describe('OHIF Context Menu', function () {
cy.expectMinimumThumbnails(3);
cy.initCommonElementsAliases();
cy.initCornerstoneToolsAliases();
cy.resetViewport().wait(50);
cy.waitDicomImage();
});
it('checks context menu customization', function () {
@@ -13,10 +13,10 @@ describe('OHIF Cornerstone Hotkeys', () => {
cy.expectMinimumThumbnails(3);
cy.initCornerstoneToolsAliases();
cy.initCommonElementsAliases();
cy.waitDicomImage();
});
it('checks if hotkeys "R" and "L" can rotate the image', () => {
// Hotkey R
cy.get('body').type('R');
cy.get('@viewportInfoMidLeft').should('contains.text', 'P');
cy.get('@viewportInfoMidTop').should('contains.text', 'R');
@@ -9,6 +9,7 @@ describe('OHIF Cornerstone Toolbar', () => {
//const expectedText = 'Ser: 1';
//cy.get('@viewportInfoBottomLeft').should('contains.text', expectedText);
cy.waitDicomImage();
});
it('checks if all primary buttons are being displayed', () => {
@@ -66,13 +67,12 @@ describe('OHIF Cornerstone Toolbar', () => {
// });
it('checks if Levels tool will change the window width and center of an image', () => {
//Click on button and verify if icon is active on toolbar
cy.waitDicomImage();
cy.get('@wwwcBtnPrimary')
.click()
.then($wwwcBtn => {
cy.wrap($wwwcBtn).should('have.class', 'active');
});
// Wait for the DICOM image to load
// Assign an alias to the button element
cy.get('@wwwcBtnPrimary').as('wwwcButton');
cy.get('@wwwcButton').click();
cy.get('@wwwcButton').should('have.class', 'active');
//drags the mouse inside the viewport to be able to interact with series
cy.get('@viewport')
@@ -90,13 +90,16 @@ describe('OHIF Cornerstone Toolbar', () => {
});
it('checks if Pan tool will move the image inside the viewport', () => {
//Click on button and verify if icon is active on toolbar
cy.get('@panBtn')
.click()
.then($panBtn => {
cy.wrap($panBtn).should('have.class', 'active');
});
// Assign an alias to the button element
cy.get('@panBtn').as('panButton');
// Click on the button
cy.get('@panButton').click();
// Assert that the button has the 'active' class
cy.get('@panButton').should('have.class', 'active');
// Trigger the pan actions on the viewport
cy.get('@viewport')
.trigger('mousedown', 'center', { buttons: 1 })
.trigger('mousemove', 'bottom', { buttons: 1 })
@@ -5,7 +5,6 @@ describe('OHIF Measurement Panel', function () {
cy.expectMinimumThumbnails(3);
cy.initCommonElementsAliases();
cy.initCornerstoneToolsAliases();
cy.resetViewport().wait(50);
cy.waitDicomImage();
});
@@ -5,7 +5,6 @@ describe('OHIF Study Viewer Page', function () {
cy.expectMinimumThumbnails(3);
cy.initCommonElementsAliases();
cy.initCornerstoneToolsAliases();
cy.resetViewport().wait(50);
});
it('checks if series thumbnails are being displayed', function () {
+33 -23
View File
@@ -144,18 +144,19 @@ Cypress.Commands.add('drag', { prevSubject: 'element' }, (...args) =>
* @param {number[]} secondClick - Click position [x, y]
*/
Cypress.Commands.add('addLine', (viewport, firstClick, secondClick) => {
cy.get(viewport).then($viewport => {
const [x1, y1] = firstClick;
const [x2, y2] = secondClick;
cy.get(viewport).as('viewportAlias');
const [x1, y1] = firstClick;
const [x2, y2] = secondClick;
// The wait is necessary because of double click testing
cy.wrap($viewport)
.click(x1, y1)
.wait(250)
.trigger('mousemove', { clientX: x2, clientY: y2 })
.click(x2, y2)
.wait(250);
});
// First click
cy.get('@viewportAlias').click(x1, y1, { force: true, multiple: true }).wait(250);
// Move the mouse and then click again
cy.get('@viewportAlias')
.trigger('mousemove', { clientX: x2, clientY: y2 })
.get('@viewportAlias')
.click(x2, y2, { force: true, multiple: true })
.wait(250);
});
/**
@@ -183,8 +184,10 @@ Cypress.Commands.add('addAngle', (viewport, firstClick, secondClick, thirdClick)
});
Cypress.Commands.add('expectMinimumThumbnails', (seriesToWait = 1) => {
cy.get('[data-cy="study-browser-thumbnail"]', { timeout: 50000 }).should($itemList => {
expect($itemList.length >= seriesToWait).to.be.true;
cy.get('[data-cy="study-browser-thumbnail"]', { timeout: 50000 }).as('thumbnails');
cy.get('@thumbnails').should($itemList => {
expect($itemList.length).to.be.gte(seriesToWait);
});
});
@@ -211,11 +214,13 @@ Cypress.Commands.add('waitDicomImage', (mode = '/basic-test', timeout = 50000) =
//Command to reset and clear all the changes made to the viewport
Cypress.Commands.add('resetViewport', () => {
//Click on More button
// Assign an alias to the More button
cy.get('[data-cy="MoreTools-split-button-primary"]')
.should('have.attr', 'data-tool', 'Reset')
.as('moreBtn')
.click();
.as('moreBtn');
// Use the alias to click on the More button
cy.get('@moreBtn').click();
});
Cypress.Commands.add('imageZoomIn', () => {
@@ -266,12 +271,13 @@ Cypress.Commands.add('initStudyListAliasesOnDesktop', () => {
Cypress.Commands.add(
'addLengthMeasurement',
(firstClick = [150, 100], secondClick = [130, 170]) => {
cy.get('@measurementToolsBtnPrimary')
.should('have.attr', 'data-tool', 'Length')
.click()
.then($lengthBtn => {
cy.wrap($lengthBtn).should('have.class', 'active');
});
// Assign an alias to the button element
cy.get('@measurementToolsBtnPrimary').as('lengthButton');
cy.get('@lengthButton').should('have.attr', 'data-tool', 'Length');
cy.get('@lengthButton').click();
cy.get('@lengthButton').should('have.class', 'active');
cy.addLine('.viewport-element', firstClick, secondClick);
}
@@ -463,7 +469,11 @@ Cypress.Commands.add('closePreferences', () => {
Cypress.Commands.add('selectPreferencesTab', tabAlias => {
cy.initPreferencesModalAliases();
cy.get(tabAlias).click().should('have.class', 'active');
cy.get(tabAlias).as('selectedTab');
cy.get('@selectedTab').click();
cy.get('@selectedTab').should('have.class', 'active');
initPreferencesModalFooterBtnAliases();
});
+1 -1
View File
@@ -94,7 +94,7 @@
"devDependencies": {
"@babel/plugin-proposal-private-methods": "^7.18.6",
"@percy/cypress": "^3.1.1",
"cypress": "^12.6.0",
"cypress": "^13.2.0",
"cypress-file-upload": "^3.5.3",
"glob": "^8.0.3",
"identity-obj-proxy": "3.0.x",
+3
View File
@@ -57,6 +57,9 @@
{
"packageName": "@ohif/mode-longitudinal"
},
{
"packageName": "@ohif/mode-segmentation"
},
{
"packageName": "@ohif/mode-tmtv"
},
-1
View File
@@ -23,7 +23,6 @@ window.config = {
qidoRoot: 'https://myserver.com/dicomweb',
wadoRoot: 'https://myserver.com/dicomweb',
qidoSupportsIncludeField: false,
supportsReject: false,
imageRendering: 'wadors',
thumbnailRendering: 'wadors',
enableStudyLazyLoad: true,
+1 -5
View File
@@ -3,10 +3,7 @@ window.config = {
// whiteLabeling: {},
extensions: [],
modes: [],
customizationService: {
// Shows a custom route -access via http://localhost:3000/custom
// helloPage: '@ohif/extension-default.customizationModule.helloPage',
},
customizationService: {},
showStudyList: true,
// some windows systems have issues with more than 3 web workers
maxNumberOfWebWorkers: 3,
@@ -45,7 +42,6 @@ window.config = {
qidoRoot: 'https://d33do7qe4w26qo.cloudfront.net/dicomweb',
wadoRoot: 'https://d33do7qe4w26qo.cloudfront.net/dicomweb',
qidoSupportsIncludeField: false,
supportsReject: false,
imageRendering: 'wadors',
thumbnailRendering: 'wadors',
enableStudyLazyLoad: true,
@@ -22,7 +22,6 @@ window.config = {
qidoRoot: '/dicomweb',
wadoRoot: '/dicomweb',
qidoSupportsIncludeField: false,
supportsReject: false,
imageRendering: 'wadors',
thumbnailRendering: 'wadors',
enableStudyLazyLoad: true,
-2
View File
@@ -23,7 +23,6 @@ window.config = {
qidoRoot: '/viewer-testdata',
wadoRoot: '/viewer-testdata',
qidoSupportsIncludeField: false,
supportsReject: false,
imageRendering: 'wadors',
thumbnailRendering: 'wadors',
enableStudyLazyLoad: true,
@@ -63,7 +62,6 @@ window.config = {
qidoRoot: 'https://d33do7qe4w26qo.cloudfront.net/dicomweb',
wadoRoot: 'https://d33do7qe4w26qo.cloudfront.net/dicomweb',
qidoSupportsIncludeField: false,
supportsReject: false,
imageRendering: 'wadors',
thumbnailRendering: 'wadors',
enableStudyLazyLoad: true,
@@ -22,7 +22,6 @@ window.config = {
qidoRoot: '/dicomweb',
wadoRoot: '/dicomweb',
qidoSupportsIncludeField: false,
supportsReject: false,
imageRendering: 'wadors',
thumbnailRendering: 'wadors',
enableStudyLazyLoad: true,
-3
View File
@@ -59,7 +59,6 @@ window.config = {
qidoRoot: 'https://d33do7qe4w26qo.cloudfront.net/dicomweb',
wadoRoot: 'https://d33do7qe4w26qo.cloudfront.net/dicomweb',
qidoSupportsIncludeField: false,
supportsReject: false,
imageRendering: 'wadors',
thumbnailRendering: 'wadors',
enableStudyLazyLoad: true,
@@ -78,7 +77,6 @@ window.config = {
qidoRoot: 'https://dd32w2rfebxel.cloudfront.net/dicomweb',
wadoRoot: 'https://dd32w2rfebxel.cloudfront.net/dicomweb',
qidoSupportsIncludeField: false,
supportsReject: false,
supportsStow: false,
imageRendering: 'wadors',
thumbnailRendering: 'wadors',
@@ -99,7 +97,6 @@ window.config = {
qidoRoot: '/viewer-testdata',
wadoRoot: '/viewer-testdata',
qidoSupportsIncludeField: false,
supportsReject: false,
supportsStow: false,
imageRendering: 'wadors',
thumbnailRendering: 'wadors',
-1
View File
@@ -22,7 +22,6 @@ window.config = {
wadoRoot: 'https://d33do7qe4w26qo.cloudfront.net/dicomweb',
qidoSupportsIncludeField: false,
supportsReject: false,
imageRendering: 'wadors',
thumbnailRendering: 'wadors',
enableStudyLazyLoad: true,
+1 -1
View File
@@ -382,7 +382,7 @@ function _getViewportComponent(displaySets, viewportComponents, uiNotificationSe
console.log("Can't show displaySet", SOPClassHandlerId, displaySets[0]);
uiNotificationService.show({
title: 'Viewport Not Supported Yet',
message: `Cannot display SOPClassId of ${displaySets[0].SOPClassUID} yet`,
message: `Cannot display SOPClassUID of ${displaySets[0].SOPClassUID} yet`,
type: 'error',
});
@@ -53,7 +53,6 @@ function modeFactory({ modeConfiguration }) {
const activateTool = () => {
toolbarService.recordInteraction({
groupId: 'WindowLevel',
itemId: 'WindowLevel',
interactionType: 'tool',
commands: [
{
@@ -178,8 +178,15 @@ export default class DisplaySetService extends PubSubService {
* @param {string} displaySetInstanceUID
* @returns {object} displaySet
*/
public getDisplaySetByUID = (displaySetInstanceUid: string): DisplaySet =>
displaySetCache.get(displaySetInstanceUid);
public getDisplaySetByUID = (displaySetInstanceUid: string): DisplaySet => {
if (typeof displaySetInstanceUid !== 'string') {
throw new Error(
`getDisplaySetByUID: displaySetInstanceUid must be a string, you passed ${displaySetInstanceUid}`
);
}
return displaySetCache.get(displaySetInstanceUid);
};
/**
*
@@ -523,22 +523,32 @@ export default class HangingProtocolService extends PubSubService {
stage.viewports.push({
viewportOptions: {
...defaultViewportOptions,
viewportId: uuidv4(),
// Use 'default' for the first viewport, and UUIDs for the rest.
viewportId: i === 0 ? 'default' : uuidv4(),
},
displaySets: [],
});
}
} else {
// Clone each viewport to ensure independent objects
stage.viewports = stage.viewports.map(viewport => ({
...viewport,
viewportOptions: {
...(viewport.viewportOptions || defaultViewportOptions),
viewportId: viewport.viewportOptions?.viewportId || uuidv4(),
},
displaySets: viewport.displaySets || [],
}));
stage.viewports = stage.viewports.map((viewport, index) => {
const existingViewportId = viewport.viewportOptions?.viewportId;
return {
...viewport,
viewportOptions: {
...(viewport.viewportOptions || defaultViewportOptions),
// use provided viewportId when available, otherwise use default for first viewport
// and uuid for the rest
viewportId: existingViewportId
? existingViewportId
: index === 0
? 'default'
: uuidv4(),
},
displaySets: viewport.displaySets || [],
};
});
stage.viewports.forEach(viewport => {
viewport.displaySets.forEach(displaySet => {
displaySet.options = displaySet.options || {};
@@ -77,7 +77,7 @@ validate.validators.doesNotEqual = function (value, options, key) {
}
}
} else if (testValue === dicomArrayValue[0]) {
console.debug(dicomArrayValue, testValue);
console.log(dicomArrayValue, testValue);
return `${key} must not equal to ${testValue}`;
}
};
@@ -94,8 +94,10 @@ export default class ToolbarService extends PubSubService {
commandsManager.runCommand(commandName, commandOptions, context);
});
// only set the primary tool if no error was thrown
this.state.primaryToolId = itemId;
// only set the primary tool if no error was thrown.
// if the itemId is not undefined use it; otherwise, set the first tool in
// the commands as the primary tool
this.state.primaryToolId = itemId || commands[0].commandOptions?.toolName;
} catch (error) {
console.warn(error);
}
@@ -164,7 +166,7 @@ export default class ToolbarService extends PubSubService {
this.state.groups[groupId] = itemId;
}
this._broadcastEvent(this.EVENTS.TOOL_BAR_STATE_MODIFIED, {});
this._broadcastEvent(this.EVENTS.TOOL_BAR_STATE_MODIFIED, { ...this.state });
}
getButtons() {
@@ -293,6 +295,10 @@ export default class ToolbarService extends PubSubService {
* @param {*} props - Props set by the Viewer layer
*/
_mapButtonToDisplay(btn, btnSection, metadata, props) {
if (!btn) {
return;
}
const { id, type, component } = btn;
const buttonType = this._buttonTypes()[type];
@@ -74,6 +74,11 @@ class ViewportGridService extends PubSubService {
return this.serviceImplementation._getState();
}
public getActiveViewportId() {
const state = this.getState();
return state.activeViewportId;
}
public setDisplaySetsForViewport(props) {
// Just update a single viewport, but use the multi-viewport update for it.
this.setDisplaySetsForViewports([props]);
@@ -116,6 +116,26 @@ In the video below notice that there is `platform/viewer` which has been renamed
<iframe src="https://player.vimeo.com/video/551957266?badge=0&amp;autopause=0&amp;player_id=0&amp;app_id=58479" frameBorder="0" allow="autoplay; fullscreen; picture-in-picture" allowFullScreen style= {{ position:"absolute",top:0,left:0,width:"100%",height:"100%"}} title="measurement-report"></iframe>
</div>
### Build for non-root path
If you would like to access the viewer from a non-root path (e.g., `/my-awesome-viewer` instead of `/`),
You can achieve so by using the `PUBLIC_URL` environment variable AND the `routerBasename` configuration option.
1. use a config (e.g. config/myConfig.js) file that is using the `routerBasename` of your choice `/my-awesome-viewer` (note there is only one / - it is not /my-awesome-viewer/).
2. build the viewer with `PUBLIC_URL=/my-awesome-viewer/ APP_CONFIG=config/myConfig.js yarn build` (note there are two / - it is not /my-awesome-viewer).
:::tip
The PUBLIC_URL tells the application where to find the static assets and the routerBasename will tell the application how to handle the routes
:::
:::tip
Testing, you can use `npx http-server` to serve the files in the generated `dist` folder and access the viewer from `http://localhost:8080/my-awesome-viewer`. To achieve
so, you should first rename the `dist` folder to `my-awesome-viewer` and then change the working directory
to the `platform/app` folder and run `npx http-server ./`. Then on the browser, you can access the viewer from `http://localhost:8080/my-awesome-viewer`
:::
### Automating Builds and Deployments
If you found setting up your environment and running all of these steps to be a
+1 -1
View File
@@ -28,7 +28,7 @@ It is also required that the PUBLIC_URL environment variable is set to the same
`<iframe src="./ohif" />` (which means there is a `ohif` folder containing the build in your main app), then you need to:
1. use a config (e.g. config/myConfig.js) file that is using the `routerBasename` of `/ohif` (note the one / - it is not /ohif/).
2. build the viewer with `PUBLIC_URL=./ohif/ APP_CONFIG=config/myConfig.js yarn build` (note the one / - it is not /ohif/).
2. build the viewer with `PUBLIC_URL=/ohif/ APP_CONFIG=config/myConfig.js yarn build` (note the two / - it is not /ohif).
:::tip
Check to make sure the `app-config.js` in the build is reflecting the correct routerBasename.
@@ -21,7 +21,7 @@ specific `SOPClassHandlerUID`, so if you want to fork just a single Viewport
component for a specialized mode, this is possible.
```jsx
// displaySet, viewportIndex, dataSource
// displaySet, dataSource
const getViewportModule = () => {
const wrappedViewport = props => {
return (
@@ -45,8 +45,8 @@ There are seven events that get publish in `MeasurementService`:
### Segment Behavior
- setSegmentLockedForSegmentation, removeSegment, addSegment, setSegmentLockedForSegmentation, setSegmentLabel, setActiveSegmentForSegmentation,
setSegmentRGBAColorForSegmentation
- setSegmentLocked, removeSegment, addSegment, setSegmentLocked, setSegmentLabel, setActiveSegment,
setSegmentRGBAColor
### Segmentation Configuration
@@ -44,6 +44,7 @@ In the `value` of each customizations, you will define customization prototype(s
These customization prototype(s) can be considered like "Prototype" in Javascript.
These can be used to extend the customization definitions from configurations.
Default customizations will be often used to define all the customization prototypes,
Default customizations will be often used to define all the customization prototypes,
as they will be loaded automatically along with the defining extension or mode.
@@ -58,7 +59,6 @@ For example, the `@ohif/extension-default` extension defines,
value: [
{
id: 'ohif.overlayItem',
uiType: 'uiType',
content: function (props) {
if (this.condition && !this.condition(props)) return null;
@@ -92,8 +92,9 @@ For example, the `@ohif/extension-default` extension defines,
],
```
And this `ohif.overlayItem` object will be used as a prototype to define items
to be displayed on `CustomizableViewportOverlay`. See the next section.
And this `ohif.overlayItem` object will be used as a prototype (and template) to define items
to be displayed on `CustomizableViewportOverlay`. See how we use the `ohif.overlayItem` in
the example below.
## Configuring customizations
@@ -119,13 +120,16 @@ window.config = {
customizationService: {
cornerstoneOverlayTopRight: {
id: 'cornerstoneOverlayTopRight',
customizationType: 'ohif.cornerstoneOverlay',
items: [
{
id: 'PatientNameOverlay',
// Note the overlayItem as a parent type - this provides the
// rendering functionality to read the attribute and use the label.
// Note below that here we are using the customization prototype of
// `ohif.overlayItem` which was registered to the customization module in
// `ohif/extension-default` extension.
customizationType: 'ohif.overlayItem',
// the following props are passed to the `ohif.overlayItem` prototype
// which is used to render the overlay item based on the label, color,
// conditions, etc.
attribute: 'PatientName',
label: 'PN:',
title: 'Patient Name',
@@ -157,20 +161,19 @@ The `customizationType` field is simply the id of another customization object.
### Mode Customizations
Mode-specific customizations are no different from the global ones,
except that the mode customizations are cleared before the mode `onModeEnter`
is called, and they can have new values registered in the `onModeEnter`
except that the mode customizations are specific to one mode and
are not globally applied. Mode-specific customizations are also cleared
before the mode `onModeEnter` is called, and they can have new values registered in the `onModeEnter`
In the mode customization, the overlay is then further customized
with a bottom-right overlay, which extends the customizationService configuration.
Following on our example above to customize the overlay, we can now add a mode customization
with a bottom-right overlay.
```js
// Import the type from the extension itself
import OverlayUICustomization from '@ohif/cornerstone-extension';
import OverlayUICustomization from "@ohif/cornerstone-extension";
// In the mode itself, customizations can be registered:
onModeEnter() {
...
onModeEnter: {
// Note how the object can be strongly typed
const bottomRight: OverlayUICustomization = {
id: 'cornerstoneOverlayBottomRight',
@@ -182,6 +185,7 @@ onModeEnter() {
],
};
customizationService.addModeCustomizations(bottomRight);
}
```
The mode customizations are retrieved via the `getModeCustomization` function,
@@ -198,9 +202,17 @@ can then be used in a way defined by the extension provided that customization
point.
```ts
cornerstoneOverlay = uiConfigurationService.getModeCustomization("cornerstoneOverlay", {customizationType: "ohif.cornerstoneOverlay", ...});
const { component: overlayComponent, props} = uiConfigurationService.getComponent(cornerstoneOverlay);
return (<defaultComponent {...props} overlay={cornerstoneOverlay}....></defaultComponent>);
const cornerstoneOverlay = customizationService.getModeCustomization(
"cornerstoneOverlay",
{ customizationType: "ohif.cornerstoneOverlay" },
);
const { component: overlayComponent, props } =
customizationService.getComponent(cornerstoneOverlay);
return (
<defaultComponent {...props} overlay={cornerstoneOverlay}></defaultComponent>
);
```
This example shows fetching the default component to render this object. The
@@ -210,8 +222,11 @@ example (this example comes from the context menu customizations as that one
uses commands lists):
```ts
cornerstoneContextMenu = uiConfigurationService.get("cornerstoneContextMenu", defaultMenu);
commandsManager.run(cornerstoneContextMenu, extraProps);
cornerstoneContextMenu = customizationService.get(
"cornerstoneContextMenu",
defaultMenu,
);
commandsManager.run(cornerstoneContextMenu, extraProps);
```
### Global Customizations
@@ -339,7 +354,6 @@ window.config = {
customizationService: {
cornerstoneOverlayTopLeft: {
id: 'cornerstoneOverlayTopLeft',
customizationType: 'ohif.cornerstoneOverlay',
items: [
{
id: 'WindowLevel',
@@ -398,7 +412,6 @@ window.config = {
},
cornerstoneOverlayTopRight: {
id: 'cornerstoneOverlayTopRight',
customizationType: 'ohif.cornerstoneOverlay',
items: [
{
@@ -438,7 +451,6 @@ window.config = {
},
cornerstoneOverlayBottomLeft: {
id: 'cornerstoneOverlayBottomLeft',
customizationType: 'ohif.cornerstoneOverlay',
items: [
{
@@ -32,6 +32,7 @@ is expected to support, [check out it's interface in `@ohif/core`][interface]
| `reset()` | Resets the default states |
| `getNumViewportPanes()` | Gets the number of visible viewport panes |
| `getLayoutOptionsFromState(gridState)` | Utility method that produces a `ViewportLayoutOptions` based on the passed in state|
| `getActiveViewportId()` | Returns the viewport Id of the active viewport in the grid|
## Implementations
+9 -5
View File
@@ -1,12 +1,12 @@
import path from 'path';
import path, { dirname, join } from 'path';
import remarkGfm from 'remark-gfm';
import type { StorybookConfig } from '@storybook/react-webpack5';
const config: StorybookConfig = {
stories: ['../src/**/*.stories.@(mdx)'],
addons: [
'@storybook/addon-links',
'@storybook/addon-essentials',
getAbsolutePath('@storybook/addon-links'),
getAbsolutePath('@storybook/addon-essentials'),
// Other addons go here
{
name: '@storybook/addon-docs',
@@ -19,9 +19,9 @@ const config: StorybookConfig = {
},
},
],
core: { builder: '@storybook/builder-webpack5' },
core: {},
framework: {
name: '@storybook/react-webpack5',
name: getAbsolutePath('@storybook/react-webpack5'),
options: {},
},
docs: {
@@ -91,3 +91,7 @@ const config: StorybookConfig = {
};
export default config;
function getAbsolutePath(value: string): any {
return dirname(require.resolve(join(value, 'package.json')));
}
+10 -9
View File
@@ -23,6 +23,7 @@
"build": "cross-env NODE_ENV=production webpack --config .webpack/webpack.prod.js",
"build:package": "yarn run build",
"storybook": "storybook dev -p 6006",
"clean": "rm -rf node_modules/.cache/storybook",
"dev": "storybook dev -p 6006",
"build-storybook": "storybook build"
},
@@ -53,14 +54,14 @@
},
"devDependencies": {
"@babel/core": "^7.21.4",
"@storybook/addon-actions": "^7.0.7",
"@storybook/addon-docs": "^7.0.7",
"@storybook/addon-essentials": "^7.0.7",
"@storybook/addon-links": "^7.0.7",
"@storybook/cli": "^7.0.7",
"@storybook/react": "^7.0.7",
"@storybook/react-webpack5": "^7.0.7",
"@storybook/source-loader": "^7.0.7",
"@storybook/addon-actions": "^7.2.2",
"@storybook/addon-docs": "^7.2.2",
"@storybook/addon-essentials": "^7.2.2",
"@storybook/addon-links": "^7.2.2",
"@storybook/cli": "^7.2.2",
"@storybook/react": "^7.2.2",
"@storybook/react-webpack5": "^7.2.2",
"@storybook/source-loader": "^7.2.2",
"autoprefixer": "^10.4.14",
"babel-loader": "^9.1.2",
"dotenv-webpack": "^8.0.1",
@@ -68,7 +69,7 @@
"postcss-loader": "^7.2.4",
"prop-types": "^15.8.1",
"remark-gfm": "^3.0.1",
"storybook": "^7.0.7",
"storybook": "^7.2.2",
"tailwindcss": "3.2.4"
}
}
@@ -1,3 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20">
<path fill="currentColor" fill-rule="evenodd" d="M10 13L5 7.737 5.7 7 10 11.526 14.3 7 15 7.737z" transform="rotate(90 10 10)"/>
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 20 20">
<path fill="currentColor" fill-rule="evenodd" stroke="currentColor" d="M10 13L5 7.737 5.7 7 10 11.526 14.3 7 15 7.737z" transform="rotate(90 10 10)"/>
</svg>

Before

Width:  |  Height:  |  Size: 201 B

After

Width:  |  Height:  |  Size: 246 B

+15
View File
@@ -0,0 +1,15 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="12.0833333px" height="12.0833333px" viewBox="0 0 12.0833333 12.0833333" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<title>icon-add</title>
<g id="Artboards" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g id="Segmentation---assets" transform="translate(-78.875, -64.375)">
<g id="icon-add" transform="translate(75, 60)">
<rect id="Rectangle" x="0" y="0" width="20" height="20"></rect>
<g id="Group-18" transform="translate(4.5, 5)" stroke="#348CFD" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.25">
<line x1="5.41666667" y1="0" x2="5.41666667" y2="10.8333333" id="Path"></line>
<line x1="10.8333333" y1="5.41666667" x2="0" y2="5.41666667" id="Path"></line>
</g>
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 961 B

@@ -0,0 +1,17 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="10.25px" height="10.25px" viewBox="0 0 10.25 10.25" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<title>icon-delete</title>
<g id="Artboards" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g id="Segmentation---assets" transform="translate(-254.875, -64.875)">
<g id="icon-delete" transform="translate(250, 60)">
<rect id="Rectangle" x="0" y="0" width="20" height="20"></rect>
<g id="Group-31" transform="translate(5, 5)" stroke="#348CFD" stroke-linecap="round" stroke-width="1.25">
<g id="Group-30">
<line x1="0.5" y1="0.5" x2="9.5" y2="9.5" id="Line-4"></line>
<line x1="0.5" y1="0.5" x2="9.5" y2="9.5" id="Line-4" transform="translate(5, 5) scale(-1, 1) translate(-5, -5)"></line>
</g>
</g>
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1019 B

@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="8.88756485px" height="5.15088921px" viewBox="0 0 8.88756485 5.15088921" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<title>icon-disclosure-close</title>
<g id="Artboards" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g id="Segmentation---assets" transform="translate(-115.6464, -67.6464)">
<g id="icon-disclosure-close" transform="translate(110, 60)">
<polyline id="Path-2" stroke="#348CFD" points="6 8 10.090229 12.090229 14.1804581 8"></polyline>
<rect id="Rectangle" x="0" y="0" width="20" height="20"></rect>
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 737 B

@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="5.15088921px" height="8.88756485px" viewBox="0 0 5.15088921 8.88756485" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<title>icon-disclosure-open</title>
<g id="Artboards" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g id="Segmentation---assets" transform="translate(-152.338, -65.6013)">
<g id="icon-disclosure-open" transform="translate(145, 60)">
<polyline id="Path-2" stroke="#348CFD" transform="translate(10.0902, 10.0451) rotate(90) translate(-10.0902, -10.0451)" points="6 8 10.090229 12.090229 14.1804581 8"></polyline>
<rect id="Rectangle" x="0" y="0" width="20" height="20"></rect>
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 815 B

@@ -0,0 +1,21 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="15.7826087px" height="15.7826087px" viewBox="0 0 15.7826087 15.7826087" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<title>icon-display-settings</title>
<g id="Artboards" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd" stroke-linecap="round" stroke-linejoin="round">
<g id="Segmentation---assets" transform="translate(-42.127, -62.1087)" stroke="#348CFD">
<g id="icon-display-settings" transform="translate(40, 60)">
<circle id="Oval" cx="10" cy="4.34782609" r="1.73913043"></circle>
<line x1="11.7373913" y1="4.34782609" x2="17.4095652" y2="4.34782609" id="Path"></line>
<line x1="2.62695652" y1="4.34782609" x2="8.2573913" y2="4.34782609" id="Path"></line>
<g id="Group-26" transform="translate(10.0183, 10) scale(-1, 1) translate(-10.0183, -10)translate(2.627, 8.2609)">
<circle id="Oval" transform="translate(3.8948, 1.7391) scale(-1, 1) translate(-3.8948, -1.7391)" cx="3.89478261" cy="1.73913043" r="1.73913043"></circle>
<line x1="5.63043478" y1="1.73913043" x2="14.7826087" y2="1.73913043" id="Path" transform="translate(10.2065, 1.7391) scale(-1, 1) translate(-10.2065, -1.7391)"></line>
<line x1="0" y1="1.73913043" x2="2.15217391" y2="1.73913043" id="Path" transform="translate(1.0761, 1.7391) scale(-1, 1) translate(-1.0761, -1.7391)"></line>
</g>
<circle id="Oval" cx="8.26086957" cy="15.6521739" r="1.73913043"></circle>
<line x1="9.99652174" y1="15.6521739" x2="17.4095652" y2="15.6521739" id="Path"></line>
<line x1="2.62695652" y1="15.6521739" x2="6.52173913" y2="15.6521739" id="Path"></line>
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.8 KiB

@@ -0,0 +1,17 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="18px" height="18px" viewBox="0 0 18 18" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<title>icon-more-menu</title>
<g id="Artboards" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g id="Segmentation---assets" transform="translate(-286, -61)">
<g id="icon-more-menu" transform="translate(285, 60)">
<g id="Group-28" transform="translate(1, 1)">
<circle id="Oval" fill="#348CFD" cx="5.625" cy="9" r="1.125"></circle>
<circle id="Oval" fill="#348CFD" cx="9" cy="9" r="1.125"></circle>
<circle id="Oval" fill="#348CFD" cx="12.375" cy="9" r="1.125"></circle>
<circle id="Oval" stroke="#348CFD" cx="9" cy="9" r="8.5"></circle>
</g>
<rect id="Rectangle" x="0" y="0" width="20" height="20"></rect>
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1008 B

@@ -0,0 +1,15 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="12.8339446px" height="12.73108px" viewBox="0 0 12.8339446 12.73108" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<title>icon-rename</title>
<g id="Artboards" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g id="Segmentation---assets" transform="translate(-218.9974, -64)">
<g id="icon-rename" transform="translate(215, 60)">
<rect id="Rectangle" x="0" y="0" width="20" height="20"></rect>
<g id="Group-29" transform="translate(5, 5)" stroke="#348CFD" stroke-linecap="round" stroke-linejoin="round">
<path d="M7.62511295,0.871568536 L9.85691246,3.10336805 L2.82291656,10.1381292 L-0.897323283,11.6258043 L0.590351811,7.90556444 L7.62511295,0.871568536 Z" id="Path"></path>
<path d="M9.75273119,-0.5 C10.1567445,-0.5 10.5607578,-0.345874112 10.8690096,-0.0376223372 C11.1772614,0.270629438 11.3313873,0.674642763 11.3313873,1.07865609 C11.3313873,1.48266941 11.1772614,1.88668274 10.8688429,2.19510116 L9.75256454,3.31032741 L7.52105985,1.07882272 L8.63645277,-0.0376223372 C8.94470454,-0.345874112 9.34871787,-0.5 9.75273119,-0.5 Z" id="Path"></path>
</g>
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

@@ -0,0 +1,22 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="15.4692312px" height="13.3478261px" viewBox="0 0 15.4692312 13.3478261" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<title>icon-toggle-all-visibility</title>
<g id="Artboards" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g id="Segmentation---assets" transform="translate(-181.8615, -62.9135)">
<g id="icon-toggle-all-visibility" transform="translate(180, 60)">
<rect id="Rectangle" x="0" y="0" width="20" height="20"></rect>
<g id="Group-15" transform="translate(2.4615, 3.5385)" stroke="#348CFD" stroke-linecap="round" stroke-linejoin="round">
<g id="view-off">
<g id="Group-14" opacity="0.718994867" transform="translate(0, 2.0163)" stroke-width="1.2">
<path d="M11.7987926,1.72967893 C12.7139877,2.39263252 13.5437544,3.16613677 14.2692308,4.0326087 C14.2692308,4.0326087 11.0754047,8.06521739 7.13461538,8.06521739 C6.50340139,8.06171444 5.87618966,7.96470569 5.27341137,7.77735117" id="Path"></path>
<path d="M2.46175251,6.32809365 C1.55002525,5.66691798 0.723215132,4.89595088 0,4.0326087 C0,4.0326087 3.19382609,0 7.13461538,0 C7.65800586,0.00220851946 8.17906018,0.0699455819 8.68561873,0.201630435" id="Path"></path>
<path d="M4.65301003,4.0326087 C4.65301003,2.66205591 5.76406259,1.55100334 7.13461538,1.55100334" id="Path"></path>
<path d="M9.61622074,4.0326087 C9.61622074,5.40316149 8.50516818,6.51421405 7.13461538,6.51421405" id="Path"></path>
</g>
<line x1="13.1835284" y1="0" x2="1.08570234" y2="12.0978261" id="Path" stroke-width="1.25"></line>
</g>
</g>
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.9 KiB

@@ -0,0 +1,15 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="23.7493737px" height="24.7494038px" viewBox="0 0 23.7493737 24.7494038" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<title>icon-tool-brush</title>
<g id="Artboards" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g id="Segmentation---assets" transform="translate(-40.625, -105.125)">
<g id="icon-tool-brush" transform="translate(40, 105)">
<g id="brush" transform="translate(1.5, 1)" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.75">
<path d="M0.285314129,22.3807117 C0.106218646,22.2955642 -0.00563032043,22.1126205 0.000218787638,21.9144007 C0.0060678957,21.716181 0.128509536,21.5401507 0.312314129,21.4657117 C2.69331413,20.5017117 1.92631413,17.7877117 2.63331413,16.3367117 C3.49915231,14.5926292 5.60557215,13.8677677 7.36131413,14.7097117 C12.6773141,17.3017117 6.34931413,25.2667117 0.285314129,22.3807117 Z" id="Path"></path>
<path d="M21.5333141,0.349711738 C20.9877208,-0.135928959 20.1584241,-0.113148488 19.6403141,0.401711738 L7.74931413,12.2607117 C9.32532634,12.7428889 10.6345655,13.8504286 11.3713141,15.3247117 L21.7003141,2.24271174 C22.156142,1.66747397 22.0828125,0.836259875 21.5333141,0.349711738 Z" id="Path"></path>
</g>
<rect id="Rectangle" x="0" y="0" width="25" height="25"></rect>
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

@@ -0,0 +1,16 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="24.25px" height="22.418445px" viewBox="0 0 24.25 22.418445" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<title>icon-tool-eraser</title>
<g id="Artboards" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g id="Segmentation---assets" transform="translate(-83.375, -106.3752)">
<g id="icon-tool-eraser" transform="translate(83, 105)">
<g id="eraser-1" transform="translate(1, 2)" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.25">
<path d="M7,19.1705986 C5.7337971,19.1021794 4.52894862,18.6033051 3.585,17.7565986 L1.414,15.5835986 C0.635868537,14.8011205 0.635868537,13.5370767 1.414,12.7545986 L13.585,0.583598597 C14.3674781,-0.194532866 15.6315219,-0.194532866 16.414,0.583598597 L21.585,5.7545986 C22.3631315,6.53707671 22.3631315,7.80112049 21.585,8.5835986 L12.414,17.7545986 C11.4705925,18.601822 10.2661029,19.1013994 9,19.1705986 L7,19.1705986 Z" id="Path"></path>
<line x1="9" y1="5.1685986" x2="17" y2="13.1685986" id="Path"></line>
<line x1="0" y1="21.1685986" x2="23" y2="21.1685986" id="Path"></line>
</g>
<rect id="Rectangle" x="0" y="0" width="25" height="25"></rect>
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.4 KiB

@@ -0,0 +1,17 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="24.2500949px" height="20.25px" viewBox="0 0 24.2500949 20.25" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<title>icon-tool-scissor</title>
<g id="Artboards" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g id="Segmentation---assets" transform="translate(-126.375, -107.375)">
<g id="icon-tool-scissor" transform="translate(126, 105)">
<rect id="Rectangle" x="0" y="0" width="25" height="25"></rect>
<g id="scissors-2" transform="translate(1, 3)" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.25">
<circle id="Oval" cx="3.5" cy="3.5" r="3.5"></circle>
<path d="M10.648,8.232 L23,15.587 C22.3744808,16.5550109 21.1109418,16.8812258 20.095,16.337 L4.154,6.939" id="Path"></path>
<circle id="Oval" cx="3.5" cy="15.5" r="3.5"></circle>
<path d="M12.779,9.501 L23,3.414 C22.3747822,2.44557124 21.1108803,2.11926264 20.095,2.664 L4.154,12.061" id="Path"></path>
</g>
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

@@ -0,0 +1,21 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="24.5px" height="16.5px" viewBox="0 0 24.5 16.5" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<title>icon-tool-threshold</title>
<g id="Artboards" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g id="Segmentation---assets" transform="translate(-169.25, -109.25)">
<g id="icon-tool-threshold" transform="translate(169, 105)">
<g id="Group-16" transform="translate(1, 5)" stroke-width="1.5">
<g id="Group-13">
<line x1="19.5" y1="12.5" x2="23" y2="12.5" id="Line-2" stroke="currentColor" stroke-linecap="round"></line>
<line x1="7.63278329e-17" y1="12.5" x2="14" y2="12.5" id="Line-2" stroke="currentColor" stroke-linecap="round"></line>
<circle id="Oval" stroke="currentColor" cx="16.5" cy="12.5" r="2.5"></circle>
<line x1="10.5" y1="2.5" x2="22.5" y2="2.5" id="Line-2" stroke="currentColor" stroke-linecap="round"></line>
<line x1="2.27456942e-13" y1="2.5" x2="5.5" y2="2.5" id="Line-2" stroke="currentColor" stroke-linecap="round"></line>
<circle id="Oval" stroke="currentColor" cx="8" cy="2.5" r="2.5"></circle>
</g>
</g>
<rect id="Rectangle" x="0" y="0" width="25" height="25"></rect>
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

+3 -3
View File
@@ -2,9 +2,9 @@
<g fill="none" fill-rule="evenodd">
<path d="M0 0h21v21H0z"/>
<path d="M0 0h21v21H0z"/>
<path d="M17.29 10.6c-.676-.77-3.378-3.6-6.61-3.6-3.23 0-5.933 2.83-6.609 3.6a.293.293 0 0 0 0 .383c.676.77 3.378 3.6 6.61 3.6 3.23 0 5.933-2.83 6.609-3.6a.293.293 0 0 0 0-.383z" stroke="#3D5871" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M10.68 8.75c.69 0 1.298.344 1.666.87l-2.84 2.84c-.52-.37-.859-.98-.859-1.668 0-1.128.91-2.042 2.033-2.042zM12.689 10.47a2.038 2.038 0 0 1-2.337 2.337z" fill="#3D5871"/>
<g stroke="#3D5871" stroke-linecap="round" stroke-linejoin="round">
<path d="M17.29 10.6c-.676-.77-3.378-3.6-6.61-3.6-3.23 0-5.933 2.83-6.609 3.6a.293.293 0 0 0 0 .383c.676.77 3.378 3.6 6.61 3.6 3.23 0 5.933-2.83 6.609-3.6a.293.293 0 0 0 0-.383z" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M10.68 8.75c.69 0 1.298.344 1.666.87l-2.84 2.84c-.52-.37-.859-.98-.859-1.668 0-1.128.91-2.042 2.033-2.042zM12.689 10.47a2.038 2.038 0 0 1-2.337 2.337z" fill="currentColor"/>
<g stroke="currentColor" stroke-linecap="round" stroke-linejoin="round">
<path d="M16.396 5 5.02 16.375"/>
</g>
</g>

Before

Width:  |  Height:  |  Size: 782 B

After

Width:  |  Height:  |  Size: 797 B

@@ -1,11 +0,0 @@
<svg width="21" height="21" viewBox="0 0 21 21" xmlns="http://www.w3.org/2000/svg">
<g fill="none" fill-rule="evenodd">
<path d="M0 0h21v21H0z"/>
<path d="M0 0h21v21H0z"/>
<path d="M17.29 10.6c-.676-.77-3.378-3.6-6.61-3.6-3.23 0-5.933 2.83-6.609 3.6a.293.293 0 0 0 0 .383c.676.77 3.378 3.6 6.61 3.6 3.23 0 5.933-2.83 6.609-3.6a.293.293 0 0 0 0-.383z" stroke="#348CFD" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M10.68 8.75c.69 0 1.298.344 1.666.87l-2.84 2.84c-.52-.37-.859-.98-.859-1.668 0-1.128.91-2.042 2.033-2.042zM12.689 10.47a2.038 2.038 0 0 1-2.337 2.337z" fill="#348CFD"/>
<g stroke="#348CFD" stroke-linecap="round" stroke-linejoin="round">
<path d="M16.396 5 5.02 16.375"/>
</g>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 782 B

@@ -1,7 +1,7 @@
<svg width="21" height="21" viewBox="0 0 21 21" xmlns="http://www.w3.org/2000/svg">
<g fill="none" fill-rule="evenodd">
<path d="M0 0h21v21H0z"/>
<path d="M8 10.5V8.559A2.554 2.554 0 0 1 10.549 6a2.554 2.554 0 0 1 2.548 2.559v2.483" stroke="#FFF" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M14.468 11c0-.552-.42-1-.938-1H7.437c-.517 0-.937.448-.937 1v5c0 .552.42 1 .937 1h6.093c.518 0 .938-.448.938-1v-5z" fill="#FFF"/>
<path d="M8 10.5V8.559A2.554 2.554 0 0 1 10.549 6a2.554 2.554 0 0 1 2.548 2.559v2.483" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M14.468 11c0-.552-.42-1-.938-1H7.437c-.517 0-.937.448-.937 1v5c0 .552.42 1 .937 1h6.093c.518 0 .938-.448.938-1v-5z" stroke="currentColor"/>
</g>
</svg>
@@ -1,9 +0,0 @@
<svg width="21" height="21" viewBox="0 0 21 21" xmlns="http://www.w3.org/2000/svg">
<g fill="none" fill-rule="evenodd">
<path d="M0 0h21v21H0z"/>
<g transform="translate(4 7)">
<path d="M13.29 3.6C12.614 2.83 9.912 0 6.68 0 3.45 0 .747 2.83.071 3.6a.293.293 0 0 0 0 .383c.676.77 3.378 3.6 6.61 3.6 3.23 0 5.933-2.83 6.609-3.6a.293.293 0 0 0 0-.383z" stroke="#348CFD" stroke-linecap="round" stroke-linejoin="round"/>
<ellipse fill="#348CFD" cx="6.68" cy="3.792" rx="2.033" ry="2.042"/>
</g>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 558 B

@@ -2,8 +2,8 @@
<g fill="none" fill-rule="evenodd">
<path d="M0 0h21v21H0z"/>
<g transform="translate(4 7)">
<path d="M13.29 3.6C12.614 2.83 9.912 0 6.68 0 3.45 0 .747 2.83.071 3.6a.293.293 0 0 0 0 .383c.676.77 3.378 3.6 6.61 3.6 3.23 0 5.933-2.83 6.609-3.6a.293.293 0 0 0 0-.383z" stroke="#FFF" stroke-linecap="round" stroke-linejoin="round"/>
<ellipse fill="#FFF" cx="6.68" cy="3.792" rx="2.033" ry="2.042"/>
<path d="M13.29 3.6C12.614 2.83 9.912 0 6.68 0 3.45 0 .747 2.83.071 3.6a.293.293 0 0 0 0 .383c.676.77 3.378 3.6 6.61 3.6 3.23 0 5.933-2.83 6.609-3.6a.293.293 0 0 0 0-.383z" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"/>
<ellipse fill="currentColor" cx="6.68" cy="3.792" rx="2.033" ry="2.042"/>
</g>
</g>
</svg>
@@ -1,11 +0,0 @@
<svg width="21" height="21" viewBox="0 0 21 21" xmlns="http://www.w3.org/2000/svg">
<g fill="none" fill-rule="evenodd">
<path d="M0 0h21v21H0z"/>
<path d="M0 0h21v21H0z"/>
<path d="M17.29 10.6c-.676-.77-3.378-3.6-6.61-3.6-3.23 0-5.933 2.83-6.609 3.6a.293.293 0 0 0 0 .383c.676.77 3.378 3.6 6.61 3.6 3.23 0 5.933-2.83 6.609-3.6a.293.293 0 0 0 0-.383z" stroke="#FFF" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M10.68 8.75c.69 0 1.298.344 1.666.87l-2.84 2.84c-.52-.37-.859-.98-.859-1.668 0-1.128.91-2.042 2.033-2.042zM12.689 10.47a2.038 2.038 0 0 1-2.337 2.337z" fill="#FFF"/>
<g stroke="#FFF" stroke-linecap="round" stroke-linejoin="round">
<path d="M16.396 5 5.02 16.375"/>
</g>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 773 B

@@ -1,7 +1,7 @@
<svg width="21" height="21" viewBox="0 0 21 21" xmlns="http://www.w3.org/2000/svg">
<g fill="none" fill-rule="evenodd">
<path d="M0 0h21v21H0z"/>
<path d="M16.014 7.559A2.554 2.554 0 0 0 13.466 5a2.554 2.554 0 0 0-2.549 2.559v2.483" stroke="#FFF" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M12.968 10.824c0-.553-.42-1-.938-1H5.937c-.517 0-.937.447-.937 1v5c0 .552.42 1 .937 1h6.093c.518 0 .938-.448.938-1v-5z" fill="#FFF"/>
<path d="M16.014 7.559A2.554 2.554 0 0 0 13.466 5a2.554 2.554 0 0 0-2.549 2.559v2.483" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M12.968 10.824c0-.553-.42-1-.938-1H5.937c-.517 0-.937.447-.937 1v5c0 .552.42 1 .937 1h6.093c.518 0 .938-.448.938-1v-5z" fill="currentColor"/>
</g>
</svg>
@@ -1,3 +1,3 @@
<svg width="6" height="5" viewBox="0 0 6 5" xmlns="http://www.w3.org/2000/svg">
<path d="M3 5 6 .5H0z" fill="#726F7E" fill-rule="evenodd"/>
<path d="M3 5 6 .5H0z" fill="currnetColor" fill-rule="evenodd"/>
</svg>

Before

Width:  |  Height:  |  Size: 151 B

After

Width:  |  Height:  |  Size: 156 B

@@ -0,0 +1,3 @@
<svg width="5" height="6" viewBox="0 0 5 6" xmlns="http://www.w3.org/2000/svg">
<path d="M0.5 3L5 0V6z" fill="currentColor" fill-rule="evenodd"/>
</svg>

After

Width:  |  Height:  |  Size: 157 B

@@ -0,0 +1,3 @@
<svg width="5" height="6" viewBox="0 0 5 6" xmlns="http://www.w3.org/2000/svg">
<path transform="rotate(90 2.5 2.5)" d="M3 5 6 .5H0z" fill="currentColor" fill-rule="evenodd"/>
</svg>

After

Width:  |  Height:  |  Size: 187 B

@@ -0,0 +1,64 @@
import React, { useState, useEffect } from 'react';
import classnames from 'classnames';
import { PanelSection, Icon, Tooltip } from '../../components';
import ToolSettings from './ToolSettings';
const AdvancedToolbox = ({ title, items }) => {
const [activeItemName, setActiveItemName] = useState(null);
useEffect(() => {
// see if any of the items are active from the outside
const activeItem = items?.find(item => item.active);
setActiveItemName(activeItem ? activeItem.name : null);
}, [items]);
const activeItemOptions = items?.find(item => item.name === activeItemName)?.options;
return (
<PanelSection title={title}>
<div className="flex flex-col bg-black">
<div className="bg-primary-dark mt-0.5 flex flex-wrap py-2">
{items?.map(item => {
return (
<Tooltip
position="bottom"
content={<span className="text-white">{item.name}</span>}
key={item.name}
>
<div
className="ml-2 mb-2"
onClick={() => {
if (item.disabled) {
return;
}
setActiveItemName(item.name);
item.onClick(item.name);
}}
>
<div
className={classnames(
'text-primary-active grid h-[40px] w-[40px] place-items-center rounded-md bg-black ',
activeItemName === item.name && 'bg-primary-light text-black',
item.disabled && 'opacity-50',
!item.disabled &&
'hover:bg-primary-light cursor-pointer hover:cursor-pointer hover:text-black'
)}
>
<Icon name={item.icon} />
</div>
</div>
</Tooltip>
);
})}
</div>
<div className="bg-primary-dark h-auto px-2">
<ToolSettings options={activeItemOptions} />
</div>
</div>
</PanelSection>
);
};
AdvancedToolbox.propTypes = {};
export default AdvancedToolbox;
@@ -0,0 +1,84 @@
import React from 'react';
import { ButtonGroup, InputRange, ButtonEnums } from '../../components';
const SETTING_TYPES = {
RANGE: 'range',
RADIO: 'radio',
CUSTOM: 'custom',
};
function ToolSettings({ options }) {
if (!options) {
return null;
}
const getButtons = option => {
const buttons = [];
option.values?.map(({ label, value: optionValue }, index) => {
buttons.push({
children: label,
onClick: () => option.onChange(optionValue),
key: `button-${option.id}-${index}`, // A unique key
});
});
return buttons;
};
return (
<div className="space-y-2 py-2 text-white">
{options?.map(option => {
if (option.type === SETTING_TYPES.RANGE) {
return (
<div
className="flex items-center"
key={option.id}
>
<div className="w-1/3 text-xs text-[13px]">{option.name}</div>
<div className="w-2/3">
<InputRange
minValue={option.min}
maxValue={option.max}
step={option.step}
value={option.value}
onChange={e => option.onChange(e)}
allowNumberEdit={true}
showAdjustmentArrows={false}
inputClassName="ml-2 w-4/5"
/>
</div>
</div>
);
}
if (option.type === SETTING_TYPES.RADIO) {
return (
<div
className="flex items-center justify-between text-[13px]"
key={option.id}
>
<span>{option.name}</span>
<div className="max-w-1/2">
<ButtonGroup
buttons={getButtons(option)}
defaultActiveIndex={option.defaultActiveIndex}
size={ButtonEnums.size.small}
/>
</div>
</div>
);
}
if (option.type === SETTING_TYPES.CUSTOM) {
return (
<div key={option.id}>
{typeof option.children === 'function' ? option.children() : option.children}
</div>
);
}
})}
</div>
);
}
export default ToolSettings;
@@ -0,0 +1,144 @@
import AdvancedToolbox from '../../AdvancedToolbox';
import { ArgsTable, Story, Canvas, Meta } from '@storybook/addon-docs';
import {
createComponentTemplate,
createStoryMetaSettings,
} from '../../../storybook/functions/create-component-story';
export const argTypes = {
component: AdvancedToolbox,
title: 'Components/AdvancedToolbox',
};
<Meta
title="Components/AdvancedToolbox"
component={AdvancedToolbox}
/>
export const advancedToolboxTemplate = args => (
<div className="bg-primary-dark h-96 w-64">
<AdvancedToolbox {...args} />
</div>
);
<Heading
title="Advanced Toolbox"
componentRelativePath="AdvancedToolbox/AdvancedToolbox.tsx"
/>
- [Overview](#overview)
- [Props](#props)
- [Usage](#usage)
- [Contribute](#contribute)
## Overview
OHIF advanced toolbox which can host set of tools that require more space for customization.
<Canvas>
<Story
name="Overview"
args={{
title: 'Segmentation Tools',
items: [
{
name: 'Brush',
icon: 'icon-tool-brush',
active: false,
onClick: () => console.log('Brush clicked'),
options: [
{
name: 'Radius (mm)',
type: 'range',
min: 1,
max: 10,
value: 5,
step: 1,
onChange: value => console.log('Brush size changed', value),
},
{
name: 'Mode',
type: 'radio',
value: 'Circle',
values: [
{ value: 'Circle', label: 'Circle' },
{ value: 'Sphere', label: 'Sphere' },
{ value: 'Rectangle', label: 'Rectangle' },
],
onChange: value => console.log('Brush mode changed', value),
},
],
},
{
name: 'Eraser',
icon: 'icon-tool-eraser',
onClick: () => console.log('eraser clicked'),
options: [
{
name: 'Mode',
type: 'radio',
value: 'EraserSphere',
values: [
{ value: 'EraserCircle', label: 'Circle' },
{ value: 'EraserSphere', label: 'Sphere' },
],
onChange: value => console.log('Brush mode changed', value),
},
],
},
{
name: 'Threshold',
icon: 'icon-tool-threshold',
active: true,
onClick: () => console.log('eraser clicked'),
options: [
{
name: 'Radius (mm)',
type: 'range',
min: 1,
max: 10,
value: 5,
step: 1,
onChange: value => console.log('Brush size changed', value),
},
{
name: 'Mode',
type: 'radio',
value: 'Circle',
values: [
{ value: 'Circle', label: 'Circle' },
{ value: 'Sphere', label: 'Sphere' },
{ value: 'Rectangle', label: 'Rectangle' },
],
onChange: value => console.log('Brush mode changed', value),
},
{
name: 'custom',
type: 'custom',
children: () => {
return (
<div>
<div>Custom</div>
<input type="text" />
</div>
);
},
},
],
},
],
}}
>
{advancedToolboxTemplate.bind({})}
</Story>
</Canvas>
## Props
<ArgsTable of={AdvancedToolbox} />
## Usage
## Contribute
<Footer componentRelativePath="AdvancedToolbox/__stories__/AdvancedToolbox.stories.mdx" />
@@ -0,0 +1,2 @@
import AdvancedToolbox from './AdvancedToolbox';
export default AdvancedToolbox;
@@ -7,4 +7,9 @@ enum size {
small = 'small',
}
export { type, size };
enum orientation {
horizontal = 'horizontal',
vertical = 'vertical',
}
export { type, size, orientation };
@@ -66,7 +66,7 @@ There are different sizes for the button: `small`, and `medium`. The size refers
height.
<Canvas>
<Story name="Colors">
<Story name="Sizes">
<div className="flex items-center space-x-2">
<Button size={ButtonEnums.size.small}>Small Button</Button>
<Button size={ButtonEnums.size.medium}>Medium Button</Button>
@@ -1,150 +1,63 @@
import React, { useRef } from 'react';
import React, { useState } from 'react';
import PropTypes from 'prop-types';
import classnames from 'classnames';
const baseButtonClass = 'border outline-none';
const roundedClasses = {
vertical: {
none: '',
small: 'first:rounded-t last:rounded-b',
medium: 'first:rounded-t-md last:rounded-b-md',
large: 'first:rounded-t-lg last:rounded-b-lg',
full: 'first:rounded-t-full last:rounded-b-full',
},
horizontal: {
none: '',
small: 'first:rounded-l last:rounded-r',
medium: 'first:rounded-l-md last:rounded-r-md',
large: 'first:rounded-l-lg last:rounded-r-lg',
full: 'first:rounded-l-full last:rounded-r-full',
},
};
const borderClasses = {
text: {
vertical: 'border-t-0 border-l-0 border-r-0 last:border-b-0',
horizontal: 'border-l-0 border-t-0 border-b-0 last:border-r-0',
},
outlined: {
vertical: 'border border-b-0 last:border-b',
horizontal: 'border border-r-0 last:border-r',
},
contained: {
vertical: 'border-t-0 border-l-0 border-r-0 last:border-b-0',
horizontal: 'border-l-0 border-t-0 border-b-0 last:border-r-0',
},
};
const variantClasses = {
text: {
default: 'border-primary-light',
primary: 'border-primary-main',
secondary: 'border-secondary-light',
white: 'border-white',
black: 'border-primary-main',
},
outlined: {
default: '',
primary: '',
secondary: 'border border-secondary-light',
white: '',
black: 'border-primary-main group-hover:bg-primary-main group-hover:border-black',
},
contained: {
default: 'border-white',
primary: 'border-white',
secondary: 'border-white',
white: 'border-black',
black: 'border-primary-main',
},
};
const orientationClasses = {
vertical: 'flex-col',
horizontal: 'flex-row',
};
const baseDisplayClass = 'inline-flex';
const fullWidthDisplayClass = 'flex';
// css class that are applied for buttons that not the first or last
const nonFirstLastClasses = {
vertical: 'border-t-0 border-b-0',
horizontal: 'border-l-0 last:border-r-0',
};
import { ButtonEnums } from '../../components';
const ButtonGroup = ({
children,
buttons,
onActiveIndexChange,
className,
disabled = false,
fullWidth = false,
color = 'default',
orientation = 'horizontal',
rounded = 'medium',
size = 'medium',
variant = 'outlined',
splitBorder = true,
...other
orientation = ButtonEnums.orientation.horizontal,
defaultActiveIndex = 0,
}) => {
const ref = useRef(null);
const [activeIndex, setActiveIndex] = useState(defaultActiveIndex);
const buttonClasses = classnames(
baseButtonClass,
borderClasses[variant] && borderClasses[variant][orientation],
variantClasses[variant] && variantClasses[variant][color],
roundedClasses[orientation] && roundedClasses[orientation][rounded]
);
const handleButtonClick = (e, index) => {
setActiveIndex(index);
onActiveIndexChange && onActiveIndexChange(index);
buttons[index].onClick && buttons[index].onClick(e);
};
const orientationClasses = {
horizontal: 'flex-row',
vertical: 'flex-col',
};
const wrapperClasses = classnames('inline-flex', orientationClasses[orientation], className);
return (
<div
role="group"
className={classnames(
'group',
className,
orientationClasses[orientation],
fullWidth ? fullWidthDisplayClass : baseDisplayClass
wrapperClasses,
'border-secondary-light rounded-[5px] border bg-black text-[13px] '
)}
ref={ref}
{...other}
>
{React.Children.map(children, (child, index) => {
const isFirst = index === 0;
const isLast = index === React.Children.count(children) - 1;
const isNotFirstOrLast = !isFirst && !isLast;
if (!React.isValidElement(child)) {
return null;
}
return React.cloneElement(child, {
className: classnames(
buttonClasses,
child.props.className,
!splitBorder && isNotFirstOrLast && nonFirstLastClasses[orientation],
!splitBorder && isLast && 'last:border-l-0'
),
disabled: child.props.disabled || disabled,
color: child.props.color || color,
fullWidth,
rounded: 'none',
size: child.props.size || size,
variant: child.props.variant || variant,
});
{buttons.map((buttonProps, index) => {
const isActive = index === activeIndex;
return (
<button
{...buttonProps}
key={index}
className={classnames(
'rounded-[4px] px-2 py-1',
isActive ? 'bg-customblue-40 text-white' : 'text-primary-active bg-black'
)}
onClick={e => handleButtonClick(e, index)}
/>
);
})}
</div>
);
};
ButtonGroup.propTypes = {
children: PropTypes.node.isRequired,
buttons: PropTypes.arrayOf(PropTypes.object).isRequired,
orientation: PropTypes.oneOf(Object.values(ButtonEnums.orientation)),
type: PropTypes.oneOf(Object.values(ButtonEnums.type)),
size: PropTypes.oneOf(Object.values(ButtonEnums.size)),
defaultActiveIndex: PropTypes.number,
onActiveIndexChange: PropTypes.func,
className: PropTypes.string,
color: PropTypes.oneOf(['default', 'inherit', 'primary', 'secondary', 'white', 'black']),
disabled: PropTypes.bool,
fullWidth: PropTypes.bool,
orientation: PropTypes.oneOf(['vertical', 'horizontal']),
rounded: PropTypes.oneOf(['none', 'small', 'medium', 'large', 'full']),
size: PropTypes.oneOf(['small', 'medium', 'large', 'inherit']),
variant: PropTypes.oneOf(['text', 'outlined', 'contained']),
};
export default ButtonGroup;
@@ -1,5 +1,5 @@
import ButtonGroup from '../ButtonGroup';
import Button from '../../Button';
import { Button, ButtonGroup, ButtonEnums } from '../../../components';
import { ArgsTable, Story, Canvas, Meta } from '@storybook/addon-docs';
import {
createComponentTemplate,
@@ -16,13 +16,9 @@ export const argTypes = {
component={ButtonGroup}
/>
export const buttonGroupTemplate = args => (
<ButtonGroup {...args}>
<Button>One</Button>
<Button>Two</Button>
<Button>Three</Button>
</ButtonGroup>
);
export const buttonGroupTemplate = args => {
return <ButtonGroup {...args} />;
};
<Heading
title="ButtonGroup"
@@ -39,7 +35,32 @@ export const buttonGroupTemplate = args => (
ButtonGroup is a container for a group of buttons.
<Canvas>
<Story name="Overview">{buttonGroupTemplate.bind({})}</Story>
<Story
name="Overview"
args={{
type: ButtonEnums.type.primary,
size: ButtonEnums.size.medium,
orientation: ButtonEnums.orientation.horizontal,
buttons: [
{
children: 'Button 1',
onClick: () => console.log('Button 1 clicked'),
},
{
children: 'Button 2',
onClick: () => console.log('Button 2 clicked'),
},
{
children: 'Button 3',
onClick: () => console.log('Button 3 clicked'),
},
],
activeIndex: 0,
onActiveIndexChange: index => console.log(index),
}}
>
{buttonGroupTemplate.bind({})}
</Story>
</Canvas>
## Props
@@ -48,56 +69,6 @@ ButtonGroup is a container for a group of buttons.
## Usage
### Variants
There are three different variants of ButtonGroup:
<Canvas>
<Story name="Variants">
<div className="flex flex-col space-y-2">
<ButtonGroup variant="text">
<Button>Text</Button>
<Button>Text</Button>
<Button>Text</Button>
</ButtonGroup>
<ButtonGroup variant="outlined">
<Button>Outlined</Button>
<Button>Outlined</Button>
<Button>Outlined</Button>
</ButtonGroup>
<ButtonGroup variant="contained">
<Button>Contained</Button>
<Button>Contained</Button>
<Button>Contained</Button>
</ButtonGroup>
</div>
</Story>
</Canvas>
### Orientation
You can change the orientation of the buttons in the group by setting the `orientation` prop.
<Canvas>
<Story name="Orientation Horizontal">
<ButtonGroup orientation="horizontal">
<Button>Horizontal-1</Button>
<Button>Horizontal-2</Button>
</ButtonGroup>
</Story>
</Canvas>
Or vertical
<Canvas>
<Story name="Orientation Vertical">
<ButtonGroup orientation="vertical">
<Button>Vertical-1</Button>
<Button>Vertical-2</Button>
</ButtonGroup>
</Story>
</Canvas>
## Contribute
<Footer componentRelativePath="ButtonGroup/__stories__/buttonGroup.stories.mdx" />
@@ -5,44 +5,88 @@ import classnames from 'classnames';
import Icon from '../Icon';
import Typography from '../Typography';
const Dropdown = ({ id, children, showDropdownIcon, list, titleClassName }) => {
const borderStyle = 'border-b last:border-b-0 border-secondary-main';
const Dropdown = ({
id,
children,
showDropdownIcon,
list,
itemsClassName,
titleClassName,
showBorders,
alignment,
// By default the max characters per line is the longest title
// if you wish to override this, you can pass in a number
maxCharactersPerLine,
}) => {
const [open, setOpen] = useState(false);
const element = useRef(null);
const DropdownItem = useCallback(({ id, title, icon, onClick }) => {
return (
<div
key={title}
className={classnames(
'hover:bg-secondary-main border-secondary-main flex cursor-pointer items-center border-b px-4 py-2 transition duration-300 last:border-b-0'
)}
onClick={() => {
setOpen(false);
onClick();
}}
data-cy={id}
>
{!!icon && (
<Icon
name={icon}
className="mr-2 w-4 text-white"
/>
)}
<Typography className={titleClassName}>{title}</Typography>
</div>
);
}, []);
// choose the max characters per line based on the longest title
const longestTitle = list.reduce((acc, item) => {
if (item.title.length > acc) {
return item.title.length;
}
return acc;
}, 0);
DropdownItem.defaultProps = {
icon: '',
};
maxCharactersPerLine = maxCharactersPerLine ?? longestTitle;
DropdownItem.propTypes = {
id: PropTypes.string,
title: PropTypes.string.isRequired,
icon: PropTypes.string,
onClick: PropTypes.func.isRequired,
};
const DropdownItem = useCallback(
({ id, title, icon, onClick }) => {
// Split the title into lines of length maxCharactersPerLine
const lines = [];
for (let i = 0; i < title.length; i += maxCharactersPerLine) {
lines.push(title.substring(i, i + maxCharactersPerLine));
}
return (
<div
key={title}
className={classnames(
'hover:bg-secondary-main flex cursor-pointer items-center px-4 py-2 transition duration-300 ',
titleClassName,
showBorders && borderStyle
)}
onClick={() => {
setOpen(false);
onClick();
}}
data-cy={id}
>
{!!icon && (
<Icon
name={icon}
className="mr-2 w-4 text-white"
/>
)}
<div
style={{
whiteSpace: 'nowrap',
}}
>
{title.length > maxCharactersPerLine && (
<div>
{lines.map((line, index) => (
<Typography
key={index}
className={itemsClassName}
>
{line}
</Typography>
))}
</div>
)}
{title.length <= maxCharactersPerLine && (
<Typography className={itemsClassName}>{title}</Typography>
)}
</div>
</div>
);
},
[maxCharactersPerLine, itemsClassName, titleClassName, showBorders]
);
const renderTitleElement = () => {
return (
@@ -72,8 +116,10 @@ const Dropdown = ({ id, children, showDropdownIcon, list, titleClassName }) => {
return (
<div
className={classnames(
'top-100 bg-primary-dark border-secondary-main absolute right-0 z-10 mt-2 origin-top-right transform rounded border shadow transition duration-300',
'top-100 border-secondary-main absolute z-10 mt-2 transform rounded border bg-black shadow transition duration-300',
{
'right-0 origin-top-right': alignment === 'right',
'left-0 origin-top-left': alignment === 'left',
'scale-0': !open,
'scale-100': open,
}
@@ -118,9 +164,10 @@ const Dropdown = ({ id, children, showDropdownIcon, list, titleClassName }) => {
</div>
);
};
Dropdown.defaultProps = {
showDropdownIcon: true,
maxCharactersPerLine: 20,
showBorders: true,
};
Dropdown.propTypes = {
@@ -136,6 +183,9 @@ Dropdown.propTypes = {
onClick: PropTypes.func.isRequired,
})
).isRequired,
alignment: PropTypes.oneOf(['left', 'right']),
maxCharactersPerLine: PropTypes.number,
showBorders: PropTypes.bool,
};
export default Dropdown;
+32 -14
View File
@@ -73,6 +73,8 @@ import tabROIThreshold from './../../assets/icons/tab-roi-threshold.svg';
import tabSegmentation from './../../assets/icons/tab-segmentation.svg';
import uiArrowDown from './../../assets/icons/ui-arrow-down.svg';
import uiArrowUp from './../../assets/icons/ui-arrow-up.svg';
import uiArrowLeft from './../../assets/icons/ui-arrow-left.svg';
import uiArrowRight from './../../assets/icons/ui-arrow-right.svg';
import loadingOHIFMark from './../../assets/icons/loading-ohif-mark.svg';
import notificationsInfo from './../../assets/icons/notifications-info.svg';
import notificationsWarning from './../../assets/icons/notifications-warning.svg';
@@ -115,16 +117,24 @@ import panelGroupOpenClose from './../../assets/icons/panel-group-open-close.svg
import rowAdd from './../../assets/icons/row-add.svg';
import rowEdit from './../../assets/icons/row-edit.svg';
import rowHidden from './../../assets/icons/row-hidden.svg';
import rowHideAll from './../../assets/icons/row-hide-all.svg';
import rowHide from './../../assets/icons/row-hide.svg';
import rowLocked from './../../assets/icons/row-locked.svg';
import rowShowAll from './../../assets/icons/row-show-all.svg';
import rowUnhide from './../../assets/icons/row-unhide.svg';
import rowUnlocked from './../../assets/icons/row-unlocked.svg';
import rowShown from './../../assets/icons/row-shown.svg';
import rowLock from './../../assets/icons/row-lock.svg';
import rowUnlock from './../../assets/icons/row-unlock.svg';
import iconMPR from './../../assets/icons/icon-mpr-alt.svg';
import checkboxDefault from './../../assets/icons/checkbox-default.svg';
import checkboxActive from './../../assets/icons/checkbox-active.svg';
import referenceLines from './../../assets/icons/reference-lines.svg';
import chevronDownNew from './../../assets/icons/icon-disclosure-close.svg';
import chevronLeftNew from './../../assets/icons/icon-disclosure-open.svg';
import settingsBars from './../../assets/icons/icon-display-settings.svg';
import iconAdd from './../../assets/icons/icon-add.svg';
import iconRename from './../../assets/icons/icon-rename.svg';
import iconDelete from './../../assets/icons/icon-delete.svg';
import iconMoreMenu from './../../assets/icons/icon-more-menu.svg';
import iconToolBrush from './../../assets/icons/icon-tool-brush.svg';
import iconToolEraser from './../../assets/icons/icon-tool-eraser.svg';
import iconToolScissor from './../../assets/icons/icon-tool-scissor.svg';
import iconToolThreshold from './../../assets/icons/icon-tool-threshold.svg';
/** Old OHIF */
import oldTrash from './../../assets/icons/old-trash.svg';
@@ -164,7 +174,6 @@ const ICONS = {
'icon-transferring': iconTransferring,
'info-action': infoAction,
'info-link': infoLink,
'arrow-left': arrowLeft,
'launch-arrow': launchArrow,
'launch-info': launchInfo,
link: link,
@@ -195,12 +204,9 @@ const ICONS = {
'row-add': rowAdd,
'row-edit': rowEdit,
'row-hidden': rowHidden,
'row-hide-all': rowHideAll,
'row-hide': rowHide,
'row-locked': rowLocked,
'row-show-all': rowShowAll,
'row-unhide': rowUnhide,
'row-unlocked': rowUnlocked,
'row-shown': rowShown,
'row-lock': rowLock,
'row-unlock': rowUnlock,
'checkbox-checked': checkboxChecked,
'checkbox-unchecked': checkboxUnchecked,
'loading-ohif-mark': loadingOHIFMark,
@@ -237,7 +243,6 @@ const ICONS = {
'tool-create-threshold': toolCreateThreshold,
'tool-calibration': toolCalibration,
'tool-point': toolCircle,
'tool-circle': toolCircle,
'tool-freehand-line': toolFreehand,
'tool-freehand-polygon': toolFreehandPolygon,
'tool-polygon': toolPolygon,
@@ -256,9 +261,22 @@ const ICONS = {
'tab-segmentation': tabSegmentation,
'ui-arrow-down': uiArrowDown,
'ui-arrow-up': uiArrowUp,
'ui-arrow-left': uiArrowLeft,
'ui-arrow-right': uiArrowRight,
'checkbox-default': checkboxDefault,
'checkbox-active': checkboxActive,
'tool-referenceLines': referenceLines,
'chevron-left-new': chevronLeftNew,
'chevron-down-new': chevronDownNew,
'settings-bars': settingsBars,
'icon-rename': iconRename,
'icon-add': iconAdd,
'icon-delete': iconDelete,
'icon-more-menu': iconMoreMenu,
'icon-tool-brush': iconToolBrush,
'icon-tool-eraser': iconToolEraser,
'icon-tool-scissor': iconToolScissor,
'icon-tool-threshold': iconToolThreshold,
/** Old OHIF */
'old-trash': oldTrash,
@@ -0,0 +1,4 @@
.input-range-thumb-design {
@apply bg-primary-light border-primary-dark border-[2px] border-solid;
border-radius: 50%;
}
@@ -0,0 +1,218 @@
import React, { useState, useCallback, useEffect, useRef } from 'react';
import classNames from 'classnames';
import { InputNumber } from '../../components'; // Import InputNumber component
import './InputDoubleRange.css';
type InputDoubleRangeProps = {
values: [number, number];
onChange: (values: [number, number]) => void;
minValue?: number;
maxValue?: number;
step?: number;
unit?: string;
containerClassName?: string;
inputClassName?: string;
labelClassName?: string;
labelVariant?: string;
showLabel?: boolean;
labelPosition?: 'left' | 'right';
trackColor?: string;
allowNumberEdit?: boolean;
showAdjustmentArrows?: boolean;
};
const InputDoubleRange: React.FC<InputDoubleRangeProps> = ({
values,
onChange,
minValue,
maxValue,
step,
unit,
containerClassName,
inputClassName,
labelClassName,
labelVariant,
showLabel,
labelPosition,
trackColor,
allowNumberEdit,
showAdjustmentArrows,
}) => {
// Set initial thumb positions as percentages
const initialPercentageStart = Math.round(((values[0] - minValue) / (maxValue - minValue)) * 100);
const initialPercentageEnd = Math.round(((values[1] - minValue) / (maxValue - minValue)) * 100);
const [percentageStart, setPercentageStart] = useState(initialPercentageStart);
const [percentageEnd, setPercentageEnd] = useState(initialPercentageEnd);
const [rangeValue, setRangeValue] = useState(values);
const selectedThumbRef = useRef(null);
const sliderRef = useRef(null);
const updateRangeValues = (newValues, index = null) => {
const updatedRangeValue = Array.isArray(newValues) ? [...newValues] : [...rangeValue];
if (index !== null) {
updatedRangeValue[index] = newValues;
}
const calculatePercentage = value => ((value - minValue) / (maxValue - minValue)) * 100;
const newPercentageStart = calculatePercentage(updatedRangeValue[0]);
const newPercentageEnd = calculatePercentage(updatedRangeValue[1]);
setRangeValue(updatedRangeValue);
onChange(updatedRangeValue);
setPercentageStart(newPercentageStart);
setPercentageEnd(newPercentageEnd);
};
useEffect(() => {
updateRangeValues(values);
}, [values, minValue, maxValue]);
const LabelOrEditableNumber = (val, index) => {
return allowNumberEdit ? (
<InputNumber
minValue={minValue}
maxValue={maxValue}
value={val}
onChange={newValue => {
updateRangeValues(newValue, index);
}}
step={step}
labelClassName="text-white"
showAdjustmentArrows={showAdjustmentArrows}
/>
) : (
<span className={classNames(labelClassName ?? 'text-white')}>
{val}
{unit}
</span>
);
};
useEffect(() => {
return () => {
window.removeEventListener('mousemove', handleMouseMove);
window.removeEventListener('mouseup', handleGlobalMouseUp);
};
}, []);
const handleGlobalMouseUp = () => {
// Remove global mouse event listeners
window.removeEventListener('mousemove', handleMouseMove);
window.removeEventListener('mouseup', handleGlobalMouseUp);
selectedThumbRef.current = null;
};
const handleMouseDown = e => {
const rect = sliderRef.current.getBoundingClientRect();
const x = e.clientX - rect.left;
const percentageClicked = (x / rect.width) * 100;
// Calculate the distances from the clicked point to both thumbs' positions
const distanceToStartThumb = Math.abs(percentageClicked - percentageStart);
const distanceToEndThumb = Math.abs(percentageClicked - percentageEnd);
// Check if the clicked point is within a threshold distance to either thumb
if (distanceToStartThumb < 10) {
selectedThumbRef.current = 0;
} else if (distanceToEndThumb < 10) {
selectedThumbRef.current = 1;
}
window.addEventListener('mousemove', handleMouseMove);
window.addEventListener('mouseup', handleGlobalMouseUp);
};
const handleMouseMove = e => {
const selectedThumbValue = selectedThumbRef.current;
if (selectedThumbValue === null) {
return;
}
const rect = sliderRef.current.getBoundingClientRect();
const x = e.clientX - rect.left;
const newValue =
Math.round(((x / rect.width) * (maxValue - minValue) + minValue) / step) * step;
// Make sure newValue is within [minValue, maxValue]
const clampedValue = Math.min(Math.max(newValue, minValue), maxValue);
// Ensure that left and right thumbs don't switch positions
if (selectedThumbValue === 0 && clampedValue >= rangeValue[1]) {
return;
}
if (selectedThumbValue === 1 && clampedValue <= rangeValue[0]) {
return;
}
// Update the correct values in the rangeValue array
const updatedRangeValue = [...rangeValue];
updatedRangeValue[selectedThumbValue] = clampedValue;
setRangeValue(updatedRangeValue);
onChange(updatedRangeValue);
// Update the thumb position
const percentage = Math.round(((clampedValue - minValue) / (maxValue - minValue)) * 100);
if (selectedThumbValue === 0) {
setPercentageStart(percentage);
} else {
setPercentageEnd(percentage);
}
};
// Calculate the range values percentages for gradient background
const rangeValuePercentageStart = ((rangeValue[0] - minValue) / (maxValue - minValue)) * 100;
const rangeValuePercentageEnd = ((rangeValue[1] - minValue) / (maxValue - minValue)) * 100;
return (
<div className={`flex select-none items-center space-x-2 ${containerClassName ?? ''}`}>
{showLabel && LabelOrEditableNumber(rangeValue[0], 0)}
<div
className="relative flex h-10 w-full items-center"
onMouseDown={handleMouseDown}
ref={sliderRef}
>
<div
className="h-[3px] w-full rounded-lg"
style={{
background: `linear-gradient(to right, #3a3f99 0%, #3a3f99 ${rangeValuePercentageStart}%, #5acce6 ${rangeValuePercentageStart}%, #5acce6 ${rangeValuePercentageEnd}%, #3a3f99 ${rangeValuePercentageEnd}%, #3a3f99 100%)`,
}}
></div>
<div
className="input-range-thumb-design absolute h-3 w-3 cursor-pointer"
style={{
left: `calc(${percentageStart}% - 3px)`,
}}
></div>
<div
className="input-range-thumb-design absolute h-3 w-3 cursor-pointer rounded-full"
style={{ left: `calc(${percentageEnd}% - 3px)` }}
></div>
</div>
{showLabel && LabelOrEditableNumber(rangeValue[1], 1)}
</div>
);
};
InputDoubleRange.defaultProps = {
minValue: 0,
maxValue: 100,
step: 1,
unit: '',
containerClassName: '',
inputClassName: '',
labelClassName: '',
labelVariant: 'body1',
showLabel: false,
labelPosition: 'left',
trackColor: 'primary',
allowNumberEdit: false,
showAdjustmentArrows: false,
};
export default InputDoubleRange;
@@ -0,0 +1,56 @@
import { InputDoubleRange } from '../../../components';
import { ArgsTable, Story, Canvas, Meta } from '@storybook/addon-docs';
export const argTypes = {
component: InputDoubleRange,
title: 'Components/InputDoubleRange',
};
<Meta
title="Components/InputDoubleRange"
component={InputDoubleRange}
/>
export const RangeInputTemplate = args => (
<div className="w-12">
<InputDoubleRange {...args} />
</div>
);
<Heading
title="InputDoubleRange"
componentRelativePath="InputDoubleRange/InputDoubleRange.tsx"
/>
- [Overview](#overview)
- [Props](#props)
- [Contribute](#contribute)
## Overview
InputDoubleRange is a component that allows you to use as a boolean value
<Canvas>
<Story
name="Overview"
args={{
values: [100, 200],
minValue: 0,
maxValue: 300,
step: 10,
unit: '%',
onChange: () => console.log('input range change'),
labelClassName: 'text-black',
}}
>
{RangeInputTemplate.bind({})}
</Story>
</Canvas>
## Props
<ArgsTable of={InputDoubleRange} />
## Contribute
<Footer componentRelativePath="InputDoubleRange/__stories__/InputDoubleRange.stories.mdx" />
@@ -0,0 +1,3 @@
import InputDoubleRange from './InputDoubleRange';
export default InputDoubleRange;
@@ -2,6 +2,18 @@ input[type='text'] {
-webkit-appearance: none;
}
/* For most modern browsers */
input[type='number']::-webkit-inner-spin-button,
input[type='number']::-webkit-outer-spin-button {
-webkit-appearance: none;
margin: 0;
}
/* For Firefox */
input[type='number'] {
-moz-appearance: textfield;
}
.input-number:focus {
border: none;
outline: none;
@@ -1,8 +1,9 @@
import React, { useState, useCallback } from 'react';
import React, { useState, useCallback, useEffect } from 'react';
import IconButton from '../IconButton';
import Icon from '../Icon';
import './InputNumber.css';
import Label from '../Label';
import getMaxDigits from '../../utils/getMaxDigits';
/**
* React Number Input component'
@@ -20,14 +21,15 @@ const sizesClasses = {
const InputNumber: React.FC<{
value: number;
onChange: (value) => void;
onChange: (value: number) => void;
minValue?: number;
maxValue?: number;
step: number;
size?: string;
step?: number;
size?: 'sm' | 'lg';
className?: string;
labelClassName?: string;
label?: string;
showAdjustmentArrows?: boolean;
}> = ({
value,
onChange,
@@ -38,96 +40,99 @@ const InputNumber: React.FC<{
maxValue = 100,
labelClassName,
label,
showAdjustmentArrows = true,
}) => {
const [numberValue, setNumberValue] = useState(value);
const maxDigits = getMaxDigits(maxValue, step);
const inputWidth = Math.max(maxDigits * 10, showAdjustmentArrows ? 20 : 28);
const arrowWidth = showAdjustmentArrows ? 20 : 0;
const containerWidth = `${inputWidth + arrowWidth}px`;
useEffect(() => {
setNumberValue(value);
}, [value]);
const handleMinMax = useCallback(
(value: number) => {
if (value > maxValue) {
return maxValue;
} else if (value < minValue) {
return minValue;
} else {
return value;
}
},
(val: number) => Math.min(Math.max(val, minValue), maxValue),
[maxValue, minValue]
);
const handleChange = useCallback(
e => {
const numberValue = handleMinMax(Number(e.target.value));
setNumberValue(numberValue);
onChange(numberValue);
},
[onChange, setNumberValue, handleMinMax]
);
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const inputValue = e.target.value;
const handleIncrement = useCallback(
e => {
const newNum = handleMinMax(Number(numberValue) + step);
setNumberValue(newNum);
onChange(newNum);
},
[onChange, setNumberValue, step, numberValue, handleMinMax]
);
// Allow negative sign, empty string, or single decimal point for user flexibility
if (inputValue === '-' || inputValue === '' || inputValue === '.') {
setNumberValue(inputValue);
return;
}
const handleDecrement = useCallback(
e => {
const newNum = handleMinMax(Number(numberValue) - step);
setNumberValue(newNum);
onChange(newNum);
},
[onChange, setNumberValue, step, numberValue, handleMinMax]
);
const number = Number(inputValue);
// Filter out invalid inputs like 'NaN'
if (!isNaN(number)) {
updateValue(number);
}
};
const updateValue = (val: number) => {
const newValue = handleMinMax(val);
setNumberValue(newValue);
onChange(newValue);
};
const increment = () => updateValue(numberValue + step);
const decrement = () => updateValue(numberValue - step);
return (
<div className={'flex flex-1 flex-col'}>
<div className="flex flex-1 flex-col">
{label && (
<Label
className={labelClassName}
text={label}
></Label>
/>
)}
<div
className={`border-secondary-light flex items-center justify-center overflow-hidden rounded-md border-2 bg-black px-1 ${
sizesClasses[size]
} ${className ? className : ''}`}
} ${className || ''}`}
style={{ width: containerWidth }}
>
<div className="flex">
<input
type="text"
type="number"
value={numberValue}
step={step}
onChange={handleChange}
className={`input-number w-full bg-black text-center text-[12px] text-white`}
className={'input-number w-full bg-black text-center text-[12px] text-white'}
style={{ width: inputWidth }}
/>
<div className="up-arrowsize flex flex-col items-center justify-around">
<IconButton
id={'down-arrow-icon'}
variant="text"
color="inherit"
size="initial"
className="rotate-180 transform text-[#726f7e]"
onClick={handleIncrement}
>
<Icon name="ui-arrow-down" />
</IconButton>
<IconButton
id={'down-arrow-icon'}
variant="text"
color="inherit"
size="initial"
className="text-[#726f7e]"
onClick={handleDecrement}
>
<Icon name="ui-arrow-down" />
</IconButton>
</div>
{showAdjustmentArrows && (
<div className="up-arrowsize flex flex-col items-center justify-around">
<ArrowButton
onClick={increment}
rotate
/>
<ArrowButton onClick={decrement} />
</div>
)}
</div>
</div>
</div>
);
};
const ArrowButton = ({ onClick, rotate = false }: { onClick: () => void; rotate?: boolean }) => (
<IconButton
id="arrow-icon"
variant="text"
color="inherit"
size="initial"
className={`text-[#726f7e] ${rotate ? 'rotate-180 transform' : ''}`}
onClick={onClick}
>
<Icon name="ui-arrow-down" />
</IconButton>
);
export default InputNumber;
@@ -35,8 +35,11 @@ InputNumber is a component that allows you to use as a number value
name="Overview"
args={{
step: 1,
value: 3,
value: 1000,
minValue: 0,
maxValue: 1000,
size: 'sm',
showAdjustmentArrows: true,
onChange: () => console.log('input range change'),
}}
>
@@ -4,23 +4,30 @@ input[type='range'] {
input[type='range']::-webkit-slider-thumb {
-webkit-appearance: none;
border: none;
@apply border-primary-dark bg-primary-light border-[2px] border-solid;
height: 13px;
width: 13px;
border-radius: 50%;
background: #5acce6;
}
input[type='range']::-moz-range-thumb {
-webkit-appearance: none;
border: none;
@apply border-primary-dark bg-primary-light border-[2px] border-solid;
height: 13px;
width: 13px;
border-radius: 50%;
background: #5acce6;
}
.input-range-thumb-design[type='range']::-webkit-slider-thumb {
box-shadow: 1px 1px 1px rgba(0, 0, 0, 0.521);
border: 1.5px solid #090c29;
input[type='range']::-webkit-slider-runnable-track {
border: none !important;
}
input[type='range']::-moz-range-track {
border: none !important;
}
.range-track {
position: absolute;
top: 0;
left: 0;
right: 0;
}
@@ -1,7 +1,8 @@
import React, { useState, useCallback, useEffect } from 'react';
import classNames from 'classnames';
import Typography from '../Typography';
import { InputNumber } from '../../components';
import './InputRange.css';
import getMaxDigits from '../../utils/getMaxDigits';
/**
* React Range Input component
@@ -11,10 +12,9 @@ import './InputRange.css';
*
*
*/
const InputRange: React.FC<{
type InputRangeProps = {
value: number;
onChange: (value) => void;
onChange: (value: number) => void;
minValue: number;
maxValue: number;
step: number;
@@ -26,7 +26,11 @@ const InputRange: React.FC<{
showLabel?: boolean;
labelPosition?: string;
trackColor?: string;
}> = ({
allowNumberEdit?: boolean;
showAdjustmentArrows?: boolean;
};
const InputRange: React.FC<InputRangeProps> = ({
value,
onChange,
minValue,
@@ -38,72 +42,76 @@ const InputRange: React.FC<{
labelClassName,
labelVariant,
showLabel = true,
labelPosition = '',
labelPosition = 'right',
trackColor,
allowNumberEdit = false,
showAdjustmentArrows = true,
}) => {
const [rangeValue, setRangeValue] = useState(value);
// Allow for the value property to update the range value.
const maxDigits = getMaxDigits(maxValue, step);
const labelWidth = `${maxDigits * 10}px`;
useEffect(() => setRangeValue(value), [value]);
const handleChange = useCallback(
e => {
const rangeValue = Number(e.target.value);
setRangeValue(rangeValue);
onChange(rangeValue);
const val = Number(e.target.value);
setRangeValue(val);
onChange(val);
},
[onChange, setRangeValue]
[onChange]
);
const rangeValuePercentage = ((rangeValue - minValue) / (maxValue - minValue)) * 100;
const rangeValueForStr = step >= 1 ? rangeValue.toFixed(0) : rangeValue.toFixed(1);
const LabelOrEditableNumber = allowNumberEdit ? (
<InputNumber
minValue={minValue}
maxValue={maxValue}
value={rangeValue}
onChange={val => {
setRangeValue(val);
onChange(val);
}}
step={step}
showAdjustmentArrows={showAdjustmentArrows}
/>
) : (
<span className={classNames(labelClassName ?? 'text-white')}>
{rangeValue}
{unit}
</span>
);
return (
<div
className={`flex cursor-pointer items-center space-x-1 ${
containerClassName ? containerClassName : ''
}`}
className={`flex cursor-pointer items-center ${containerClassName ?? ''}`}
onClick={e => {
e.stopPropagation();
e.preventDefault();
}}
>
{showLabel && labelPosition === 'left' && (
<Typography
variant={labelVariant ?? 'subtitle'}
component="p"
className={classNames('w-8', labelClassName ?? 'text-white')}
>
{rangeValueForStr}
{unit}
</Typography>
)}
<input
type="range"
min={minValue}
max={maxValue}
value={rangeValue}
className={`input-range-thumb-design h-[3px] appearance-none rounded-lg ${
inputClassName ? inputClassName : ''
}`}
style={{
background:
trackColor ||
`linear-gradient(to right, #5acce6 0%, #5acce6 ${rangeValuePercentage - 10}%, #3a3f99 ${
rangeValuePercentage + 10
}%, #3a3f99 100%)`,
}}
onChange={handleChange}
id="myRange"
step={step}
/>
{showLabel && (!labelPosition || labelPosition === 'right') && (
<Typography
variant={labelVariant ?? 'subtitle'}
component="p"
className={classNames('w-8', labelClassName ?? 'text-white')}
>
{rangeValueForStr}
{unit}
</Typography>
)}
<div className="relative flex w-full items-center">
{showLabel && labelPosition === 'left' && LabelOrEditableNumber}
<div className="range-track"></div>
<input
type="range"
min={minValue}
max={maxValue}
value={rangeValue}
className={`h-[3px] appearance-none rounded-md ${inputClassName ?? ''}`}
style={{
background: `linear-gradient(to right, #5acce6 0%, #5acce6 ${rangeValuePercentage}%, #3a3f99 ${rangeValuePercentage}%, #3a3f99 100%)`,
}}
onChange={handleChange}
id="myRange"
step={step}
/>
{showLabel && labelPosition === 'right' && (
<div style={{ width: labelWidth }}>{LabelOrEditableNumber}</div>
)}
</div>
</div>
);
};
@@ -1,4 +1,4 @@
import InputRange from '../InputRange';
import { InputRange } from '../../../components';
import { ArgsTable, Story, Canvas, Meta } from '@storybook/addon-docs';
export const argTypes = {
@@ -36,7 +36,7 @@ InputRange is a component that allows you to use as a boolean value
args={{
value: 10,
minValue: 0,
maxValue: 300,
maxValue: 100,
step: 10,
unit: '%',
onChange: () => console.log('input range change'),
@@ -0,0 +1,150 @@
import React, { useRef } from 'react';
import PropTypes from 'prop-types';
import classnames from 'classnames';
const baseButtonClass = 'border outline-none';
const roundedClasses = {
vertical: {
none: '',
small: 'first:rounded-t last:rounded-b',
medium: 'first:rounded-t-md last:rounded-b-md',
large: 'first:rounded-t-lg last:rounded-b-lg',
full: 'first:rounded-t-full last:rounded-b-full',
},
horizontal: {
none: '',
small: 'first:rounded-l last:rounded-r',
medium: 'first:rounded-l-md last:rounded-r-md',
large: 'first:rounded-l-lg last:rounded-r-lg',
full: 'first:rounded-l-full last:rounded-r-full',
},
};
const borderClasses = {
text: {
vertical: 'border-t-0 border-l-0 border-r-0 last:border-b-0',
horizontal: 'border-l-0 border-t-0 border-b-0 last:border-r-0',
},
outlined: {
vertical: 'border border-b-0 last:border-b',
horizontal: 'border border-r-0 last:border-r',
},
contained: {
vertical: 'border-t-0 border-l-0 border-r-0 last:border-b-0',
horizontal: 'border-l-0 border-t-0 border-b-0 last:border-r-0',
},
};
const variantClasses = {
text: {
default: 'border-primary-light',
primary: 'border-primary-main',
secondary: 'border-secondary-light',
white: 'border-white',
black: 'border-primary-main',
},
outlined: {
default: '',
primary: '',
secondary: 'border border-secondary-light',
white: '',
black: 'border-primary-main group-hover:bg-primary-main group-hover:border-black',
},
contained: {
default: 'border-white ',
primary: 'border-white ',
secondary: 'border-white ',
white: 'border-black',
black: 'border-primary-main',
},
};
const orientationClasses = {
vertical: 'flex-col',
horizontal: 'flex-row',
};
const baseDisplayClass = 'inline-flex';
const fullWidthDisplayClass = 'flex';
// css class that are applied for buttons that not the first or last
const nonFirstLastClasses = {
vertical: 'border-t-0 border-b-0',
horizontal: 'border-l-0 last:border-r-0',
};
const LegacyButtonGroup = ({
children,
className,
disabled = false,
fullWidth = false,
color = 'default',
orientation = 'horizontal',
rounded = 'medium',
size = 'medium',
variant = 'outlined',
splitBorder = true,
...other
}) => {
const ref = useRef(null);
const buttonClasses = classnames(
baseButtonClass,
borderClasses[variant] && borderClasses[variant][orientation],
variantClasses[variant] && variantClasses[variant][color],
roundedClasses[orientation] && roundedClasses[orientation][rounded]
);
return (
<div
role="group"
className={classnames(
'group',
className,
orientationClasses[orientation],
fullWidth ? fullWidthDisplayClass : baseDisplayClass
)}
ref={ref}
{...other}
>
{React.Children.map(children, (child, index) => {
const isFirst = index === 0;
const isLast = index === React.Children.count(children) - 1;
const isNotFirstOrLast = !isFirst && !isLast;
if (!React.isValidElement(child)) {
return null;
}
return React.cloneElement(child, {
className: classnames(
buttonClasses,
child.props.className,
!splitBorder && isNotFirstOrLast && nonFirstLastClasses[orientation],
!splitBorder && isLast && 'last:border-l-0'
),
disabled: child.props.disabled || disabled,
color: child.props.color || color,
fullWidth,
rounded: 'none',
size: child.props.size || size,
variant: child.props.variant || variant,
});
})}
</div>
);
};
LegacyButtonGroup.propTypes = {
children: PropTypes.node.isRequired,
className: PropTypes.string,
color: PropTypes.oneOf(['default', 'inherit', 'primary', 'secondary', 'white', 'black']),
disabled: PropTypes.bool,
fullWidth: PropTypes.bool,
orientation: PropTypes.oneOf(['vertical', 'horizontal']),
rounded: PropTypes.oneOf(['none', 'small', 'medium', 'large', 'full']),
size: PropTypes.oneOf(['small', 'medium', 'large', 'inherit']),
variant: PropTypes.oneOf(['text', 'outlined', 'contained']),
};
export default LegacyButtonGroup;
@@ -0,0 +1,102 @@
import { LegacyButtonGroup, LegacyButton } from '../../../components';
import { ArgsTable, Story, Canvas, Meta } from '@storybook/addon-docs';
import {
createComponentTemplate,
createStoryMetaSettings,
} from '../../../storybook/functions/create-component-story';
export const argTypes = {
component: LegacyButtonGroup,
title: 'Components/LegacyButtonGroup',
};
<Meta
title="Components/LegacyButtonGroup"
component={LegacyButtonGroup}
/>
export const LegacybuttonGroupTemplate = args => (
<LegacyButtonGroup {...args}>
<LegacyButton>One</LegacyButton>
<LegacyButton>Two</LegacyButton>
<LegacyButton>Three</LegacyButton>
</LegacyButtonGroup>
);
<Heading
title="LegacyButtonGroup"
componentRelativePath="LegacyButtonGroup/LegacyButtonGroup.tsx"
/>
- [Overview](#overview)
- [Props](#props)
- [Usage](#usage)
- [Contribute](#contribute)
## Overview
LegacyButtonGroup is a container for a group of buttons.
<Canvas>
<Story name="Overview">{LegacybuttonGroupTemplate.bind({})}</Story>
</Canvas>
## Props
<ArgsTable of={LegacyButtonGroup} />
## Usage
### Variants
There are three different variants of LegacyButtonGroup:
<Canvas>
<Story name="Variants">
<div className="flex flex-col space-y-2">
<LegacyButtonGroup variant="text">
<LegacyButton>Text</LegacyButton>
<LegacyButton>Text</LegacyButton>
<LegacyButton>Text</LegacyButton>
</LegacyButtonGroup>
<LegacyButtonGroup variant="outlined">
<LegacyButton>Outlined</LegacyButton>
<LegacyButton>Outlined</LegacyButton>
<LegacyButton>Outlined</LegacyButton>
</LegacyButtonGroup>
<LegacyButtonGroup variant="contained">
<LegacyButton>Contained</LegacyButton>
<LegacyButton>Contained</LegacyButton>
<LegacyButton>Contained</LegacyButton>
</LegacyButtonGroup>
</div>
</Story>
</Canvas>
### Orientation
You can change the orientation of the buttons in the group by setting the `orientation` prop.
<Canvas>
<Story name="Orientation Horizontal">
<LegacyButtonGroup orientation="horizontal">
<LegacyButton>Horizontal-1</LegacyButton>
<LegacyButton>Horizontal-2</LegacyButton>
</LegacyButtonGroup>
</Story>
</Canvas>
Or vertical
<Canvas>
<Story name="Orientation Vertical">
<LegacyButtonGroup orientation="vertical">
<LegacyButton>Vertical-1</LegacyButton>
<LegacyButton>Vertical-2</LegacyButton>
</LegacyButtonGroup>
</Story>
</Canvas>
## Contribute
<Footer componentRelativePath="LEgacyButtonGroup/__stories__/legacyButtonGroup.stories.mdx" />
@@ -0,0 +1,2 @@
import LegacyButtonGroup from './LegacyButtonGroup';
export default LegacyButtonGroup;
@@ -5,7 +5,7 @@ import { StringNumber } from '../../types';
import LegacyPatientInfo from '../LegacyPatientInfo';
import Icon from '../Icon';
import ButtonGroup from '../ButtonGroup';
import LegacyButtonGroup from '../LegacyButtonGroup';
import LegacyButton from '../LegacyButton';
import LegacyCinePlayer from '../LegacyCinePlayer';
@@ -89,8 +89,8 @@ const LegacyViewportActionBar = ({
className="mt-2"
style={{ pointerEvents: 'all' }}
>
{/* TODO Revisit design of ButtonGroup later - for now use LegacyButton for its children.*/}
<ButtonGroup>
{/* TODO Revisit design of LegacyButtonGroup later - for now use LegacyButton for its children.*/}
<LegacyButtonGroup>
<LegacyButton
size="initial"
className="bg-black px-2 py-1"
@@ -113,7 +113,7 @@ const LegacyViewportActionBar = ({
className="w-4 text-white"
/>
</LegacyButton>
</ButtonGroup>
</LegacyButtonGroup>
</div>
)}
{showCine && !showNavArrows && (
@@ -0,0 +1,61 @@
import React, { useState } from 'react';
import { Icon } from '@ohif/ui';
import PropTypes from 'prop-types';
const PanelSection = ({ title, children, actionIcons = [] }) => {
const [areChildrenVisible, setChildrenVisible] = useState(true);
const handleHeaderClick = () => {
setChildrenVisible(!areChildrenVisible);
};
return (
<>
<div
className="bg-secondary-dark mt-[2px] flex h-7 cursor-pointer select-none items-center justify-between rounded-[4px] pl-2.5 text-[13px]"
onClick={handleHeaderClick}
>
<div className="text-aqua-pale">{title}</div>
<div className="flex items-center space-x-1">
{actionIcons.map((icon, index) => (
<Icon
key={index}
name={icon.name}
onClick={e => {
e.stopPropagation();
if (!areChildrenVisible) {
setChildrenVisible(true);
}
icon.onClick();
}}
/>
))}
<div className="grid h-[28px] w-[28px] place-items-center">
<Icon name={areChildrenVisible ? 'chevron-down-new' : 'chevron-left-new'} />
</div>
</div>
</div>
{areChildrenVisible && (
<>
<div className="h-[2px] bg-black"></div>
<div className="bg-primary-dark rounded-b-[4px]">{children}</div>
</>
)}
</>
);
};
PanelSection.defaultProps = {};
PanelSection.propTypes = {
title: PropTypes.string,
children: PropTypes.node,
actionIcons: PropTypes.arrayOf(
PropTypes.shape({
name: PropTypes.string,
onClick: PropTypes.func,
})
),
};
export default PanelSection;
@@ -0,0 +1,71 @@
import { PanelSection, SidePanel } from '../../../components';
import { ArgsTable, Story, Canvas, Meta } from '@storybook/addon-docs';
import { createComponentTemplate } from '../../../storybook/functions/create-component-story';
export const argTypes = {
component: PanelSection,
title: 'Components/PanelSection',
};
<Meta
title="Components/PanelSection"
component={PanelSection}
/>
export const PanelSectionTemplate = args => (
<div className="w-96">
<SidePanel
side="left"
defaultComponentOpen="Example"
tabs={[
{
id: 'tab1',
name: 'tab1',
label: 'Tab 1',
iconName: 'group-layers',
iconLabel: 'group-layers',
content: () => (
<PanelSection {...args}>
<div className="text-white">{'this is children'}</div>
</PanelSection>
),
},
]}
/>
</div>
);
<Heading
title="PanelSection"
componentRelativePath="PanelSection/PanelSection.tsx"
/>
- [Overview](#overview)
- [Props](#props)
- [Contribute](#contribute)
## Overview
PanelSection is a component that renders the PanelSections.
<Canvas>
<Story
name="Overview"
args={{
title: 'Segmentation Tools',
}}
>
{PanelSectionTemplate.bind({})}
</Story>
</Canvas>
## Props
<ArgsTable of={PanelSection} />
## Usage
## Contribute
<Footer componentRelativePath="PanelSection/__stories__/PanelSection.stories.mdx" />
@@ -0,0 +1,2 @@
import PanelSection from './PanelSection';
export default PanelSection;
@@ -0,0 +1,23 @@
import React from 'react';
import Icon from '../Icon';
function AddSegmentRow({ onClick }) {
return (
<div
className="flex hover:cursor-pointer"
onClick={onClick}
>
<div className="h-[28px] w-[28px]"></div>
<div className="group ml-2.5 mt-1">
<div className="text-primary-active group-hover:bg-secondary-dark flex items-center rounded-[4px] pr-2">
<div className="grid h-[28px] w-[28px] place-items-center">
<Icon name="icon-add" />
</div>
<span className="text-[13px]">Add Segment</span>
</div>
</div>
</div>
);
}
export default AddSegmentRow;
@@ -0,0 +1,20 @@
import React from 'react';
import Icon from '../Icon';
function NoSegmentationRow({ onSegmentationAdd }) {
return (
<div
className="group"
onClick={onSegmentationAdd}
>
<div className="text-primary-active group-hover:bg-secondary-dark flex items-center rounded-[4px] group-hover:cursor-pointer">
<div className="grid h-[28px] w-[28px] place-items-center">
<Icon name="icon-add" />
</div>
<span className="text-[13px]">Add Segmentation</span>
</div>
</div>
);
}
export default NoSegmentationRow;
@@ -1,10 +1,13 @@
import React, { useState } from 'react';
import classNames from 'classnames';
import Icon from '../Icon';
import InputRange from '../InputRange';
import CheckBox from '../CheckBox';
import InputNumber from '../InputNumber';
import classNames from 'classnames';
const getRoundedValue = value => {
return Math.round(value * 100) / 100;
};
const ActiveSegmentationConfig = ({
config,
@@ -15,7 +18,7 @@ const ActiveSegmentationConfig = ({
setFillAlpha,
}) => {
return (
<div className="flex justify-between px-2 pt-[13px] text-[12px]">
<div className="flex justify-between px-3 pt-[13px] text-[12px]">
<div className="flex flex-col items-start">
<div className="mb-[12px] text-white">Active</div>
<CheckBox
@@ -39,7 +42,7 @@ const ActiveSegmentationConfig = ({
<InputRange
minValue={0}
maxValue={100}
value={config.outlineOpacity * 100}
value={getRoundedValue(config.outlineOpacity * 100)}
onChange={setOutlineOpacityActive}
step={1}
containerClassName="mt-[4px] mb-[9px]"
@@ -48,9 +51,9 @@ const ActiveSegmentationConfig = ({
unit="%"
/>
<InputRange
minValue={0}
minValue={4}
maxValue={100}
value={config.fillAlpha * 100}
value={getRoundedValue(config.fillAlpha * 100)}
onChange={setFillAlpha}
step={1}
containerClassName="mt-[4px] mb-[9px]"
@@ -80,11 +83,11 @@ const InactiveSegmentationConfig = ({
setFillAlphaInactive,
}) => {
return (
<div className="px-2">
<div className="px-3">
<CheckBox
label="Display Inactive Segmentations"
checked={config.renderInactiveSegmentations}
labelClassName="text-[12px] pt-1"
labelClassName="text-[12px]"
className="mb-[9px]"
onChange={setRenderInactiveSegmentations}
/>
@@ -94,7 +97,7 @@ const InactiveSegmentationConfig = ({
<InputRange
minValue={0}
maxValue={100}
value={config.fillAlphaInactive * 100}
value={getRoundedValue(config.fillAlphaInactive * 100)}
onChange={setFillAlphaInactive}
step={1}
containerClassName="mt-[4px]"
@@ -120,51 +123,37 @@ const SegmentationConfig = ({
const { initialConfig } = segmentationConfig;
const [isMinimized, setIsMinimized] = useState(true);
return (
<div className="bg-primary-dark">
<div
className="flex cursor-pointer items-center"
onClick={evt => {
evt.stopPropagation();
setIsMinimized(!isMinimized);
}}
>
<Icon
name="panel-group-open-close"
className={classNames('h-5 w-5 cursor-pointer text-white transition duration-300', {
'rotate-90 transform': !isMinimized,
})}
<div className="bg-primary-dark select-none">
<div>
<ActiveSegmentationConfig
config={initialConfig}
setFillAlpha={setFillAlpha}
setOutlineWidthActive={setOutlineWidthActive}
setOutlineOpacityActive={setOutlineOpacityActive}
setRenderFill={setRenderFill}
setRenderOutline={setRenderOutline}
/>
<span className="text-[12px] font-[300] text-[#d8d8d8]">{'Segmentation Appearance'}</span>
</div>
{/* active segmentation */}
{!isMinimized && (
<div>
<ActiveSegmentationConfig
config={initialConfig}
setFillAlpha={setFillAlpha}
setOutlineWidthActive={setOutlineWidthActive}
setOutlineOpacityActive={setOutlineOpacityActive}
setRenderFill={setRenderFill}
setRenderOutline={setRenderOutline}
<div className="mx-1 mb-[8px] h-[1px] bg-[#212456]"></div>
<div
onClick={() => setIsMinimized(!isMinimized)}
className="flex cursor-pointer items-center pl-2 pb-[9px]"
>
<Icon
name="panel-group-open-close"
className={classNames('h-5 w-5 cursor-pointer text-white transition duration-300', {
'rotate-90 transform': !isMinimized,
})}
/>
{/* A small line */}
<div className="mx-1 mb-[8px] h-[1px] bg-[#212456]"></div>
{/* inactive segmentation */}
<div
onClick={() => setIsMinimized(!isMinimized)}
className="flex cursor-pointer items-center pl-2 pb-[9px]"
>
<span className="text-[12px] font-[300] text-[#d8d8d8]">
{'Inactive Segmentations'}
</span>
</div>
<span className="text-[12px] font-[300] text-[#d8d8d8]">{'Inactive Segmentations'}</span>
</div>
{!isMinimized && (
<InactiveSegmentationConfig
config={initialConfig}
setRenderInactiveSegmentations={setRenderInactiveSegmentations}
setFillAlphaInactive={setFillAlphaInactive}
/>
</div>
)}
)}
</div>
<div className="h-[6px] bg-black "></div>
</div>
);
@@ -0,0 +1,160 @@
import React from 'react';
import { Select, Icon, Dropdown } from '../../components';
import PropTypes from 'prop-types';
function SegmentationDropDownRow({
segmentations = [],
activeSegmentation,
onActiveSegmentationChange,
disableEditing,
onToggleSegmentationVisibility,
onSegmentationEdit,
onSegmentationDownload,
storeSegmentation,
onSegmentationDelete,
onSegmentationAdd,
}) {
const handleChange = option => {
onActiveSegmentationChange(option.value); // Notify the parent
};
const selectOptions = segmentations.map(s => ({
value: s.id,
label: s.label,
}));
if (!activeSegmentation) {
return null;
}
return (
<div className="group mx-0.5 mt-[8px] flex items-center">
<div
onClick={e => {
e.stopPropagation();
}}
>
<Dropdown
id="segmentation-dropdown"
showDropdownIcon={false}
alignment="left"
itemsClassName="text-primary-active"
showBorders={false}
list={[
...(!disableEditing
? [
{
title: 'Add New Segmentation',
onClick: () => {
onSegmentationAdd();
},
},
]
: []),
...(!disableEditing
? [
{
title: 'Rename',
onClick: () => {
onSegmentationEdit(activeSegmentation.id);
},
},
]
: []),
{
title: 'Delete',
onClick: () => {
onSegmentationDelete(activeSegmentation.id);
},
},
...(!disableEditing
? [
{
title: 'Export DICOM SEG',
onClick: () => {
storeSegmentation(activeSegmentation.id);
},
},
{
title: 'Download',
onClick: () => {
onSegmentationDownload(activeSegmentation.id);
},
},
]
: []),
]}
>
<div className="hover:bg-secondary-dark mx-1 grid h-[28px] w-[28px] cursor-pointer place-items-center rounded-[4px]">
<Icon name="icon-more-menu"></Icon>
</div>
</Dropdown>
</div>
{selectOptions?.length && (
<Select
id="segmentation-select"
isClearable={false}
onChange={handleChange}
components={{
DropdownIndicator: () => (
<Icon
name={'chevron-down-new'}
className="mr-2"
/>
),
}}
isSearchable={false}
options={selectOptions}
value={selectOptions?.find(o => o.value === activeSegmentation.id)}
className="text-aqua-pale h-[26px] w-1/2 text-[13px]"
/>
)}
<div className="flex items-center">
<div
className="hover:bg-secondary-dark ml-3 mr-1 grid h-[28px] w-[28px] cursor-pointer place-items-center rounded-[4px]"
onClick={() => onToggleSegmentationVisibility(activeSegmentation.id)}
>
{activeSegmentation.isVisible ? (
<Icon
name="row-shown"
className="text-primary-active"
/>
) : (
<Icon
name="row-hidden"
className="text-primary-active"
/>
)}
</div>
</div>
</div>
);
}
SegmentationDropDownRow.propTypes = {
segmentations: PropTypes.arrayOf(
PropTypes.shape({
id: PropTypes.string.isRequired,
label: PropTypes.string.isRequired,
})
).isRequired,
activeSegmentation: PropTypes.shape({
id: PropTypes.string.isRequired,
isVisible: PropTypes.bool.isRequired,
}),
onActiveSegmentationChange: PropTypes.func.isRequired,
disableEditing: PropTypes.bool,
onToggleSegmentationVisibility: PropTypes.func,
onSegmentationEdit: PropTypes.func,
onSegmentationDownload: PropTypes.func,
storeSegmentation: PropTypes.func,
onSegmentationDelete: PropTypes.func,
onSegmentationAdd: PropTypes.func,
};
SegmentationDropDownRow.defaultProps = {
segmentations: [],
disableEditing: false,
};
export default SegmentationDropDownRow;
@@ -1,287 +0,0 @@
import React from 'react';
import PropTypes from 'prop-types';
import SegmentationGroupSegment from './SegmentationGroupSegment';
import Dropdown from '../Dropdown';
import classnames from 'classnames';
import Icon from '../Icon';
import IconButton from '../IconButton';
const AddNewSegmentRow = ({
id,
onConfigChange,
onToggleSegmentationVisibility,
onSegmentAdd,
isVisible,
showAddSegment,
disableEditing,
}) => {
return (
<div>
<div className="text-primary-active flex cursor-pointer items-center bg-black py-1 pl-[29px] text-[12px] hover:opacity-80">
{showAddSegment && !disableEditing && (
<div
className="flex items-center"
onClick={() => onSegmentAdd()}
>
<Icon
name="row-add"
className="h-5 w-5"
/>
<div className="">Add Segment</div>
</div>
)}
<div className="flex-grow" />
<div
className="flex items-center pr-2"
onClick={() => {
onToggleSegmentationVisibility(id);
}}
>
{isVisible ? (
<Icon
name="row-show-all"
className="h-5 w-5"
/>
) : (
<Icon
name="row-hide-all"
className="h-5 w-5"
/>
)}
</div>
</div>
{/* {isSegmentationConfigOpen && (
<SegmentationConfig onConfigChange={onConfigChange} />
)} */}
</div>
);
};
const SegmentGroupHeader = ({
isMinimized,
onToggleMinimizeSegmentation,
onSegmentationClick,
id,
label,
isActive,
segmentCount,
onSegmentationEdit,
disableEditing,
onSegmentationDelete,
}) => {
return (
<div
className={classnames(
'flex-static border-secondary-light flex h-[27px] cursor-pointer items-center gap-2 rounded-t-md border-b pr-2 pl-[3px] text-[12px]',
{
'bg-secondary-main': isActive,
'bg-secondary-dark': !isActive,
}
)}
onClick={evt => {
evt.stopPropagation();
onSegmentationClick(id);
}}
>
<Icon
name="panel-group-open-close"
onClick={evt => {
evt.stopPropagation();
onToggleMinimizeSegmentation(id);
}}
className={classnames('h-5 w-5 cursor-pointer text-white transition duration-300', {
'rotate-90 transform': !isMinimized,
})}
/>
<span className="w-full overflow-hidden text-ellipsis whitespace-nowrap text-white">
{label.toUpperCase()}
</span>
<div className="flex-grow" />
<span className="text-white ">{segmentCount}</span>
<div
onClick={e => {
e.stopPropagation();
}}
>
{!disableEditing && (
<Dropdown
id="segmentation-dropdown"
showDropdownIcon={false}
list={[
{
title: 'Rename',
onClick: () => {
onSegmentationEdit(id);
},
},
{
title: 'Delete',
onClick: () => {
onSegmentationDelete(id);
},
},
]}
>
<IconButton
id={''}
variant="text"
color="inherit"
size="initial"
className="text-primary-active"
>
<Icon name="panel-group-more" />
</IconButton>
</Dropdown>
)}
</div>
</div>
);
};
const SegmentationGroup = ({
label,
id,
segmentCount,
isVisible,
isActive,
onSegmentClick,
isMinimized,
onSegmentColorClick,
disableEditing,
showAddSegment,
segments,
activeSegmentIndex,
onSegmentAdd,
onSegmentationClick,
onClickSegmentColor,
onSegmentationEdit,
onToggleSegmentVisibility,
onToggleSegmentationVisibility,
onSegmentDelete,
showSegmentDelete,
onToggleMinimizeSegmentation,
onSegmentationConfigChange,
onSegmentationDelete,
onSegmentEdit,
}) => {
return (
<div className="flex min-h-0 flex-auto flex-col">
<SegmentGroupHeader
id={id}
label={label}
isActive={isActive}
isMinimized={isMinimized}
onToggleMinimizeSegmentation={onToggleMinimizeSegmentation}
onSegmentationClick={onSegmentationClick}
segmentCount={segmentCount}
onSegmentationEdit={onSegmentationEdit}
onSegmentationDelete={onSegmentationDelete}
disableEditing={disableEditing}
/>
{!isMinimized && (
<div className="flex min-h-0 flex-auto flex-col">
<AddNewSegmentRow
onConfigChange={onSegmentationConfigChange}
onSegmentAdd={onSegmentAdd}
isVisible={isVisible}
onToggleSegmentationVisibility={onToggleSegmentationVisibility}
id={id}
disableEditing={disableEditing}
showAddSegment={showAddSegment}
/>
<div className="ohif-scrollbar flex min-h-0 flex-col overflow-y-hidden">
{!!segments.length &&
segments.map(segment => {
if (segment === undefined || segment === null) {
return null;
}
const { segmentIndex, color, label, isVisible, isLocked } = segment;
return (
<div
className="mb-[1px]"
key={segmentIndex}
>
<SegmentationGroupSegment
segmentationId={id}
segmentIndex={segmentIndex}
label={label}
color={color}
isActive={activeSegmentIndex === segmentIndex}
isLocked={isLocked}
isVisible={isVisible}
disableEditing={disableEditing}
onClick={onSegmentClick}
onEdit={onSegmentEdit}
onDelete={onSegmentDelete}
showSegmentDelete={showSegmentDelete}
onColor={onSegmentColorClick}
onToggleVisibility={onToggleSegmentVisibility}
/>
</div>
);
})}
</div>
</div>
)}
</div>
);
};
SegmentationGroup.propTypes = {
label: PropTypes.string.isRequired,
id: PropTypes.string.isRequired,
segments: PropTypes.array.isRequired,
segmentCount: PropTypes.number.isRequired,
isVisible: PropTypes.bool.isRequired,
isActive: PropTypes.bool.isRequired,
isMinimized: PropTypes.bool.isRequired,
activeSegmentIndex: PropTypes.number,
onClickNewSegment: PropTypes.func.isRequired,
onClickSegment: PropTypes.func.isRequired,
onClickSegmentEdit: PropTypes.func.isRequired,
onClickSegmentDelete: PropTypes.func.isRequired,
onToggleSegmentLocked: PropTypes.func,
onToggleSegmentVisibility: PropTypes.func.isRequired,
onToggleSegmentationVisibility: PropTypes.func.isRequired,
onSegmentClick: PropTypes.func.isRequired,
showAddSegment: PropTypes.bool.isRequired,
onSegmentAdd: PropTypes.func.isRequired,
onSegmentationClick: PropTypes.func.isRequired,
onClickSegmentColor: PropTypes.func.isRequired,
onSegmentationEdit: PropTypes.func.isRequired,
onSegmentDelete: PropTypes.func.isRequired,
onToggleMinimizeSegmentation: PropTypes.func.isRequired,
onSegmentationConfigChange: PropTypes.func.isRequired,
onSegmentationDelete: PropTypes.func.isRequired,
onSegmentEdit: PropTypes.func.isRequired,
disableEditing: PropTypes.bool,
};
SegmentationGroup.defaultProps = {
label: '',
segmentCount: 0,
segments: [],
isVisible: true,
isMinimized: false,
onClickNewSegment: () => {},
onClickSegment: () => {},
onClickSegmentEdit: () => {},
onClickSegmentDelete: () => {},
onToggleSegmentLocked: () => {},
onToggleSegmentVisibility: () => {},
onToggleSegmentationVisibility: () => {},
onSegmentClick: () => {},
showAddSegment: false,
onSegmentAdd: () => {},
onSegmentationClick: () => {},
onClickSegmentColor: () => {},
onSegmentationEdit: () => {},
onSegmentDelete: () => {},
onToggleMinimizeSegmentation: () => {},
onSegmentationConfigChange: () => {},
onSegmentationDelete: () => {},
onSegmentEdit: () => {},
};
export default SegmentationGroup;
@@ -11,7 +11,7 @@ const SegmentItem = ({
isActive,
isVisible,
color,
showSegmentDelete,
showDelete,
disableEditing,
isLocked = false,
onClick,
@@ -21,200 +21,168 @@ const SegmentItem = ({
onToggleVisibility,
onToggleLocked,
}) => {
const [isHovering, setIsHovering] = useState(false);
const [isSegmentIndexHovering, setIsSegmentIndexHovering] = useState(false);
const onMouseEnter = () => setIsHovering(true);
const onMouseLeave = () => setIsHovering(false);
const [isNumberBoxHovering, setIsNumberBoxHovering] = useState(false);
const cssColor = `rgb(${color[0]},${color[1]},${color[2]})`;
return (
<div
className={classnames(
'bg-primary-dark group relative flex cursor-pointer overflow-hidden border text-[12px] transition duration-300',
{
'border-primary-light rounded-[3px]': isActive,
},
{
'border-transparent': !isActive,
}
)}
onMouseEnter={onMouseEnter}
onMouseLeave={onMouseLeave}
className={classnames('text-aqua-pale group/row flex min-h-[28px] bg-black')}
onClick={e => {
e.stopPropagation();
onClick(segmentationId, segmentIndex);
}}
role="button"
tabIndex={0}
data-cy={'segment-item'}
>
<div
className={classnames(
'flex w-[27px] items-center justify-center border-r border-r-black text-[12px]',
{
'bg-primary-light border-l-primary-light rounded-l-sm border-l text-black': isActive,
'rounded-l-sm border-l border-l-[#1d3c58] bg-[#1d3c58]': !isActive && isHovering,
'bg-primary-dark text-aqua-pale': !isActive && !isHovering && isVisible,
'bg-[#140e2e] text-[#537594] opacity-60': !isActive && !isVisible,
}
)}
onMouseEnter={() => setIsSegmentIndexHovering(true)}
onMouseLeave={() => setIsSegmentIndexHovering(false)}
className={classnames('bg-primary-dark group/number grid w-[32px] place-items-center', {
'bg-primary-light border-primary-light rounded-l-[4px] border text-black': isActive,
'border-primary-dark border': !isActive,
})}
onMouseEnter={() => setIsNumberBoxHovering(true)}
onMouseLeave={() => setIsNumberBoxHovering(false)}
>
{isSegmentIndexHovering && showSegmentDelete ? (
{isNumberBoxHovering && showDelete ? (
<Icon
name="close"
className={classnames('pr-0.5')}
className={classnames('h-[8px] w-[8px]', {
'hover:cursor-pointer hover:opacity-60': !disableEditing,
})}
onClick={e => {
if (disableEditing) {
return;
}
e.stopPropagation();
onDelete(segmentationId, segmentIndex);
}}
/>
) : (
<div className={classnames('flex items-center pr-2 ')}>{segmentIndex}</div>
<div>{segmentIndex}</div>
)}
</div>
<div
className={classnames(
'relative flex w-full items-center justify-between border-r border-r-black py-1 pl-2 text-white ',
{
'bg-primary-dark text-primary-light': isActive,
'bg-primary-dark text-aqua-pale': !isActive && isVisible,
'bg-[#140e2e] text-[#3d5871] opacity-60': !isVisible,
}
)}
className={classnames('relative flex w-full', {
'border-primary-light bg-primary-dark rounded-r-[4px] border border-l-0': isActive,
'border border-l-0 border-transparent': !isActive,
})}
>
<div className={classnames('flex items-baseline gap-2')}>
<div
className={classnames('h-[8px] w-[8px] shrink-0 rounded-full')}
style={{ backgroundColor: cssColor }}
onClick={e => {
e.stopPropagation();
onColor(segmentationId, segmentIndex);
}}
/>
<div className="">{label}</div>
</div>
{/* with faded background */}
<div className="bg-black/15 absolute right-0 rounded-lg pr-[7px]">
{!isVisible && !isHovering && (
<div>
<Icon
name="row-hidden"
className={classnames('h-5 w-5 text-[#3d5871] ')}
onClick={e => {
e.stopPropagation();
onToggleVisibility(segmentationId, segmentIndex);
}}
/>
</div>
)}
{isHovering && (
<div className={classnames('flex items-center')}>
{!disableEditing && (
<Icon
name="row-edit"
className={classnames('h-5 w-5', {
'text-white': isLocked,
'text-primary-light': !isLocked,
})}
onClick={e => {
e.stopPropagation();
onEdit(segmentationId, segmentIndex);
}}
/>
)}
{isVisible ? (
<Icon
name="row-hide"
className={classnames('h-5 w-5', {
'text-white': isLocked,
'text-primary-light': !isLocked,
})}
onClick={e => {
e.stopPropagation();
onToggleVisibility(segmentationId, segmentIndex);
}}
/>
) : (
<Icon
name="row-unhide"
className={classnames('h-5 w-5', {
'text-white': isLocked,
'text-primary-light': !isLocked,
})}
onClick={e => {
e.stopPropagation();
onToggleVisibility(segmentationId, segmentIndex);
}}
/>
)}
</div>
)}
</div>
</div>
{/* <div className="relative flex flex-col w-full border-t border-t-black">
<div className="fl`ex items-center mb-1 ml-2">
<div className="flex items-center flex-1 text-base text-primary-light">
<div className="group-hover/row:bg-primary-dark flex h-full w-full flex-grow items-center">
<div className="pl-2 pr-1.5">
<div
className={classnames(
'w-3 h-3 mt-1 mr-2 rounded-full cursor-pointer transition duration-300 hover:opacity-80'
)}
className={classnames('h-[8px] w-[8px] grow-0 rounded-full', {
'hover:cursor-pointer hover:opacity-60': !disableEditing,
})}
style={{ backgroundColor: cssColor }}
onClick={e => {
if (disableEditing) {
return;
}
e.stopPropagation();
onColor(segmentationId, segmentIndex);
}}
style={{ backgroundColor: cssColor }}
></div>
{label}
/>
</div>
<div className="flex items-center w-1/3">
<div className="px-1">
<div className="flex items-center py-1 hover:cursor-pointer">{label}</div>
</div>
<div
className={classnames(
'absolute right-3 top-0 flex flex-row-reverse rounded-lg pt-[3px]',
{}
)}
>
<div className="group-hover/row:hidden">
{!isVisible && (
<Icon
className={classnames(
'text-white w-4 cursor-pointer transition duration-300 hover:opacity-80'
)}
name={isVisible ? 'eye-visible' : 'eye-hidden'}
name="row-hidden"
className="h-5 w-5 text-[#3d5871]"
onClick={e => {
// stopPropagation needed to avoid disable the current active item
e.stopPropagation();
onToggleVisibility(segmentationId, segmentIndex);
}}
/>
</div>
{onToggleLocked !== undefined ? (
<div className="px-1">
)}
</div>
{/* Icon for 'row-lock' that shows when NOT hovering and 'isLocked' is true */}
<div className="group-hover/row:hidden">
{isLocked && (
<div className="flex">
<Icon
className={classnames(
'text-white w-4 cursor-pointer transition duration-300 hover:opacity-80'
)}
name={isLocked ? 'lock' : 'dotted-circle'}
name="row-lock"
className="h-5 w-5 text-[#3d5871]"
onClick={e => {
// stopPropagation needed to avoid disable the current active item
e.stopPropagation();
onToggleLocked(segmentationId, segmentIndex);
}}
/>
</div>
) : null}
<div className="px-1">
<Icon
className={classnames(
'text-white w-4 cursor-pointer transition duration-300 hover:opacity-80'
{/* This icon is visible when 'isVisible' is true */}
{isVisible && (
<Icon
name="row-hidden"
className="h-5 w-5 opacity-0"
/>
)}
name={'pencil'}
onClick={e => {
// stopPropagation needed to avoid disable the current active item
e.stopPropagation();
onEdit(segmentationId, segmentIndex);
}}
/>
</div>
</div>
)}
</div>
{/* Icons that show only when hovering */}
<div className="hidden group-hover/row:flex">
<HoveringIcons
onEdit={onEdit}
isLocked={isLocked}
isVisible={isVisible}
onToggleLocked={onToggleLocked}
onToggleVisibility={onToggleVisibility}
segmentationId={segmentationId}
segmentIndex={segmentIndex}
/>
</div>
</div>
</div> */}
</div>
</div>
);
};
const HoveringIcons = ({
onEdit,
isLocked,
isVisible,
onToggleLocked,
onToggleVisibility,
segmentationId,
segmentIndex,
}) => {
const iconClass = 'w-5 h-5 hover:cursor-pointer hover:opacity-60';
const handleIconClick = (e, action) => {
e.stopPropagation();
action(segmentationId, segmentIndex);
};
const createIcon = (name, action, color = null) => (
<Icon
name={name}
className={classnames(iconClass, color ?? 'text-white')}
onClick={e => handleIconClick(e, action)}
/>
);
return (
<div className="flex items-center">
{createIcon('row-edit', onEdit)}
{createIcon(
isLocked ? 'row-lock' : 'row-unlock',
onToggleLocked,
isLocked ? 'text-[#3d5871]' : null
)}
{createIcon(
isVisible ? 'row-shown' : 'row-hidden',
onToggleVisibility,
!isVisible ? 'text-[#3d5871]' : null
)}
</div>
);
};
@@ -1,28 +1,38 @@
import React from 'react';
import React, { useEffect, useState } from 'react';
import PropTypes from 'prop-types';
import Icon from '../Icon';
import SegmentationGroup from './SegmentationGroup';
import { PanelSection } from '../../components';
import SegmentationConfig from './SegmentationConfig';
import SegmentationDropDownRow from './SegmentationDropDownRow';
import NoSegmentationRow from './NoSegmentationRow';
import AddSegmentRow from './AddSegmentRow';
import SegmentationGroupSegment from './SegmentationGroupSegment';
const SegmentationGroupTable = ({
segmentations,
// segmentation initial config
segmentationConfig,
// UI show/hide
disableEditing,
showAddSegmentation,
showAddSegment,
showDeleteSegment,
// segmentation/segment handlers
onSegmentationAdd,
onSegmentationEdit,
onSegmentationClick,
onSegmentationDelete,
showAddSegmentation,
showAddSegment,
onSegmentationDownload,
storeSegmentation,
// segment handlers
onSegmentClick,
onSegmentAdd,
segmentationConfig,
disableEditing,
onSegmentDelete,
onSegmentEdit,
onToggleSegmentationVisibility,
onToggleSegmentVisibility,
onToggleSegmentLock,
onSegmentColorClick,
isMinimized,
onToggleMinimizeSegmentation,
// segmentation config handlers
setFillAlpha,
setFillAlphaInactive,
setOutlineWidthActive,
@@ -31,111 +41,191 @@ const SegmentationGroupTable = ({
setRenderInactiveSegmentations,
setRenderOutline,
}) => {
const [isConfigOpen, setIsConfigOpen] = useState(false);
const [activeSegmentationId, setActiveSegmentationId] = useState(null);
const onActiveSegmentationChange = segmentationId => {
onSegmentationClick(segmentationId);
setActiveSegmentationId(segmentationId);
};
useEffect(() => {
// find the first active segmentation to set
let activeSegmentationIdToSet = segmentations?.find(segmentation => segmentation.isActive)?.id;
// If there is no active segmentation, set the first one to be active
if (!activeSegmentationIdToSet && segmentations?.length > 0) {
activeSegmentationIdToSet = segmentations[0].id;
}
// If there is no segmentation, set the active segmentation to null
if (segmentations?.length === 0) {
activeSegmentationIdToSet = null;
}
setActiveSegmentationId(activeSegmentationIdToSet);
}, [segmentations]);
const activeSegmentation = segmentations?.find(
segmentation => segmentation.id === activeSegmentationId
);
return (
<div className="font-inter flex min-h-0 flex-col font-[300]">
<SegmentationConfig
setFillAlpha={setFillAlpha}
setFillAlphaInactive={setFillAlphaInactive}
setOutlineWidthActive={setOutlineWidthActive}
setOutlineOpacityActive={setOutlineOpacityActive}
setRenderFill={setRenderFill}
setRenderInactiveSegmentations={setRenderInactiveSegmentations}
setRenderOutline={setRenderOutline}
segmentationConfig={segmentationConfig}
/>
<div className="mt-1 flex min-h-0 flex-col pr-[1px]">
{!!segmentations.length &&
segmentations.map((segmentation, index) => {
const {
id,
label,
displayText = [],
segmentCount,
segments,
isVisible,
isActive,
activeSegmentIndex,
} = segmentation;
return (
<SegmentationGroup
id={id}
key={id}
label={label}
disableEditing={disableEditing}
isMinimized={isMinimized[id]}
segments={segments}
showAddSegment={showAddSegment}
segmentCount={segmentCount}
isActive={isActive}
isVisible={isVisible}
onSegmentColorClick={onSegmentColorClick}
onSegmentationClick={() => onSegmentationClick(id)}
activeSegmentIndex={activeSegmentIndex}
onToggleMinimizeSegmentation={onToggleMinimizeSegmentation}
onSegmentationEdit={onSegmentationEdit}
onSegmentationDelete={onSegmentationDelete}
onSegmentClick={onSegmentClick}
onSegmentEdit={onSegmentEdit}
onToggleSegmentVisibility={onToggleSegmentVisibility}
onToggleSegmentationVisibility={onToggleSegmentationVisibility}
onSegmentAdd={onSegmentAdd}
showSegmentDelete={false}
/>
);
})}
</div>
{showAddSegmentation && !disableEditing && (
<div
className="text-primary-active flex h-[45px] cursor-pointer items-center bg-black pl-1 text-[12px] hover:opacity-80"
onClick={() => onSegmentationAdd()}
>
<Icon
name="row-add"
className="h-5 w-5"
<div className="flex min-h-0 flex-col bg-black text-[13px] font-[300]">
<PanelSection
title="Segmentation"
actionIcons={
activeSegmentation && [
{
name: 'settings-bars',
onClick: () => setIsConfigOpen(isOpen => !isOpen),
},
]
}
>
{isConfigOpen && (
<SegmentationConfig
setFillAlpha={setFillAlpha}
setFillAlphaInactive={setFillAlphaInactive}
setOutlineWidthActive={setOutlineWidthActive}
setOutlineOpacityActive={setOutlineOpacityActive}
setRenderFill={setRenderFill}
setRenderInactiveSegmentations={setRenderInactiveSegmentations}
setRenderOutline={setRenderOutline}
segmentationConfig={segmentationConfig}
/>
<div className="pl-1">Add New Segmentation</div>
)}
<div className="bg-primary-dark">
{segmentations?.length === 0 ? (
<div className="select-none rounded-[4px]">
{showAddSegmentation && !disableEditing && (
<NoSegmentationRow onSegmentationAdd={onSegmentationAdd} />
)}
</div>
) : (
<div className="mt-1 select-none">
<SegmentationDropDownRow
segmentations={segmentations}
disableEditing={disableEditing}
activeSegmentation={activeSegmentation}
onActiveSegmentationChange={onActiveSegmentationChange}
onSegmentationDelete={onSegmentationDelete}
onSegmentationEdit={onSegmentationEdit}
onSegmentationDownload={onSegmentationDownload}
storeSegmentation={storeSegmentation}
onSegmentationAdd={onSegmentationAdd}
onToggleSegmentationVisibility={onToggleSegmentationVisibility}
/>
{!disableEditing && showAddSegment && (
<AddSegmentRow onClick={() => onSegmentAdd(activeSegmentationId)} />
)}
</div>
)}
</div>
)}
{activeSegmentation && (
<div className="ohif-scrollbar mt-1.5 flex min-h-0 flex-col overflow-y-hidden">
{activeSegmentation?.segments?.map(segment => {
if (!segment) {
return null;
}
const { segmentIndex, color, label, isVisible, isLocked } = segment;
return (
<div
className="mb-[1px]"
key={segmentIndex}
>
<SegmentationGroupSegment
segmentationId={activeSegmentationId}
segmentIndex={segmentIndex}
label={label}
color={color}
isActive={activeSegmentation.activeSegmentIndex === segmentIndex}
disableEditing={disableEditing}
isLocked={isLocked}
isVisible={isVisible}
onClick={onSegmentClick}
onEdit={onSegmentEdit}
onDelete={onSegmentDelete}
showDelete={showDeleteSegment}
onColor={onSegmentColorClick}
onToggleVisibility={onToggleSegmentVisibility}
onToggleLocked={onToggleSegmentLock}
/>
</div>
);
})}
</div>
)}
</PanelSection>
</div>
);
};
SegmentationGroupTable.propTypes = {
title: PropTypes.string.isRequired,
segmentations: PropTypes.array.isRequired,
activeSegmentationId: PropTypes.string.isRequired,
onClick: PropTypes.func.isRequired,
onEdit: PropTypes.func.isRequired,
onDelete: PropTypes.func.isRequired,
onToggleLocked: PropTypes.func,
onToggleVisibility: PropTypes.func.isRequired,
onToggleVisibilityAll: PropTypes.func.isRequired,
segmentationConfig: PropTypes.object,
segmentations: PropTypes.arrayOf(
PropTypes.shape({
id: PropTypes.string.isRequired,
isActive: PropTypes.bool.isRequired,
segments: PropTypes.arrayOf(
PropTypes.shape({
segmentIndex: PropTypes.number.isRequired,
color: PropTypes.array.isRequired,
label: PropTypes.string.isRequired,
isVisible: PropTypes.bool.isRequired,
isLocked: PropTypes.bool.isRequired,
})
),
})
),
segmentationConfig: PropTypes.object.isRequired,
disableEditing: PropTypes.bool,
showAddSegmentation: PropTypes.bool,
showAddSegment: PropTypes.bool,
showDeleteSegment: PropTypes.bool,
onSegmentationAdd: PropTypes.func.isRequired,
onSegmentationEdit: PropTypes.func.isRequired,
onSegmentationClick: PropTypes.func.isRequired,
onSegmentationDelete: PropTypes.func.isRequired,
onSegmentationDownload: PropTypes.func.isRequired,
storeSegmentation: PropTypes.func.isRequired,
onSegmentClick: PropTypes.func.isRequired,
onSegmentAdd: PropTypes.func.isRequired,
onSegmentDelete: PropTypes.func.isRequired,
onSegmentEdit: PropTypes.func.isRequired,
onToggleSegmentationVisibility: PropTypes.func.isRequired,
onToggleSegmentVisibility: PropTypes.func.isRequired,
onToggleSegmentLock: PropTypes.func.isRequired,
onSegmentColorClick: PropTypes.func.isRequired,
setFillAlpha: PropTypes.func.isRequired,
setFillAlphaInactive: PropTypes.func.isRequired,
setOutlineWidthActive: PropTypes.func.isRequired,
setOutlineOpacityActive: PropTypes.func.isRequired,
setRenderFill: PropTypes.func.isRequired,
setRenderInactiveSegmentations: PropTypes.func.isRequired,
setRenderOutline: PropTypes.func.isRequired,
};
SegmentationGroupTable.defaultProps = {
title: '',
segmentations: [],
activeSegmentationId: '',
onClick: () => {},
onEdit: () => {},
onDelete: () => {},
onToggleLocked: () => {},
onToggleVisibility: () => {},
onToggleVisibilityAll: () => {},
segmentationConfig: {
initialConfig: {
fillAlpha: 0.5,
fillAlphaInactive: 0.5,
outlineWidthActive: 1,
outlineOpacity: 1,
outlineOpacityInactive: 0.5,
renderFill: true,
renderInactiveSegmentations: true,
renderOutline: true,
},
},
disableEditing: false,
showAddSegmentation: true,
showAddSegment: true,
showDeleteSegment: true,
onSegmentationAdd: () => {},
onSegmentationEdit: () => {},
onSegmentationClick: () => {},
onSegmentationDelete: () => {},
onSegmentationDownload: () => {},
storeSegmentation: () => {},
onSegmentClick: () => {},
onSegmentAdd: () => {},
onSegmentDelete: () => {},
onSegmentEdit: () => {},
onToggleSegmentationVisibility: () => {},
onToggleSegmentVisibility: () => {},
onToggleSegmentLock: () => {},
onSegmentColorClick: () => {},
setFillAlpha: () => {},
setFillAlphaInactive: () => {},
setOutlineWidthActive: () => {},
@@ -144,5 +234,4 @@ SegmentationGroupTable.defaultProps = {
setRenderInactiveSegmentations: () => {},
setRenderOutline: () => {},
};
export default SegmentationGroupTable;
@@ -1,7 +1,5 @@
import SegmentationGroupTable from '../SegmentationGroupTable';
import { ArgsTable, Story, Canvas, Meta } from '@storybook/addon-docs';
import { createComponentTemplate } from '../../../storybook/functions/create-component-story';
import { SegmentationGroupTable } from '../../../components';
export const argTypes = {
component: SegmentationGroupTable,
@@ -40,13 +38,14 @@ SegmentationGroupTable is a component that renders the SegmentationGroupTables.
showAddSegmentation: true,
isMinimized: { '4be5-b9b2-ed33-fa86970568e7': false },
showAddSegment: true,
showDeleteSegment: true,
segmentations: [
{
id: '4be5-b9b2-ed33-fa86970568e7',
volumeId: 'a3b0f15f-4be5-b9b2-ed33-fa86970568e7',
activeSegmentIndex: 1,
activeSegmentIndex: 2,
cachedStats: {},
label: 'Segmentation',
label: 'Segmentation 1',
segmentsLocked: {},
type: 'LABELMAP',
displayText: [],
@@ -55,7 +54,7 @@ SegmentationGroupTable is a component that renders the SegmentationGroupTables.
segments: [
null,
{
label: 'Segment 1',
label: 'This is a very long segment label',
segmentIndex: 1,
color: [128, 174, 128],
opacity: 255,
@@ -88,7 +87,7 @@ SegmentationGroupTable is a component that renders the SegmentationGroupTables.
volumeId: 'a3b0f15f-4be5-b9b2-ed33-fa86970568e7',
activeSegmentIndex: 1,
cachedStats: {},
label: 'Segmentation2',
label: '2 Segmentation',
segmentsLocked: {},
type: 'LABELMAP',
displayText: [],
+19 -5
View File
@@ -1,6 +1,6 @@
.customSelect__wrapper .customSelect__control {
@apply border-primary-main w-full rounded border bg-black text-sm leading-tight text-white shadow transition duration-300;
min-height: 33px;
@apply border-secondary-light w-full rounded border bg-black text-sm leading-tight text-white shadow transition duration-300;
min-height: 28px;
}
.customSelect__wrapper .customSelect__control:hover {
@apply border-gray-500;
@@ -15,6 +15,10 @@
@apply pointer-events-none;
}
.css-1hwfws3 {
padding: 0;
}
.customSelect__wrapper .customSelect__indicator-separator {
@apply hidden;
}
@@ -36,7 +40,11 @@
}
.customSelect__wrapper .customSelect__single-value {
@apply text-white;
@apply text-aqua-pale pl-1;
max-width: calc(100% - 20px);
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.customSelect__wrapper.customSelect--is-disabled {
@@ -54,8 +62,7 @@
.ohif-select .customSelect__menu-list {
overflow-x: hidden !important;
margin-right: 4px;
margin-top: 4px;
margin: 4px;
padding-top: 0px !important;
}
@@ -63,6 +70,9 @@
width: 6px;
border-radius: 10px;
}
.ohif-select .customSelect__placeholder {
@apply text-aqua-pale pl-1.5;
}
.ohif-select .customSelect__menu-list::-webkit-scrollbar-thumb {
background-color: #0944b3;
@@ -90,3 +100,7 @@
border-radius: 6px;
background-color: #151515 !important;
}
.css-1hwfws3 {
all: unset !important;
}
+7 -2
View File
@@ -54,6 +54,7 @@ const Select = ({
placeholder,
noIcons,
menuPlacement,
components,
value,
}) => {
const _noIconComponents = {
@@ -61,7 +62,10 @@ const Select = ({
IndicatorSeparator: () => null,
};
let _components = isMulti ? { Option, MultiValue } : {};
_components = noIcons ? { ..._components, ..._noIconComponents } : _components;
_components = noIcons
? { ..._components, ..._noIconComponents }
: { ..._components, ...components };
const selectedOptions = [];
// Map array of values to an array of selected options
@@ -104,8 +108,9 @@ const Select = ({
Select.defaultProps = {
className: '',
closeMenuOnSelect: true,
hideSelectedOptions: true,
hideSelectedOptions: false,
isClearable: true,
components: {},
isDisabled: false,
isMulti: false,
isSearchable: true,
@@ -0,0 +1,56 @@
import { Select } from '../../../components';
import { ArgsTable, Story, Canvas, Meta } from '@storybook/addon-docs';
export const argTypes = {
component: Select,
title: 'Components/Select',
};
<Meta
title="Components/Select"
component={Select}
/>
export const SelectTemplate = args => (
<div className="h-80 w-40">
<Select {...args} />
</div>
);
<Heading
title="Select"
componentRelativePath="Select/Select.tsx"
/>
- [Overview](#overview)
- [Props](#props)
- [Contribute](#contribute)
## Overview
Select is a component that allows you to select from a list
<Canvas>
<Story
name="Overview"
args={{
isClearable: false,
onChange: item => alert(item.label),
options: [
{ label: 'item1', value: 1 },
{ label: 'item2', value: 2 },
],
value: 2,
}}
>
{SelectTemplate.bind({})}
</Story>
</Canvas>
## Props
<ArgsTable of={Select} />
## Contribute
<Footer componentRelativePath="Select/__stories__/Select.stories.mdx" />
@@ -207,10 +207,7 @@ const SidePanel = ({
<React.Fragment>
{/** Panel Header with Arrow and Close Actions */}
<div
className={classnames(
'flex-static bg-primary-dark flex h-9 cursor-pointer px-[10px]',
tabs.length === 1 && 'mb-1'
)}
className={classnames('flex-static bg-primary-dark flex h-9 cursor-pointer px-[10px]')}
onClick={() => {
updatePanelOpen(prev => !prev);
// slideToActivePanel();
@@ -7,13 +7,12 @@ import { useTranslation } from 'react-i18next';
import Icon from '../Icon';
import Tooltip from '../Tooltip';
import ListMenu from '../ListMenu';
import ToolbarButton from '../ToolbarButton';
const baseClasses = {
Button: 'flex items-center rounded-md border-transparent cursor-pointer group/button',
Primary:
// By default border on left, top and bottom for hover effect and only rounded on left side.
// Extra padding on right to componensate for no right border.
// Extra padding on right to compensate for no right border.
'h-full border-l-2 border-t-2 border-b-2 rounded-tl-md rounded-bl-md group/primary !pl-2 !py-2',
Secondary:
'h-full flex items-center justify-center rounded-tr-md rounded-br-md w-4 border-2 border-transparent group/secondary',
@@ -72,154 +71,57 @@ const classes = {
};
const SplitButton = ({
isRadio,
isAction,
//
bState,
//
isToggle,
groupId,
primary: _primary,
primary,
secondary,
items: _items,
items,
renderer,
isActive,
onInteraction,
servicesManager,
Component,
}) => {
const { t } = useTranslation('Buttons');
const [state, setState] = useState({ isHovering: false, isExpanded: false });
const { toolbarService } = servicesManager?.services || {};
const toggleExpanded = () => setState({ ...state, isExpanded: !state.isExpanded });
const setHover = hovering => setState({ ...state, isHovering: hovering });
const collapse = () => setState({ ...state, isExpanded: false });
const { primaryToolId, toggles } = bState;
/* Bubbles up individual item clicks */
const getSplitButtonItems = items =>
items.map((item, index) => ({
...item,
index,
onClick: () => {
onInteraction({
groupId,
//
itemId: item.id,
interactionType: item.type,
// splitButtonId? (so we can track group?)
// info to fire item's command/event?
commands: item.commands,
});
setState(state => ({
...state,
primary: !isAction && isRadio ? { ...item, index } : state.primary,
isExpanded: false,
items: getSplitButtonItems(_items).filter(item =>
isRadio && !isAction ? item.index !== index : true
),
}));
},
}));
const [state, setState] = useState({
primary: _primary,
items: getSplitButtonItems(_items).filter(item =>
isRadio && !isAction ? item.id !== _primary.id : true
),
isHovering: false,
isExpanded: false,
});
const onSecondaryClickHandler = () =>
setState(state => ({ ...state, isExpanded: !state.isExpanded }));
const onMouseEnterHandler = () => setState(state => ({ ...state, isHovering: true }));
const onMouseLeaveHandler = () => setState(state => ({ ...state, isHovering: false }));
const outsideClickHandler = () => setState(state => ({ ...state, isExpanded: false }));
const isPrimaryToggle = state.primary.type === 'toggle';
const isPrimaryActive =
(state.primary.type === 'tool' && primaryToolId === state.primary.id) ||
(isPrimaryToggle && toggles[state.primary.id] === true);
const PrimaryButtonComponent =
toolbarService?.getButtonComponentForUIType(state.primary.uiType) ?? ToolbarButton;
const primaryButtonClassName = classes.Primary({
...state,
primary: { isActive: isPrimaryActive, isToggle: isPrimaryToggle },
});
const DefaultListItemRenderer = ({ type, icon, label, t, id }) => {
const isActive = type === 'toggle' && toggles[id] === true;
return (
<div
className={classNames(
'hover:bg-primary-dark flex h-8 w-full flex-row items-center p-3',
'whitespace-pre text-base',
isActive && 'bg-primary-dark',
isActive
? 'text-[#348CFD]'
: 'text-common-bright hover:bg-primary-dark hover:text-primary-light'
)}
>
{icon && (
<span className="mr-4">
<Icon
name={icon}
className="h-5 w-5"
/>
</span>
)}
<span className="mr-5">{t(label)}</span>
</div>
);
};
const listItemRenderer = renderer || DefaultListItemRenderer;
const renderPrimaryButton = () => (
<Component
key={primary.id}
{...primary}
isActive={isActive}
onInteraction={onInteraction}
rounded="none"
className={classes.Primary({ ...state, primary: { isActive, isToggle } })}
data-tool={primary.id}
data-cy={`${groupId}-split-button-primary`}
/>
);
return (
<OutsideClickHandler
onOutsideClick={outsideClickHandler}
onOutsideClick={collapse}
disabled={!state.isExpanded}
>
<div
name="SplitButton"
id="SplitButton"
className="relative"
>
<div
className={classes.Button({
...state,
primary: { isActive: isPrimaryActive },
})}
className={classes.Button({ ...state, primary: { isActive } })}
style={{ height: '40px' }}
onMouseEnter={onMouseEnterHandler}
onMouseLeave={onMouseLeaveHandler}
onMouseEnter={() => setHover(true)}
onMouseLeave={() => setHover(false)}
>
<div className={classes.Interface}>
<div onClick={outsideClickHandler}>
<PrimaryButtonComponent
key={state.primary.id}
{...state.primary}
bState={bState}
isActive={isPrimaryActive}
onInteraction={args => toolbarService.recordInteraction(args)}
servicesManager={servicesManager}
// All rounding is taken care of by className
rounded="none"
className={primaryButtonClassName}
data-tool={state.primary.id}
data-cy={`${groupId}-split-button-primary`}
/>
</div>
<div onClick={collapse}>{renderPrimaryButton()}</div>
<div className={classes.Separator({ ...state, primary: { isActive } })}></div>
<div
className={classes.Separator({
...state,
primary: { isActive: isPrimaryActive },
})}
></div>
<div
className={classes.Secondary({
...state,
primary: { isActive: isPrimaryActive },
})}
onClick={onSecondaryClickHandler}
className={classes.Secondary({ ...state, primary: { isActive } })}
onClick={toggleExpanded}
data-cy={`${groupId}-split-button-secondary`}
>
<Tooltip
@@ -229,24 +131,20 @@ const SplitButton = ({
>
<Icon
name={secondary.icon}
className={classes.SecondaryIcon({
...state,
primary: { isActive: isPrimaryActive },
})}
className={classes.SecondaryIcon({ ...state, primary: { isActive } })}
/>
</Tooltip>
</div>
</div>
</div>
{/* EXPANDED LIST OF OPTIONS */}
<div
className={classes.Content({ ...state })}
data-cy={`${groupId}-list-menu`}
>
<ListMenu
items={state.items}
bState={bState}
renderer={args => listItemRenderer({ ...args, t })}
items={items}
onClick={collapse}
renderer={args => renderer({ ...args, t })}
/>
</div>
</div>
@@ -254,51 +152,23 @@ const SplitButton = ({
);
};
SplitButton.defaultProps = {
isRadio: false,
isAction: false,
primary: {
label: null,
tooltip: null,
},
secondary: {
icon: 'chevron-down',
label: null,
isActive: true,
tooltip: 'More Measure Tools',
},
items: [],
renderer: null,
SplitButton.propTypes = {
isToggle: PropTypes.bool,
groupId: PropTypes.string.isRequired,
primary: PropTypes.object.isRequired,
secondary: PropTypes.object.isRequired,
items: PropTypes.array.isRequired,
renderer: PropTypes.func,
isActive: PropTypes.bool,
onInteraction: PropTypes.func.isRequired,
Component: PropTypes.elementType,
};
SplitButton.propTypes = {
primary: PropTypes.shape({
id: PropTypes.string.isRequired,
icon: PropTypes.string,
label: PropTypes.string,
type: PropTypes.oneOf(['tool', 'action', 'toggle']).isRequired,
tooltip: PropTypes.string,
}),
secondary: PropTypes.shape({
id: PropTypes.string,
icon: PropTypes.string,
label: PropTypes.string,
tooltip: PropTypes.string,
isActive: PropTypes.bool,
}),
renderer: PropTypes.func,
items: PropTypes.arrayOf(
PropTypes.shape({
id: PropTypes.string.isRequired,
icon: PropTypes.string,
label: PropTypes.string,
type: PropTypes.oneOf(['tool', 'action', 'toggle']).isRequired,
tooltip: PropTypes.string,
isActive: PropTypes.bool,
})
),
/** Callback function to inform toolbarService of important events */
onInteraction: PropTypes.func.isRequired,
SplitButton.defaultProps = {
isToggle: false,
renderer: null,
isActive: false,
Component: null,
};
export default SplitButton;
@@ -3,7 +3,7 @@ import PropTypes from 'prop-types';
import { useTranslation } from 'react-i18next';
import StudyItem from '../StudyItem';
import ButtonGroup from '../ButtonGroup';
import LegacyButtonGroup from '../LegacyButtonGroup';
import LegacyButton from '../LegacyButton';
import ThumbnailList from '../ThumbnailList';
import { StringNumber } from '../../types';
@@ -74,8 +74,8 @@ const StudyBrowser = ({
className="w-100 border-secondary-light bg-primary-dark flex h-16 flex-row items-center justify-center border-b p-4"
data-cy={'studyBrowser-panel'}
>
{/* TODO Revisit design of ButtonGroup later - for now use LegacyButton for its children.*/}
<ButtonGroup
{/* TODO Revisit design of LegacyButtonGroup later - for now use LegacyButton for its children.*/}
<LegacyButtonGroup
variant="outlined"
color="secondary"
splitBorder={false}
@@ -108,7 +108,7 @@ const StudyBrowser = ({
</LegacyButton>
);
})}
</ButtonGroup>
</LegacyButtonGroup>
</div>
<div className="ohif-scrollbar invisible-scrollbar flex flex-1 flex-col overflow-auto">
{getTabContent()}
@@ -3,7 +3,7 @@ import PropTypes from 'prop-types';
import { useTranslation } from 'react-i18next';
import LegacyButton from '../LegacyButton';
import ButtonGroup from '../ButtonGroup';
import LegacyButtonGroup from '../LegacyButtonGroup';
import Typography from '../Typography';
import Select from '../Select';
@@ -50,8 +50,8 @@ const StudyListPagination = ({ onChangePage, currentPage, perPage, onChangePerPa
<Typography className="mr-4 text-base opacity-60">
{t('Page')} {currentPage}
</Typography>
{/* TODO Revisit design of ButtonGroup later - for now use LegacyButton for its children.*/}
<ButtonGroup>
{/* TODO Revisit design of LegacyButtonGroup later - for now use LegacyButton for its children.*/}
<LegacyButtonGroup>
<LegacyButton
size="initial"
className="px-4 py-2 text-base"
@@ -82,7 +82,7 @@ const StudyListPagination = ({ onChangePage, currentPage, perPage, onChangePerPa
>
{t('Next')}
</LegacyButton>
</ButtonGroup>
</LegacyButtonGroup>
</div>
</div>
</div>
Loaded 100 of 186 files, more files were not shown because too many files have changed in this diff. Show more