feat(segmentation mode): Add create, and export SEG with Brushes (#3632)
No files matched your search
@@ -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 () {
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -57,6 +57,9 @@
|
||||
{
|
||||
"packageName": "@ohif/mode-longitudinal"
|
||||
},
|
||||
{
|
||||
"packageName": "@ohif/mode-segmentation"
|
||||
},
|
||||
{
|
||||
"packageName": "@ohif/mode-tmtv"
|
||||
},
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -22,7 +22,6 @@ window.config = {
|
||||
wadoRoot: 'https://d33do7qe4w26qo.cloudfront.net/dicomweb',
|
||||
|
||||
qidoSupportsIncludeField: false,
|
||||
supportsReject: false,
|
||||
imageRendering: 'wadors',
|
||||
thumbnailRendering: 'wadors',
|
||||
enableStudyLazyLoad: true,
|
||||
|
||||
@@ -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&autopause=0&player_id=0&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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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')));
|
||||
}
|
||||
@@ -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 |
@@ -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 |
@@ -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;
|
||||
@@ -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: [],
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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>
|
||||
|
||||