feat: Add DICOM SEG support and MPR, referenceLines and StackSync and more (#3015)

* feat: add initial sop class handler for SEG

* feat: move segmentation service

* update segmentation service methods

* fix: viewport data structure

* feat: Add initial render for the DICOM SEG for each displaySet

* fix: various wrong architectural dependencies between services

* feat: initial separate SEG display in each viewport

* feat: refactore viewport action bar

* initial work for SEG hydration

* fix: various bugs regarding drag and dropping different viewports

* fix: rendering issues for multiple seg displaysets

* fix: bugs for thumbnail and hydration

* feat: fix the initial segment color

* update after rebase

* feat: initial design for the segmentation group table

* feat: initial new design for the segmentation panel

* feat: segmentation panel

* feat: make segmentation appear on all related viewports

* fix: segmentation load bug based on functional groups

* initial work for segmentation crosshairs

* fix: various stylings and functionality

* fix: various stylings for the seg panel

* fix: overflow styles

* feat: add more ui components

* feat: added segmentation config

* feat: add jump to segment

* feat: add jump to segment for DICOM SEG viewport

* fix: bugs after rebase

* feat: jump in segmentation viewport

* fix: mpr support for seg

* feat: add more icons

* feat: new icons

* feat: add new side panel

* feat: add segmentation config

* feat: add loading indicator to ohif

* feat: make hanging protocols follow matching rules for viewports

* feat: enhance drag and drop to be hanging protocol aware

* fix: mpr restore previous layout

* fix: crosshairs toggle

* fix: add auth headers to the dicom loader via dicomwebclient

* fix: bug for crosshairs toggle in mpr

* fix: seg viewport reusing old toolGroup

* feat: add loading animation with lottie

* fix: various bugs for Segmentation hydration in mpr

* feat: change outline alpha to outline opacity

* feat: loading indicator for seg viewport

* fix: various segmentation group styles

* fix: local mode for seg

* feat: add animation for the highlight

* fix: loading indicatro to show segment indices

* feat: enhance modality drop down ui

* fix: panels

* fix: download form

* fix: layout shift in loading indicator

* fix: loading indicator to have correct values

* fix: update software number

* fix: image jump between MPR and default

* fix: segmentation cleanup and cine service cleanup

* fix: segmentation toolgroup clena up

* fix: highlight interval should not trigger again

* fix: issue with multiframe sorting

* rename: change onSeriesChange to onArrowsClick

* fix: buttons for tmtv and layout shift

* fix: various bugs wrt crosshairs

* fix: crosshairs re init on reset camera

* fix: middle slice calculation different from cs middle reset camera

* fix: reset camera should reset viewport camera

* fix: loading segments in MPR mode

* fix: tmtv hp back to before

* fix: layout shift

* feat: orientation markers for volume viewport

* fix: capture for volume viewports

* fix: memory leak for back to worklist

* fix: loading bar bg color

* fix: side panel for only one panel

* fix: react select style in production

* fix: various styling for segmentation groups

* fix: various ui styles

* feat: add new segmentation config styles

* fix: hover state for segmentation item

* fix: side panel layout shift

* temp add panels

* try to fix scrollbar for thimbnail

* fix: scroll not appearing for side panels

* feat: changed styles for scrollbar

* feat: add cpu fallback warning

* fix: webworker destroy

* fix: select styles

* fix: orientation marker color and position

* fix: capture screenshot for volume viewports

* fix: hide segment visibility on mpr

* fix: reloading an already loaded seg displayset

* feat: add is equal

* fix: mpr jump to measurements

* apply review comments

* fix: optimization for the segmentation load

* fix: remove unnecessary context menu and hp service reset

* fix: segmentation service wrt brush settings

* feat: initial work for the stack synchronization

* fix: add validateDisplaySetSelectorsForNewDisplaySets to make sure drag and drop can follow requirements

* fix: various react proptypes

* fix: thumbnails for the tmtv and change default props to default params

* feat: make showing loading indicator configurable and add docs

* bump versions and add more docs

* fix demo

* update cornerstone versions

* apply review comments

* feat: add reference lines

* fix build

* fix unit tests

* add reference lines icon

* fix: e2e tests

* fix static wado config

* docs: add segmentation service docs

* fix: docker build

* fix: keep camera and bump versions

* Try re-enabling minification to fix deploy previews

Co-authored-by: Erik Ziegler <erik.sweed@gmail.com>
This commit is contained in:
AlirezaandErik Ziegler authored and GitHub committed 2022-11-16 14:19:52 -05:00
1 parent 08c58b0634
commit 83ea228d86
254 files changed
+12856 -3141

No files matched your search

+3 -1
View File
@@ -43,7 +43,9 @@
"react-modal": "3.11.2",
"react-outside-click-handler": "^1.3.0",
"react-select": "3.0.8",
"react-with-direction": "^1.3.1"
"react-with-direction": "^1.3.1",
"lottie-react": "^2.3.1",
"swiper": "^8.4.2"
},
"devDependencies": {
"@babel/core": "^7.14.8",
@@ -0,0 +1,9 @@
<svg width="15" height="15" viewBox="0 0 15 15" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<defs>
<path id="bf9k9cj5la" d="m3 7.463.994-.982 2.131 2.113L10.756 4l.994.987-5.625 5.575z"/>
</defs>
<g fill="none" fill-rule="evenodd">
<rect stroke="#5ACCE6" fill="#5ACCE6" x=".5" y=".5" width="14" height="14" rx="3"/>
<use fill="#000" xlink:href="#bf9k9cj5la"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 447 B

@@ -0,0 +1,9 @@
<svg width="12" height="12" viewBox="0 0 12 12" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<defs>
<path id="3nvolf8jsa" d="M4.9 8.45 2.4 5.97l.795-.785L4.9 6.875 8.605 3.2l.795.79z"/>
</defs>
<g fill="none" fill-rule="evenodd">
<rect stroke="#348CFD" fill="#348CFD" x=".5" y=".5" width="11" height="11" rx="3"/>
<use fill="#000" xlink:href="#3nvolf8jsa"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 444 B

@@ -0,0 +1,3 @@
<svg width="15" height="15" viewBox="0 0 15 15" xmlns="http://www.w3.org/2000/svg">
<rect x="807.5" y="496.5" width="14" height="14" rx="3" transform="translate(-807 -496)" fill="#000" stroke="#5ACCE6" fill-rule="evenodd"/>
</svg>

After

Width:  |  Height:  |  Size: 235 B

@@ -0,0 +1,3 @@
<svg width="12" height="12" viewBox="0 0 12 12" xmlns="http://www.w3.org/2000/svg">
<rect x=".5" y=".5" width="11" height="11" rx="3" stroke="#348CFD" fill="none" fill-rule="evenodd"/>
</svg>

After

Width:  |  Height:  |  Size: 196 B

@@ -0,0 +1,9 @@
<svg width="19" height="19" viewBox="0 0 19 19" xmlns="http://www.w3.org/2000/svg">
<g fill="none" stroke="currentColor" fill-rule="evenodd">
<g stroke="currentColor" stroke-linecap="round" stroke-linejoin="round">
<path d="m9.75.5-8.5 3.719 8.5 3.718 8.5-3.718z"/>
<path d="M1.25 4.219v10.093l8.5 3.72 8.5-3.72V4.22M9.75 7.938v10.093"/>
</g>
<path fill="currentColor" d="M1.25 4.576v9.736l8.05 3.72.45-10.095z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 482 B

@@ -0,0 +1,6 @@
<svg width="17" height="17" viewBox="0 0 17 17" xmlns="http://www.w3.org/2000/svg">
<g transform="translate(1 1)" fill="none" fill-rule="evenodd">
<circle stroke="#1C498E" stroke-width=".5" cx="7.5" cy="7.5" r="7.5"/>
<path fill="#1C498E" fill-rule="nonzero" d="M6.504 3 5.5 4.004l3.259 3.259L5.5 10.52l1.004 1.004 4.263-4.262z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 367 B

@@ -0,0 +1,6 @@
<svg width="17" height="17" viewBox="0 0 17 17" xmlns="http://www.w3.org/2000/svg">
<g transform="translate(1 1)" fill="none" fill-rule="evenodd">
<circle stroke="#348CFD" stroke-width=".5" cx="7.5" cy="7.5" r="7.5"/>
<path fill="#348CFD" fill-rule="nonzero" d="M6.504 3 5.5 4.004l3.259 3.259L5.5 10.52l1.004 1.004 4.263-4.262z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 367 B

@@ -0,0 +1,6 @@
<svg width="17" height="17" viewBox="0 0 17 17" xmlns="http://www.w3.org/2000/svg">
<g transform="matrix(-1 0 0 1 16 1)" fill="none" fill-rule="evenodd">
<circle stroke="#1C498E" stroke-width=".5" cx="7.5" cy="7.5" r="7.5"/>
<path fill="#1C498E" fill-rule="nonzero" d="M6.504 3 5.5 4.004l3.259 3.259L5.5 10.52l1.004 1.004 4.263-4.262z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 374 B

@@ -0,0 +1,6 @@
<svg width="17" height="17" viewBox="0 0 17 17" xmlns="http://www.w3.org/2000/svg">
<g transform="matrix(-1 0 0 1 16 1)" fill="none" fill-rule="evenodd">
<circle stroke="#348CFD" stroke-width=".5" cx="7.5" cy="7.5" r="7.5"/>
<path fill="#348CFD" fill-rule="nonzero" d="M6.504 3 5.5 4.004l3.259 3.259L5.5 10.52l1.004 1.004 4.263-4.262z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 374 B

@@ -0,0 +1,7 @@
<svg width="14" height="14" viewBox="0 0 14 14" xmlns="http://www.w3.org/2000/svg">
<g fill="#348CFD" fill-rule="evenodd">
<path d="M7 .102A6.899 6.899 0 1 0 7 13.9 6.899 6.899 0 0 0 7 .102zm0 .875a6.024 6.024 0 1 1 0 12.048A6.024 6.024 0 0 1 7 .977z"/>
<path d="M6.462 5.486c.503 0 .917.38.97.87l.006.106v3.769a.438.438 0 0 1-.868.078l-.007-.078V6.46a.101.101 0 0 0-.07-.095l-.031-.005H5.385a.437.437 0 0 1-.079-.868l.079-.007h1.077zM6.192 2.793l.089.006a.707.707 0 1 1-.177 0l.088-.006z"/>
<path d="M8.615 9.794c.242 0 .438.224.438.5 0 .246-.155.45-.359.492l-.079.008h-3.23c-.242 0-.438-.224-.438-.5 0-.245.155-.45.359-.492l.079-.008h3.23z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 689 B

@@ -0,0 +1,5 @@
<svg width="47" height="47" viewBox="0 0 47 47" xmlns="http://www.w3.org/2000/svg">
<g fill="#FFF" fill-rule="nonzero">
<path d="M43.585 0H29.763a3.415 3.415 0 0 0-3.415 3.415v13.822a3.415 3.415 0 0 0 3.415 3.415h13.822A3.415 3.415 0 0 0 47 17.237V3.415A3.415 3.415 0 0 0 43.585 0zM29.763 1.366h13.822c1.132 0 2.05.917 2.05 2.049v13.822a2.049 2.049 0 0 1-2.05 2.049H29.763a2.049 2.049 0 0 1-2.049-2.05V3.416c0-1.132.918-2.05 2.05-2.05zM17.237 0H3.415A3.415 3.415 0 0 0 0 3.415v13.822a3.415 3.415 0 0 0 3.415 3.415h13.822a3.415 3.415 0 0 0 3.415-3.415V3.415A3.415 3.415 0 0 0 17.237 0zM3.415 1.366h13.822c1.131 0 2.049.917 2.049 2.049v13.822a2.049 2.049 0 0 1-2.05 2.049H3.416a2.049 2.049 0 0 1-2.05-2.05V3.416c0-1.132.918-2.05 2.05-2.05zM43.585 26.348H29.763a3.415 3.415 0 0 0-3.415 3.415v13.822A3.415 3.415 0 0 0 29.763 47h13.822A3.415 3.415 0 0 0 47 43.585V29.763a3.415 3.415 0 0 0-3.415-3.415zm-13.822 1.366h13.822c1.132 0 2.05.918 2.05 2.05v13.821a2.049 2.049 0 0 1-2.05 2.05H29.763a2.049 2.049 0 0 1-2.049-2.05V29.763c0-1.131.918-2.049 2.05-2.049zM17.237 26.348H3.415A3.415 3.415 0 0 0 0 29.763v13.822A3.415 3.415 0 0 0 3.415 47h13.822a3.415 3.415 0 0 0 3.415-3.415V29.763a3.415 3.415 0 0 0-3.415-3.415zM3.415 27.714h13.822c1.131 0 2.049.918 2.049 2.05v13.821a2.049 2.049 0 0 1-2.05 2.05H3.416a2.049 2.049 0 0 1-2.05-2.05V29.763c0-1.131.918-2.049 2.05-2.049z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.4 KiB

@@ -0,0 +1,5 @@
<svg width="16" height="11" viewBox="0 0 16 11" xmlns="http://www.w3.org/2000/svg">
<g stroke="#348CFD" stroke-width="1.5" fill="none" fill-rule="evenodd" stroke-linecap="round" stroke-linejoin="round">
<path d="M1 5.406h9.834M6.765 1.337l4.069 4.07-4.07 4.068M15.242 9.475V1.337"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 311 B

@@ -0,0 +1,5 @@
<svg width="17" height="10" viewBox="0 0 17 10" xmlns="http://www.w3.org/2000/svg">
<g stroke="#348CFD" stroke-width="1.5" fill="none" fill-rule="evenodd" stroke-linecap="round" stroke-linejoin="round">
<path d="M10.856 5.078H1M5.078 1 1 5.078l4.078 4.078M15.582 9.156V1"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 302 B

@@ -0,0 +1,6 @@
<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="M5.25 9a1.75 1.75 0 1 0-.001 3.499A1.75 1.75 0 0 0 5.25 9zm10.5 0a1.75 1.75 0 1 0-.001 3.499A1.75 1.75 0 0 0 15.75 9zM10.5 9a1.75 1.75 0 1 0-.001 3.499A1.75 1.75 0 0 0 10.5 9z" fill="#348CFD" fill-rule="nonzero"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 405 B

@@ -0,0 +1,6 @@
<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 fill="#FFF" d="m13.837 11-6 4.837V6.163z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 232 B

@@ -0,0 +1,11 @@
<svg width="25px" height="25px" viewBox="0 0 25 25" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<g id="reference-lines" stroke="currentColor" stroke-width="1" fill="none" fill-rule="evenodd">
<g id="Group" transform="translate(3.250000, 4.926395)">
<path d="M0.75,8.32360458 L2.75,8.32360458 C3.16421356,8.32360458 3.5,7.98781815 3.5,7.57360458 C3.5,7.15939102 3.16421356,6.82360458 2.75,6.82360458 L0.75,6.82360458 C0.335786438,6.82360458 0,7.15939102 0,7.57360458 C0,7.98781815 0.335786438,8.32360458 0.75,8.32360458 Z M5.75,8.32360458 L7.75,8.32360458 C8.16421356,8.32360458 8.5,7.98781815 8.5,7.57360458 C8.5,7.15939102 8.16421356,6.82360458 7.75,6.82360458 L5.75,6.82360458 C5.33578644,6.82360458 5,7.15939102 5,7.57360458 C5,7.98781815 5.33578644,8.32360458 5.75,8.32360458 Z M10.75,8.32360458 L12.75,8.32360458 C13.1642136,8.32360458 13.5,7.98781815 13.5,7.57360458 C13.5,7.15939102 13.1642136,6.82360458 12.75,6.82360458 L10.75,6.82360458 C10.3357864,6.82360458 10,7.15939102 10,7.57360458 C10,7.98781815 10.3357864,8.32360458 10.75,8.32360458 Z M15.75,8.32360458 L17.75,8.32360458 C18.1642136,8.32360458 18.5,7.98781815 18.5,7.57360458 C18.5,7.15939102 18.1642136,6.82360458 17.75,6.82360458 L15.75,6.82360458 C15.3357864,6.82360458 15,7.15939102 15,7.57360458 C15,7.98781815 15.3357864,8.32360458 15.75,8.32360458 Z" id="Line-7" fill="#000000" fill-rule="nonzero"></path>
<g id="Group-2" transform="translate(1.250000, 0.000000)" stroke="currentColor" stroke-width="1.5">
<path d="M0,5 L0,1 C-6.76353751e-17,0.44771525 0.44771525,1.01453063e-16 1,0 L15,0 C15.5522847,-1.01453063e-16 16,0.44771525 16,1 L16,5 L16,5" id="Path-7"></path>
<path d="M0,15 L0,11 C-6.76353751e-17,10.4477153 0.44771525,10 1,10 L15,10 C15.5522847,10 16,10.4477153 16,11 L16,15 L16,15" id="Path-7" transform="translate(8.000000, 12.500000) scale(1, -1) translate(-8.000000, -12.500000) "></path>
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.0 KiB

+8
View File
@@ -0,0 +1,8 @@
<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 stroke="#348CFD" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.25">
<path d="M10.5 5.5v10M15.5 10.5h-10"/>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 334 B

@@ -0,0 +1,9 @@
<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 stroke="#FFF" stroke-linecap="round" stroke-linejoin="round">
<path d="m12.625 5.872 2.232 2.231-7.034 7.035-3.72 1.488 1.487-3.72 7.035-7.034z"/>
<path d="M14.753 4.5c.404 0 .808.154 1.116.462.308.309.462.713.462 1.117s-.154.808-.462 1.116L14.753 8.31 12.52 6.08l1.115-1.117a1.574 1.574 0 0 1 1.117-.462z"/>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 531 B

@@ -0,0 +1,11 @@
<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="#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="M16.396 5 5.02 16.375"/>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 782 B

@@ -0,0 +1,11 @@
<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>

After

Width:  |  Height:  |  Size: 782 B

@@ -0,0 +1,9 @@
<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="#FFF" stroke-linecap="round" stroke-linejoin="round"/>
<ellipse fill="#FFF" cx="6.68" cy="3.792" rx="2.033" ry="2.042"/>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 552 B

@@ -0,0 +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"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 479 B

@@ -0,0 +1,9 @@
<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>

After

Width:  |  Height:  |  Size: 558 B

@@ -0,0 +1,11 @@
<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>

After

Width:  |  Height:  |  Size: 773 B

@@ -0,0 +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"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 483 B

@@ -0,0 +1,8 @@
<svg width="22" height="22" viewBox="0 0 22 22" xmlns="http://www.w3.org/2000/svg">
<g fill="none" fill-rule="evenodd">
<rect stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" x="1.5" y="16.37" width="4.13" height="4.13" rx="1"/>
<rect stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" x="16.37" y="1.5" width="4.13" height="4.13" rx="1"/>
<path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" d="M5.388 16.612 16.612 5.388"/>
<path d="M0 0h22v22H0z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 565 B

@@ -0,0 +1,7 @@
<svg width="22" height="22" viewBox="0 0 22 22" xmlns="http://www.w3.org/2000/svg">
<g fill="none" fill-rule="evenodd">
<path d="M3.638 13.16a17.265 17.265 0 0 1 3.652-1.586c.565-.21.626-1.504.221-1.95-.584-.642-1.08-1.396-1.08-3.215a2.572 2.572 0 0 1 2.632-2.811 2.572 2.572 0 0 1 2.631 2.811c0 1.822-.495 2.573-1.08 3.215-.404.446-.343 1.74.222 1.95 1.275.386 2.5.919 3.652 1.587" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M17.15 11.064V2.369a.87.87 0 0 0-.87-.869H2.37a.87.87 0 0 0-.87.87v13.91c0 .48.39.87.87.87h8.694M15.846 20.628l-3.043.87.87-3.044 5.2-5.2a1.537 1.537 0 0 1 2.174 2.173l-5.201 5.2zM18.221 13.905l2.174 2.174M13.672 18.454l2.174 2.174" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M0 0h22v22H0z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 837 B

@@ -0,0 +1,10 @@
<svg width="23" height="22" viewBox="0 0 23 22" xmlns="http://www.w3.org/2000/svg">
<g fill="none" fill-rule="evenodd">
<path d="m13.34 11.74-2.971.425.425-2.971 7.64-7.64A1.8 1.8 0 1 1 20.98 4.1l-7.64 7.64zM8.644 19.034h7.383c.865 0 1.566-.701 1.566-1.566V10.03M12.504 4.94H5.066C4.2 4.94 3.5 5.643 3.5 6.508v1.972" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"/>
<path stroke="currentColor" d="m11.5 8.5 2.333 2.333"/>
<g stroke="currentColor" stroke-linecap="round" stroke-linejoin="round">
<path d="m1 14 2.5-2.5L6 14M1 17.5 3.5 20 6 17.5"/>
</g>
<path d="M0 0h22v22H0z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 672 B

@@ -0,0 +1,11 @@
<svg width="22" height="22" viewBox="0 0 22 22" xmlns="http://www.w3.org/2000/svg">
<g fill="none" fill-rule="evenodd">
<circle stroke="currentColor" cx="11.037" cy="10.912" r="8"/>
<g stroke="currentColor">
<path stroke-linecap="square" d="m11.354 3.575-7.779 7.779M17.364 6.757 6.757 17.364"/>
<path d="m18.955 9.763-9.192 9.192"/>
<path stroke-linecap="square" d="M15.066 4.46 4.459 15.065"/>
</g>
<path d="M0 0h22v22H0z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 516 B

@@ -0,0 +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"/>
</svg>

After

Width:  |  Height:  |  Size: 151 B

@@ -0,0 +1,3 @@
<svg width="6" height="5" viewBox="0 0 6 5" xmlns="http://www.w3.org/2000/svg">
<path d="m3 0 3 4.5H0z" fill="#726F7E" fill-rule="evenodd"/>
</svg>

After

Width:  |  Height:  |  Size: 152 B

+4 -4
View File
@@ -1,6 +1,6 @@
/* CUSTOM OHIF SCROLLBAR */
.ohif-scrollbar {
scrollbar-color: #3E5967 transparent;
scrollbar-color: #173239 transparent;
scrollbar-gutter: stable;
}
@@ -9,7 +9,7 @@
}
.ohif-scrollbar::-webkit-scrollbar {
@apply w-2;
width: 8px;
}
.ohif-scrollbar::-webkit-scrollbar-track {
@@ -19,12 +19,12 @@
.ohif-scrollbar::-webkit-scrollbar-thumb {
@apply rounded;
@apply bg-primary-main;
background-color: #3e5967;
background-color: #173239;
}
.ohif-scrollbar::-webkit-scrollbar-thumb:window-inactive {
@apply bg-primary-main;
background-color: #3e5967;
background-color: #173239;
}
/* INVISIBLE SCROLLBAR */
@@ -46,7 +46,7 @@ const variantClasses = {
outlined: {
default: '',
primary: '',
secondary: '',
secondary: 'border border-secondary-light',
white: '',
black:
'border-primary-main group-hover:bg-primary-main group-hover:border-black',
@@ -68,6 +68,12 @@ const orientationClasses = {
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 ButtonGroup = ({
children,
className,
@@ -78,6 +84,7 @@ const ButtonGroup = ({
rounded = 'medium',
size = 'medium',
variant = 'outlined',
splitBorder = true,
...other
}) => {
const ref = useRef(null);
@@ -101,13 +108,24 @@ const ButtonGroup = ({
ref={ref}
{...other}
>
{React.Children.map(children, child => {
{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),
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,
@@ -0,0 +1,69 @@
import React, { useState, useCallback } from 'react';
import PropTypes, { string } from 'prop-types';
import Icon from '../Icon';
import Typography from '../Typography';
/**
* REACT CheckBox component
* it has two props, checked and onChange
* checked is a boolean value
* onChange is a function that will be called when the checkbox is clicked
*
* CheckBox is a component that allows you to use as a boolean
*/
const CheckBox: React.FC<{
checked: boolean;
onChange: (state) => void;
className?: string;
label: string;
labelClassName?: string;
labelVariant?: string;
}> = ({
checked,
onChange,
label,
labelClassName,
labelVariant = 'body',
className,
}) => {
const [isChecked, setIsChecked] = useState(checked);
const handleClick = useCallback(() => {
setIsChecked(!isChecked);
onChange(!isChecked);
}, [isChecked, onChange]);
return (
<div
className={`flex items-center cursor-pointer space-x-1 ${
className ? className : ''
}`}
onClick={handleClick}
>
{isChecked ? (
<Icon name="checkbox-checked" />
) : (
<Icon name="checkbox-unchecked" />
)}
<Typography
variant={labelVariant ?? 'subtitle'}
component="p"
className={labelClassName ?? 'text-white '}
>
{label}
</Typography>
</div>
);
};
CheckBox.propTypes = {
checked: PropTypes.bool,
onChange: PropTypes.func,
label: PropTypes.string,
labelClassName: PropTypes.string,
labelVariant: PropTypes.string,
};
export default CheckBox;
@@ -0,0 +1,43 @@
import CheckBox from '../CheckBox';
import { ArgsTable, Story, Canvas, Meta } from '@storybook/addon-docs';
export const argTypes = {
component: CheckBox,
title: 'Components/CheckBox',
};
<Meta title="Components/CheckBox" component={CheckBox} />
export const CheckBoxTemplate = args => <CheckBox {...args} />
<Heading title="CheckBox" componentRelativePath="CheckBox/CheckBox.tsx" />
- [Overview](#overview)
- [Props](#props)
- [Contribute](#contribute)
## Overview
CheckBox is a component that allows you to use as a boolean value
<Canvas>
<Story
name="Overview"
args={{
checked: false,
onChange: () => console.log('on change callback'),
label: "checkBoxLabel",
labelClassName: "text-black text-sm",
}}
>
{CheckBoxTemplate.bind({})}
</Story>
</Canvas>
## Props
<ArgsTable of={CheckBox} />
## Contribute
<Footer componentRelativePath="CheckBox/__stories__/CheckBox.stories.mdx" />
@@ -0,0 +1,3 @@
import CheckBox from './CheckBox';
export default CheckBox;
@@ -4,12 +4,12 @@ import classnames from 'classnames';
import { Icon, Typography } from '../';
// TODO: Add loading spinner to OHIF + use it here.
const EmptyStudies = ({ className, isLoading }) => {
const EmptyStudies = ({ className }) => {
return (
<div className={classnames('flex-col inline-flex items-center', className)}>
<Icon name="magnifier" className="mb-4" />
<Typography className="text-primary-light" variant="h5">
{!isLoading ? 'No studies available' : 'Loading...'}
{'No studies available'}
</Typography>
</div>
);
@@ -38,6 +38,21 @@ import sortingActiveDown from './../../assets/icons/sorting-active-down.svg';
import sortingActiveUp from './../../assets/icons/sorting-active-up.svg';
import tracked from './../../assets/icons/tracked.svg';
import unlink from './../../assets/icons/unlink.svg';
import checkboxChecked from './../../assets/icons/checkbox-checked.svg';
import checkboxUnchecked from './../../assets/icons/checkbox-unchecked.svg';
import iconNextInactive from './../../assets/icons/icon-next-inactive.svg';
import iconNext from './../../assets/icons/icon-next.svg';
import iconPrevInactive from './../../assets/icons/icon-prev-inactive.svg';
import iconPrev from './../../assets/icons/icon-prev.svg';
import navigationPanelRightHide from './../../assets/icons/navigation-panel-right-hide.svg';
import navigationPanelRightReveal from './../../assets/icons/navigation-panel-right-reveal.svg';
import tabLinear from './../../assets/icons/tab-linear.svg';
import tabPatientInfo from './../../assets/icons/tab-patient-info.svg';
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 loadingOHIFMark from './../../assets/icons/loading-ohif-mark.svg';
/** Tools */
import toolZoom from './../../assets/icons/tool-zoom.svg';
@@ -64,6 +79,21 @@ import toolRectangle from './../../assets/icons/tool-rectangle.svg';
import toolFusionColor from './../../assets/icons/tool-fusion-color.svg';
import toolCreateThreshold from './../../assets/icons/tool-create-threshold.svg';
import editPatient from './../../assets/icons/edit-patient.svg';
import panelGroupMore from './../../assets/icons/panel-group-more.svg';
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 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';
/** Old OHIF */
import oldTrash from './../../assets/icons/old-trash.svg';
@@ -108,6 +138,20 @@ const ICONS = {
sorting: sorting,
tracked: tracked,
unlink: unlink,
'panel-group-more': panelGroupMore,
'panel-group-open-close': panelGroupOpenClose,
'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,
'checkbox-checked': checkboxChecked,
'checkbox-unchecked': checkboxUnchecked,
'loading-ohif-mark': loadingOHIFMark,
/** Tools */
'tool-zoom': toolZoom,
@@ -134,6 +178,22 @@ const ICONS = {
'tool-fusion-color': toolFusionColor,
'tool-create-threshold': toolCreateThreshold,
'edit-patient': editPatient,
'icon-mpr': iconMPR,
'icon-next-inactive': iconNextInactive,
'icon-next': iconNext,
'icon-prev-inactive': iconPrevInactive,
'icon-prev': iconPrev,
'navigation-panel-right-hide': navigationPanelRightHide,
'navigation-panel-right-reveal': navigationPanelRightReveal,
'tab-linear': tabLinear,
'tab-patient-info': tabPatientInfo,
'tab-roi-threshold': tabROIThreshold,
'tab-segmentation': tabSegmentation,
'ui-arrow-down': uiArrowDown,
'ui-arrow-up': uiArrowUp,
'checkbox-default': checkboxDefault,
'checkbox-active': checkboxActive,
'tool-referenceLines': referenceLines,
/** Old OHIF */
'old-trash': oldTrash,
@@ -8,6 +8,7 @@ class ImageScrollbar extends PureComponent {
max: PropTypes.number.isRequired,
height: PropTypes.string.isRequired,
onChange: PropTypes.func.isRequired,
onContextMenu: PropTypes.func,
};
render() {
@@ -20,7 +21,7 @@ class ImageScrollbar extends PureComponent {
};
return (
<div className="scroll">
<div className="scroll" onContextMenu={this.props.onContextMenu}>
<div className="scroll-holder">
<input
// adding mousetrap let the mousetrap know about the scrollbar otherwise,
@@ -66,4 +67,8 @@ class ImageScrollbar extends PureComponent {
};
}
ImageScrollbar.defaultProps = {
onContextMenu: e => e.preventDefault(),
};
export default ImageScrollbar;
@@ -0,0 +1,17 @@
input[type='text'] {
-webkit-appearance: none;
}
input[type='text']:focus {
border: none;
outline: none;
}
.up-arrowsize svg {
width: 7px;
height: 7px;
}
.up-arrowsize svg path {
fill: #726f7e;
}
@@ -0,0 +1,96 @@
import React, { useState, useCallback } from 'react';
import IconButton from '../IconButton';
import Icon from '../Icon';
import './InputNumber.css';
/**
* React Number Input component'
* it has two props, value and onChange
* value is a number value
* onChange is a function that will be called when the number input is changed
* it can get changed by clicking on the up and down buttons
* or by typing a number in the input
*/
const sizesClasses = {
sm: 'w-[45px] h-[28px]',
};
const InputNumber: React.FC<{
value: number;
onChange: (value) => void;
step: number;
size?: string;
className?: string;
}> = ({ value, onChange, step = 1, className, size = 'sm' }) => {
const [numberValue, setNumberValue] = useState(value);
const handleChange = useCallback(
e => {
const numberValue = e.target.value;
setNumberValue(numberValue);
onChange(numberValue);
},
[onChange, setNumberValue]
);
const handleIncrement = useCallback(
e => {
const newNum = Number(numberValue) + step;
setNumberValue(newNum);
onChange(newNum);
},
[onChange, setNumberValue, step, numberValue]
);
const handleDecrement = useCallback(
e => {
const newNum = Number(numberValue) - step;
setNumberValue(newNum);
onChange(newNum);
},
[onChange, setNumberValue, step, numberValue]
);
return (
<div
className={`flex items-center bg-black border-2 px-1 overflow-hidden justify-center border-secondary-light rounded-md ${
sizesClasses[size]
} ${className ? className : ''}`}
>
<div className="flex">
<input
type="text"
value={numberValue}
onChange={handleChange}
className={`bg-black text-white text-[12px] w-full text-center`}
/>
<div className="up-arrowsize flex flex-col items-center justify-around">
<IconButton
id={'down-arrow-icon'}
variant="text"
color="inherit"
size="initial"
className="text-[#726f7e] transform rotate-180"
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>
</div>
</div>
);
};
export default InputNumber;
@@ -0,0 +1,47 @@
import InputNumber from '../InputNumber';
import { ArgsTable, Story, Canvas, Meta } from '@storybook/addon-docs';
export const argTypes = {
component: InputNumber,
title: 'Components/InputNumber',
};
<Meta title="Components/InputNumber" component={InputNumber} />
export const InputNumberTemplate = args => (
<div className="w-16">
<InputNumber {...args} />
</div>
)
<Heading title="InputNumber" componentRelativePath="InputNumber/InputNumber.tsx" />
- [Overview](#overview)
- [Props](#props)
- [Contribute](#contribute)
## Overview
InputNumber is a component that allows you to use as a number value
<Canvas>
<Story
name="Overview"
args={{
step: 1,
value: 3,
size: 'sm',
onChange: () => console.log('input range change'),
}}
>
{InputNumberTemplate.bind({})}
</Story>
</Canvas>
## Props
<ArgsTable of={InputNumber} />
## Contribute
<Footer componentRelativePath="InputNumber/__stories__/InputNumber.stories.mdx" />
@@ -0,0 +1,3 @@
import InputNumber from './InputNumber';
export default InputNumber;
@@ -0,0 +1,26 @@
input[type='range'] {
-webkit-appearance: none;
}
input[type='range']::-webkit-slider-thumb {
-webkit-appearance: none;
border: none;
height: 10px;
width: 10px;
border-radius: 50%;
background: #5acce6;
}
input[type='range']::-moz-range-thumb {
-webkit-appearance: none;
border: none;
height: 10px;
width: 10px;
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;
}
@@ -0,0 +1,86 @@
import React, { useState, useCallback } from 'react';
import classNames from 'classnames';
import Typography from '../Typography';
import './InputRange.css';
/**
* React Range Input component
* it has two props, value and onChange
* value is a number value
* onChange is a function that will be called when the range input is changed
*
*
*/
const InputRange: React.FC<{
value: number;
onChange: (value) => void;
minValue: number;
maxValue: number;
step: number;
unit?: string;
containerClassName?: string;
inputClassName?: string;
labelClassName?: string;
labelVariant?: string;
}> = ({
value,
onChange,
minValue,
maxValue,
step = 1,
unit = '',
containerClassName,
inputClassName,
labelClassName,
labelVariant,
}) => {
const [rangeValue, setRangeValue] = useState(value);
const handleChange = useCallback(
e => {
const rangeValue = e.target.value;
setRangeValue(rangeValue);
onChange(rangeValue);
},
[onChange, setRangeValue]
);
const rangeValuePercentage =
((rangeValue - minValue) / (maxValue - minValue)) * 100;
return (
<div
className={`flex items-center cursor-pointer space-x-1 ${
containerClassName ? containerClassName : ''
}`}
>
<input
type="range"
min={minValue}
max={maxValue}
value={rangeValue}
className={`appearance-none h-[3px] rounded-lg input-range-thumb-design ${
inputClassName ? inputClassName : ''
}`}
style={{
background: `linear-gradient(to right, #5acce6 0%, #5acce6 ${rangeValuePercentage -
10}%, #3a3f99 ${rangeValuePercentage + 10}%, #3a3f99 100%)`,
}}
onChange={handleChange}
id="myRange"
step={step}
/>
<Typography
variant={labelVariant ?? 'subtitle'}
component="p"
className={classNames('w-8', labelClassName ?? 'text-white')}
>
{rangeValue}
{unit}
</Typography>
</div>
);
};
export default InputRange;
@@ -0,0 +1,50 @@
import InputRange from '../InputRange';
import { ArgsTable, Story, Canvas, Meta } from '@storybook/addon-docs';
export const argTypes = {
component: InputRange,
title: 'Components/InputRange',
};
<Meta title="Components/InputRange" component={InputRange} />
export const RangeInputTemplate = args => (
<div className="w-3">
<InputRange {...args} />
</div>
)
<Heading title="InputRange" componentRelativePath="InputRange/InputRange.tsx" />
- [Overview](#overview)
- [Props](#props)
- [Contribute](#contribute)
## Overview
InputRange is a component that allows you to use as a boolean value
<Canvas>
<Story
name="Overview"
args={{
value: 10,
minValue: 0,
maxValue: 300,
step: 10,
unit: '%',
onChange: () => console.log('input range change'),
labelClassName: 'text-black'
}}
>
{RangeInputTemplate.bind({})}
</Story>
</Canvas>
## Props
<ArgsTable of={InputRange} />
## Contribute
<Footer componentRelativePath="InputRange/__stories__/InputRange.stories.mdx" />
@@ -0,0 +1,3 @@
import InputRange from './InputRange';
export default InputRange;
@@ -0,0 +1,793 @@
import React from 'react';
import Lottie from 'lottie-react';
import classNames from 'classnames';
const LoadingAnimation = {
v: '5.9.6',
fr: 60,
ip: 0,
op: 106,
w: 215,
h: 215,
nm: 'ohif-progress',
ddd: 0,
assets: [],
layers: [
{
ddd: 0,
ind: 1,
ty: 4,
nm: 'Layer 1/ohif-shape-new Outlines',
sr: 1,
ks: {
o: { a: 0, k: 100, ix: 11 },
r: {
a: 1,
k: [
{
i: { x: [0.833], y: [0.797] },
o: { x: [0.274], y: [0.135] },
t: 25,
s: [0],
},
{
i: { x: [0.828], y: [0.97] },
o: { x: [0.446], y: [0.039] },
t: 52,
s: [90],
},
{
i: { x: [0.83], y: [1.207] },
o: { x: [0.167], y: [-0.246] },
t: 65,
s: [90],
},
{ t: 85, s: [180] },
],
ix: 10,
},
p: {
a: 1,
k: [
{
i: { x: 0.667, y: 0.667 },
o: { x: 0.167, y: 0.167 },
t: 0,
s: [130.5, 131.75, 0],
to: [0, 0, 0],
ti: [0, 0, 0],
},
{
i: { x: 0.833, y: 0.806 },
o: { x: 0.167, y: 0.006 },
t: 9,
s: [130.5, 131.75, 0],
to: [2.917, 2.875, 0],
ti: [-2.917, -2.875, 0],
},
{
i: { x: 0.872, y: 0.872 },
o: { x: 0.167, y: 0.167 },
t: 40,
s: [148, 149, 0],
to: [0, 0, 0],
ti: [0, 0, 0],
},
{
i: { x: 0.856, y: 0.811 },
o: { x: 0.167, y: 0.17 },
t: 52,
s: [148, 149, 0],
to: [-0.881, -0.866, 0],
ti: [1.995, 1.961, 0],
},
{
i: { x: 0.833, y: 0.833 },
o: { x: 0.167, y: 0 },
t: 76,
s: [108.4, 113.023, 0],
to: [-1.196, -1.176, 0],
ti: [-2.169, -1.843, 0],
},
{
i: { x: 0.833, y: 0.832 },
o: { x: 0.167, y: 0.167 },
t: 85,
s: [108.4, 113, 0],
to: [1.272, 1.081, 0],
ti: [-1.819, -1.575, 0],
},
{
i: { x: 0.833, y: 0.941 },
o: { x: 0.167, y: 0.008 },
t: 95,
s: [115.214, 118.607, 0],
to: [7.038, 6.091, 0],
ti: [1.198, 1.177, 0],
},
{ t: 106, s: [130.5, 131.8, 0] },
],
ix: 2,
l: 2,
},
a: { a: 0, k: [21, 21, 0], ix: 1, l: 2 },
s: { a: 0, k: [100, 100, 100], ix: 6, l: 2 },
},
ao: 0,
shapes: [
{
ty: 'gr',
it: [
{
ind: 0,
ty: 'sh',
ix: 1,
ks: {
a: 0,
k: {
i: [
[-1.657, 0],
[0, 0],
[0, -1.657],
[0, 0],
[1.657, 0],
[0, 0],
[0, 1.657],
[0, 0],
],
o: [
[0, 0],
[1.657, 0],
[0, 0],
[0, 1.657],
[0, 0],
[-1.657, 0],
[0, 0],
[0, -1.657],
],
v: [
[-13, -16],
[13, -16],
[16, -13],
[16, 13],
[13, 16],
[-13, 16],
[-16, 13],
[-16, -13],
],
c: true,
},
ix: 2,
},
nm: 'Path 1',
mn: 'ADBE Vector Shape - Group',
hd: false,
},
{
ty: 'st',
c: { a: 0, k: [1, 1, 1, 1], ix: 3 },
o: { a: 0, k: 100, ix: 4 },
w: { a: 0, k: 2, ix: 5 },
lc: 1,
lj: 1,
ml: 10,
bm: 0,
nm: 'Stroke 1',
mn: 'ADBE Vector Graphic - Stroke',
hd: false,
},
{
ty: 'tr',
p: { a: 0, k: [21, 21], ix: 2 },
a: { a: 0, k: [0, 0], ix: 1 },
s: { a: 0, k: [100, 100], ix: 3 },
r: { a: 0, k: 0, ix: 6 },
o: { a: 0, k: 100, ix: 7 },
sk: { a: 0, k: 0, ix: 4 },
sa: { a: 0, k: 0, ix: 5 },
nm: 'Transform',
},
],
nm: 'Group 1',
np: 2,
cix: 2,
bm: 0,
ix: 1,
mn: 'ADBE Vector Group',
hd: false,
},
],
ip: 0,
op: 106,
st: 0,
ct: 1,
bm: 0,
},
{
ddd: 0,
ind: 3,
ty: 4,
nm: 'Layer 1/ohif-shape-new Outlines',
sr: 1,
ks: {
o: { a: 0, k: 100, ix: 11 },
r: {
a: 1,
k: [
{
i: { x: [0.833], y: [0.79] },
o: { x: [0.278], y: [0.149] },
t: 25,
s: [0],
},
{
i: { x: [0.828], y: [1.005] },
o: { x: [0.446], y: [0.022] },
t: 52,
s: [90],
},
{
i: { x: [0.83], y: [1.207] },
o: { x: [0.167], y: [-0.246] },
t: 65,
s: [90],
},
{ t: 85, s: [180] },
],
ix: 10,
},
p: {
a: 1,
k: [
{
i: { x: 0.833, y: 0.833 },
o: { x: 0.167, y: 0.167 },
t: 0,
s: [81, 132, 0],
to: [0, 0, 0],
ti: [0, 0, 0],
},
{
i: { x: 0.833, y: 0.808 },
o: { x: 0.167, y: 0.011 },
t: 9,
s: [81, 132, 0],
to: [-3, 3, 0],
ti: [3, -3, 0],
},
{
i: { x: 0.872, y: 0.872 },
o: { x: 0.167, y: 0.167 },
t: 40,
s: [63, 150, 0],
to: [0, 0, 0],
ti: [0, 0, 0],
},
{
i: { x: 0.856, y: 0.812 },
o: { x: 0.167, y: 0.174 },
t: 52,
s: [63, 150, 0],
to: [0.906, -0.906, 0],
ti: [-2.052, 2.052, 0],
},
{
i: { x: 0.833, y: 0.833 },
o: { x: 0.167, y: 0.002 },
t: 76,
s: [108.685, 113.315, 0],
to: [1.217, -1.217, 0],
ti: [2.674, -1.792, 0],
},
{
i: { x: 0.833, y: 0.834 },
o: { x: 0.167, y: 0.167 },
t: 85,
s: [108.7, 113.3, 0],
to: [-1.593, 1.067, 0],
ti: [2.246, -1.581, 0],
},
{
i: { x: 0.833, y: 0.942 },
o: { x: 0.167, y: 0.014 },
t: 95,
s: [99.965, 118.742, 0],
to: [-8.674, 6.104, 0],
ti: [-1.236, 1.236, 0],
},
{ t: 106, s: [81, 132, 0] },
],
ix: 2,
l: 2,
},
a: { a: 0, k: [21, 21, 0], ix: 1, l: 2 },
s: { a: 0, k: [100, 100, 100], ix: 6, l: 2 },
},
ao: 0,
shapes: [
{
ty: 'gr',
it: [
{
ind: 0,
ty: 'sh',
ix: 1,
ks: {
a: 0,
k: {
i: [
[-1.657, 0],
[0, 0],
[0, -1.657],
[0, 0],
[1.657, 0],
[0, 0],
[0, 1.657],
[0, 0],
],
o: [
[0, 0],
[1.657, 0],
[0, 0],
[0, 1.657],
[0, 0],
[-1.657, 0],
[0, 0],
[0, -1.657],
],
v: [
[-13, -16],
[13, -16],
[16, -13],
[16, 13],
[13, 16],
[-13, 16],
[-16, 13],
[-16, -13],
],
c: true,
},
ix: 2,
},
nm: 'Path 1',
mn: 'ADBE Vector Shape - Group',
hd: false,
},
{
ty: 'st',
c: { a: 0, k: [1, 1, 1, 1], ix: 3 },
o: { a: 0, k: 100, ix: 4 },
w: { a: 0, k: 2, ix: 5 },
lc: 1,
lj: 1,
ml: 10,
bm: 0,
nm: 'Stroke 1',
mn: 'ADBE Vector Graphic - Stroke',
hd: false,
},
{
ty: 'tr',
p: { a: 0, k: [21, 21], ix: 2 },
a: { a: 0, k: [0, 0], ix: 1 },
s: { a: 0, k: [100, 100], ix: 3 },
r: { a: 0, k: 0, ix: 6 },
o: { a: 0, k: 100, ix: 7 },
sk: { a: 0, k: 0, ix: 4 },
sa: { a: 0, k: 0, ix: 5 },
nm: 'Transform',
},
],
nm: 'Group 1',
np: 2,
cix: 2,
bm: 0,
ix: 1,
mn: 'ADBE Vector Group',
hd: false,
},
],
ip: 0,
op: 106,
st: 0,
ct: 1,
bm: 0,
},
{
ddd: 0,
ind: 5,
ty: 4,
nm: 'Layer 1/ohif-shape-new Outlines',
sr: 1,
ks: {
o: { a: 0, k: 100, ix: 11 },
r: {
a: 1,
k: [
{
i: { x: [0.833], y: [0.79] },
o: { x: [0.278], y: [0.149] },
t: 25,
s: [0],
},
{
i: { x: [0.828], y: [0.943] },
o: { x: [0.446], y: [0.022] },
t: 52,
s: [90],
},
{
i: { x: [0.83], y: [1.207] },
o: { x: [0.167], y: [-0.246] },
t: 65,
s: [90],
},
{ t: 85, s: [180] },
],
ix: 10,
},
p: {
a: 1,
k: [
{
i: { x: 0.833, y: 0.833 },
o: { x: 0.167, y: 0.167 },
t: 0,
s: [131, 84.75, 0],
to: [0, 0, 0],
ti: [0, 0, 0],
},
{
i: { x: 0.833, y: 0.808 },
o: { x: 0.167, y: 0.014 },
t: 9,
s: [131, 84.75, 0],
to: [3, -3.125, 0],
ti: [-3, 3.125, 0],
},
{
i: { x: 0.872, y: 0.872 },
o: { x: 0.167, y: 0.167 },
t: 40,
s: [149, 66, 0],
to: [0, 0, 0],
ti: [0, 0, 0],
},
{
i: { x: 0.856, y: 0.812 },
o: { x: 0.167, y: 0.175 },
t: 52,
s: [149, 66, 0],
to: [-0.906, 0.946, 0],
ti: [2.052, -2.143, 0],
},
{
i: { x: 0.833, y: 0.833 },
o: { x: 0.167, y: 0.013 },
t: 76,
s: [109.315, 112.893, 0],
to: [-1.222, 1.276, 0],
ti: [-2.102, 2.732, 0],
},
{
i: { x: 0.833, y: 0.835 },
o: { x: 0.167, y: 0.167 },
t: 85,
s: [109.3, 112.9, 0],
to: [1.242, -1.615, 0],
ti: [-1.796, 2.277, 0],
},
{
i: { x: 0.833, y: 0.943 },
o: { x: 0.167, y: 0.016 },
t: 95,
s: [115.852, 104.071, 0],
to: [6.96, -8.823, 0],
ti: [1.236, -1.29, 0],
},
{ t: 106, s: [131, 84.8, 0] },
],
ix: 2,
l: 2,
},
a: { a: 0, k: [21, 21, 0], ix: 1, l: 2 },
s: { a: 0, k: [100, 100, 100], ix: 6, l: 2 },
},
ao: 0,
shapes: [
{
ty: 'gr',
it: [
{
ind: 0,
ty: 'sh',
ix: 1,
ks: {
a: 0,
k: {
i: [
[-1.657, 0],
[0, 0],
[0, -1.657],
[0, 0],
[1.657, 0],
[0, 0],
[0, 1.657],
[0, 0],
],
o: [
[0, 0],
[1.657, 0],
[0, 0],
[0, 1.657],
[0, 0],
[-1.657, 0],
[0, 0],
[0, -1.657],
],
v: [
[-13, -16],
[13, -16],
[16, -13],
[16, 13],
[13, 16],
[-13, 16],
[-16, 13],
[-16, -13],
],
c: true,
},
ix: 2,
},
nm: 'Path 1',
mn: 'ADBE Vector Shape - Group',
hd: false,
},
{
ty: 'st',
c: { a: 0, k: [1, 1, 1, 1], ix: 3 },
o: { a: 0, k: 100, ix: 4 },
w: { a: 0, k: 2, ix: 5 },
lc: 1,
lj: 1,
ml: 10,
bm: 0,
nm: 'Stroke 1',
mn: 'ADBE Vector Graphic - Stroke',
hd: false,
},
{
ty: 'tr',
p: { a: 0, k: [21, 21], ix: 2 },
a: { a: 0, k: [0, 0], ix: 1 },
s: { a: 0, k: [100, 100], ix: 3 },
r: { a: 0, k: 0, ix: 6 },
o: { a: 0, k: 100, ix: 7 },
sk: { a: 0, k: 0, ix: 4 },
sa: { a: 0, k: 0, ix: 5 },
nm: 'Transform',
},
],
nm: 'Group 1',
np: 2,
cix: 2,
bm: 0,
ix: 1,
mn: 'ADBE Vector Group',
hd: false,
},
],
ip: 0,
op: 106,
st: 0,
ct: 1,
bm: 0,
},
{
ddd: 0,
ind: 7,
ty: 4,
nm: 'Layer 1/ohif-shape-new Outlines',
sr: 1,
ks: {
o: { a: 0, k: 100, ix: 11 },
r: {
a: 1,
k: [
{
i: { x: [0.833], y: [0.79] },
o: { x: [0.278], y: [0.149] },
t: 25,
s: [0],
},
{
i: { x: [0.823], y: [1] },
o: { x: [0.446], y: [-0.058] },
t: 52,
s: [90],
},
{
i: { x: [0.83], y: [1.207] },
o: { x: [0.167], y: [-0.246] },
t: 65,
s: [90],
},
{ t: 85, s: [180] },
],
ix: 10,
},
p: {
a: 1,
k: [
{
i: { x: 0.833, y: 0.833 },
o: { x: 0.167, y: 0.167 },
t: 0,
s: [80.5, 85, 0],
to: [0, 0, 0],
ti: [0, 0, 0],
},
{
i: { x: 0.833, y: 0.806 },
o: { x: 0.167, y: 0.005 },
t: 9,
s: [80.5, 85, 0],
to: [-2.75, -3, 0],
ti: [2.75, 3, 0],
},
{
i: { x: 0.872, y: 0.872 },
o: { x: 0.167, y: 0.167 },
t: 40,
s: [64, 67, 0],
to: [0, 0, 0],
ti: [0, 0, 0],
},
{
i: { x: 0.856, y: 0.812 },
o: { x: 0.167, y: 0.177 },
t: 52,
s: [64, 67, 0],
to: [0.831, 0.906, 0],
ti: [-1.881, -2.052, 0],
},
{
i: { x: 0.833, y: 0.833 },
o: { x: 0.167, y: 0.025 },
t: 76,
s: [109.295, 112.685, 0],
to: [1.112, 1.213, 0],
ti: [2.768, 2.657, 0],
},
{
i: { x: 0.833, y: 0.837 },
o: { x: 0.167, y: 0.167 },
t: 85,
s: [109.3, 112.7, 0],
to: [-1.648, -1.581, 0],
ti: [2.299, 2.234, 0],
},
{
i: { x: 0.833, y: 0.943 },
o: { x: 0.167, y: 0.019 },
t: 95,
s: [100.116, 104.028, 0],
to: [-8.935, -8.681, 0],
ti: [-1.136, -1.239, 0],
},
{ t: 106, s: [80.5, 85, 0] },
],
ix: 2,
l: 2,
},
a: { a: 0, k: [21, 21, 0], ix: 1, l: 2 },
s: { a: 0, k: [100, 100, 100], ix: 6, l: 2 },
},
ao: 0,
shapes: [
{
ty: 'gr',
it: [
{
ind: 0,
ty: 'sh',
ix: 1,
ks: {
a: 0,
k: {
i: [
[-1.657, 0],
[0, 0],
[0, -1.657],
[0, 0],
[1.657, 0],
[0, 0],
[0, 1.657],
[0, 0],
],
o: [
[0, 0],
[1.657, 0],
[0, 0],
[0, 1.657],
[0, 0],
[-1.657, 0],
[0, 0],
[0, -1.657],
],
v: [
[-13, -16],
[13, -16],
[16, -13],
[16, 13],
[13, 16],
[-13, 16],
[-16, 13],
[-16, -13],
],
c: true,
},
ix: 2,
},
nm: 'Path 1',
mn: 'ADBE Vector Shape - Group',
hd: false,
},
{
ty: 'st',
c: { a: 0, k: [1, 1, 1, 1], ix: 3 },
o: { a: 0, k: 100, ix: 4 },
w: { a: 0, k: 2, ix: 5 },
lc: 1,
lj: 1,
ml: 10,
bm: 0,
nm: 'Stroke 1',
mn: 'ADBE Vector Graphic - Stroke',
hd: false,
},
{
ty: 'tr',
p: { a: 0, k: [21, 21], ix: 2 },
a: { a: 0, k: [0, 0], ix: 1 },
s: { a: 0, k: [100, 100], ix: 3 },
r: { a: 0, k: 0, ix: 6 },
o: { a: 0, k: 100, ix: 7 },
sk: { a: 0, k: 0, ix: 4 },
sa: { a: 0, k: 0, ix: 5 },
nm: 'Transform',
},
],
nm: 'Group 1',
np: 2,
cix: 2,
bm: 0,
ix: 1,
mn: 'ADBE Vector Group',
hd: false,
},
],
ip: 0,
op: 106,
st: 0,
ct: 1,
bm: 0,
},
],
markers: [],
};
const LoadingIndicator = ({ className }) => {
return (
<div
className={classNames(
'absolute z-50 top-0 left-0 flex flex-col items-center justify-center',
className
)}
>
<Lottie animationData={LoadingAnimation} />;
</div>
);
};
export default LoadingIndicator;
@@ -0,0 +1,2 @@
import LoadingIndicator from './LoadingIndicator';
export default LoadingIndicator;
@@ -0,0 +1,34 @@
.loading {
background-color: #091731;
height: 8px;
border-radius: 4px;
overflow: hidden;
position: relative;
width: 12em;
}
.infinite-loading-bar {
animation: side2side 2s ease-in-out infinite;
height: 100%;
position: absolute;
border-radius: 4px;
width: 50%;
}
.loading-bar {
animation: side2side 2s ease-in-out infinite;
height: 100%;
border-radius: 4px;
position: absolute;
width: 50%;
}
@keyframes side2side {
0%,
100% {
transform: translateX(-50%);
}
50% {
transform: translateX(150%);
}
}
@@ -0,0 +1,40 @@
import React from 'react';
import classNames from 'classnames';
import { Icon } from '@ohif/ui';
import './LoadingIndicatorProgress.css';
/**
* A React component that renders a loading indicator.
* if progress is not provided, it will render an infinite loading indicator
* if progress is provided, it will render a progress bar
* Optionally a textBlock can be provided to display a message
*/
function LoadingIndicatorProgress({ className, textBlock, progress }) {
return (
<div
className={classNames(
'absolute z-50 top-0 left-0 flex flex-col items-center justify-center space-y-5',
className
)}
>
<Icon name="loading-ohif-mark" className="text-white w-12 h-12" />
<div className="loading">
{progress === undefined || progress === null ? (
<div className="infinite-loading-bar bg-primary-light"></div>
) : (
<div
className="bg-primary-light"
style={{
width: `${progress}%`,
height: '8px',
}}
></div>
)}
</div>
{textBlock}
</div>
);
}
export default LoadingIndicatorProgress;
@@ -0,0 +1,2 @@
import LoadingIndicatorProgress from './LoadingIndicatorProgress';
export default LoadingIndicatorProgress;
@@ -4,8 +4,9 @@ import { useTranslation } from 'react-i18next';
import MeasurementItem from './MeasurementItem';
const MeasurementTable = ({ data, title, amount, onClick, onEdit }) => {
const MeasurementTable = ({ data, title, onClick, onEdit }) => {
const { t } = useTranslation('MeasurementTable');
const amount = data.length;
return (
<div>
@@ -45,7 +46,6 @@ const MeasurementTable = ({ data, title, amount, onClick, onEdit }) => {
};
MeasurementTable.defaultProps = {
amount: null,
data: [],
onClick: () => {},
onEdit: () => {},
@@ -53,7 +53,6 @@ MeasurementTable.defaultProps = {
MeasurementTable.propTypes = {
title: PropTypes.string.isRequired,
amount: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
data: PropTypes.arrayOf(
PropTypes.shape({
uid: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
+1 -1
View File
@@ -9,7 +9,7 @@ const NavBar = ({ className, children, isSticky }) => {
return (
<div
className={classnames(
'flex flex-row items-center bg-secondary-dark px-3 border-b-4 border-black z-20',
'flex flex-row items-center bg-secondary-dark px-1 border-b-4 border-black z-20',
isSticky && stickyClasses,
!isSticky && notStickyClasses,
className
@@ -0,0 +1,149 @@
import React from 'react';
import classnames from 'classnames';
import PropTypes from 'prop-types';
import { Icon, Tooltip } from '../';
import { useTranslation } from 'react-i18next';
const classes = {
infoHeader: 'text-base text-primary-light',
infoText: 'text-base text-white max-w-24 truncate',
firstRow: 'flex flex-col',
row: 'flex flex-col ml-4',
};
function PatientInfo({
patientName,
patientSex,
patientAge,
MRN,
thickness,
spacing,
scanner,
isOpen,
showPatientInfoRef,
}) {
const { t } = useTranslation('PatientInfo');
while (patientAge.charAt(0) === '0') {
patientAge = patientAge.substr(1);
}
return (
<div ref={showPatientInfoRef}>
<Tooltip
isSticky
isDisabled={!isOpen}
position="bottom-right"
content={
isOpen && (
<div className="flex py-2">
<div className="flex pt-1">
<Icon name="info-link" className="w-4 text-primary-main" />
</div>
<div className="flex flex-col ml-2">
<span
className="text-base font-bold text-white"
title={patientName}
>
{patientName}
</span>
<div className="flex pb-4 mt-4 mb-4 border-b border-secondary-main">
<div className={classnames(classes.firstRow)}>
<span className={classnames(classes.infoHeader)}>
{t('Sex')}
</span>
<span
className={classnames(classes.infoText)}
title={patientSex}
>
{patientSex}
</span>
</div>
<div className={classnames(classes.row)}>
<span className={classnames(classes.infoHeader)}>
{t('Age')}
</span>
<span
className={classnames(classes.infoText)}
title={patientAge}
>
{patientAge}
</span>
</div>
<div className={classnames(classes.row)}>
<span className={classnames(classes.infoHeader)}>
{t('MRN')}
</span>
<span className={classnames(classes.infoText)} title={MRN}>
{MRN}
</span>
</div>
</div>
<div className="flex">
<div className={classnames(classes.firstRow)}>
<span className={classnames(classes.infoHeader)}>
{t('Thickness')}
</span>
<span
className={classnames(classes.infoText)}
title={thickness}
>
{thickness}
</span>
</div>
<div className={classnames(classes.row)}>
<span className={classnames(classes.infoHeader)}>
{t('Spacing')}
</span>
<span
className={classnames(classes.infoText)}
title={spacing}
>
{spacing}
</span>
</div>
<div className={classnames(classes.row)}>
<span className={classnames(classes.infoHeader)}>
{t('Scanner')}
</span>
<span
className={classnames(classes.infoText)}
title={scanner}
>
{scanner}
</span>
</div>
</div>
</div>
</div>
)
}
>
<div className="relative flex justify-end cursor-pointer">
<div className="relative">
<Icon name="profile" className="w-5 text-white" />
<Icon
name="info-link"
className="absolute w-5 text-white bg-black"
style={{ right: -7, bottom: -10 }}
/>
</div>
</div>
</Tooltip>
</div>
);
}
PatientInfo.propTypes = {
patientName: PropTypes.string,
patientSex: PropTypes.string,
patientAge: PropTypes.string,
MRN: PropTypes.string,
thickness: PropTypes.string,
spacing: PropTypes.string,
scanner: PropTypes.string,
isOpen: PropTypes.bool,
showPatientInfoRef: PropTypes.object,
};
export default PatientInfo;
@@ -0,0 +1,2 @@
import PatientInfo from './PatientInfo';
export default PatientInfo;
@@ -0,0 +1,256 @@
import React, { useState } from 'react';
import { Icon, InputRange, CheckBox, InputNumber } from '../';
import classNames from 'classnames';
import { reducer } from './segmentationConfigReducer';
const ActiveSegmentationConfig = ({
config,
dispatch,
setRenderOutline,
setOutlineOpacityActive,
setOutlineWidthActive,
setRenderFill,
setFillAlpha,
usePercentage,
}) => {
return (
<div className="flex justify-between text-[12px] pt-[13px] px-2">
<div className="flex flex-col items-start">
<div className="text-white mb-[12px]">Active</div>
<CheckBox
label="Outline"
checked={config.renderOutline}
labelClassName="text-[12px] pl-1 pt-1"
className="mb-[9px]"
onChange={value => {
dispatch({
type: 'RENDER_OUTLINE',
payload: {
value,
},
});
setRenderOutline(value);
}}
/>
<CheckBox
label="Fill"
checked={config.renderFill}
labelClassName="text-[12px] pl-1 pt-1"
className="mb-[9px]"
onChange={value => {
dispatch({
type: 'RENDER_FILL',
payload: {
value,
},
});
setRenderFill(value);
}}
/>
</div>
<div className="flex flex-col items-center col-span-2">
<div className="text-[#b3b3b3] text-[10px] mb-[12px]">Opacity</div>
<InputRange
minValue={0}
maxValue={usePercentage ? 100 : 1}
value={
usePercentage ? config.outlineOpacity * 100 : config.outlineOpacity
}
onChange={value => {
dispatch({
type: 'SET_OUTLINE_OPACITY',
payload: {
value: value,
},
});
setOutlineOpacityActive(value);
}}
step={1}
containerClassName="mt-[4px] mb-[9px]"
inputClassName="w-[64px]"
labelClassName="text-white text-[12px]"
unit="%"
/>
<InputRange
minValue={0}
maxValue={usePercentage ? 100 : 1}
value={usePercentage ? config.fillAlpha * 100 : config.fillAlpha}
onChange={value => {
dispatch({
type: 'SET_FILL_ALPHA',
payload: {
value,
},
});
setFillAlpha(value);
}}
step={1}
containerClassName="mt-[4px] mb-[9px]"
inputClassName="w-[64px]"
labelClassName="text-white text-[12px]"
unit="%"
/>
</div>
<div className="flex flex-col items-center">
<div className="text-[#b3b3b3] text-[10px] mb-[12px]">Size</div>
<InputNumber
value={config.outlineWidthActive}
onChange={value => {
dispatch({
type: 'SET_OUTLINE_WIDTH',
payload: {
value,
},
});
setOutlineWidthActive(value);
}}
className="-mt-1"
/>
</div>
</div>
);
};
const InactiveSegmentationConfig = ({
config,
dispatch,
setRenderInactiveSegmentations,
setFillAlphaInactive,
usePercentage,
}) => {
return (
<div className="px-2">
<CheckBox
label="Display Inactive Segmentations"
checked={config.renderInactiveSegmentations}
labelClassName="text-[12px] pt-1"
className="mb-[9px]"
onChange={value => {
dispatch({
type: 'RENDER_INACTIVE_SEGMENTATIONS',
payload: {
value,
},
});
setRenderInactiveSegmentations(value);
}}
/>
<div className="flex pl-4 items-center space-x-2">
<span className="text-[10px] text-[#b3b3b3]">Opacity</span>
<InputRange
minValue={0}
maxValue={usePercentage ? 100 : 1}
value={
usePercentage
? config.fillAlphaInactive * 100
: config.fillAlphaInactive
}
onChange={value => {
dispatch({
type: 'SET_FILL_ALPHA_INACTIVE',
payload: {
value: value,
},
});
setFillAlphaInactive(value);
}}
step={1}
containerClassName="mt-[4px]"
inputClassName="w-[64px]"
labelClassName="text-white text-[12px]"
unit="%"
/>
</div>
</div>
);
};
const SegmentationConfig = ({
segmentationConfig,
setFillAlpha,
setFillAlphaInactive,
setOutlineWidthActive,
setOutlineOpacityActive,
setRenderFill,
setRenderInactiveSegmentations,
setRenderOutline,
}) => {
const [config, dispatch] = React.useReducer(
reducer,
segmentationConfig.initialConfig
);
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(
'w-5 h-5 text-white transition duration-300 cursor-pointer',
{
'transform rotate-90': !isMinimized,
}
)}
/>
<span className="text-[#d8d8d8] text-[12px] font-[300]">
{'Segmentation Appearance'}
</span>
</div>
{/* active segmentation */}
{!isMinimized && (
<div>
<ActiveSegmentationConfig
config={config}
dispatch={dispatch}
setFillAlpha={setFillAlpha}
setOutlineWidthActive={setOutlineWidthActive}
setOutlineOpacityActive={setOutlineOpacityActive}
setRenderFill={setRenderFill}
setRenderOutline={setRenderOutline}
usePercentage={segmentationConfig.usePercentage}
/>
{/* A small line */}
<div className="h-[1px] bg-[#212456] mb-[8px] mx-1"></div>
{/* inactive segmentation */}
<div
onClick={() => setIsMinimized(!isMinimized)}
className="flex items-center cursor-pointer pl-2 pb-[9px]"
>
<span className="text-[#d8d8d8] text-[12px] font-[300]">
{'Inactive Segmentations'}
</span>
</div>
<InactiveSegmentationConfig
config={config}
dispatch={dispatch}
setRenderInactiveSegmentations={setRenderInactiveSegmentations}
setFillAlphaInactive={setFillAlphaInactive}
usePercentage={segmentationConfig.usePercentage}
/>
</div>
)}
<div className="h-[6px] bg-black "></div>
</div>
);
};
SegmentationConfig.propTypes = {};
export default SegmentationConfig;
@@ -0,0 +1,273 @@
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,
}) => {
return (
<div>
<div className="flex items-center pl-[29px] bg-black text-primary-active hover:opacity-80 cursor-pointer text-[12px] py-1">
{showAddSegment && (
<div className="flex items-center" onClick={() => onSegmentAdd()}>
<Icon name="row-add" className="w-5 h-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="w-5 h-5" />
) : (
<Icon name="row-hide-all" className="w-5 h-5" />
)}
</div>
</div>
{/* {isSegmentationConfigOpen && (
<SegmentationConfig onConfigChange={onConfigChange} />
)} */}
</div>
);
};
const SegmentGroupHeader = ({
isMinimized,
onToggleMinimizeSegmentation,
onSegmentationClick,
id,
label,
isActive,
segmentCount,
onSegmentationEdit,
onSegmentationDelete,
}) => {
return (
<div
className={classnames(
'flex items-center pr-2 pl-[3px] h-[27px] gap-2 rounded-t-md border-b border-secondary-light cursor-pointer 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(
'w-5 h-5 text-white transition duration-300 cursor-pointer',
{
'transform rotate-90': !isMinimized,
}
)}
/>
<span className="text-white ">{label.toUpperCase()}</span>
<div className="flex-grow" />
<span className="text-white ">{segmentCount}</span>
<div
onClick={e => {
e.stopPropagation();
}}
>
<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,
showAddSegment,
segments,
activeSegmentIndex,
onSegmentAdd,
onSegmentationClick,
onClickSegmentColor,
onSegmentationEdit,
onToggleSegmentVisibility,
onToggleSegmentationVisibility,
onSegmentDelete,
showSegmentDelete,
onToggleMinimizeSegmentation,
onSegmentationConfigChange,
onSegmentationDelete,
onSegmentEdit,
}) => {
return (
<div className="flex flex-col">
<SegmentGroupHeader
id={id}
label={label}
isActive={isActive}
isMinimized={isMinimized}
onToggleMinimizeSegmentation={onToggleMinimizeSegmentation}
onSegmentationClick={onSegmentationClick}
segmentCount={segmentCount}
onSegmentationEdit={onSegmentationEdit}
onSegmentationDelete={onSegmentationDelete}
/>
{!isMinimized && (
<div className="">
<AddNewSegmentRow
onConfigChange={onSegmentationConfigChange}
onSegmentAdd={onSegmentAdd}
isVisible={isVisible}
onToggleSegmentationVisibility={onToggleSegmentationVisibility}
id={id}
showAddSegment={showAddSegment}
/>
<div
className="ohif-scrollbar overflow-y-hidden"
style={{ maxHeight: '40rem' }}
>
{!!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}
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,
};
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;
@@ -0,0 +1,242 @@
import React, { useState } from 'react';
import PropTypes from 'prop-types';
import classnames from 'classnames';
import { Icon } from '@ohif/ui';
const SegmentItem = ({
segmentIndex,
segmentationId,
label,
isActive,
isVisible,
color,
showSegmentDelete,
isLocked = false,
onClick,
onEdit,
onDelete,
onColor,
onToggleVisibility,
onToggleLocked,
}) => {
const [isHovering, setIsHovering] = useState(false);
const [isSegmentIndexHovering, setIsSegmentIndexHovering] = useState(false);
const onMouseEnter = () => setIsHovering(true);
const onMouseLeave = () => setIsHovering(false);
const cssColor = `rgb(${color[0]},${color[1]},${color[2]})`;
return (
<div
className={classnames(
'group relative flex cursor-pointer bg-primary-dark transition duration-300 text-[12px] overflow-hidden border',
{
'border-primary-light rounded-[3px]': isActive,
},
{
'border-transparent': !isActive,
}
)}
onMouseEnter={onMouseEnter}
onMouseLeave={onMouseLeave}
onClick={e => {
e.stopPropagation();
onClick(segmentationId, segmentIndex);
}}
role="button"
tabIndex={0}
data-cy={'segment-item'}
>
<div
className={classnames(
'w-[27px] flex items-center justify-center border-r border-r-black text-[12px]',
{
'bg-primary-light text-black border-l border-l-primary-light rounded-l-sm': isActive,
'bg-[#1d3c58] border-l border-l-[#1d3c58] rounded-l-sm':
!isActive && isHovering,
'bg-primary-dark text-aqua-pale':
!isActive && !isHovering && isVisible,
'bg-[#140e2e] opacity-60 text-[#537594]': !isActive && !isVisible,
}
)}
onMouseEnter={() => setIsSegmentIndexHovering(true)}
onMouseLeave={() => setIsSegmentIndexHovering(false)}
>
{isSegmentIndexHovering && showSegmentDelete ? (
<Icon
name="close"
className={classnames('w-5 h-5 pr-2')}
onClick={e => {
e.stopPropagation();
onDelete(segmentationId, segmentIndex);
}}
/>
) : (
<div className={classnames('flex items-center pr-2 ')}>
{segmentIndex}
</div>
)}
</div>
<div
className={classnames(
'flex items-center justify-between w-full pl-2 py-1 text-white border-r border-r-black relative ',
{
'bg-primary-dark text-primary-light': isActive,
'bg-primary-dark text-aqua-pale': !isActive && isVisible,
'bg-[#140e2e] opacity-60 text-[#3d5871]': !isVisible,
}
)}
>
<div className={classnames('flex items-baseline gap-2')}>
<div
className={classnames('shrink-0 w-[8px] h-[8px] rounded-full')}
style={{ backgroundColor: cssColor }}
onClick={e => {
e.stopPropagation();
onColor(segmentationId, segmentIndex);
}}
/>
<div className="">{label}</div>
</div>
{/* with faded background */}
<div className="absolute right-0 bg-black/15 rounded-lg pr-[7px]">
{!isVisible && !isHovering && (
<div>
<Icon
name="row-hidden"
className={classnames('w-5 h-5 text-[#3d5871] ')}
onClick={e => {
e.stopPropagation();
onToggleVisibility(segmentationId, segmentIndex);
}}
/>
</div>
)}
{isHovering && (
<div className={classnames('flex items-center')}>
<Icon
name="row-edit"
className={classnames('w-5 h-5', {
'text-white': isLocked,
'text-primary-light': !isLocked,
})}
onClick={e => {
e.stopPropagation();
onEdit(segmentationId, segmentIndex);
}}
/>
{isVisible ? (
<Icon
name="row-hide"
className={classnames('w-5 h-5', {
'text-white': isLocked,
'text-primary-light': !isLocked,
})}
onClick={e => {
e.stopPropagation();
onToggleVisibility(segmentationId, segmentIndex);
}}
/>
) : (
<Icon
name="row-unhide"
className={classnames('w-5 h-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={classnames(
'w-3 h-3 mt-1 mr-2 rounded-full cursor-pointer transition duration-300 hover:opacity-80'
)}
onClick={e => {
e.stopPropagation();
onColor(segmentationId, segmentIndex);
}}
style={{ backgroundColor: cssColor }}
></div>
{label}
</div>
<div className="flex items-center w-1/3">
<div className="px-1">
<Icon
className={classnames(
'text-white w-4 cursor-pointer transition duration-300 hover:opacity-80'
)}
name={isVisible ? 'eye-visible' : 'eye-hidden'}
onClick={e => {
// stopPropagation needed to avoid disable the current active item
e.stopPropagation();
onToggleVisibility(segmentationId, segmentIndex);
}}
/>
</div>
{onToggleLocked !== undefined ? (
<div className="px-1">
<Icon
className={classnames(
'text-white w-4 cursor-pointer transition duration-300 hover:opacity-80'
)}
name={isLocked ? 'lock' : 'dotted-circle'}
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'
)}
name={'pencil'}
onClick={e => {
// stopPropagation needed to avoid disable the current active item
e.stopPropagation();
onEdit(segmentationId, segmentIndex);
}}
/>
</div>
</div>
</div>
</div> */}
</div>
);
};
SegmentItem.propTypes = {
segmentIndex: PropTypes.number.isRequired,
segmentationId: PropTypes.string.isRequired,
label: PropTypes.string,
// color as array
color: PropTypes.array,
isActive: PropTypes.bool.isRequired,
isVisible: PropTypes.bool.isRequired,
isLocked: PropTypes.bool,
onClick: PropTypes.func.isRequired,
onEdit: PropTypes.func.isRequired,
onDelete: PropTypes.func.isRequired,
onToggleVisibility: PropTypes.func.isRequired,
onToggleLocked: PropTypes.func,
};
SegmentItem.defaultProps = {
isActive: false,
};
export default SegmentItem;
@@ -0,0 +1,173 @@
import React from 'react';
import PropTypes from 'prop-types';
import Icon from '../Icon';
import SegmentationGroup from './SegmentationGroup';
import SegmentationConfig from './SegmentationConfig';
const GetSegmentationConfig = ({
setFillAlpha,
setFillAlphaInactive,
setOutlineWidthActive,
setRenderFill,
setRenderInactiveSegmentations,
setRenderOutline,
setOutlineOpacityActive,
segmentationConfig,
}) => {
return (
<SegmentationConfig
setFillAlpha={setFillAlpha}
setFillAlphaInactive={setFillAlphaInactive}
setOutlineWidthActive={setOutlineWidthActive}
setOutlineOpacityActive={setOutlineOpacityActive}
setRenderFill={setRenderFill}
setRenderInactiveSegmentations={setRenderInactiveSegmentations}
setRenderOutline={setRenderOutline}
segmentationConfig={segmentationConfig}
/>
);
};
const SegmentationGroupTable = ({
segmentations,
onSegmentationAdd,
onSegmentationEdit,
onSegmentationClick,
onSegmentationDelete,
showAddSegmentation,
showAddSegment,
onSegmentClick,
onSegmentAdd,
segmentationConfig,
onSegmentDelete,
onSegmentEdit,
onToggleSegmentationVisibility,
onToggleSegmentVisibility,
onSegmentColorClick,
isMinimized,
onToggleMinimizeSegmentation,
setFillAlpha,
setFillAlphaInactive,
setOutlineWidthActive,
setOutlineOpacityActive,
setRenderFill,
setRenderInactiveSegmentations,
setRenderOutline,
}) => {
return (
<div className="font-inter font-[300]">
<GetSegmentationConfig
// showAddSegmentation={showAddSegmentation}
// onSegmentationAdd={onSegmentationAdd}
segmentationConfig={segmentationConfig}
setFillAlpha={setFillAlpha}
setFillAlphaInactive={setFillAlphaInactive}
setOutlineWidthActive={setOutlineWidthActive}
setOutlineOpacityActive={setOutlineOpacityActive}
setRenderFill={setRenderFill}
setRenderInactiveSegmentations={setRenderInactiveSegmentations}
setRenderOutline={setRenderOutline}
/>
<div className="flex flex-col pr-[1px] mt-1">
{!!segmentations.length &&
segmentations.map((segmentation, index) => {
const {
id,
label,
displayText = [],
segmentCount,
segments,
isVisible,
isActive,
activeSegmentIndex,
} = segmentation;
return (
<>
<SegmentationGroup
id={id}
label={label}
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 className="h-2 bg-black"></div>
</>
);
})}
</div>
{showAddSegmentation && (
<div
className="flex items-center cursor-pointer hover:opacity-80 text-primary-active bg-black text-[12px] pl-1 h-[45px]"
onClick={() => onSegmentationAdd()}
>
<Icon name="row-add" className="w-5 h-5" />
<div className="pl-1">Add New Segmentation</div>
</div>
)}
</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,
};
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,
},
usePercentage: true,
},
setFillAlpha: () => {},
setFillAlphaInactive: () => {},
setOutlineWidthActive: () => {},
setOutlineOpacityActive: () => {},
setRenderFill: () => {},
setRenderInactiveSegmentations: () => {},
setRenderOutline: () => {},
};
export default SegmentationGroupTable;
@@ -0,0 +1,141 @@
import SegmentationGroupTable from '../SegmentationGroupTable';
import { ArgsTable, Story, Canvas, Meta } from '@storybook/addon-docs';
import { createComponentTemplate } from '../../../storybook/functions/create-component-story';
export const argTypes = {
component: SegmentationGroupTable,
title: 'Components/SegmentationGroupTable',
};
<Meta
title="Components/SegmentationGroupTable"
component={SegmentationGroupTable}
/>
export const SegmentationGroupTableTemplate = args => (
<div className="w-64 h-96 bg-primary-dark">
<SegmentationGroupTable {...args} />
</div>
);
<Heading
title="SegmentationGroupTable"
componentRelativePath="SegmentationGroupTable/SegmentationGroupTable.tsx"
/>
- [Overview](#overview)
- [Props](#props)
- [Contribute](#contribute)
## Overview
SegmentationGroupTable is a component that renders the SegmentationGroupTables.
<Canvas>
<Story
name="Overview"
args={{
activeSegmentationId: 'ed8d6f8d-4035-438d-9032-36d5912aaa3a',
showAddSegmentation: true,
isMinimized: {"4be5-b9b2-ed33-fa86970568e7": false},
showAddSegment: true,
segmentations: [
{
"id": "4be5-b9b2-ed33-fa86970568e7",
"volumeId": "a3b0f15f-4be5-b9b2-ed33-fa86970568e7",
"activeSegmentIndex": 1,
"cachedStats": {},
"label": "Segmentation",
"segmentsLocked": {},
"type": "LABELMAP",
"displayText": [],
"hydrated": true,
"segmentCount": 3,
"segments": [
null,
{
"label": "Segment 1",
"segmentIndex": 1,
"color": [
128,
174,
128
],
"opacity": 255,
"isVisible": true,
"isLocked": false,
},
{
"label": "Segment 2",
"segmentIndex": 2,
"color": [
241,
214,
145
],
"opacity": 255,
"isVisible": true,
"isLocked": false,
},
{
"label": "Segment 3",
"segmentIndex": 3,
"color": [
141,
114,
145
],
"opacity": 255,
"isVisible": false,
"isLocked": false
}
],
"isActive": false,
"colorLUTIndex": 1,
"isVisible": true
},
{
"id": "a3b0f15f-4be5-b9b2-ed33-fa86970568e7",
"volumeId": "a3b0f15f-4be5-b9b2-ed33-fa86970568e7",
"activeSegmentIndex": 1,
"cachedStats": {},
"label": "Segmentation2",
"segmentsLocked": {},
"type": "LABELMAP",
"displayText": [],
"hydrated": true,
"segmentCount": 2,
"segments": [
null,
{
"label": "Segment 1",
"segmentIndex": 1,
"color": [
228,
174,
128
],
"opacity": 255,
"isVisible": true,
"isLocked": false,
},
],
"isActive": true,
"colorLUTIndex": 1,
"isVisible": true
}
]
}}
>
{SegmentationGroupTableTemplate.bind({})}
</Story>
</Canvas>
## Props
<ArgsTable of={SegmentationGroupTable} />
## Contribute
<Footer componentRelativePath="SegmentationGroupTable/__stories__/SegmentationGroupTable.stories.mdx" />
@@ -0,0 +1,3 @@
import SegmentationGroupTable from './SegmentationGroupTable';
export default SegmentationGroupTable;
@@ -0,0 +1,22 @@
const reducer = (state, action) => {
switch (action.type) {
case 'RENDER_OUTLINE':
return { ...state, renderOutline: action.payload.value };
case 'RENDER_FILL':
return { ...state, renderFill: action.payload.value };
case 'SET_OUTLINE_OPACITY':
return { ...state, outlineOpacity: action.payload.value };
case 'SET_OUTLINE_WIDTH':
return { ...state, outlineWidth: action.payload.value };
case 'SET_FILL_ALPHA':
return { ...state, fillAlpha: action.payload.value };
case 'SET_FILL_ALPHA_INACTIVE':
return { ...state, fillAlphaInactive: action.payload.value };
case 'RENDER_INACTIVE_SEGMENTATIONS':
return { ...state, renderInactiveSegmentations: action.payload.value };
default:
return state;
}
};
export { reducer };
@@ -5,7 +5,6 @@ import SegmentationItem from './SegmentationItem';
const SegmentationTable = ({
title,
amount,
segmentations,
activeSegmentationId,
onClick,
@@ -15,6 +14,7 @@ const SegmentationTable = ({
onToggleVisibilityAll,
}) => {
const [hiddenSegmentationIds, setHiddenSegmentationIds] = useState([]);
const amount = segmentations.length;
const handleToggleAll = () => {
// filter segmentation ids that are hidden
@@ -106,7 +106,6 @@ const SegmentationTable = ({
SegmentationTable.propTypes = {
title: PropTypes.string.isRequired,
amount: PropTypes.number.isRequired,
segmentations: PropTypes.array.isRequired,
activeSegmentationId: PropTypes.string.isRequired,
onClick: PropTypes.func.isRequired,
@@ -118,7 +117,6 @@ SegmentationTable.propTypes = {
SegmentationTable.defaultProps = {
title: '',
amount: 0,
segmentations: [],
activeSegmentationId: '',
onClick: () => {},
+50 -1
View File
@@ -24,7 +24,7 @@
}
.customSelect__wrapper .customSelect__option {
@apply text-black flex flex-row items-center;
@apply flex flex-row items-center;
}
.customSelect__wrapper .customSelect__option:focus {
@@ -46,3 +46,52 @@
.customSelect__wrapper .customSelect__value-container--is-multi {
@apply px-3 py-2 inline-block truncate;
}
.customSelect__menu {
background-color: transparent !important;
border-radius: 6px;
}
.css-4ljt47-MenuList {
overflow-x: hidden !important;
margin-right: 4px;
margin-top: 4px;
padding-top: 0px !important;
}
.css-4ljt47-MenuList::-webkit-scrollbar {
width: 6px;
border-radius: 10px;
}
.css-4ljt47-MenuList::-webkit-scrollbar-thumb {
background-color: #0944b3;
border-radius: 10px;
}
.customSelect__option:focus {
background-color: #0944b391 !important;
}
.customSelect__option label {
color: #d6d6d6;
font-size: 14px;
display: flex;
align-items: center;
}
.css-1n7v3ny-option {
background-color: #0944b391 !important;
}
.css-1n7v3ny-option:focus {
background-color: #0944b391 !important;
}
.customSelect__option:hover {
background-color: #0944b391;
}
.css-26l3qy-menu {
border: 2px solid #0944b3;
border-radius: 6px;
background-color: #151515 !important;
}
+15 -11
View File
@@ -2,6 +2,7 @@ import React from 'react';
import PropTypes from 'prop-types';
import classnames from 'classnames';
import ReactSelect, { components } from 'react-select';
import { Icon } from '@ohif/ui';
import './Select.css';
@@ -18,17 +19,20 @@ const MultiValue = props => {
const Option = props => {
return (
<div>
<components.Option {...props}>
<input
type="checkbox"
checked={props.isSelected}
className="w-6 h-6 mr-2"
onChange={e => null}
/>
<label>{props.value} </label>
</components.Option>
</div>
<components.Option {...props}>
<div className="flex items-center">
<div className="w-2 h-2">
{props.isSelected ? (
<Icon name={'checkbox-active'} />
) : (
<Icon name={'checkbox-default'} />
)}
</div>
<label id={props.data.value} className="ml-3 mt-1">
<span>{props.value}</span>
</label>
</div>
</components.Option>
);
};
@@ -1,13 +1,25 @@
import React, { useState } from 'react';
import React, { useState, useRef, useEffect, useCallback } from 'react';
import PropTypes from 'prop-types';
import classnames from 'classnames';
import { useTranslation } from 'react-i18next';
import { Swiper, SwiperSlide } from 'swiper/react';
import SwiperCore, {
A11y,
Controller,
Pagination,
Scrollbar,
Navigation,
} from 'swiper';
import { Button, Icon } from '../';
import { Button, Icon, IconButton, Tooltip } from '../';
import 'swiper/css';
import 'swiper/css/navigation';
import './style.css';
const borderSize = 4;
const expandedWidth = 248;
const collapsedWidth = 56;
const collapsedWidth = 25;
const baseStyle = {
maxWidth: `${expandedWidth}px`,
@@ -27,20 +39,20 @@ const styleMap = {
};
const baseClasses =
'transition-all duration-300 ease-in-out h-100 bg-primary-dark border-black flex flex-col justify-start box-content';
'transition-all duration-300 ease-in-out h-100 bg-black border-black justify-start box-content flex flex-col';
const classesMap = {
open: {
left: `border-r-4`,
right: `border-l-4`,
left: `mr-1`,
right: `ml-1`,
},
closed: {
left: `border-r-4 items-end`,
right: `border-l-4 items-start`,
left: `mr-2 items-end`,
right: `ml-2 items-start`,
},
};
const openIconName = {
const openStateIconName = {
left: 'push-left',
right: 'push-right',
};
@@ -54,62 +66,100 @@ const position = {
},
};
const getChildComponent = (childComponents, componentOpen) => {
if (Array.isArray(childComponents)) {
return childComponents.find(
_childComponent => _childComponent.name === componentOpen
);
} else {
return childComponents;
}
};
const SidePanel = ({
side,
className,
defaultComponentOpen,
childComponents,
activeTabIndex: activeTabIndexProp,
tabs,
}) => {
const { t } = useTranslation('SidePanel');
const [componentOpen, setComponentOpen] = useState(defaultComponentOpen);
const [panelOpen, setPanelOpen] = useState(activeTabIndexProp !== null);
const [activeTabIndex, setActiveTabIndex] = useState(activeTabIndexProp || 0);
const swiperRef = useRef() as any;
const [swiper, setSwiper] = useState<any>();
const openStatus = componentOpen ? 'open' : 'closed';
const prevRef = React.useRef();
const nextRef = React.useRef();
const openStatus = panelOpen ? 'open' : 'closed';
const style = Object.assign({}, styleMap[openStatus][side], baseStyle);
const childComponent = getChildComponent(childComponents, componentOpen);
const ActiveComponent = tabs[activeTabIndex].content;
const getPanelButtons = () => {
const _childComponents = Array.isArray(childComponents)
? childComponents
: [childComponents];
return _childComponents.map((childComponent, i) => {
return (
<Button
key={i}
variant="text"
color="inherit"
border="none"
useEffect(() => {
if (panelOpen && swiper) {
swiper.slideTo(activeTabIndex, 500);
}
}, [panelOpen, swiper]);
useEffect(() => {
if (swiper) {
swiper.params.navigation.prevEl = prevRef.current;
swiper.params.navigation.nextEl = nextRef.current;
swiper.navigation.init();
swiper.navigation.update();
}
}, [swiper]);
const getCloseStateComponent = () => {
const _childComponents = Array.isArray(tabs) ? tabs : [tabs];
return (
<>
<div
className={classnames(
'bg-secondary-dark h-[28px] flex items-center w-full rounded-md cursor-pointer',
side === 'left' ? 'pr-2 justify-end' : 'pl-2 justify-start'
)}
onClick={() => {
setComponentOpen(childComponent.name);
setPanelOpen(prev => !prev);
}}
style={{
minWidth: `${collapsedWidth}px`,
width: `${collapsedWidth}px`,
}}
name={childComponent.name}
className="flex flex-col text-xs px-1 text-white border-transparent border-b"
data-cy={`side-panel-header-${side}`}
>
<Icon
name={childComponent.iconName}
className="text-primary-active"
name={'navigation-panel-right-reveal'}
className={classnames(
'text-primary-active',
side === 'left' && 'transform rotate-180'
)}
/>
<span className="mt-2 text-white text-xs">
{t(childComponent.iconLabel)}
</span>
</Button>
);
});
</div>
<div className={classnames('flex flex-col space-y-3 mt-3')}>
{_childComponents.map((childComponent, index) => (
<Tooltip
position={side === 'left' ? 'right' : 'left'}
key={index}
content={`${childComponent.label}`}
className={classnames(
'flex items-center',
side === 'left' ? 'justify-end ' : 'justify-start '
)}
>
<IconButton
id={`${childComponent.name}-btn`}
variant="text"
color="inherit"
size="initial"
className="text-primary-active"
onClick={() => {
setActiveTabIndex(index);
setPanelOpen(true);
}}
>
<Icon
name={childComponent.iconName}
className="text-primary-active"
style={{
width: '22px',
height: '22px',
}}
/>
</IconButton>
</Tooltip>
))}
</div>
</>
);
};
return (
@@ -121,37 +171,75 @@ const SidePanel = ({
)}
style={style}
>
{componentOpen ? (
{panelOpen ? (
<React.Fragment>
<div className="px-3 border-b border-secondary-light">
{/** Panel Header with Arrow and Close Actions */}
<div
className={classnames(
'px-[10px] bg-primary-dark h-9 cursor-pointer flex shrink-0',
tabs.length === 1 && 'mb-1'
)}
onClick={() => {
setPanelOpen(prev => !prev);
// slideToActivePanel();
}}
data-cy={`side-panel-header-${side}`}
>
<Button
variant="text"
color="inherit"
border="none"
rounded="none"
onClick={() => {
setComponentOpen(null);
}}
name={childComponent.name}
className="flex flex-row items-center px-3 h-12 relative w-full"
className="flex flex-row items-center px-3 relative w-full"
name={tabs.length === 1 ? `${tabs[activeTabIndex].name}` : ''}
>
<Icon
name={openIconName[side]}
name={openStateIconName[side]}
className={classnames(
'text-primary-active absolute',
side === 'left' && 'order-last'
)}
style={{ ...position[side] }}
/>
<span className="flex-1 text-primary-active">
{t(childComponent.label)}
{/* Todo: ass secondary label here */}
<span className="text-primary-active">
{tabs.length === 1 && tabs[activeTabIndex].label}
</span>
</Button>
</div>
<childComponent.content />
{tabs.length > 1 &&
_getMoreThanOneTabLayout(
swiperRef,
setSwiper,
prevRef,
nextRef,
tabs,
activeTabIndex,
setActiveTabIndex,
setPanelOpen
)}
{/** carousel navigation with the arrows */}
{/** only show carousel nav if tabs are more than 3 tabs */}
{tabs.length > 3 && (
<div className="text-primary-active w-full flex justify-end gap-2 bg-primary-dark py-1 px-2">
<button ref={prevRef} className="swiper-button-prev-custom">
<Icon
name={'icon-prev'}
className={classnames('text-primary-active')}
/>
</button>
<button ref={nextRef} className="swiper-button-next-custom">
<Icon
name={'icon-next'}
className={classnames('text-primary-active')}
/>
</button>
</div>
)}
<ActiveComponent />
</React.Fragment>
) : (
<React.Fragment>{getPanelButtons()}</React.Fragment>
<React.Fragment>{getCloseStateComponent()}</React.Fragment>
)}
</div>
);
@@ -164,15 +252,8 @@ SidePanel.defaultProps = {
SidePanel.propTypes = {
side: PropTypes.oneOf(['left', 'right']).isRequired,
className: PropTypes.string,
defaultComponentOpen: PropTypes.string,
childComponents: PropTypes.oneOfType([
PropTypes.shape({
iconName: PropTypes.string.isRequired,
iconLabel: PropTypes.string.isRequired,
name: PropTypes.string.isRequired,
label: PropTypes.string.isRequired,
content: PropTypes.func, // TODO: Should be node, but it keeps complaining?
}),
activeTabIndex: PropTypes.number,
tabs: PropTypes.oneOfType([
PropTypes.arrayOf(
PropTypes.shape({
iconName: PropTypes.string.isRequired,
@@ -185,4 +266,78 @@ SidePanel.propTypes = {
]),
};
function _getMoreThanOneTabLayout(
swiperRef: any,
setSwiper: React.Dispatch<any>,
prevRef: React.MutableRefObject<undefined>,
nextRef: React.MutableRefObject<undefined>,
tabs: any,
activeTabIndex: any,
setActiveTabIndex: React.Dispatch<any>,
setPanelOpen: React.Dispatch<React.SetStateAction<boolean>>
) {
return (
<div
className="collapse-sidebar relative"
style={{
backgroundColor: '#06081f',
}}
>
<div className="w-full">
<Swiper
onInit={(core: SwiperCore) => {
swiperRef.current = core.el;
}}
simulateTouch={false}
modules={[Navigation, Pagination, Scrollbar, A11y, Controller]}
slidesPerView={3}
spaceBetween={5}
onSwiper={swiper => setSwiper(swiper)}
navigation={{
prevEl: prevRef?.current,
nextEl: nextRef?.current,
}}
>
{tabs.map((obj, index) => (
<SwiperSlide key={index}>
<div
className={classnames(
index === activeTabIndex
? 'bg-secondary-main text-white'
: 'text-aqua-pale',
'flex cursor-pointer px-4 py-1 rounded-[4px] flex-col justify-center items-center text-center hover:text-white'
)}
key={index}
onClick={() => {
setActiveTabIndex(index);
setPanelOpen(true);
}}
data-cy={`${obj.name}-btn`}
>
<span>
<Icon
name={obj.iconName}
className={classnames(
index === activeTabIndex
? 'text-white'
: 'text-primary-active'
)}
style={{
width: '22px',
height: '22px',
}}
/>
</span>
<span className="text-[10px] select-none font-medium whitespace-nowrap mt-[5px]">
{obj.label}
</span>
</div>
</SwiperSlide>
))}
</Swiper>
</div>
</div>
);
}
export default SidePanel;
@@ -0,0 +1,27 @@
.swiper-button-next.swiper-button-disabled {
pointer-events: none;
opacity: 0;
}
.swiper-button-prev.swiper-button-disabled {
pointer-events: none;
opacity: 0;
}
.swiper-button-prev {
pointer-events: none;
opacity: 0;
}
.swiper-button-next {
pointer-events: none;
opacity: 0;
}
.swiper-button-next-custom.swiper-button-disabled {
pointer-events: none;
opacity: 0.6;
}
.swiper-button-prev-custom.swiper-button-disabled {
pointer-events: none;
opacity: 0.6;
}
@@ -38,45 +38,10 @@
margin: 0 0 10px;
}
.sb-closeBtn {
text-shadow: none;
width: 20px;
height: 20px;
right: 5px;
top: 5px;
@apply overflow-hidden opacity-100 rounded-full p-1 bg-white cursor-pointer absolute text-center duration-300 transition-all ease-in-out;
}
.sb-closeBtn:hover {
background: #fff;
}
.sb-closeIcon {
@apply w-full relative overflow-hidden h-full block leading-none;
}
.sb-closeIcon:after,
.sb-closeIcon:before {
content: ' ';
height: 2px;
width: 12px;
@apply duration-300 transition-all ease-in-out block bg-black opacity-100 absolute;
}
.sb-closeIcon:before {
left: 4px;
top: 3px;
transform: rotate(45deg);
transform-origin: 0px 50%;
}
.sb-closeIcon:after {
right: 3px;
top: 5px;
transform: rotate(-45deg);
transform-origin: calc(100% - 3px) 50%;
}
.sb-title {
@apply break-normal text-lg font-bold;
}
@@ -1,5 +1,7 @@
import React, { useEffect } from 'react';
import classNames from 'classnames';
import Icon from '../Icon';
import IconButton from '../IconButton';
import SnackbarTypes from './SnackbarTypes';
@@ -16,10 +18,11 @@ const SnackbarItem = ({ options, onClose }) => {
[SnackbarTypes.INFO]: 'bg-primary-active',
[SnackbarTypes.WARNING]: 'bg-yellow-600',
[SnackbarTypes.SUCCESS]: 'bg-green-600',
[SnackbarTypes.ERROR]: 'bg-red-600'
[SnackbarTypes.ERROR]: 'bg-red-600',
};
const hidden = 'duration-300 transition-all ease-in-out h-0 opacity-0 pt-0 mb-0 pb-0';
const hidden =
'duration-300 transition-all ease-in-out h-0 opacity-0 pt-0 mb-0 pb-0';
return (
<div
@@ -28,9 +31,15 @@ const SnackbarItem = ({ options, onClose }) => {
typeClasses[options.type]
)}
>
<span className="sb-closeBtn" onClick={handleClose}>
<span className="sb-closeIcon">x</span>
</span>
{/* <span className="sb-closeBtn" onClick={handleClose}> */}
{/* <span className="sb-closeIcon">x</span> */}
<div
onClick={handleClose}
className="w-5 h-5 rounded-full absolute bg-white flex items-center justify-center right-0 top-0 mr-2 mt-2"
>
<Icon name="close" className="w-4 text-black" />
</div>
{/* </span> */}
{options.title && <div className="sb-title">{options.title}</div>}
{options.message && <div className="sb-message">{options.message}</div>}
</div>
@@ -1,13 +1,10 @@
import React from 'react';
import PropTypes from 'prop-types';
import classnames from 'classnames';
import { useTranslation } from 'react-i18next';
import { ButtonGroup, Button, StudyItem, ThumbnailList } from '../';
import { StringNumber } from '../../types';
const buttonClasses = 'text-white text-base border-none p-2 min-w-18';
const getTrackedSeries = displaySets => {
let trackedSeries = 0;
displaySets.forEach(displaySet => {
@@ -79,11 +76,7 @@ const StudyBrowser = ({
className="flex flex-row items-center justify-center h-16 p-4 border-b w-100 border-secondary-light bg-primary-dark"
data-cy={'studyBrowser-panel'}
>
<ButtonGroup
variant="outlined"
color="inherit"
className="border rounded-md border-secondary-light"
>
<ButtonGroup variant="outlined" color="secondary" splitBorder={false}>
{tabs.map(tab => {
const { name, label, studies } = tab;
const isActive = activeTabName === name;
@@ -91,7 +84,7 @@ const StudyBrowser = ({
return (
<Button
key={name}
className={classnames(buttonClasses)}
className={'text-white text-base p-2 min-w-18'}
size="initial"
bgColor={isActive ? 'bg-primary-main' : 'bg-black'}
onClick={() => {
@@ -147,6 +147,8 @@ function _getModalityTooltip(modality) {
const _modalityTooltips = {
SR: 'Structured Report',
SEG: 'Segmentation',
RT: 'RT Structure Set',
};
export default ThumbnailList;
@@ -29,6 +29,7 @@ const Tooltip = ({
content,
isSticky,
position,
className,
tight,
children,
isDisabled,
@@ -52,7 +53,7 @@ const Tooltip = ({
return (
<div
className="relative"
className={classnames('relative', className)}
onMouseOver={handleMouseOver}
onFocus={handleMouseOver}
onMouseOut={handleMouseOut}
@@ -110,6 +111,7 @@ Tooltip.propTypes = {
isSticky: PropTypes.bool,
tight: PropTypes.bool,
children: PropTypes.node.isRequired,
className: PropTypes.string,
};
export default Tooltip;
@@ -2,12 +2,12 @@ import React from 'react';
import PropTypes from 'prop-types';
import { ViewportActionBar, Notification } from '../';
const Viewport = ({ viewportIndex, onSeriesChange, studyData, children }) => {
const Viewport = ({ viewportIndex, onArrowsClick, studyData, children }) => {
return (
<div className="relative flex flex-col h-full">
<div className="absolute top-0 left-0 w-full">
<ViewportActionBar
onSeriesChange={onSeriesChange}
onArrowsClick={onArrowsClick}
studyData={studyData}
/>
@@ -48,7 +48,7 @@ const Viewport = ({ viewportIndex, onSeriesChange, studyData, children }) => {
Viewport.propTypes = {
viewportIndex: PropTypes.number.isRequired,
onSeriesChange: PropTypes.func.isRequired,
onArrowsClick: PropTypes.func.isRequired,
studyData: PropTypes.shape({
label: PropTypes.string.isRequired,
isTracked: PropTypes.bool.isRequired,
@@ -31,7 +31,7 @@ Viewport is a component that renders the Viewport action bar.
name="Overview"
args={{
viewportIndex: 0,
onSeriesChange: direction => alert(`Annotation ${direction}`),
onArrowsClick: direction => alert(`Annotation ${direction}`),
studyData: {
label: 'A',
isTracked: true,
@@ -1,35 +1,35 @@
import React from 'react'
import ViewportActionBar from './ViewportActionBar'
import React from 'react';
import ViewportActionBar from './ViewportActionBar';
export default {
component: ViewportActionBar,
title: 'Components/ViewportActionBar',
}
};
export const Default = () => (
<div className="w-1/2 p-4 h-64">
<ViewportActionBar
onSeriesChange={direction => alert(`Series ${direction}`)}
studyData={{
label: 'A',
isTracked: false,
isLocked: false,
isRehydratable: true,
studyDate: '07-Sep-2010',
currentSeries: 1,
seriesDescription:
'Series description lorem ipsum dolor sit Series description lorem ipsum dolor sit Series description lorem ipsum dolor sit ',
modality: 'CT',
patientInformation: {
patientName: 'Smith, Jane',
patientSex: 'F',
patientAge: '59',
MRN: '10000001',
thickness: '2.0mm',
spacing: '1.25mm',
scanner: 'Aquilion',
},
}}
/>
</div>
)
<ViewportActionBar
onArrowsClick={direction => alert(`Series ${direction}`)}
studyData={{
label: 'A',
isTracked: false,
isLocked: false,
isRehydratable: true,
studyDate: '07-Sep-2010',
currentSeries: 1,
seriesDescription:
'Series description lorem ipsum dolor sit Series description lorem ipsum dolor sit Series description lorem ipsum dolor sit ',
modality: 'CT',
patientInformation: {
patientName: 'Smith, Jane',
patientSex: 'F',
patientAge: '59',
MRN: '10000001',
thickness: '2.0mm',
spacing: '1.25mm',
scanner: 'Aquilion',
},
}}
/>
</div>
);
@@ -1,18 +1,10 @@
import React, { useState, useRef, useEffect } from 'react';
import PropTypes from 'prop-types';
import classnames from 'classnames';
import { Icon, ButtonGroup, Button, Tooltip, CinePlayer } from '../';
import { Icon, ButtonGroup, Button, CinePlayer } from '../';
import useOnClickOutside from '../../utils/useOnClickOutside';
import { useTranslation } from 'react-i18next';
import PatientInfo from '../PatientInfo';
import { StringNumber } from '../../types';
const classes = {
infoHeader: 'text-base text-primary-light',
infoText: 'text-base text-white max-w-24 truncate',
firstRow: 'flex flex-col',
row: 'flex flex-col ml-4',
};
const ViewportActionBar = ({
studyData,
showNavArrows,
@@ -20,27 +12,15 @@ const ViewportActionBar = ({
showCine,
cineProps,
showPatientInfo: patientInfoVisibility,
onSeriesChange,
onArrowsClick,
onDoubleClick,
//
onPillClick,
getStatusComponent,
}) => {
const [showPatientInfo, setShowPatientInfo] = useState(patientInfoVisibility);
// TODO -> Remake this component with a bunch of generic slots that can be filled,
// Its not generic at all, isTracked etc shouldn't be parts of this component.
// It shouldn't care that a tracking mode or SR exists.
// Things like the right/left buttons should be made into smaller
// Components you can compose.
// OHIF-200 ticket.
const {
label,
isTracked,
isLocked,
isRehydratable,
useAltStyling,
modality,
studyDate,
currentSeries,
seriesDescription,
@@ -75,174 +55,10 @@ const ViewportActionBar = ({
return () => clickOutsideListener.remove();
}, [clickOutsideListener, showPatientInfo]);
const renderIconStatus = () => {
if (modality === 'SR') {
// 1 - Incompatible
// 2 - Locked
// 3 - Rehydratable / Open
const state =
isRehydratable && !isLocked ? 3 : isRehydratable && isLocked ? 2 : 1;
let ToolTipMessage = null;
let StatusIcon = null;
switch (state) {
case 1:
StatusIcon = () => (
<div
className="flex items-center justify-center -mr-1 rounded-full"
style={{
width: '18px',
height: '18px',
backgroundColor: '#98e5c1',
border: 'solid 1.5px #000000',
}}
>
<Icon
name="exclamation"
style={{ color: '#000', width: '12px', height: '12px' }}
/>
</div>
);
ToolTipMessage = () => (
<div>
This structured report is not compatible
<br />
with this application.
</div>
);
break;
case 2:
StatusIcon = () => (
<div
className="flex items-center justify-center -mr-1 bg-black rounded-full"
style={{
width: '18px',
height: '18px',
}}
>
<Icon
name="lock"
style={{ color: '#05D97C', width: '8px', height: '11px' }}
/>
</div>
);
ToolTipMessage = () => (
<div>
This structured report is currently read-only
<br />
because you are tracking measurements in
<br />
another viewport.
</div>
);
break;
case 3:
StatusIcon = () => (
<div
className="flex items-center justify-center -mr-1 bg-white rounded-full group-hover:bg-customblue-200"
style={{
width: '18px',
height: '18px',
border: 'solid 1.5px #000000',
}}
>
<Icon
name="arrow-left"
style={{ color: '#000', width: '14px', height: '14px' }}
/>
</div>
);
ToolTipMessage = () => <div>Click to restore measurements.</div>;
}
const StatusPill = () => (
<div
className={classnames(
'group relative flex items-center justify-center px-2 rounded-full cursor-default bg-customgreen-100',
{
'hover:bg-customblue-100': state === 3,
'cursor-pointer': state === 3,
}
)}
style={{
height: '24px',
width: '55px',
}}
onClick={() => {
if (state === 3) {
// TODO: Gatsby build failing due
// to ESLint's "no-unused-expressions"
//onPillClick?.();
if (onPillClick) {
onPillClick();
}
}
}}
>
<span className="pr-1 text-lg font-bold leading-none text-black">
SR
</span>
<StatusIcon />
</div>
);
return (
<>
{ToolTipMessage && (
<Tooltip content={<ToolTipMessage />} position="bottom-left">
<StatusPill />
</Tooltip>
)}
{!ToolTipMessage && <StatusPill />}
</>
);
}
const trackedIcon = isTracked ? 'tracked' : 'dotted-circle';
return (
<div className="relative">
<Tooltip
position="bottom-left"
content={
<div className="flex py-2">
<div className="flex pt-1">
<Icon name="info-link" className="w-4 text-primary-main" />
</div>
<div className="flex ml-4">
<span className="text-base text-common-light">
{isTracked ? (
<>
Series is
<span className="font-bold text-white"> tracked</span> and
can be viewed <br /> in the measurement panel
</>
) : (
<>
Measurements for
<span className="font-bold text-white"> untracked </span>
series <br /> will not be shown in the <br /> measurements
panel
</>
)}
</span>
</div>
</div>
}
>
<Icon name={trackedIcon} className="w-6 text-primary-light" />
</Tooltip>
</div>
);
};
const borderColor = useAltStyling ? '#365A6A' : '#1D205A';
let backgroundColor = '#020424';
if (useAltStyling || isTracked) {
if (useAltStyling) {
backgroundColor = '#031923';
}
@@ -254,11 +70,12 @@ const ViewportActionBar = ({
backgroundColor: backgroundColor,
}}
onDoubleClick={onDoubleClick}
onContextMenu={e => e.preventDefault()}
>
<div className="flex flex-1 grow mt-2 min-w-48">
<div className="flex items-center">
<span className="mr-2 text-white text-large">{label}</span>
{showStatus && renderIconStatus()}
{showStatus && getStatusComponent()}
</div>
<div className="flex flex-col justify-start ml-4">
<div className="flex">
@@ -288,7 +105,7 @@ const ViewportActionBar = ({
size="initial"
className="px-2 py-1 bg-black"
border="light"
onClick={() => onSeriesChange('left')}
onClick={() => onArrowsClick('left')}
>
<Icon name="chevron-left" className="w-4 text-white" />
</Button>
@@ -296,7 +113,7 @@ const ViewportActionBar = ({
size="initial"
border="light"
className="px-2 py-1 bg-black"
onClick={() => onSeriesChange('right')}
onClick={() => onArrowsClick('right')}
>
<Icon name="chevron-right" className="w-4 text-white" />
</Button>
@@ -326,7 +143,7 @@ const ViewportActionBar = ({
};
ViewportActionBar.propTypes = {
onSeriesChange: PropTypes.func.isRequired,
onArrowsClick: PropTypes.func.isRequired,
showNavArrows: PropTypes.bool,
showCine: PropTypes.bool,
cineProps: PropTypes.object,
@@ -336,12 +153,9 @@ ViewportActionBar.propTypes = {
useAltStyling: PropTypes.bool,
//
label: PropTypes.string.isRequired,
isTracked: PropTypes.bool.isRequired,
isRehydratable: PropTypes.bool.isRequired,
studyDate: PropTypes.string.isRequired,
currentSeries: StringNumber.isRequired,
seriesDescription: PropTypes.string.isRequired,
modality: PropTypes.string.isRequired,
patientInformation: PropTypes.shape({
patientName: PropTypes.string.isRequired,
patientSex: PropTypes.string.isRequired,
@@ -352,6 +166,7 @@ ViewportActionBar.propTypes = {
scanner: PropTypes.string.isRequired,
}),
}).isRequired,
getStatusComponent: PropTypes.func.isRequired,
};
ViewportActionBar.defaultProps = {
@@ -362,127 +177,4 @@ ViewportActionBar.defaultProps = {
showPatientInfo: false,
};
function PatientInfo({
patientName,
patientSex,
patientAge,
MRN,
thickness,
spacing,
scanner,
isOpen,
showPatientInfoRef,
}) {
const { t } = useTranslation('PatientInfo');
while (patientAge.charAt(0) === '0') {
patientAge = patientAge.substr(1);
}
return (
<div ref={showPatientInfoRef}>
<Tooltip
isSticky
isDisabled={!isOpen}
position="bottom-right"
content={
isOpen && (
<div className="flex py-2">
<div className="flex pt-1">
<Icon name="info-link" className="w-4 text-primary-main" />
</div>
<div className="flex flex-col ml-2">
<span
className="text-base font-bold text-white"
title={patientName}
>
{patientName}
</span>
<div className="flex pb-4 mt-4 mb-4 border-b border-secondary-main">
<div className={classnames(classes.firstRow)}>
<span className={classnames(classes.infoHeader)}>
{t('Sex')}
</span>
<span
className={classnames(classes.infoText)}
title={patientSex}
>
{patientSex}
</span>
</div>
<div className={classnames(classes.row)}>
<span className={classnames(classes.infoHeader)}>
{t('Age')}
</span>
<span
className={classnames(classes.infoText)}
title={patientAge}
>
{patientAge}
</span>
</div>
<div className={classnames(classes.row)}>
<span className={classnames(classes.infoHeader)}>
{t('MRN')}
</span>
<span className={classnames(classes.infoText)} title={MRN}>
{MRN}
</span>
</div>
</div>
<div className="flex">
<div className={classnames(classes.firstRow)}>
<span className={classnames(classes.infoHeader)}>
{t('Thickness')}
</span>
<span
className={classnames(classes.infoText)}
title={thickness}
>
{thickness}
</span>
</div>
<div className={classnames(classes.row)}>
<span className={classnames(classes.infoHeader)}>
{t('Spacing')}
</span>
<span
className={classnames(classes.infoText)}
title={spacing}
>
{spacing}
</span>
</div>
<div className={classnames(classes.row)}>
<span className={classnames(classes.infoHeader)}>
{t('Scanner')}
</span>
<span
className={classnames(classes.infoText)}
title={scanner}
>
{scanner}
</span>
</div>
</div>
</div>
</div>
)
}
>
<div className="relative flex justify-end cursor-pointer">
<div className="relative">
<Icon name="profile" className="w-5 text-white" />
<Icon
name="info-link"
className="absolute w-5 text-white bg-black"
style={{ right: -7, bottom: -10 }}
/>
</div>
</div>
</Tooltip>
</div>
);
}
export default ViewportActionBar;
@@ -7,8 +7,6 @@ import React, {
} from 'react';
import { useTranslation } from 'react-i18next';
import classnames from 'classnames';
import {
Typography,
Input,
@@ -349,7 +347,7 @@ const ViewportDownloadForm = ({
onLabelClick={() => {}}
>
<Select
className="mt-2"
className="mt-2 text-white"
isClearable={false}
value={fileType}
data-cy="file-type"
@@ -387,7 +385,7 @@ const ViewportDownloadForm = ({
<Typography variant="h5">{t('Image preview')}</Typography>
{activeViewportElement && (
<div
className="mx-auto my-0"
className="mx-auto my-2"
style={{
height: viewportElementDimensions.height,
width: viewportElementDimensions.width,
@@ -404,7 +402,15 @@ const ViewportDownloadForm = ({
</div>
<div className="flex justify-end mt-4">
<Button data-cy="cancel-btn" variant="outlined" onClick={onClose}>
<Button
data-cy="cancel-btn"
variant="outlined"
size="initial"
color="black"
border="secondary"
onClick={onClose}
className="p-2"
>
{t('Cancel')}
</Button>
<Button
@@ -64,7 +64,7 @@ function ViewportPane({
onScroll={onInteractionHandler}
onWheel={onInteractionHandler}
className={classnames(
'w-full h-full rounded-lg overflow-hidden hover:border-primary-light transition duration-300 group',
'w-full h-full rounded-md overflow-hidden hover:border-primary-light transition duration-300 group',
{
'border-2 border-primary-light': isActive,
'border-2 border-transparent': !isActive,
@@ -77,7 +77,7 @@ function ViewportPane({
>
<div
className={classnames(
'rounded-lg w-full h-full overflow-hidden group-hover:border-transparent',
'rounded-md w-full h-full overflow-hidden group-hover:border-transparent',
{
'border border-transparent': isActive,
'border border-secondary-light': !isActive,
+12
View File
@@ -24,6 +24,7 @@ import NavBar from './NavBar';
import Notification from './Notification';
import Select from './Select';
import SegmentationTable from './SegmentationTable';
import SegmentationGroupTable from './SegmentationGroupTable';
import SidePanel from './SidePanel';
import SplitButton from './SplitButton';
import StudyBrowser from './StudyBrowser';
@@ -63,6 +64,11 @@ import HotkeyField from './HotkeyField';
import Header from './Header';
import ImageScrollbar from './ImageScrollbar';
import ViewportOverlay from './ViewportOverlay';
import InputRange from './InputRange';
import InputNumber from './InputNumber';
import CheckBox from './CheckBox';
import LoadingIndicatorProgress from './LoadingIndicatorProgress';
import LoadingIndicator from './LoadingIndicator';
export {
AboutModal,
@@ -74,6 +80,7 @@ export {
ButtonGroup,
ContextMenu,
CinePlayer,
CheckBox,
DateRange,
Dialog,
Dropdown,
@@ -86,18 +93,23 @@ export {
Input,
InputDateRange,
InputGroup,
InputRange,
InputNumber,
InputLabelWrapper,
InputMultiSelect,
InputText,
ImageScrollbar,
Label,
LayoutSelector,
LoadingIndicator,
LoadingIndicatorProgress,
MeasurementTable,
Modal,
NavBar,
Notification,
Select,
SegmentationTable,
SegmentationGroupTable,
SidePanel,
SplitButton,
StudyBrowser,
@@ -26,6 +26,7 @@ const DEFAULT_STATE = {
},
],
activeViewportIndex: 0,
cachedLayout: {},
};
export const ViewportGridContext = createContext(DEFAULT_STATE);
@@ -73,10 +74,15 @@ export function ViewportGridProvider({ children, service }) {
viewportLabel: viewportLabels[viewportIndex],
};
return { ...state, ...{ viewports }, cachedLayout: null };
return { ...state, ...{ viewports } };
}
case 'SET_LAYOUT': {
const { numCols, numRows, layoutType, layoutOptions } = action.payload;
const {
numCols,
numRows,
layoutOptions,
layoutType = 'grid',
} = action.payload;
// If empty viewportOptions, we use numRow and numCols to calculate number of viewports
const numPanes = layoutOptions.length || numRows * numCols;
@@ -119,7 +125,6 @@ export function ViewportGridProvider({ children, service }) {
layoutType,
viewports,
},
cachedLayout: null,
};
}
case 'RESET': {
@@ -139,13 +144,41 @@ export function ViewportGridProvider({ children, service }) {
height: 100,
},
],
cachedLayout: null,
cachedLayout: {},
};
}
// The SET_CACHE_LAYOUT action can be used for caching a layout
// for instance double clicking a viewport to maximize it.
// and then restoring the previous layout when the viewport is
// double clicked again.
case 'SET_CACHED_LAYOUT': {
return { ...state, cachedLayout: action.payload };
const { cacheId, cachedLayout } = action.payload;
// deep copy the cachedLayout into the state
return {
...state,
cachedLayout: {
...state.cachedLayout,
[cacheId]: JSON.parse(JSON.stringify(cachedLayout)),
},
};
}
case 'RESTORE_CACHED_LAYOUT': {
const cacheId = action.payload;
if (!state.cachedLayout[cacheId]) {
console.warn(
`No cached layout found for cacheId: ${cacheId}. Ignoring...`
);
return state;
}
const cachedLayout = state.cachedLayout;
return { ...state.cachedLayout[cacheId], cachedLayout };
}
case 'SET': {
return {
...state,
@@ -163,7 +196,9 @@ export function ViewportGridProvider({ children, service }) {
DEFAULT_STATE
);
const getState = useCallback(() => viewportGridState, [viewportGridState]);
const getState = useCallback(() => {
return viewportGridState;
}, [viewportGridState]);
const setActiveViewportIndex = useCallback(
index => dispatch({ type: 'SET_ACTIVE_VIEWPORT_INDEX', payload: index }),
@@ -189,6 +224,15 @@ export function ViewportGridProvider({ children, service }) {
[dispatch]
);
const setDisplaySetsForViewports = useCallback(
viewports => {
viewports.forEach(data => {
setDisplaySetsForViewport(data);
});
},
[setDisplaySetsForViewport]
);
const setLayout = useCallback(
({ layoutType, numRows, numCols, layoutOptions = [] }) =>
dispatch({
@@ -221,6 +265,16 @@ export function ViewportGridProvider({ children, service }) {
[dispatch]
);
const restoreCachedLayout = useCallback(
cacheId => {
dispatch({
type: 'RESTORE_CACHED_LAYOUT',
payload: cacheId,
});
},
[dispatch]
);
const set = useCallback(
payload =>
dispatch({
@@ -231,7 +285,7 @@ export function ViewportGridProvider({ children, service }) {
);
/**
* Sets the implementation of a modal service that can be used by extensions.
* Sets the implementation of ViewportGridService that can be used by extensions.
*
* @returns void
*/
@@ -241,9 +295,11 @@ export function ViewportGridProvider({ children, service }) {
getState,
setActiveViewportIndex,
setDisplaySetsForViewport,
setDisplaySetsForViewports,
setLayout,
reset,
setCachedLayout,
restoreCachedLayout,
set,
});
}
@@ -252,18 +308,22 @@ export function ViewportGridProvider({ children, service }) {
service,
setActiveViewportIndex,
setDisplaySetsForViewport,
setDisplaySetsForViewports,
setLayout,
reset,
setCachedLayout,
restoreCachedLayout,
set,
]);
const api = {
getState,
setActiveViewportIndex,
setActiveViewportIndex: index => service.setActiveViewportIndex(index), // run it through the service itself since we want to publish events
setDisplaySetsForViewport,
setDisplaySetsForViewports,
setLayout,
setCachedLayout,
restoreCachedLayout,
reset,
set,
};
+3
View File
@@ -61,12 +61,15 @@ export {
InputText,
Label,
LayoutSelector,
LoadingIndicator,
LoadingIndicatorProgress,
MeasurementTable,
Modal,
NavBar,
Notification,
Select,
SegmentationTable,
SegmentationGroupTable,
SidePanel,
SplitButton,
StudyBrowser,
+52 -2
View File
@@ -1,11 +1,13 @@
/* IMPORT CUSTOM FONT */
@import url('https://fonts.googleapis.com/css?family=Lato:100,300,400,500,700&display=swap');
@import url('https://fonts.googleapis.com/css?family=Inter:100,300,400,500,700&display=swap');
@tailwind base;
@tailwind components;
@tailwind utilities;
body {
background-color: #000;
}
/* Truncate 2 lines utility */
.truncate-2-lines {
@@ -17,3 +19,51 @@
height: 2.1rem;
}
.customSelect__menu {
background-color: transparent !important;
border-radius: 6px;
}
.css-4ljt47-MenuList {
overflow-x: hidden !important;
margin-right: 4px;
margin-top: 4px;
padding-top: 0px !important;
}
.css-4ljt47-MenuList::-webkit-scrollbar {
width: 6px;
border-radius: 10px;
}
.css-4ljt47-MenuList::-webkit-scrollbar-thumb {
background-color: #0944b3;
border-radius: 10px;
}
.customSelect__option:focus {
background-color: #0944b391 !important;
}
.customSelect__option label {
color: #d6d6d6;
font-size: 14px;
display: flex;
align-items: center;
}
.css-1n7v3ny-option {
background-color: #0944b391 !important;
}
.css-1n7v3ny-option:focus {
background-color: #0944b391 !important;
}
.customSelect__option:hover {
background-color: #0944b391;
}
.css-26l3qy-menu {
border: 2px solid #0944b3;
border-radius: 6px;
background-color: #151515 !important;
}
+7 -85
View File
@@ -1,4 +1,10 @@
module.exports = {
// Note: in Tailwind 3.0, JIT will purge unused styles by default
// but in development, it is often useful to disable this to see
// and try out all the styles that are available.
// ...(process.env.NODE_ENV === 'development' && {
// safelist: [{ pattern: /.*/ }],
// }),
content: ['./src/**/*.{jsx,js,ts,tsx,css,mdx}'],
theme: {
screens: {
@@ -167,36 +173,6 @@ module.exports = {
900: '#702459',
},
},
spacing: {
px: '1px',
'0': '0',
'1': '0.15rem',
'2': '0.5rem',
'3': '0.75rem',
'4': '1rem',
'5': '1.25rem',
'6': '1.5rem',
'8': '2rem',
'10': '2.5rem',
'12': '3rem',
'14': '3.5rem',
'16': '4rem',
'18': '4.5rem',
'20': '5rem',
'24': '6rem',
'32': '8rem',
'40': '10rem',
'48': '12rem',
'56': '14rem',
'64': '16rem',
'72': '18rem',
'80': '20rem',
'88': '22rem',
'96': '24rem',
'104': '26rem',
'112': '28rem',
'250px': '250px',
},
backgroundColor: theme => theme('colors'),
backgroundPosition: {
bottom: 'bottom',
@@ -218,21 +194,6 @@ module.exports = {
...theme('colors'),
DEFAULT: theme('colors.gray.300', 'currentColor'),
}),
borderRadius: {
none: '0',
sm: '0.125rem',
DEFAULT: '0.25rem',
md: '0.375rem',
lg: '0.5rem',
full: '9999px',
},
borderWidth: {
DEFAULT: '1px',
'0': '0',
'2': '2px',
'4': '4px',
'8': '8px',
},
boxShadow: {
xs: '0 0 0 1px rgba(0, 0, 0, 0.05)',
sm: '0 1px 2px 0 rgba(0, 0, 0, 0.05)',
@@ -278,50 +239,11 @@ module.exports = {
'0': '0',
DEFAULT: '1',
},
fontFamily: {
sans: [
'Lato',
'system-ui',
'-apple-system',
'BlinkMacSystemFont',
'"Segoe UI"',
'Roboto',
'"Helvetica Neue"',
'Arial',
'"Noto Sans"',
'sans-serif',
'"Apple Color Emoji"',
'"Segoe UI Emoji"',
'"Segoe UI Symbol"',
'"Noto Color Emoji"',
],
serif: ['Georgia', 'Cambria', '"Times New Roman"', 'Times', 'serif'],
mono: [
'Menlo',
'Monaco',
'Consolas',
'"Liberation Mono"',
'"Courier New"',
'monospace',
],
},
fontSize: {
xs: '0.65rem',
sm: '0.75rem',
base: '0.875rem',
lg: '1rem',
xl: '1.25rem',
'2xl': '1.5rem',
'3xl': '1.875rem',
'4xl': '2.25rem',
'5xl': '3rem',
'6xl': '4rem',
},
fontWeight: {
hairline: '100',
thin: '200',
light: '300',
normal: '400',
normal: '300',
medium: '500',
semibold: '600',
bold: '700',