Lesion Tracker Redesign so far
This commit is contained in:
1 parent
b247e89d8b
commit
a9a4f3f82b
109 files changed
+2972
-936
No files matched your search
+77
@@ -0,0 +1,77 @@
|
||||
<template name="additionalMeasurements">
|
||||
<div id="additionalMeasurements">
|
||||
<div class="scrollArea">
|
||||
<div class="section">
|
||||
<div class="header">
|
||||
Additional Findings: Baseline
|
||||
</div>
|
||||
<div>
|
||||
{{ >radioOptionGroup measurableDiseaseButtonData}}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="section">
|
||||
<div class="header">
|
||||
Supplementary Measurements
|
||||
</div>
|
||||
<div class="entrySection">
|
||||
<label>Number of bone lesions</label>
|
||||
<div>
|
||||
<select>
|
||||
<option>0</option>
|
||||
<option>1-2</option>
|
||||
<option selected>2-4</option>
|
||||
<option>>5</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="entrySection">
|
||||
<label>Regions of metastatic disease</label>
|
||||
<select id="regionsOfMetastaticDisease">
|
||||
<option>None</option>
|
||||
<option>Lymph node</option>
|
||||
</select>
|
||||
</div>
|
||||
{{ >radioOptionGroup metastaticDiseaseButtonData}}
|
||||
</div>
|
||||
<div class="section">
|
||||
<div class="header">
|
||||
Image Quality
|
||||
</div>
|
||||
{{ >radioOptionGroup imageQualityButtonData}}
|
||||
{{ >radioOptionGroup anatomicalCoverageButtonData}}
|
||||
{{ >radioOptionGroup presenceOfContrastButtonData}}
|
||||
</div>
|
||||
<div class="section">
|
||||
<div class="header">
|
||||
Additional Screen Captures
|
||||
</div>
|
||||
<div class="entrySection buttonSection">
|
||||
<div class="svgContainer">
|
||||
<svg>
|
||||
<use xlink:href="/packages/lesiontracker/assets/icons.svg#icon-create-screen-capture"></use>
|
||||
</svg>
|
||||
</div>
|
||||
<div class="buttonLabel noselect">
|
||||
Add new screen capture
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="section">
|
||||
<div class="header">
|
||||
Comments
|
||||
</div>
|
||||
<div class="entrySection buttonSection">
|
||||
<div class="svgContainer">
|
||||
<svg>
|
||||
<use xlink:href="/packages/lesiontracker/assets/icons.svg#icon-create-comment"></use>
|
||||
</svg>
|
||||
</div>
|
||||
<div class="buttonLabel noselect">
|
||||
Add a comment
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
+87
@@ -0,0 +1,87 @@
|
||||
var yesNoOptions = [{
|
||||
key: 'yes',
|
||||
text: 'Yes'
|
||||
}, {
|
||||
key: 'no',
|
||||
text: 'No'
|
||||
}];
|
||||
|
||||
Template.additionalMeasurements.onCreated(function additionalMeasurementsOnCreated() {
|
||||
const instance = this;
|
||||
|
||||
instance.measurableDisease = new ReactiveVar();
|
||||
instance.numberOfBoneLesions = new ReactiveVar();
|
||||
instance.regionsOfMetastaticDisease = new ReactiveVar();
|
||||
instance.tracerRelatedToMetastaticDisease = new ReactiveVar();
|
||||
instance.acceptableImageQuality = new ReactiveVar();
|
||||
instance.adequateAnatomicalCoverage = new ReactiveVar();
|
||||
instance.presenceOfContrast = new ReactiveVar();
|
||||
instance.phaseOfContrast = new ReactiveVar();
|
||||
|
||||
instance.measurableDisease.set('absent');
|
||||
instance.tracerRelatedToMetastaticDisease.set('no');
|
||||
instance.acceptableImageQuality.set('yes');
|
||||
instance.adequateAnatomicalCoverage.set('yes');
|
||||
instance.presenceOfContrast.set('yes');
|
||||
});
|
||||
|
||||
Template.additionalMeasurements.onRendered(function additionalMeasurementsOnRendered() {
|
||||
const instance = this;
|
||||
|
||||
instance.$("#regionsOfMetastaticDisease").select2({
|
||||
multiple: true
|
||||
});
|
||||
});
|
||||
|
||||
Template.additionalMeasurements.helpers({
|
||||
measurableDiseaseButtonData() {
|
||||
const instance = Template.instance();
|
||||
return {
|
||||
text: 'Measurable disease',
|
||||
value: instance.measurableDisease,
|
||||
options: [{
|
||||
key: 'present',
|
||||
text: 'Present'
|
||||
}, {
|
||||
key: 'absent',
|
||||
text: 'Absent'
|
||||
}]
|
||||
};
|
||||
},
|
||||
|
||||
metastaticDiseaseButtonData() {
|
||||
const instance = Template.instance();
|
||||
return {
|
||||
text: 'Tracer related to metastatic disease',
|
||||
value: instance.tracerRelatedToMetastaticDisease,
|
||||
options: yesNoOptions
|
||||
};
|
||||
},
|
||||
|
||||
imageQualityButtonData() {
|
||||
const instance = Template.instance();
|
||||
return {
|
||||
text: 'Acceptable image quality',
|
||||
value: instance.acceptableImageQuality,
|
||||
options: yesNoOptions
|
||||
};
|
||||
},
|
||||
|
||||
anatomicalCoverageButtonData() {
|
||||
const instance = Template.instance();
|
||||
return {
|
||||
text: 'Adequate anatomical coverage',
|
||||
value: instance.adequateAnatomicalCoverage,
|
||||
options: yesNoOptions
|
||||
};
|
||||
},
|
||||
|
||||
presenceOfContrastButtonData() {
|
||||
const instance = Template.instance();
|
||||
return {
|
||||
text: 'Presence of contrast',
|
||||
value: instance.presenceOfContrast,
|
||||
options: yesNoOptions
|
||||
};
|
||||
}
|
||||
});
|
||||
+74
@@ -0,0 +1,74 @@
|
||||
@import "{design}/app"
|
||||
|
||||
#additionalMeasurements
|
||||
width: 100%
|
||||
height: 100%
|
||||
background-color: #000000
|
||||
|
||||
.scrollArea
|
||||
height: 100%
|
||||
overflow-x: hidden
|
||||
overflow-y: auto
|
||||
margin-right: -16px
|
||||
padding-bottom: 100px
|
||||
padding-right: 16px
|
||||
|
||||
-webkit-overflow-scrolling: touch
|
||||
|
||||
&::-webkit-scrollbar
|
||||
display: none
|
||||
|
||||
.header
|
||||
height: 33px
|
||||
line-height: 33px
|
||||
font-weight: 400
|
||||
font-size: 14px
|
||||
padding: 0 10px
|
||||
color: $textPrimaryColor
|
||||
background-color: $darkUiColor
|
||||
|
||||
.entrySection
|
||||
#regionsOfMetastaticDisease
|
||||
width: 285px
|
||||
height: 30px
|
||||
border-radius: 2px
|
||||
background-color: #b6b6b6
|
||||
color: #3e3e3e
|
||||
font-size: 14px
|
||||
|
||||
.buttonSection
|
||||
cursor: pointer
|
||||
height: 100px
|
||||
color: $defaultColor
|
||||
|
||||
&:hover
|
||||
color: $hoverColor
|
||||
|
||||
.svgContainer
|
||||
svg
|
||||
fill: $hoverColor
|
||||
|
||||
&:active
|
||||
color: $activeColor
|
||||
|
||||
.svgContainer
|
||||
svg
|
||||
fill: $activeColor
|
||||
|
||||
div
|
||||
float: left
|
||||
display: inline-block
|
||||
height: 80px
|
||||
|
||||
.buttonLabel
|
||||
line-height: 80px
|
||||
padding: 0 30px
|
||||
|
||||
.svgContainer
|
||||
padding: 15px 5px
|
||||
width: 40px
|
||||
|
||||
svg
|
||||
width: 40px
|
||||
height: 40px
|
||||
fill: $textPrimaryColor
|
||||
+16
@@ -0,0 +1,16 @@
|
||||
<template name="radioOptionGroup">
|
||||
<div class="{{class}} entrySection viewerRadioOptionGroup">
|
||||
<h5>{{text}}</h5>
|
||||
<div>
|
||||
{{#each item in this.options}}
|
||||
<label>
|
||||
<input type="radio"
|
||||
name="{{item.key}}"
|
||||
data-key="{{item.key}}"
|
||||
{{inlineIf value.get item.key 'selected'}}/>
|
||||
{{item.text}}
|
||||
</label>
|
||||
{{/each}}
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
+15
@@ -0,0 +1,15 @@
|
||||
Template.radioOptionGroup.onRendered(() => {
|
||||
const instance = Template.instance();
|
||||
const value = instance.data.value;
|
||||
if (!value.get()) {
|
||||
value.set(instance.data.options[0].key);
|
||||
}
|
||||
});
|
||||
|
||||
Template.radioOptionGroup.events({
|
||||
'click [data-key]'(event, instance) {
|
||||
const $element = $(event.currentTarget);
|
||||
const key = $element.attr('data-key');
|
||||
instance.data.value.set(key);
|
||||
}
|
||||
});
|
||||
+20
@@ -0,0 +1,20 @@
|
||||
@import "{design}/app"
|
||||
|
||||
.entrySection
|
||||
padding: 10px
|
||||
color: $textPrimaryColor
|
||||
|
||||
&:not(:last-child)
|
||||
border-bottom: $uiBorderThickness solid $uiBorderColorDark
|
||||
|
||||
h5
|
||||
font-weight: 300
|
||||
|
||||
label
|
||||
font-size: 14px
|
||||
font-weight: 100
|
||||
display: inline-block
|
||||
padding: 5px
|
||||
|
||||
input
|
||||
margin: 5px
|
||||
@@ -0,0 +1,11 @@
|
||||
<template name="caseProgress">
|
||||
<div class="caseProgress">
|
||||
{{ >radialProgressBar isLocked=isLocked progressPercent=progressPercent progressText=progressText}}
|
||||
{{ #if isLocked }}
|
||||
<h5>Locked</h5>
|
||||
{{ /if }}
|
||||
{{ #if progressComplete }}
|
||||
<button class="btn">Enter response</button>
|
||||
{{ /if }}
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,30 @@
|
||||
Template.caseProgress.onCreated(function caseProgressOnCreated() {
|
||||
const instance = Template.instance();
|
||||
|
||||
instance.progressPercent = new ReactiveVar();
|
||||
instance.progressPercent.set(50);
|
||||
|
||||
instance.progressText = new ReactiveVar();
|
||||
instance.progressText.set(5);
|
||||
|
||||
instance.isLocked = new ReactiveVar(false);
|
||||
});
|
||||
|
||||
Template.caseProgress.helpers({
|
||||
progressPercent() {
|
||||
return Template.instance().progressPercent.get();
|
||||
},
|
||||
|
||||
progressText() {
|
||||
return Template.instance().progressText.get();
|
||||
},
|
||||
|
||||
isLocked() {
|
||||
return Template.instance().isLocked.get();
|
||||
},
|
||||
|
||||
progressComplete() {
|
||||
var progressPercent = Template.instance().progressPercent.get();
|
||||
return progressPercent === 100;
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,3 @@
|
||||
@import "{design}/app"
|
||||
|
||||
//.caseProgress
|
||||
@@ -0,0 +1,20 @@
|
||||
<template name="flexboxLayout">
|
||||
<div class="viewerSection">
|
||||
<div class="sidebarMenu sidebar-left {{ #if studySidebarOpen }}sidebar-open{{/if}}">
|
||||
<div class="timepointButtonContainer p-t-2">
|
||||
{{>roundedButtonGroup buttonGroupData}}
|
||||
</div>
|
||||
<hr class="m-y-0">
|
||||
{{>studyTimepointBrowser timepointViewType=timepointViewType}}
|
||||
</div>
|
||||
<div class="mainContent {{ #if studySidebarOpen }}sidebar-left-open{{/if}} {{ #if rightSidebarOpen }}sidebar-right-open{{/if}}">
|
||||
{{>viewerMain }}
|
||||
</div>
|
||||
<div class="sidebarMenu sidebar-right {{ #if lesionSidebarOpen }}sidebar-open{{/if}}">
|
||||
{{>lesionTable }}
|
||||
</div>
|
||||
<div class="sidebarMenu sidebar-right {{ #if additionalMeasurementsSidebarOpen }}sidebar-open{{/if}}">
|
||||
{{>additionalMeasurements }}
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,66 @@
|
||||
Template.flexboxLayout.onCreated(() => {
|
||||
const instance = Template.instance();
|
||||
instance.state = instance.data.state;
|
||||
instance.timepointViewType = new ReactiveVar();
|
||||
});
|
||||
|
||||
var resizeTimeout;
|
||||
Template.flexboxLayout.onRendered(() => {
|
||||
const instance = Template.instance();
|
||||
|
||||
instance.autorun(function() {
|
||||
Meteor.clearTimeout(resizeTimeout);
|
||||
|
||||
// Trigger a resize any time the layout state changes
|
||||
var studySidebarOpen = instance.state.get('studySidebarOpen');
|
||||
var lesionSidebarOpen = instance.state.get('lesionSidebarOpen');
|
||||
|
||||
var additionalMeasurementsSidebarOpen = instance.state.get('additionalMeasurementsSidebarOpen');
|
||||
|
||||
resizeTimeout = Meteor.setTimeout(function() {
|
||||
handleResize();
|
||||
}, 300);
|
||||
});
|
||||
});
|
||||
|
||||
Template.flexboxLayout.helpers({
|
||||
timepointViewType() {
|
||||
return Template.instance().timepointViewType;
|
||||
},
|
||||
|
||||
buttonGroupData() {
|
||||
const instance = Template.instance();
|
||||
return {
|
||||
value: instance.timepointViewType,
|
||||
options: [{
|
||||
key: 'key',
|
||||
text: 'Key Timepoints'
|
||||
}, {
|
||||
key: 'all',
|
||||
text: 'All Timepoints'
|
||||
}]
|
||||
};
|
||||
},
|
||||
|
||||
studySidebarOpen() {
|
||||
const instance = Template.instance();
|
||||
return instance.state.get('studySidebarOpen');
|
||||
},
|
||||
|
||||
lesionSidebarOpen() {
|
||||
const instance = Template.instance();
|
||||
return instance.state.get('lesionSidebarOpen');
|
||||
},
|
||||
|
||||
additionalMeasurementsSidebarOpen() {
|
||||
const instance = Template.instance();
|
||||
return instance.state.get('additionalMeasurementsSidebarOpen');
|
||||
},
|
||||
|
||||
rightSidebarOpen() {
|
||||
const instance = Template.instance();
|
||||
const lesionSidebarOpen = instance.data.state.get('lesionSidebarOpen');
|
||||
const additionalMeasurementsSidebarOpen = instance.data.state.get('additionalMeasurementsSidebarOpen');
|
||||
return lesionSidebarOpen || additionalMeasurementsSidebarOpen;
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,69 @@
|
||||
@import "{design}/app"
|
||||
|
||||
.viewerSection
|
||||
display: inline-flex
|
||||
flex-flow: row nowrap
|
||||
align-items: stretch
|
||||
height: 'calc(100% - %s)' % $toolbarHeight
|
||||
width: 100%
|
||||
|
||||
.sidebarMenu
|
||||
position: absolute
|
||||
height: 100%
|
||||
background: $primaryBackgroundColor
|
||||
-webkit-transition: $sidebarTransition
|
||||
-ms-transition: $sidebarTransition
|
||||
-o-transition: $sidebarTransition
|
||||
-moz-transition: $sidebarTransition
|
||||
transition: $sidebarTransition
|
||||
|
||||
.timepointButtonContainer
|
||||
height: 55px
|
||||
|
||||
.sidebar-left
|
||||
width: $studiesSidebarMenuWidth
|
||||
order: 1
|
||||
flex: 0
|
||||
left: - $studiesSidebarMenuWidth
|
||||
border-right: $uiBorderThickness $uiBorderColor solid
|
||||
|
||||
&.sidebar-open
|
||||
left: 0
|
||||
|
||||
.mainContent
|
||||
order: 2
|
||||
width: 100%
|
||||
height: 100%
|
||||
-webkit-transition: $sidebarTransition
|
||||
-ms-transition: $sidebarTransition
|
||||
-o-transition: $sidebarTransition
|
||||
-moz-transition: $sidebarTransition
|
||||
transition: $sidebarTransition
|
||||
transform: translate3d(0,0,0);
|
||||
|
||||
&.sidebar-left-open.sidebar-right-open
|
||||
width: 'calc(100% - %s)' % ($studiesSidebarMenuWidth + $lesionsSidebarMenuWidth)
|
||||
|
||||
&.sidebar-right-open
|
||||
width: 'calc(100% - %s)' % $lesionsSidebarMenuWidth
|
||||
|
||||
&.sidebar-left-open
|
||||
width: 'calc(100% - %s)' % $studiesSidebarMenuWidth
|
||||
|
||||
&.sidebar-left-open
|
||||
transform: translate3d($studiesSidebarMenuWidth,0,0);
|
||||
|
||||
.sidebar-right
|
||||
width: $lesionsSidebarMenuWidth
|
||||
order: 3
|
||||
flex: 0
|
||||
right: - $lesionsSidebarMenuWidth
|
||||
border-left: $uiBorderThickness $uiBorderColor solid
|
||||
|
||||
&.sidebar-open
|
||||
right: 0
|
||||
|
||||
.studiesListedChanger
|
||||
padding: 20px 10px
|
||||
text-align: center
|
||||
border-bottom: $uiBorderThickness $uiBorderColor solid
|
||||
@@ -9,9 +9,7 @@
|
||||
</button>
|
||||
</div>
|
||||
<div class="hidingPanelContent">
|
||||
{{#if studyDateIsShown}}
|
||||
{{> studyDateList}}
|
||||
{{/if}}
|
||||
{{> studyDateList}}
|
||||
<div class="studyBrowserContainer">
|
||||
{{> studyBrowser}}
|
||||
</div>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
|
||||
function pinPanel() {
|
||||
var hidingPanel = $(".hidingPanel");
|
||||
var hidingPanel = $('.hidingPanel');
|
||||
hidingPanel.css({
|
||||
width: '122px'
|
||||
});
|
||||
@@ -12,12 +12,12 @@ function pinPanel() {
|
||||
// Calculate newWidth of viewportAndLesionTable
|
||||
var viewerWidth = $('#viewer').width();
|
||||
var newWidth = 100 - 122 / viewerWidth * 100;
|
||||
$("#viewportAndLesionTable").css('width', newWidth + '%');
|
||||
$('#viewportAndLesionTable').css('width', newWidth + '%');
|
||||
}
|
||||
|
||||
function unpinPanel() {
|
||||
// Calculate newWidth of viewportAndLesionTable
|
||||
$("#viewportAndLesionTable").css('width', '99%');
|
||||
// Calculate newWidth of viewportAndLesionTable
|
||||
$('#viewportAndLesionTable').css('width', '99%');
|
||||
}
|
||||
|
||||
Template.hidingPanel.events({
|
||||
@@ -90,9 +90,6 @@ Template.hidingPanel.events({
|
||||
});
|
||||
|
||||
Template.hidingPanel.helpers({
|
||||
studyDateIsShown: function() {
|
||||
return true;
|
||||
},
|
||||
isPinned: function() {
|
||||
return Template.instance().isPinned.get();
|
||||
}
|
||||
@@ -117,7 +114,7 @@ Template.hidingPanel.onCreated(function() {
|
||||
if (!isPanelPinned) {
|
||||
isPanelPinned = false;
|
||||
}
|
||||
|
||||
this.isPinned = new ReactiveVar(isPanelPinned);
|
||||
|
||||
|
||||
});
|
||||
@@ -1,27 +1,30 @@
|
||||
<template name="lesionTable">
|
||||
<div id="lesionTableContainer">
|
||||
<div id="dragbar"></div>
|
||||
<div id="lesionTableWrapper">
|
||||
<table class="table table-striped noselect lesionTable" id="tblLesion">
|
||||
<thead>
|
||||
<tr>
|
||||
<th id="thNumber">Lesion #</th>
|
||||
<!--<th id="thLocation">Location</th>-->
|
||||
<th id="thTarget">Target</th>
|
||||
<!--Each time point name as a column-->
|
||||
{{ #each timepoints }}
|
||||
{{ >lesionTableTimepointHeader }}
|
||||
{{ /each }}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<!--Each measurement as row-->
|
||||
{{ #each measurement }}
|
||||
{{ >lesionTableRow }}
|
||||
{{ /each }}
|
||||
</tbody>
|
||||
</table>
|
||||
<div class="lesionTableLayoutChanger viewerRoundedButtonGroup">
|
||||
{{ >roundedButtonGroup buttonGroupData }}
|
||||
</div>
|
||||
<div class="lesionTableTimepointHeaderRow">
|
||||
<div class="spacer">
|
||||
</div>
|
||||
{{ #each timepoints }}
|
||||
{{ >lesionTableTimepointHeader }}
|
||||
{{ /each }}
|
||||
</div>
|
||||
<div class="lesionTableWrapper">
|
||||
{{ >lesionTableHeaderRow id="target" type="Targets" measurements=targets}}
|
||||
{{ #each targets }}
|
||||
{{ >lesionTableRow }}
|
||||
{{ /each }}
|
||||
|
||||
{{ >lesionTableHeaderRow id="nonTarget" type="Non-targets" measurements=nonTargets}}
|
||||
{{ #each nonTargets }}
|
||||
{{ >lesionTableRow }}
|
||||
{{ /each }}
|
||||
|
||||
{{ >lesionTableHeaderRow id="newLesions" type="New lesions" measurements=newLesions}}
|
||||
{{ #each nonTargets }}
|
||||
{{ >lesionTableRow }}
|
||||
{{ /each }}
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -1,9 +1,30 @@
|
||||
Template.lesionTable.helpers({
|
||||
measurement: function() {
|
||||
targets: function() {
|
||||
// All Targets shall be listed first followed by Non-Targets
|
||||
return Measurements.find({}, {
|
||||
return Measurements.find({
|
||||
isTarget: true
|
||||
}, {
|
||||
sort: {
|
||||
lesionNumberAbsolute: 1
|
||||
}
|
||||
});
|
||||
},
|
||||
nonTargets: function() {
|
||||
// All Targets shall be listed first followed by Non-Targets
|
||||
return Measurements.find({
|
||||
isTarget: false
|
||||
}, {
|
||||
sort: {
|
||||
lesionNumberAbsolute: 1
|
||||
}
|
||||
});
|
||||
},
|
||||
newLesions: function() {
|
||||
// All Targets shall be listed first followed by Non-Targets
|
||||
return Measurements.find({
|
||||
saved: false
|
||||
}, {
|
||||
sort: {
|
||||
isTarget: -1,
|
||||
lesionNumberAbsolute: 1
|
||||
}
|
||||
});
|
||||
@@ -14,59 +35,43 @@ Template.lesionTable.helpers({
|
||||
latestDate: 1
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
buttonGroupData() {
|
||||
const instance = Template.instance();
|
||||
return {
|
||||
value: instance.lesionTableLayout,
|
||||
options: [{
|
||||
key: 'comparison',
|
||||
text: 'Comparison'
|
||||
}, {
|
||||
key: 'key',
|
||||
text: 'Key Timepoints'
|
||||
}]
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
Template.lesionTable.events({
|
||||
// 'click table#tblLesion tbody tr': function(e, template) {
|
||||
// Retrieve the lesion id from the DOM data for this row
|
||||
// var measurementId = $(e.currentTarget).data('measurementid');
|
||||
|
||||
// activateLesion(measurementId, template.data);
|
||||
//},
|
||||
'mousedown div#dragbar': function(e, template) {
|
||||
var pY = e.pageY;
|
||||
var draggableParent = $(e.currentTarget).parent();
|
||||
var startHeight = draggableParent.height();
|
||||
template.dragging.set(true);
|
||||
|
||||
$(document).on('mouseup', function(e) {
|
||||
template.dragging.set(false);
|
||||
$(document).off('mouseup').off('mousemove');
|
||||
});
|
||||
|
||||
$(document).on('mousemove', function(e) {
|
||||
var topPosition = e.pageY - pY;
|
||||
var newHeight = startHeight - topPosition;
|
||||
|
||||
// Min lesion table height = 5px
|
||||
if (newHeight < 5) {
|
||||
return;
|
||||
}
|
||||
|
||||
draggableParent.css({
|
||||
top: topPosition,
|
||||
height: newHeight
|
||||
});
|
||||
|
||||
var viewportAndLesionTableHeight = $('#viewportAndLesionTable').height();
|
||||
var newPercentageHeightofLesionTable = (startHeight - topPosition) / viewportAndLesionTableHeight * 100;
|
||||
var newPercentageHeightofViewermain = 100 - newPercentageHeightofLesionTable;
|
||||
$('.viewerMain').height(newPercentageHeightofViewermain + '%');
|
||||
|
||||
// Resize viewport
|
||||
resizeViewportElements();
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
Template.lesionTable.onCreated(function() {
|
||||
this.dragging = new ReactiveVar(false);
|
||||
/**
|
||||
* Retrieve the lesion id from the DOM data for this row
|
||||
*/
|
||||
/*'click table#tblLesion tbody tr': function(e, template) {
|
||||
var measurementId = $(e.currentTarget).data('measurementid');
|
||||
activateLesion(measurementId, template.data);
|
||||
},*/
|
||||
});
|
||||
|
||||
// Temporary until we have a real window manager with events for series/study changed
|
||||
Session.setDefault('NewSeriesLoaded', false);
|
||||
|
||||
Template.lesionTable.onCreated(function() {
|
||||
var instance = this;
|
||||
|
||||
instance.lesionTableLayout = new ReactiveVar();
|
||||
instance.lesionTableLayout.set('comparisonTimepoints');
|
||||
});
|
||||
|
||||
Template.lesionTable.onRendered(function() {
|
||||
// Find the first measurement by Lesion Number
|
||||
var firstLesion = Measurements.findOne({}, {
|
||||
|
||||
@@ -1,93 +1,23 @@
|
||||
@import "{design}/app"
|
||||
|
||||
#lesionTableContainer
|
||||
width: 100%
|
||||
height: 16%
|
||||
background-color: #000000
|
||||
|
||||
#dragbar
|
||||
width: 100%
|
||||
height: 3px
|
||||
top: 0
|
||||
left: 0
|
||||
right: 0
|
||||
bottom: 0
|
||||
background-color: #666666
|
||||
cursor: row-resize
|
||||
|
||||
#lesionTableWrapper
|
||||
position: relative
|
||||
width: 100%
|
||||
height: 100%
|
||||
margin: 0 auto
|
||||
|
||||
.lesionTable
|
||||
.lesionTableTimepointHeaderRow
|
||||
display: flex
|
||||
|
||||
td, th
|
||||
border: 1px solid #FFFFFF
|
||||
border-collapse: collapse
|
||||
width: 100px
|
||||
.spacer
|
||||
width: 20px
|
||||
|
||||
td.lesionNumber, th#thNumber
|
||||
width: 70px
|
||||
|
||||
td.location, th#thLocation
|
||||
width: 250px
|
||||
|
||||
td.target, th#thTarget
|
||||
width: 70px
|
||||
|
||||
|
||||
thead
|
||||
width: 100%
|
||||
height: 40px
|
||||
display: block
|
||||
|
||||
tr
|
||||
width: 100%
|
||||
height: 40px
|
||||
|
||||
th
|
||||
border-bottom-width: 0
|
||||
font-weight: normal
|
||||
color: #FFFFFF
|
||||
line-height: 15px
|
||||
text-align: center
|
||||
margin: 0
|
||||
padding: 5px
|
||||
background-color: #424242
|
||||
vertical-align: middle
|
||||
|
||||
tbody
|
||||
display: block
|
||||
position: absolute
|
||||
top: 40px
|
||||
left: 0
|
||||
right: 0
|
||||
bottom: 0
|
||||
overflow: auto
|
||||
width: 100%
|
||||
padding-bottom: 20px
|
||||
|
||||
tr.selectedRow
|
||||
background: #F5F5F5 !important
|
||||
color: #000000 !important
|
||||
|
||||
tr.lesionTableRow
|
||||
width: 100%
|
||||
|
||||
&:nth-child(even)
|
||||
background: #888
|
||||
&:nth-child(odd)
|
||||
background: #BBB
|
||||
&:hover
|
||||
cursor: pointer
|
||||
background-color: #009BD2
|
||||
color: white
|
||||
|
||||
td
|
||||
border-bottom-width: 0
|
||||
margin: 0
|
||||
padding: 5px
|
||||
line-height: 15px
|
||||
|
||||
&.lesionNumber, &.target, &.lesionTableTimepointCell
|
||||
text-align: center
|
||||
.lesionTableLayoutChanger
|
||||
padding: 5px
|
||||
text-align: center
|
||||
margin: 0 auto
|
||||
+22
@@ -0,0 +1,22 @@
|
||||
<template name="lesionTableHeaderRow">
|
||||
<div class="lesionTableHeaderRow">
|
||||
<div class="add">
|
||||
<svg>
|
||||
<use xlink:href=/packages/lesiontracker/assets/icons.svg#icon-ui-add></use>
|
||||
</svg>
|
||||
</div>
|
||||
<div class="type">
|
||||
{{type}}
|
||||
</div>
|
||||
<div class="max">
|
||||
{{ #if maxNumLesions }}
|
||||
<p class="maxNumLesions">
|
||||
Max {{maxNumLesions}}
|
||||
</p>
|
||||
{{/if}}
|
||||
</div>
|
||||
<div class="numberOfLesions">
|
||||
{{numberOfLesions}}
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,38 @@
|
||||
Template.lesionTableHeaderRow.helpers({
|
||||
numberOfLesions: function() {
|
||||
var instance = Template.instance();
|
||||
return instance.data.measurements.count();
|
||||
},
|
||||
maxNumLesions: function() {
|
||||
var instance = Template.instance();
|
||||
var lesionType = instance.id;
|
||||
|
||||
// TODO: Check what we are annotating
|
||||
var timepointType = 'baseline';
|
||||
|
||||
// Identify which Trial Conformance Criteria are currently being used
|
||||
// Note that there may be more than one.
|
||||
var criteriaTypes = TrialCriteriaTypes.find({
|
||||
selected: true
|
||||
}).map(function(criteria) {
|
||||
return criteria.id;
|
||||
});
|
||||
|
||||
var currentConstraints = getTrialCriteriaConstraints(criteriaTypes);
|
||||
if (!currentConstraints) {
|
||||
// For testing
|
||||
return 10;
|
||||
}
|
||||
|
||||
var criteria = currentConstraints[timepointType][lesionType];
|
||||
|
||||
return criteria.group.totalNumberOfLesions;
|
||||
}
|
||||
});
|
||||
|
||||
Template.lesionTableHeaderRow.events({
|
||||
'click .add': function() {
|
||||
console.log('Add was clicked');
|
||||
// TODO: Set active tool to new type
|
||||
}
|
||||
});
|
||||
+58
@@ -0,0 +1,58 @@
|
||||
@import "{design}/app"
|
||||
|
||||
$headerRowHeight = 63px
|
||||
|
||||
.lesionTableHeaderRow
|
||||
background: $darkUiColor
|
||||
color: $textSecondaryColor
|
||||
fill: $textSecondaryColor
|
||||
width: 100%
|
||||
height: $headerRowHeight
|
||||
display: flex
|
||||
line-height: $headerRowHeight
|
||||
|
||||
div
|
||||
flex: 1
|
||||
justify-content: space-around
|
||||
align-items: stretch
|
||||
text-align: center
|
||||
|
||||
.add
|
||||
padding: 0 10px
|
||||
max-width: 20px
|
||||
cursor: pointer
|
||||
|
||||
svg
|
||||
height: $headerRowHeight
|
||||
max-width: 11px
|
||||
|
||||
.type
|
||||
cursor: pointer
|
||||
font-weight: 200
|
||||
height: $headerRowHeight
|
||||
font-size: 13pt
|
||||
|
||||
.numberOfLesions
|
||||
float: right
|
||||
font-weight: 100
|
||||
font-size: 40px
|
||||
max-width: 30px
|
||||
margin-right: 5px
|
||||
height: $headerRowHeight
|
||||
line-height: $headerRowHeight
|
||||
|
||||
.max
|
||||
height: $headerRowHeight
|
||||
background: $darkUiColor
|
||||
|
||||
.maxNumLesions
|
||||
margin: 21px auto
|
||||
height: 20px
|
||||
line-height: 20px
|
||||
width: 47px
|
||||
font-size: 12px
|
||||
text-transform: uppercase
|
||||
font-weight: 500
|
||||
border-radius: 3px
|
||||
color: black
|
||||
background-color: $textSecondaryColor;
|
||||
@@ -1,11 +1,38 @@
|
||||
<template name="lesionTableRow">
|
||||
<tr id="{{lesionNumber}}" class="lesionTableRow" data-measurementid="{{_id}}">
|
||||
<td class='lesionNumber'>{{lesionNumberAbsolute}}</td>
|
||||
<!--<td class='location' tabindex="0">{{location}}</td>-->
|
||||
<td class='target'>{{#if isTarget}} Y {{else}} N {{/if}}</td>
|
||||
<!--Each time point as a column-->
|
||||
{{# each timepoints }}
|
||||
{{> lesionTableTimepointCell}}
|
||||
{{/ each }}
|
||||
</tr>
|
||||
<div class="lesionTableRow" data-measurementid="{{_id}}">
|
||||
<div class='lesionRowSidebar'>
|
||||
<div class="lesionNumber">
|
||||
{{lesionNumberAbsolute}}
|
||||
</div>
|
||||
<div class="lesionNumber">
|
||||
{{ #if descriptionRequired }}
|
||||
<div>
|
||||
<svg>
|
||||
<use xlink:href=/packages/lesiontracker/assets/icons.svg#icon-ui-warning></use>
|
||||
</svg>
|
||||
</div>
|
||||
{{ /if }}
|
||||
{{ #if responseStatus }}
|
||||
<div>
|
||||
CR
|
||||
</div>
|
||||
{{ /if }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="lesionDetails">
|
||||
<div class='location'>
|
||||
{{ #if location }}
|
||||
{{location}}
|
||||
{{ else }}
|
||||
(No description)
|
||||
{{ /if }}
|
||||
</div>
|
||||
<div class="timepointData">
|
||||
<!--Each time point as a column-->
|
||||
{{# each timepoints }}
|
||||
{{> lesionTableTimepointCell}}
|
||||
{{/ each }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -1,3 +1,39 @@
|
||||
.lesionTableRow .location
|
||||
&:hover
|
||||
font-weight: bold
|
||||
@import "{design}/app"
|
||||
|
||||
.lesionTableRow
|
||||
display: flex
|
||||
width: 100%
|
||||
|
||||
.lesionRowSidebar
|
||||
flex: 1
|
||||
color: $textSecondaryColor
|
||||
background: $darkUiColor
|
||||
max-width: 20px
|
||||
height: 60px
|
||||
|
||||
.lesionNumber
|
||||
padding: 3px
|
||||
|
||||
&.active
|
||||
border-left: 4px $activeColor solid
|
||||
|
||||
.lesionDetails
|
||||
flex: 1
|
||||
display: flex
|
||||
flex-direction: column
|
||||
align-items: stretch
|
||||
|
||||
.location
|
||||
flex: 1
|
||||
text-align: center
|
||||
color: $textSecondaryColor
|
||||
height: 20px
|
||||
width: 100%
|
||||
|
||||
.timepointData
|
||||
display: flex
|
||||
|
||||
div
|
||||
flex: 1
|
||||
text-align: center
|
||||
justify-content: space-around
|
||||
+6
-6
@@ -1,11 +1,11 @@
|
||||
<template name="lesionTableTimepointCell">
|
||||
{{ #if hasDataAtThisTimepoint }}
|
||||
{{ #if isBirectional }}
|
||||
<td class="lesionTableTimepointCell targetCell" tabindex="1">{{displayData}}</td>
|
||||
{{ else }}
|
||||
<td class="lesionTableTimepointCell nonTargetCell" tabindex="1">{{displayData}}</td>
|
||||
{{ /if }}
|
||||
<div class="lesionTableTimepointCell" tabindex="1">
|
||||
{{displayData}}
|
||||
</div>
|
||||
{{ else }}
|
||||
<td class="lesionTableTimepointCell empty"></td>
|
||||
<div class="lesionTableTimepointCell empty">
|
||||
...
|
||||
</div>
|
||||
{{ /if }}
|
||||
</template>
|
||||
+5
-7
@@ -1,8 +1,6 @@
|
||||
@import "{design}/app"
|
||||
|
||||
.lesionTableTimepointCell
|
||||
&:hover
|
||||
font-weight: bold
|
||||
.nonTargetCell
|
||||
&:hover
|
||||
color: red
|
||||
|
||||
|
||||
border-left: $uiBorderThickness $uiBorderColor solid
|
||||
color: $textPrimaryColor
|
||||
padding: 0 10px
|
||||
+8
-4
@@ -1,6 +1,10 @@
|
||||
<template name="lesionTableTimepointHeader">
|
||||
<th class="lesionTableTimepointCell {{ #if isLoaded }}loaded{{/if}}" title="Latest study date: {{formatDA latestDate "MM/DD/YYYY"}}"
|
||||
data-toggle="tooltip" data-placement="top">
|
||||
{{ timepointName }}
|
||||
</th>
|
||||
<div class="lesionTableTimepointHeader">
|
||||
<div class="timepointName">
|
||||
{{ timepointName }}
|
||||
</div>
|
||||
<div class="timepointDate">
|
||||
{{ formatDA latestDate "DD-MMM-YY"}}
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
+15
-3
@@ -1,3 +1,15 @@
|
||||
#lesionTableContainer .lesionTable thead tr th.lesionTableTimepointCell.loaded
|
||||
//font-weight: bold
|
||||
color: #4fbfff
|
||||
@import "{design}/app"
|
||||
|
||||
.lesionTableTimepointHeader
|
||||
flex: 1
|
||||
justify-content: space-around
|
||||
|
||||
div
|
||||
text-align: center
|
||||
|
||||
.timepointName
|
||||
color: $textSecondaryColor
|
||||
|
||||
.timepointDate
|
||||
color: $textPrimaryColor
|
||||
border-left: $uiBorderColor $uiBorderThickness solid
|
||||
@@ -0,0 +1,35 @@
|
||||
<template name="lesionTracker">
|
||||
<div class="topBar">
|
||||
<a class="brandSection pull-left" href="http://ohif.org">
|
||||
<img class="logoImage" src="/images/logo.png">
|
||||
<div class="logoText">
|
||||
Open Health Imaging Foundation
|
||||
</div>
|
||||
</a>
|
||||
{{ #unless onStudyList}}
|
||||
<div class="studyListLinkSection pull-left">
|
||||
<a id="loadStudyList">Study List</a>
|
||||
</div>
|
||||
{{ /unless }}
|
||||
<div class="userAccountSection pull-right">
|
||||
{{>userAccountMenu}}
|
||||
</div>
|
||||
</div>
|
||||
{{>timeoutCountdownDialog}}
|
||||
<div id="worklistTabs" class="tab-content">
|
||||
<div class="tab-pane active" id="worklistTab">
|
||||
<div class="worklistContainer">
|
||||
{{> worklistResult }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="tab-pane" id="viewerTab">
|
||||
</div>
|
||||
</div>
|
||||
{{ >associationModal }}
|
||||
{{ >optionsModal }}
|
||||
{{ >serverInformationModal }}
|
||||
{{ >confirmRemoveTimepointAssociation }}
|
||||
{{ >lastLoginModal }}
|
||||
{{ >studyContextMenu }}
|
||||
{{ >progressDialog}}
|
||||
</template>
|
||||
@@ -0,0 +1,65 @@
|
||||
// Define the ViewerData global object
|
||||
// If there is currently any Session data for this object,
|
||||
// use this to repopulate the variable
|
||||
ViewerData = Session.get('ViewerData') || {};
|
||||
|
||||
Template.lesionTracker.onCreated(function() {
|
||||
// showViewer to go to viewer from audit
|
||||
this.showWorklistMenu = new ReactiveVar(true);
|
||||
// Get url and check worklist
|
||||
var currentRoute = Router.current();
|
||||
if (!currentRoute || !currentRoute.route) {
|
||||
return;
|
||||
}
|
||||
|
||||
var currentPath = currentRoute.route.path(this);
|
||||
if (currentPath === '/' || currentPath === '/worklist') {
|
||||
this.showWorklistMenu.set(false);
|
||||
}
|
||||
});
|
||||
|
||||
Template.lesionTracker.onRendered(function() {
|
||||
var templateData = Template.currentData();
|
||||
if (templateData && templateData.studyInstanceUid) {
|
||||
var studyInstanceUid = templateData.studyInstanceUid;
|
||||
openNewTab(studyInstanceUid, studyInstanceUid);
|
||||
} else {
|
||||
// If there is a tab set as active in the Session,
|
||||
// switch to that now.
|
||||
var contentId = Session.get('activeContentId');
|
||||
|
||||
// TODO: FIx this it seems to be forcing two switches
|
||||
switchToTab(contentId);
|
||||
}
|
||||
|
||||
Meteor.subscribe('hangingprotocols');
|
||||
});
|
||||
|
||||
Template.lesionTracker.events({
|
||||
'click #tablist a[data-toggle="tab"]': function(e) {
|
||||
// If this tab is already active, do nothing
|
||||
var tabButton = $(e.currentTarget);
|
||||
var tabTitle = tabButton.parents('.tabTitle');
|
||||
if (tabTitle.hasClass('active')) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Otherwise, switch to the tab
|
||||
var contentId = tabButton.data('target').replace('#', '');
|
||||
switchToTab(contentId);
|
||||
},
|
||||
'click #loadStudyList': function() {
|
||||
switchToTab('worklistTab');
|
||||
}
|
||||
});
|
||||
|
||||
Session.set('defaultSignInMessage', 'Tumor tracking in your browser.');
|
||||
|
||||
Template.lesionTracker.helpers({
|
||||
showWorklistMenu: function() {
|
||||
return Template.instance().showWorklistMenu.get();
|
||||
},
|
||||
onStudyList: function() {
|
||||
return (Session.get('activeContentId') === 'worklistTab');
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,47 @@
|
||||
@import "{design}/app"
|
||||
|
||||
.topBar
|
||||
height: $topBarHeight
|
||||
color: $textPrimaryColor
|
||||
background-color: $primaryBackgroundColor
|
||||
|
||||
.brandSection
|
||||
height: 100%
|
||||
display: inline-block
|
||||
|
||||
img.logoImage
|
||||
height: 100%
|
||||
margin: 0 5px
|
||||
|
||||
.logoText
|
||||
display: inline-block
|
||||
font-family: $logoFontFamily
|
||||
font-weight: $logoFontWeight
|
||||
color: $textPrimaryColor
|
||||
|
||||
.studyListLinkSection
|
||||
border-left: $uiBorderThickness solid $uiBorderColor;
|
||||
padding: 5px
|
||||
margin: 0 10px
|
||||
|
||||
a
|
||||
text-decoration: none
|
||||
cursor: pointer
|
||||
line-height: 100%
|
||||
|
||||
.userAccountSection
|
||||
margin: 0 10px
|
||||
|
||||
#worklistTabs
|
||||
height: "calc(100% - %s)" % $topBarHeight
|
||||
|
||||
// Override Viewerbase's borders
|
||||
#imageViewerViewports .viewportContainer
|
||||
border: none !important
|
||||
outline: 0 !important // Prevent blue outline in Chrome
|
||||
|
||||
&:hover
|
||||
&.active
|
||||
&:hover.active
|
||||
border: none !important
|
||||
outline: 0 !important // Prevent blue outline in Chrome
|
||||
@@ -1,12 +1,3 @@
|
||||
<template name="lesionTrackerLayout">
|
||||
<div class="logoContainer">
|
||||
{{>userAccountMenu}}
|
||||
<a class="navbar-brand" href="http://ohif.org">
|
||||
<img src="/images/logo.png">
|
||||
<h4 class="name">Open Health Imaging Foundation</h4>
|
||||
</a>
|
||||
</div>
|
||||
{{>timeoutCountdownDialog}}
|
||||
{{>progressDialog}}
|
||||
{{>yield }}
|
||||
</template>
|
||||
@@ -1,126 +0,0 @@
|
||||
Session.set('defaultSignInMessage', 'Tumor tracking in your browser.');
|
||||
|
||||
Template.lesionTrackerLayout.helpers({
|
||||
fullName: function() {
|
||||
return Meteor.user().profile.fullName;
|
||||
},
|
||||
|
||||
showWorklistMenu: function() {
|
||||
return Template.instance().showWorklistMenu.get();
|
||||
},
|
||||
|
||||
currentUser: function() {
|
||||
var verifyEmail = Meteor.settings && Meteor.settings.public && Meteor.settings.public.verifyEmail || false;
|
||||
|
||||
if (!Meteor.user() || !Meteor.userId()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!verifyEmail) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (Meteor.user().emails[0].verified) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
});
|
||||
|
||||
Template.lesionTrackerLayout.onCreated(function() {
|
||||
// showViewer to go to viewer from audit
|
||||
this.showWorklistMenu = new ReactiveVar(true);
|
||||
// Get url and check worklist
|
||||
var currentRoute = Router.current();
|
||||
if (!currentRoute || !currentRoute.route) {
|
||||
return;
|
||||
}
|
||||
|
||||
var currentPath = currentRoute.route.path(this);
|
||||
if (currentPath === '/' || currentPath === '/worklist') {
|
||||
this.showWorklistMenu.set(false);
|
||||
}
|
||||
|
||||
// Show countdown dialog
|
||||
var handle;
|
||||
$(document).on('TriggerOpenTimeoutCountdownDialog', function(e, leftTime) {
|
||||
// Show modal dialog
|
||||
handle = setInterval(function() {
|
||||
leftTime--;
|
||||
// Set countdownDialogLeftTime session
|
||||
Session.set('countdownDialogLeftTime', leftTime);
|
||||
|
||||
// Show dialog
|
||||
var dialog = $('#timeoutCountdownDialog');
|
||||
if (dialog.css('display') === 'none') {
|
||||
dialog.css('display', 'block');
|
||||
}
|
||||
}, 1000);
|
||||
|
||||
});
|
||||
|
||||
$(document).on('TriggerCloseTimeoutCountdownDialog', function(e) {
|
||||
var dialog = $('#timeoutCountdownDialog');
|
||||
dialog.css('display', 'none');
|
||||
if (handle) {
|
||||
clearInterval(handle);
|
||||
// Close the dialog
|
||||
dialog.css('display', 'none');
|
||||
|
||||
// Remove reviewers info for the user
|
||||
Meteor.call('removeUserFromReviewers', Meteor.userId());
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
Template.lesionTrackerLayout.onRendered(function() {
|
||||
var oldUserId = undefined;
|
||||
var userName;
|
||||
var lastLoginModalInterval;
|
||||
|
||||
Tracker.autorun(function() {
|
||||
// Hook login/logout
|
||||
var newUserId = Meteor.userId();
|
||||
if (oldUserId === null && newUserId) {
|
||||
Session.set('showLastLoginModal', true);
|
||||
userName = Meteor.user().profile.fullName;
|
||||
// Log
|
||||
HipaaLogger.logEvent({
|
||||
eventType: 'init',
|
||||
userId: Meteor.userId(),
|
||||
userName: userName
|
||||
});
|
||||
} else if (newUserId === null && oldUserId) {
|
||||
// Set showLastLoginModal as null
|
||||
Session.set('showLastLoginModal', null);
|
||||
// Destroy interval for last login modal
|
||||
Meteor.clearInterval(lastLoginModalInterval);
|
||||
console.log('The user logged out');
|
||||
|
||||
// Log
|
||||
// TODO: eventype is not defined for logout in hipaa-audit-log
|
||||
/*HipaaLogger.logEvent({
|
||||
eventType: 'logout',
|
||||
userId: oldUserId,
|
||||
userName: userName
|
||||
});*/
|
||||
|
||||
// Remove the user from Reviewers
|
||||
Meteor.call('removeUserFromReviewers', oldUserId);
|
||||
}
|
||||
oldUserId = Meteor.userId();
|
||||
|
||||
// Trigger last login date popup
|
||||
if (Session.get('showLastLoginModal')) {
|
||||
Modal.show('lastLoginModal');
|
||||
lastLoginModalInterval = Meteor.setInterval( function() {
|
||||
Modal.hide('lastLoginModal');
|
||||
Session.set("showLastLoginModal", null);
|
||||
}, 3000);
|
||||
return true;
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -1,38 +0,0 @@
|
||||
.logoContainer
|
||||
position: absolute
|
||||
height: 40px
|
||||
top: 0
|
||||
right: 0
|
||||
padding: 0 10px
|
||||
color: white
|
||||
@media only screen and (min-device-width : 375px) and (max-device-width : 667px) and (orientation : portrait)
|
||||
width: 15%
|
||||
|
||||
.navbar-brand
|
||||
display: inline-block
|
||||
padding: 0
|
||||
margin-right: 5px
|
||||
height: 100%
|
||||
|
||||
img
|
||||
height: 30px
|
||||
float: left
|
||||
margin-right: 10px
|
||||
margin-top: 5px
|
||||
/* Media queries for iPhone6*/
|
||||
@media only screen and (min-device-width : 375px) and (max-device-width : 667px) and (orientation : portrait)
|
||||
height: 20px
|
||||
margin-top: 10px
|
||||
margin-bottom: 10px
|
||||
margin-right: 5px
|
||||
|
||||
h4.name
|
||||
float: left
|
||||
margin: 10px 0
|
||||
font-family: "Sanchez"
|
||||
color: white
|
||||
@media only screen and (min-device-width : 375px) and (max-device-width : 667px) and (orientation : portrait)
|
||||
font-size: 0.3em
|
||||
font-size: 0.3em
|
||||
width: 50%
|
||||
|
||||
+101
-2
@@ -1,3 +1,102 @@
|
||||
@import "{design}/app"
|
||||
|
||||
$viewportTagPadding = 20px
|
||||
$imageSliderBorderRadius = 57px
|
||||
$imageSliderColor = #22374d
|
||||
$imageSliderTrackColor = rgba(0,0,0,0)
|
||||
$imageSliderBorder = none
|
||||
|
||||
// Note that these are backwards due to the magic needed to make a cross-browser vertical slider
|
||||
$imageSliderWidth = 12px
|
||||
$imageSliderHeight = 39px
|
||||
$imageSliderCursor = grab
|
||||
|
||||
.imageViewerViewportOverlay
|
||||
.timepointName
|
||||
color: #32BFFF
|
||||
color: $textSecondaryColor
|
||||
|
||||
.dicomTag
|
||||
font-weight: 200
|
||||
|
||||
.topleft
|
||||
top: $viewportTagPadding
|
||||
left: $viewportTagPadding
|
||||
|
||||
.topright
|
||||
top: $viewportTagPadding
|
||||
right: $viewportTagPadding
|
||||
text-align: right
|
||||
|
||||
.bottomleft
|
||||
bottom: $viewportTagPadding
|
||||
left: $viewportTagPadding
|
||||
|
||||
.bottomright
|
||||
bottom: $viewportTagPadding
|
||||
right: $viewportTagPadding
|
||||
text-align: right
|
||||
|
||||
.imageControls
|
||||
#scrollbar
|
||||
#imageSlider
|
||||
-webkit-appearance: none
|
||||
background-color: $imageSliderTrackColor
|
||||
|
||||
// --- Style the range track --- //
|
||||
&::-webkit-slider-runnable-track
|
||||
height: 5px
|
||||
border: none
|
||||
cursor: pointer
|
||||
background-color: $imageSliderTrackColor
|
||||
z-index: 6
|
||||
|
||||
&::-moz-range-track
|
||||
height: 2px
|
||||
border: none
|
||||
cursor: pointer
|
||||
background-color: $imageSliderTrackColor
|
||||
z-index: 6
|
||||
|
||||
&::-ms-track
|
||||
width: 100%
|
||||
height: 2px
|
||||
border: none
|
||||
cursor: pointer
|
||||
animate: 0.2s
|
||||
background: transparent
|
||||
border-width: 15px 0
|
||||
color: $imageSliderTrackColor
|
||||
|
||||
// Hide any fill IE tries to add
|
||||
&::-ms-fill-lower
|
||||
background: $imageSliderTrackColor
|
||||
|
||||
&::-ms-fill-upper
|
||||
background: $imageSliderTrackColor
|
||||
|
||||
// --- Style the range thumb --- //
|
||||
&::-webkit-slider-thumb
|
||||
-webkit-appearance: none !important
|
||||
background-color: $imageSliderColor
|
||||
border-radius: $imageSliderBorderRadius
|
||||
border: $imageSliderBorder
|
||||
height: $imageSliderWidth
|
||||
width: $imageSliderHeight
|
||||
cursor: $imageSliderCursor
|
||||
margin-top: -7px
|
||||
|
||||
&::-moz-range-thumb
|
||||
background-color: $imageSliderColor
|
||||
border-radius: $imageSliderBorderRadius
|
||||
border: $imageSliderBorder
|
||||
height: $imageSliderWidth
|
||||
width: $imageSliderHeight
|
||||
cursor: $imageSliderCursor
|
||||
z-index: 7
|
||||
|
||||
&::-ms-thumb
|
||||
background-color: $imageSliderColor
|
||||
border-radius: $imageSliderBorderRadius
|
||||
border: $imageSliderBorder
|
||||
height: $imageSliderWidth
|
||||
width: $imageSliderHeight
|
||||
cursor: $imageSliderCursor
|
||||
+7
-3
@@ -45,7 +45,10 @@ Template[defaultTemplate].helpers({
|
||||
return;
|
||||
}
|
||||
|
||||
var timepointReviewers = Reviewers.findOne({timepointId: timepoint.timepointId});
|
||||
var timepointReviewers = Reviewers.findOne({
|
||||
timepointId: timepoint.timepointId
|
||||
});
|
||||
|
||||
if (!timepointReviewers) {
|
||||
return;
|
||||
}
|
||||
@@ -59,7 +62,7 @@ function getReviewerTipText(reviewers) {
|
||||
return;
|
||||
}
|
||||
|
||||
var newReviewers = reviewers.filter(function ( reviewer ) {
|
||||
var newReviewers = reviewers.filter(function(reviewer) {
|
||||
return reviewer.userId !== Meteor.userId();
|
||||
});
|
||||
|
||||
@@ -76,6 +79,7 @@ function getReviewerTipText(reviewers) {
|
||||
if (index > 0) {
|
||||
tipText += ',';
|
||||
}
|
||||
|
||||
tipText += reviewer.userName;
|
||||
|
||||
});
|
||||
@@ -96,4 +100,4 @@ WorklistTabs.find().observe({
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,43 @@
|
||||
<template name="radialProgressBar">
|
||||
<div class="radialProgress">
|
||||
<svg id="svg"
|
||||
width="46"
|
||||
height="46"
|
||||
viewPort="0 0 50 50"
|
||||
version="1.1"
|
||||
xmlns="http://www.w3.org/2000/svg">
|
||||
|
||||
<!-- stroke-dasharray is 2 * PI * radius !-->
|
||||
<circle r="13"
|
||||
cx="23"
|
||||
cy="23"
|
||||
fill="transparent"
|
||||
stroke-dasharray="81.64"
|
||||
stroke-dashoffset="0">
|
||||
</circle>
|
||||
<circle id="bar"
|
||||
r="13"
|
||||
cx="23"
|
||||
cy="23"
|
||||
fill="transparent"
|
||||
stroke-dasharray="81.64"
|
||||
style="stroke-dashoffset: {{progressRadius}}px;">
|
||||
</circle>
|
||||
</svg>
|
||||
<div class="progressText">
|
||||
{{ #if isLocked }}
|
||||
<svg>
|
||||
<use xlink:href=/packages/lesiontracker/assets/icons.svg#icon-status-lock></use>
|
||||
</svg>
|
||||
{{ else }}
|
||||
{{ #if progressComplete }}
|
||||
<svg>
|
||||
<use xlink:href=/packages/lesiontracker/assets/icons.svg#icon-status-complete></use>
|
||||
</svg>
|
||||
{{ else }}
|
||||
{{progressText}}
|
||||
{{ /if }}
|
||||
{{ /if }}
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,13 @@
|
||||
Template.radialProgressBar.helpers({
|
||||
progressComplete() {
|
||||
const instance = Template.instance();
|
||||
return instance.data.progressPercent === 100;
|
||||
},
|
||||
|
||||
progressRadius() {
|
||||
var radius = 13 * 2 * Math.PI;
|
||||
const instance = Template.instance();
|
||||
var percentLeft = (100 - instance.data.progressPercent) / 100;
|
||||
return percentLeft * radius;
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,41 @@
|
||||
@import "{design}/app"
|
||||
|
||||
$circleSize = 46px
|
||||
$progressTextColor = #6fbde2
|
||||
|
||||
.radialProgress
|
||||
height: $circleSize
|
||||
width: $circleSize
|
||||
box-shadow: 0 0 1em black
|
||||
border-radius: 100%
|
||||
position: relative
|
||||
|
||||
#svg circle
|
||||
stroke-dashoffset: 0
|
||||
transition: stroke-dashoffset 1s linear
|
||||
stroke: #203244
|
||||
stroke-width: 3px
|
||||
|
||||
#svg #bar
|
||||
stroke: $activeColor
|
||||
-ms-transform: rotate(270deg); /* IE 9 */
|
||||
-ms-transform-origin: center center; /* IE 9 */
|
||||
-webkit-transform: rotate(270deg); /* Chrome, Safari, Opera */
|
||||
-webkit-transform-origin: center center; /* Chrome, Safari, Opera */
|
||||
transform: rotate(270deg);
|
||||
transform-origin: center center;
|
||||
|
||||
.progressText
|
||||
position: absolute;
|
||||
width: $circleSize;
|
||||
height: $circleSize;
|
||||
line-height: $circleSize;
|
||||
font-family: Roboto-Bold;
|
||||
font-size: 17px;
|
||||
text-align: center;
|
||||
color: $progressTextColor;
|
||||
top: 0;
|
||||
|
||||
svg
|
||||
width: 17px
|
||||
height: 17px
|
||||
+8
-4
@@ -1,4 +1,3 @@
|
||||
|
||||
function parseUrl(url) {
|
||||
var parser = document.createElement('a');
|
||||
parser.href = url;
|
||||
@@ -11,14 +10,19 @@ Template.serverInformationModal.helpers({
|
||||
var serviceInfo = Meteor.settings[defaultServiceType];
|
||||
if (defaultServiceType === 'dicomWeb') {
|
||||
var serverInformationDicom = [];
|
||||
var endpoints = serviceInfo["endpoints"];
|
||||
var endpoints = serviceInfo['endpoints'];
|
||||
endpoints.forEach(function(endpoint) {
|
||||
var parsedUrl = parseUrl(endpoint.qidoRoot);
|
||||
serverInformationDicom.push({host: parsedUrl.hostname, port: parsedUrl.port, aeTitle: endpoint.name});
|
||||
serverInformationDicom.push({
|
||||
host: parsedUrl.hostname,
|
||||
port: parsedUrl.port,
|
||||
aeTitle: endpoint.name
|
||||
});
|
||||
});
|
||||
|
||||
return serverInformationDicom;
|
||||
}
|
||||
|
||||
return serviceInfo;
|
||||
}
|
||||
});
|
||||
});
|
||||
+29
@@ -0,0 +1,29 @@
|
||||
<template name="studySeriesQuickSwitch">
|
||||
<div class="quickSwitch {{side}}">
|
||||
<div class="switchSection {{#if equals side "left"}}pull-right{{/if}} js-show-studies">
|
||||
<div class="label">Study</div>
|
||||
<div class="studySwitch">
|
||||
</div>
|
||||
</div>
|
||||
<div class="switchSection {{#if equals side "left"}}pull-right{{/if}}">
|
||||
<div class="label">Series</div>
|
||||
<div class="seriesSwitch {{#if equals side "left"}}mirror-x{{/if}} js-show-series">
|
||||
<svg viewBox="0 0 53 53">
|
||||
<rect fill="#22364C" x="0" y="38" width="15" height="15" rx="3"></rect>
|
||||
<rect fill="#22364C" x="19" y="38" width="15" height="15" rx="3"></rect>
|
||||
<rect fill="#22364C" x="38" y="19" width="15" height="15" rx="3"></rect>
|
||||
<rect fill="#22364C" x="19" y="19" width="15" height="15" rx="3"></rect>
|
||||
<rect fill="#40D9B8" x="0" y="19" width="15" height="15" rx="3"></rect>
|
||||
<rect fill="#22364C" x="38" y="0" width="15" height="15" rx="3"></rect>
|
||||
<rect fill="#22364C" x="19" y="0" width="15" height="15" rx="3"></rect>
|
||||
<rect fill="#22364C" x="0" y="0" width="15" height="15" rx="3"></rect>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="studyHover js-hide-studies {{ #unless studiesOpen}}hidden{{/unless}}">
|
||||
</div>
|
||||
<div class="seriesHover js-hide-series {{ #unless seriesOpen }}tiny{{/unless}}">
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
+34
@@ -0,0 +1,34 @@
|
||||
Template.studySeriesQuickSwitch.onCreated(function studySeriesQuickSwitchOnCreated() {
|
||||
const instance = this;
|
||||
|
||||
instance.seriesOpen = new ReactiveVar(false);
|
||||
instance.studiesOpen = new ReactiveVar(false);
|
||||
});
|
||||
|
||||
Template.studySeriesQuickSwitch.events({
|
||||
'mouseenter .js-show-series'(event, instance) {
|
||||
instance.seriesOpen.set(true);
|
||||
},
|
||||
|
||||
'mouseenter .js-show-studies'(event, instance) {
|
||||
instance.studiesOpen.set(true);
|
||||
},
|
||||
|
||||
'mouseleave .js-hide-series'(event, instance) {
|
||||
instance.seriesOpen.set(false);
|
||||
},
|
||||
|
||||
'mouseleave .js-hide-studies'(event, instance) {
|
||||
instance.studiesOpen.set(false);
|
||||
}
|
||||
});
|
||||
|
||||
Template.studySeriesQuickSwitch.helpers({
|
||||
seriesOpen() {
|
||||
return Template.instance().seriesOpen.get();
|
||||
},
|
||||
|
||||
studiesOpen() {
|
||||
return Template.instance().studiesOpen.get();
|
||||
}
|
||||
});
|
||||
+68
@@ -0,0 +1,68 @@
|
||||
@import "{design}/app"
|
||||
|
||||
$switchSize = 50px
|
||||
|
||||
.quickSwitch
|
||||
position: absolute
|
||||
width: 140px
|
||||
height: 80px
|
||||
top: 5px
|
||||
|
||||
div.switchSection
|
||||
float: left
|
||||
display: inline-block
|
||||
margin: 0 5px
|
||||
|
||||
.label
|
||||
display: block
|
||||
text-transform: uppercase
|
||||
text-align: center
|
||||
font-size: 12px
|
||||
font-weight: 100
|
||||
color: $textSecondaryColor
|
||||
|
||||
.studySwitch
|
||||
display: block
|
||||
margin: 0 auto
|
||||
width: $switchSize
|
||||
height: $switchSize
|
||||
border-radius: 11px
|
||||
background-color: $darkUiColor
|
||||
border: solid 1px $activeColor
|
||||
|
||||
.seriesSwitch
|
||||
display: block
|
||||
width: $switchSize
|
||||
height: $switchSize
|
||||
|
||||
svg
|
||||
width: $switchSize
|
||||
height: $switchSize
|
||||
|
||||
.mirror-x
|
||||
transform(scaleX(-1));
|
||||
|
||||
.studyHover
|
||||
position: absolute
|
||||
z-index: 10000
|
||||
top: 70px
|
||||
left: -350px
|
||||
width: 400px
|
||||
height: 500px
|
||||
background: red
|
||||
transition(all 0.3s ease)
|
||||
|
||||
.seriesHover
|
||||
position: absolute
|
||||
z-index: 10000
|
||||
top: 70px
|
||||
left: 50px
|
||||
width: 700px
|
||||
height: 900px
|
||||
background: blue
|
||||
transition(all 0.3s ease)
|
||||
|
||||
&.tiny
|
||||
width: 0
|
||||
height: 0
|
||||
opacity: 0
|
||||
@@ -0,0 +1,9 @@
|
||||
<template name="studyTimepoint">
|
||||
<div class="studyTimepointWrapper">
|
||||
<div class="studyTimepoint">
|
||||
{{#each study in (studies this.timepoint)}}
|
||||
{{>studyTimepointStudy study=study}}
|
||||
{{/each}}
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,58 @@
|
||||
// Initialize the timepoint wrapper max-height to enable CSS transition
|
||||
Template.studyTimepoint.onRendered(() => {
|
||||
const instance = Template.instance();
|
||||
|
||||
const $studies = instance.$('.studyTimepoint');
|
||||
const $wrapper = $studies.closest('.studyTimepointWrapper');
|
||||
const $timepoint = $wrapper.closest('.timepointEntry');
|
||||
const studiesVisible = $studies.is(':visible');
|
||||
|
||||
if (!studiesVisible) {
|
||||
$timepoint.addClass('active');
|
||||
}
|
||||
|
||||
// Recalculates the timepoint height to make CSS transition smoother
|
||||
$studies.trigger('displayStateChanged');
|
||||
|
||||
if (!studiesVisible) {
|
||||
$timepoint.removeClass('active');
|
||||
}
|
||||
});
|
||||
|
||||
Template.studyTimepoint.events({
|
||||
// Changes the selected study
|
||||
'selectionChanged .studyTimepoint'(event, instance, selection) {
|
||||
const $selection = $(selection);
|
||||
const $thumbnails = $selection.find('.studyTimepointThumbnails');
|
||||
const $timepoint = instance.$('.studyTimepoint');
|
||||
|
||||
// Set the max-height to inherit to be able to expand the wrapper on its full height
|
||||
instance.$('.studyTimepointWrapper').css('max-height', 'inherit');
|
||||
|
||||
// Removes selected state from all studies but the triggered study
|
||||
instance.$('.studyTimepointStudy').not(selection).removeClass('active');
|
||||
|
||||
// Toggle selected state for the triggered study
|
||||
$selection.toggleClass('active');
|
||||
|
||||
// Recalculates the timepoint height to make CSS transition smoother
|
||||
$thumbnails.one('transitionend', () => $timepoint.trigger('displayStateChanged'));
|
||||
},
|
||||
// It should be triggered when the timepoint height is changed
|
||||
'displayStateChanged .studyTimepoint'(event, instance) {
|
||||
const $timepoint = $(event.currentTarget);
|
||||
const $wrapper = $timepoint.closest('.studyTimepointWrapper');
|
||||
|
||||
// Set the max-height for the wrapper to make CSS transition smoother
|
||||
$wrapper.css('max-height', $timepoint.height());
|
||||
}
|
||||
});
|
||||
|
||||
Template.studyTimepoint.helpers({
|
||||
studies: function(timepoint) {
|
||||
const query = {
|
||||
selected: true
|
||||
};
|
||||
return ViewerStudies.find(query);
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,8 @@
|
||||
@import "{design}/app"
|
||||
|
||||
.studyTimepointWrapper
|
||||
overflow: hidden
|
||||
transition($sidebarTransition)
|
||||
|
||||
.timepointEntry:not(.active) .studyTimepointWrapper
|
||||
max-height: 0 !important
|
||||
+29
@@ -0,0 +1,29 @@
|
||||
<template name="studyTimepointBrowser">
|
||||
<div class="studyTimepointBrowser">
|
||||
<div class="studyTimepointScrollArea">
|
||||
<div class="p-x-1">
|
||||
{{#each timepoint in timepoints}}
|
||||
{{#if shouldShowTimepoint timepoint @index}}
|
||||
<div class="timepointEntry p-y-2 active">
|
||||
<div class="timepointHeader">
|
||||
<div class="timepointDetails clearfix">
|
||||
<div class="timepointFollowupTitle pull-left">
|
||||
{{timepointTitle timepoint timepoints.count @index}}
|
||||
</div>
|
||||
<div class="expandIcon pull-right">
|
||||
<i class="fa fa-chevron-down"></i>
|
||||
</div>
|
||||
<div class="timepointDate pull-right m-r-1">{{formatDA timepoint.earliestDate 'D-MMM-YYYY'}}</div>
|
||||
</div>
|
||||
<!-- TODO: [design] find out where to get this information -->
|
||||
<div class="timepointModalities">2 MR, 2 CT</div>
|
||||
</div>
|
||||
{{>studyTimepoint timepoint=timepoint index=@index}}
|
||||
</div>
|
||||
<hr class="m-y-1">
|
||||
{{/if}}
|
||||
{{/each}}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
+46
@@ -0,0 +1,46 @@
|
||||
Template.studyTimepointBrowser.events({
|
||||
'click .timepointHeader'(event, instance) {
|
||||
const $timepoint = $(event.currentTarget).closest('.timepointEntry');
|
||||
|
||||
// Recalculates the timepoint height to make CSS transition smoother
|
||||
$timepoint.find('.studyTimepoint').trigger('displayStateChanged');
|
||||
|
||||
// Toggle active class to group/ungroup timepoint studies
|
||||
$timepoint.toggleClass('active');
|
||||
}
|
||||
});
|
||||
|
||||
Template.studyTimepointBrowser.helpers({
|
||||
timepoints: function() {
|
||||
const sort = {
|
||||
sort: {
|
||||
earliestDate: -1
|
||||
}
|
||||
};
|
||||
return Timepoints.find({}, sort);
|
||||
},
|
||||
shouldShowTimepoint(timepoint, index) {
|
||||
const instance = Template.instance();
|
||||
if (instance.data.timepointViewType.get() === 'all') {
|
||||
return true;
|
||||
}
|
||||
|
||||
return index < 4 || timepoint.timepointType === 'baseline';
|
||||
},
|
||||
// Build the timepoint title based on its date
|
||||
timepointTitle(timepoint, total, index) {
|
||||
if (timepoint.timepointType === 'baseline') {
|
||||
return 'Baseline';
|
||||
}
|
||||
|
||||
const states = {
|
||||
0: '(Current)',
|
||||
1: '(Prior)',
|
||||
2: '(Nadir)'
|
||||
};
|
||||
// TODO: [design] find out how to define the nadir timepoint
|
||||
const followUp = total - index - 1;
|
||||
const parenthesis = states[index] || '';
|
||||
return `Follow-up ${followUp} ${parenthesis}`;
|
||||
}
|
||||
});
|
||||
+68
@@ -0,0 +1,68 @@
|
||||
@import "{design}/app"
|
||||
|
||||
.studyTimepointBrowser
|
||||
background-color: $primaryBackgroundColor
|
||||
float: left
|
||||
height: calc(100% - 55px - 50px - 30px)
|
||||
overflow: hidden
|
||||
width: 100%
|
||||
|
||||
.timepointEntry
|
||||
&.active
|
||||
.timepointModalities
|
||||
height: 0
|
||||
.timepointDate
|
||||
display: none
|
||||
.expandIcon i
|
||||
transform: rotateX(180deg)
|
||||
.timepointFollowupTitle
|
||||
font-size: 14px
|
||||
color: $textSecondaryColor
|
||||
text-transform: uppercase
|
||||
|
||||
.timepointHeader
|
||||
cursor: pointer
|
||||
font-size: 13px
|
||||
|
||||
&
|
||||
.expandIcon i
|
||||
.timepointFollowupTitle
|
||||
.timepointDate
|
||||
.timepointModalities
|
||||
color: $textSecondaryColor
|
||||
transition($sidebarTransition)
|
||||
|
||||
&:hover
|
||||
&
|
||||
.timepointFollowupTitle
|
||||
.expandIcon i
|
||||
.timepointDate
|
||||
.timepointModalities
|
||||
color: $textPrimaryColor !important
|
||||
|
||||
.timepointDetails
|
||||
.timepointModalities
|
||||
line-height: 25px
|
||||
|
||||
.timepointModalities
|
||||
height: 25px
|
||||
overflow: hidden
|
||||
|
||||
.expandIcon i
|
||||
color: $defaultColor
|
||||
|
||||
.timepointFollowupTitle
|
||||
color: $textPrimaryColor
|
||||
font-family: Roboto
|
||||
padding-top: 2px
|
||||
|
||||
.studyTimepointScrollArea
|
||||
height: 100%
|
||||
overflow-x: hidden
|
||||
overflow-y: auto
|
||||
margin-right: -16px
|
||||
padding-bottom: 20px
|
||||
padding-right: 16px
|
||||
|
||||
&::-webkit-scrollbar
|
||||
display: none
|
||||
+18
@@ -0,0 +1,18 @@
|
||||
<template name="studyTimepointStudy">
|
||||
<div class="studyTimepointStudy">
|
||||
<div class="studyModality">
|
||||
<div class="studyModalityBox">
|
||||
{{this.study.modalities}}
|
||||
</div>
|
||||
<div class="studyModalityText">
|
||||
<div class="studyModalityDate">{{formatDA this.study.studyDate 'D-MMM-YYYY'}}</div>
|
||||
<div class="studyModalityDescription">{{this.study.studyDescription}}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="studyTimepointThumbnails">
|
||||
{{#each thumbnail in (thumbnails this.study)}}
|
||||
{{>thumbnailEntry thumbnail}}
|
||||
{{/each}}
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,40 @@
|
||||
// Initialize the study wrapper max-height to enable CSS transition
|
||||
Template.studyTimepointStudy.onRendered(() => {
|
||||
const instance = Template.instance();
|
||||
const $study = instance.$('.studyTimepointStudy');
|
||||
const $thumbnails = instance.$('.studyTimepointThumbnails');
|
||||
$study.addClass('active');
|
||||
$thumbnails.css('max-height', $thumbnails.height());
|
||||
$study.removeClass('active');
|
||||
});
|
||||
|
||||
Template.studyTimepointStudy.events({
|
||||
// Recalculates the timepoint height to make CSS transition smoother
|
||||
'transitionend .studyTimepointThumbnails'(event, instance) {
|
||||
if (event.target === event.currentTarget) {
|
||||
$(event.currentTarget).closest('.studyTimepoint').trigger('displayStateChanged');
|
||||
}
|
||||
},
|
||||
// Changes the current study selection for the clicked study
|
||||
'click .studyModality'(event, instance) {
|
||||
const $study = $(event.currentTarget).closest('.studyTimepointStudy');
|
||||
const $timepoint = $study.closest('.studyTimepoint');
|
||||
const study = $study[0];
|
||||
$timepoint.trigger('selectionChanged', [study]);
|
||||
}
|
||||
});
|
||||
|
||||
Template.studyTimepointStudy.helpers({
|
||||
thumbnails: function(study) {
|
||||
var stacks = createStacks(study);
|
||||
|
||||
var array = [];
|
||||
stacks.forEach(function(stack, index) {
|
||||
array.push({
|
||||
thumbnailIndex: index,
|
||||
stack: stack
|
||||
});
|
||||
});
|
||||
return array;
|
||||
}
|
||||
});
|
||||
+99
@@ -0,0 +1,99 @@
|
||||
@import "{design}/app"
|
||||
|
||||
$boxBackgroundColor = #3E5975
|
||||
$boxBorderColor = transparent
|
||||
$boxHoverBackgroundColor = #14191E
|
||||
$boxHoverBorderColor = #2d4660
|
||||
$boxActiveBackgroundColor = $activeColor
|
||||
$boxActiveTextColor = $primaryBackgroundColor
|
||||
$boxActiveBorderColor = #131D29
|
||||
$boxWidth = 50px
|
||||
$borderThickness = 2px
|
||||
$boxBorderRadius = 8px
|
||||
$nestingMargin = 6px
|
||||
$spacerX = 7px
|
||||
$spacerY = 12px
|
||||
|
||||
.studyTimepointStudy
|
||||
|
||||
&.active
|
||||
.studyModality
|
||||
box-shadow: inset 0 0 0 3px $activeColor
|
||||
.studyModalityBox
|
||||
color: $boxActiveTextColor
|
||||
&
|
||||
&:before
|
||||
&:after
|
||||
background-color: $boxActiveBackgroundColor
|
||||
border-color: $boxActiveBorderColor
|
||||
|
||||
&:not(.active) .studyTimepointThumbnails
|
||||
max-height: 0 !important
|
||||
|
||||
.studyTimepointThumbnails
|
||||
overflow: hidden
|
||||
transition($sidebarTransition)
|
||||
|
||||
.studyModality
|
||||
border: 1px solid $boxBorderColor
|
||||
border-radius: 12px
|
||||
cursor: pointer
|
||||
font-family: Roboto
|
||||
padding: $spacerY $spacerX ($spacerY - 1)
|
||||
position: relative
|
||||
transition($sidebarTransition)
|
||||
z-index: 1
|
||||
|
||||
&:hover
|
||||
background-color: $boxHoverBackgroundColor
|
||||
border-color: $boxHoverBorderColor
|
||||
|
||||
.studyModalityText
|
||||
font-size: 13px
|
||||
left: ($spacerX * 3) + $boxWidth + ($nestingMargin * 3)
|
||||
line-height: 14px
|
||||
position: absolute
|
||||
right: $spacerX
|
||||
top: $spacerY
|
||||
.studyModalityDate
|
||||
margin-top: 8px
|
||||
color: $textSecondaryColor
|
||||
.studyModalityDescription
|
||||
margin-top: 8px
|
||||
color: $textPrimaryColor
|
||||
|
||||
.studyModalityBox
|
||||
color: $textSecondaryColor
|
||||
font-size: 22px
|
||||
line-height: $boxWidth
|
||||
margin-left: $nestingMargin * 2
|
||||
margin-top: $nestingMargin * 2
|
||||
position: relative
|
||||
text-align: center
|
||||
text-transform: uppercase
|
||||
|
||||
&
|
||||
&:before
|
||||
&:after
|
||||
background-color: $boxBackgroundColor
|
||||
border: $borderThickness solid $primaryBackgroundColor
|
||||
border-radius: $boxBorderRadius
|
||||
height: $boxWidth + ($borderThickness * 2)
|
||||
transition($sidebarTransition)
|
||||
width: $boxWidth + ($borderThickness * 2)
|
||||
|
||||
&:before
|
||||
&:after
|
||||
display: block
|
||||
content: ''
|
||||
position: absolute
|
||||
|
||||
&:before
|
||||
top: -($borderThickness + $nestingMargin)
|
||||
left: -($borderThickness + $nestingMargin)
|
||||
z-index: -1
|
||||
|
||||
&:after
|
||||
top: -($borderThickness + ($nestingMargin * 2))
|
||||
left: -($borderThickness + ($nestingMargin * 2))
|
||||
z-index: -2
|
||||
+2
-2
@@ -1,10 +1,10 @@
|
||||
<template name="timeoutCountdownDialog">
|
||||
<div id="timeoutCountdownDialog">
|
||||
<div class="dialogHeader">
|
||||
<h5>Session is about to expire!</h5>
|
||||
<h5>Your session is about to expire.</h5>
|
||||
</div>
|
||||
<div class="dialogContent">
|
||||
<p>You will be log out in {{leftTime}} {{secondsText}}.</p>
|
||||
<p>You will be logged out in {{timeLeft}}.</p>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
+37
-7
@@ -1,9 +1,39 @@
|
||||
|
||||
Template.timeoutCountdownDialog.helpers({
|
||||
leftTime: function(e) {
|
||||
return Session.get("countdownDialogLeftTime");
|
||||
},
|
||||
secondsText: function() {
|
||||
return Session.get("countdownDialogLeftTime") > 1? "seconds": "second";
|
||||
timeLeft: function() {
|
||||
var timeLeft = Session.get('countdownDialogLeftTime');
|
||||
var suffix = timeLeft > 1 ? 'seconds': 'second';
|
||||
return timeLeft + suffix;
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
Template.timeoutCountdownDialog.onRendered(function() {
|
||||
// Show countdown dialog
|
||||
var timeoutCountdownInterval;
|
||||
$(document).on('TriggerOpenTimeoutCountdownDialog', function(e, leftTime) {
|
||||
// Show modal dialog
|
||||
timeoutCountdownInterval = setInterval(function() {
|
||||
// Set countdownDialogLeftTime session
|
||||
Session.set('countdownDialogLeftTime', --leftTime);
|
||||
|
||||
var dialog = $('#timeoutCountdownDialog');
|
||||
|
||||
// Show dialog
|
||||
dialog.css('display', 'block');
|
||||
}, 1000);
|
||||
|
||||
});
|
||||
|
||||
$(document).on('TriggerCloseTimeoutCountdownDialog', function() {
|
||||
var dialog = $('#timeoutCountdownDialog');
|
||||
|
||||
// Close the dialog
|
||||
dialog.css('display', 'none');
|
||||
|
||||
if (timeoutCountdownInterval) {
|
||||
clearInterval(timeoutCountdownInterval);
|
||||
|
||||
// Remove reviewer info for the user
|
||||
Meteor.call('removeUserFromReviewers', Meteor.userId());
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,79 @@
|
||||
<template name="toolbarSection">
|
||||
<div class="toolbarSection">
|
||||
<div id="studySidebarToggle" class="pull-left toolbarSectionButton">
|
||||
<div class="capsule svgContainer">
|
||||
<svg>
|
||||
<use xlink:href="/packages/lesiontracker/assets/icons.svg#icon-studies"></use>
|
||||
</svg>
|
||||
</div>
|
||||
<div class="buttonLabel">
|
||||
Studies
|
||||
</div>
|
||||
</div>
|
||||
<div class="pull-left">
|
||||
{{ #each toolbarButtons }}
|
||||
{{ >toolbarSectionButton }}
|
||||
{{ /each }}
|
||||
|
||||
<div id="moreTools" class="toolbarSectionButton">
|
||||
<div class="svgContainer">
|
||||
<svg>
|
||||
<use xlink:href="/packages/lesiontracker/assets/icons.svg#icon-tools-more"></use>
|
||||
</svg>
|
||||
</div>
|
||||
<div class="buttonLabel">
|
||||
More <i class="fa fa-caret-down" aria-hidden="true"></i>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{ >studySeriesQuickSwitch side="left"}}
|
||||
{{ >studySeriesQuickSwitch side="right"}}
|
||||
|
||||
<div class="pull-right toolbarSectionEntry">
|
||||
{{ >caseProgress }}
|
||||
</div>
|
||||
<div class="capsule-group pull-right">
|
||||
<div id="lesionSidebarToggle" class="capsule-entry">
|
||||
<div class="svgContainer capsule">
|
||||
<svg>
|
||||
<use xlink:href="/packages/lesiontracker/assets/icons.svg#icon-measurements-lesions"></use>
|
||||
</svg>
|
||||
</div>
|
||||
<div class="buttonLabel">
|
||||
Lesions
|
||||
</div>
|
||||
</div>
|
||||
<div id="additionalMeasurementsSidebarToggle" class="capsule-entry">
|
||||
<div class="svgContainer capsule">
|
||||
<svg>
|
||||
<use xlink:href="/packages/lesiontracker/assets/icons.svg#icon-measurements-additional"></use>
|
||||
</svg>
|
||||
</div>
|
||||
<div class="buttonLabel">
|
||||
Additional
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="pull-right toolbarSectionButton">
|
||||
<div class="svgContainer">
|
||||
<svg>
|
||||
<use xlink:href="/packages/lesiontracker/assets/icons.svg#icon-hud"></use>
|
||||
</svg>
|
||||
</div>
|
||||
<div class="buttonLabel">
|
||||
HUD
|
||||
</div>
|
||||
</div>
|
||||
<div class="pull-right toolbarSectionButton">
|
||||
<div class="svgContainer">
|
||||
<svg>
|
||||
<use xlink:href="/packages/lesiontracker/assets/icons.svg#icon-trial-info"></use>
|
||||
</svg>
|
||||
</div>
|
||||
<div class="buttonLabel">
|
||||
Trial
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,133 @@
|
||||
Template.toolbarSection.onRendered(function() {
|
||||
var instance = this;
|
||||
|
||||
instance.state = instance.data.state;
|
||||
});
|
||||
|
||||
Template.toolbarSection.helpers({
|
||||
toolbarButtons: function() {
|
||||
var buttonData = [];
|
||||
buttonData.push({
|
||||
id: 'zoom',
|
||||
title: 'Zoom',
|
||||
classes: 'imageViewerTool',
|
||||
svgLink: '/packages/lesiontracker/assets/icons.svg#icon-tools-zoom'
|
||||
});
|
||||
|
||||
buttonData.push({
|
||||
id: 'wwwc',
|
||||
title: 'Levels',
|
||||
classes: 'imageViewerTool',
|
||||
svgLink: '/packages/lesiontracker/assets/icons.svg#icon-tools-levels'
|
||||
});
|
||||
|
||||
buttonData.push({
|
||||
id: 'pan',
|
||||
title: 'Pan',
|
||||
classes: 'imageViewerTool',
|
||||
svgLink: '/packages/lesiontracker/assets/icons.svg#icon-tools-pan'
|
||||
});
|
||||
|
||||
buttonData.push({
|
||||
id: 'link',
|
||||
title: 'Link',
|
||||
classes: 'imageViewerCommand toolbarSectionButton',
|
||||
svgLink: '/packages/lesiontracker/assets/icons.svg#icon-tools-link'
|
||||
});
|
||||
|
||||
buttonData.push({
|
||||
id: 'bidirectional',
|
||||
title: 'Target',
|
||||
classes: 'imageViewerTool',
|
||||
svgLink: '/packages/lesiontracker/assets/icons.svg#icon-tools-measure-target'
|
||||
});
|
||||
|
||||
buttonData.push({
|
||||
id: 'nonTarget',
|
||||
title: 'Non-Target',
|
||||
classes: 'imageViewerTool toolbarSectionButton',
|
||||
svgLink: '/packages/lesiontracker/assets/icons.svg#icon-tools-measure-non-target'
|
||||
});
|
||||
|
||||
buttonData.push({
|
||||
id: 'length',
|
||||
title: 'Temp',
|
||||
classes: 'imageViewerTool toolbarSectionButton',
|
||||
svgLink: '/packages/lesiontracker/assets/icons.svg#icon-tools-measure-temp'
|
||||
});
|
||||
|
||||
return buttonData;
|
||||
}
|
||||
});
|
||||
|
||||
Template.toolbarSection.events({
|
||||
'click #studySidebarToggle': function(event, instance) {
|
||||
var isOpen = instance.data.state.get('studySidebarOpen');
|
||||
instance.data.state.set('studySidebarOpen', !isOpen);
|
||||
},
|
||||
'click #lesionSidebarToggle': function(event, instance) {
|
||||
var isOpen = instance.data.state.get('lesionSidebarOpen');
|
||||
instance.data.state.set('lesionSidebarOpen', !isOpen);
|
||||
instance.data.state.set('additionalMeasurementsSidebarOpen', false);
|
||||
},
|
||||
'click #additionalMeasurementsSidebarToggle': function(event, instance) {
|
||||
var isOpen = instance.data.state.get('additionalMeasurementsSidebarOpen');
|
||||
instance.data.state.set('additionalMeasurementsSidebarOpen', !isOpen);
|
||||
instance.data.state.set('lesionSidebarOpen', false);
|
||||
},
|
||||
// TODO: Inherit these from toolbar template somehow
|
||||
'click .imageViewerTool': function(e) {
|
||||
$(e.currentTarget).tooltip('hide');
|
||||
|
||||
var tool = e.currentTarget.id;
|
||||
|
||||
var elements = $('.imageViewerViewport');
|
||||
|
||||
var activeTool = toolManager.getActiveTool();
|
||||
$('.toolbarSectionButton').removeClass('active');
|
||||
if (tool === activeTool) {
|
||||
var defaultTool = toolManager.getDefaultTool();
|
||||
console.log('Setting active tool to: ' + defaultTool);
|
||||
toolManager.setActiveTool(defaultTool, elements);
|
||||
} else {
|
||||
console.log('Setting active tool to: ' + tool);
|
||||
toolManager.setActiveTool(tool, elements);
|
||||
}
|
||||
},
|
||||
'click .imageViewerCommand': function(e) {
|
||||
$(e.currentTarget).tooltip('hide');
|
||||
|
||||
var command = e.currentTarget.id;
|
||||
if (!OHIF.viewer.functionList.hasOwnProperty(command)) {
|
||||
return;
|
||||
}
|
||||
|
||||
var activeViewport = Session.get('activeViewport');
|
||||
var element = $('.imageViewerViewport').get(activeViewport);
|
||||
OHIF.viewer.functionList[command](element);
|
||||
}
|
||||
});
|
||||
|
||||
Template.toolbarSection.onRendered(function() {
|
||||
var tooltipButtons = $('[data-toggle="tooltip"]');
|
||||
tooltipButtons.tooltip(OHIF.viewer.tooltipConfig);
|
||||
|
||||
// Enable tooltips for the layout button
|
||||
var extraTooltipButtons = $('[rel="tooltip"]');
|
||||
extraTooltipButtons.tooltip(OHIF.viewer.tooltipConfig);
|
||||
|
||||
// Set disabled/enabled tool buttons that are set in toolManager
|
||||
var states = toolManager.getToolDefaultStates();
|
||||
var disabledToolButtons = states.disabledToolButtons;
|
||||
var allToolbarButtons = $('#toolbar').find('button');
|
||||
if (disabledToolButtons && disabledToolButtons.length > 0) {
|
||||
for (var i = 0; i < allToolbarButtons.length; i++) {
|
||||
var toolbarButton = allToolbarButtons[i];
|
||||
$(toolbarButton).prop('disabled', false);
|
||||
var index = disabledToolButtons.indexOf($(toolbarButton).attr('id'));
|
||||
if (index !== -1) {
|
||||
$(toolbarButton).prop('disabled', true);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,151 @@
|
||||
@import "{design}/app"
|
||||
|
||||
.toolbarSection
|
||||
height: $toolbarHeight
|
||||
width: 100%
|
||||
border-bottom: $uiBorderColor $uiBorderThickness solid
|
||||
|
||||
.capsule-group
|
||||
display: inline-block
|
||||
|
||||
div.capsule-entry
|
||||
display: inline-block
|
||||
color: $defaultColor
|
||||
fill: $defaultColor
|
||||
stroke: $defaultColor
|
||||
height: $toolbarHeight
|
||||
min-width: 30px
|
||||
cursor: pointer
|
||||
text-align: center
|
||||
margin: 0
|
||||
|
||||
div.capsule
|
||||
width: 66px
|
||||
height: 26px
|
||||
|
||||
&:first-child
|
||||
.capsule
|
||||
border-radius: 57px
|
||||
background-color: #16202b
|
||||
border: solid 1px #3c5d80
|
||||
border-right: none
|
||||
border-top-right-radius: 0
|
||||
border-bottom-right-radius: 0
|
||||
|
||||
&:last-child
|
||||
.capsule
|
||||
border-radius: 57px
|
||||
background-color: #16202b
|
||||
border: solid 1px #3c5d80
|
||||
border-left: none
|
||||
border-top-left-radius: 0
|
||||
border-bottom-left-radius: 0
|
||||
|
||||
&.disabled
|
||||
opacity: 0.5
|
||||
cursor: not-allowed
|
||||
|
||||
.svgContainer
|
||||
margin: 0 auto
|
||||
text-align: center
|
||||
|
||||
.capsule
|
||||
width: 62px
|
||||
height: 26px
|
||||
border-radius: 100px
|
||||
background-color: #16202b
|
||||
border: solid 1px #3c5d80
|
||||
|
||||
svg
|
||||
background-color: transparent
|
||||
margin: 5px
|
||||
width: 17px
|
||||
height: 17px
|
||||
|
||||
&:active, &.active
|
||||
color: $activeColor
|
||||
|
||||
svg
|
||||
fill: $activeColor
|
||||
stroke: $activeColor
|
||||
|
||||
&:hover
|
||||
color: $hoverColor
|
||||
|
||||
svg
|
||||
fill: $hoverColor
|
||||
stroke: $hoverColor
|
||||
|
||||
|
||||
.toolbarSectionButton
|
||||
display: inline-block
|
||||
color: $defaultColor
|
||||
fill: $defaultColor
|
||||
stroke: $defaultColor
|
||||
padding: 0 10px
|
||||
height: $toolbarHeight
|
||||
min-width: 30px
|
||||
cursor: pointer
|
||||
text-align: center
|
||||
|
||||
&.disabled
|
||||
opacity: 0.5
|
||||
cursor: not-allowed
|
||||
|
||||
.buttonLabel
|
||||
font-size: 12px
|
||||
|
||||
.svgContainer
|
||||
margin: 0 auto
|
||||
text-align: center
|
||||
|
||||
.capsule
|
||||
width: 62px
|
||||
height: 26px
|
||||
border-radius: 100px
|
||||
background-color: #16202b
|
||||
border: solid 1px #3c5d80
|
||||
|
||||
svg
|
||||
background-color: transparent
|
||||
margin: 2px
|
||||
width: 21px
|
||||
height: 21px
|
||||
|
||||
&:active, &.active
|
||||
color: $activeColor
|
||||
|
||||
svg
|
||||
fill: $activeColor
|
||||
stroke: $activeColor
|
||||
|
||||
&:hover
|
||||
color: $hoverColor
|
||||
|
||||
svg
|
||||
fill: $hoverColor
|
||||
stroke: $hoverColor
|
||||
|
||||
.toolbarSectionEntry
|
||||
display: inline-block
|
||||
color: $defaultColor
|
||||
fill: $defaultColor
|
||||
stroke: $defaultColor
|
||||
padding: 0 10px
|
||||
height: $toolbarHeight
|
||||
min-width: 30px
|
||||
cursor: pointer
|
||||
text-align: center
|
||||
|
||||
.quickSwitch
|
||||
position: absolute
|
||||
|
||||
&.middle
|
||||
left: calc(50% - 60px)
|
||||
|
||||
&.left
|
||||
left: calc(50% - 160px)
|
||||
|
||||
&.right
|
||||
left: 50%
|
||||
margin-left: 20px
|
||||
+12
@@ -0,0 +1,12 @@
|
||||
<template name="toolbarSectionButton">
|
||||
<div id="{{id}}" class="toolbarSectionButton {{classes}}" title="{{title}}">
|
||||
<div class="svgContainer">
|
||||
<svg>
|
||||
<use xlink:href={{svgLink}}></use>
|
||||
</svg>
|
||||
</div>
|
||||
<div class="buttonLabel">
|
||||
{{title}}
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
Whitespace-only changes.
Whitespace-only changes.
@@ -1,38 +1,46 @@
|
||||
<template name="userAccountMenu">
|
||||
{{#if currentUser}}
|
||||
<div id="userAccountMenu" class="row-fluid">
|
||||
<div class="list-group panel">
|
||||
<a href="#userMenu" class="list-group-item" id="userName" data-toggle="collapse" data-parent="#userAccountMenu">{{fullName}} {{currentPath}}<span class="caret"></span></a>
|
||||
|
||||
<div class="collapse pos-absolute" id="userMenu">
|
||||
|
||||
{{#if showWorklistMenu}}
|
||||
<a href="worklist" id="worklist" data-toggle="collapse" data-target="#userMenu" class="list-group-item sub-item">
|
||||
<div id="userAccountMenu" class="dropdown">
|
||||
<a class="dropdown-toggle" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
|
||||
{{name}} {{currentPath}} | <i class="fa fa-cog fa-lg"></i>
|
||||
<span class="caret"></span>
|
||||
</a>
|
||||
<ul class="dropdown-menu dropdown-menu-right">
|
||||
{{#if showWorklistMenu}}
|
||||
<li>
|
||||
<a href="worklist" id="worklist">
|
||||
<i class="fa fa-list-ul"></i> Worklist
|
||||
</a>
|
||||
{{else}}
|
||||
<!--TODO: Move this elsewhere in the Worklist? -->
|
||||
<span data-toggle="collapse" data-target="#userMenu" class="list-group-item sub-item externalItem">{{>optionsButton }}</span>
|
||||
<a href="audit" id="audit" data-toggle="collapse" data-target="#userMenu" class="list-group-item sub-item">
|
||||
</li>
|
||||
{{else}}
|
||||
<!--TODO: Move this elsewhere in the Worklist? -->
|
||||
<li>
|
||||
{{>optionsButton }}
|
||||
</li>
|
||||
<li>
|
||||
<a href="/audit" id="audit">
|
||||
<i class="fa fa-list-ul"></i> View Audit Log
|
||||
</a>
|
||||
{{/if}}
|
||||
|
||||
<a href="#itemSettings" class="list-group-item sub-item" data-toggle="collapse" data-parent="#itemSettings"><i class="fa fa-cog fa-lg"></i> Settings <span class="caret"></span></a>
|
||||
<div class="collapse list-group-submenu" id="itemSettings">
|
||||
<a href="#" id="serverInformation" class="list-group-item sub-sub-item" data-parent="#itemSettings" data-toggle="collapse" data-target="#itemSettings,#userMenu"><i class="fa fa-server fa-lg"></i> Server Information</a>
|
||||
</div>
|
||||
|
||||
<a href="#itemProfile" class="list-group-item sub-item" data-toggle="collapse" data-parent="#itemProfile" ><i class="fa fa-user fa-lg"></i> Profile <span class="caret"></span></a>
|
||||
<div class="collapse list-group-submenu" id="itemProfile">
|
||||
<a href="changePassword" id="changePassword" class="list-group-item sub-sub-item" data-parent="#itemProfile" data-toggle="collapse" data-target="#itemProfile,#userMenu"><i class="fa fa-lock"></i> Change Password</a>
|
||||
</div>
|
||||
|
||||
<a href="#" id="logoutButton" data-toggle="collapse" data-target="#userMenu" class="list-group-item sub-item">
|
||||
</li>
|
||||
{{/if}}
|
||||
<li role="separator" class="divider"></li>
|
||||
<li>
|
||||
<a id="serverInformation">
|
||||
<i class="fa fa-server fa-lg"></i> Server Information
|
||||
</a>
|
||||
</li>
|
||||
<li role="separator" class="divider"></li>
|
||||
<li>
|
||||
<a href="changePassword" id="changePassword">
|
||||
<i class="fa fa-lock"></i> Change Password
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a id="logoutButton">
|
||||
<i class="fa fa-power-off"></i> Logout
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
{{/if}}
|
||||
</template>
|
||||
@@ -1,12 +1,15 @@
|
||||
Template.userAccountMenu.helpers({
|
||||
fullName: function() {
|
||||
return Meteor.user().profile.fullName;
|
||||
name: function() {
|
||||
var nameSplit = Meteor.user().profile.fullName.split(' ');
|
||||
var lastName = nameSplit[nameSplit.length - 1];
|
||||
nameSplit[nameSplit.length - 1] = lastName.substr(0, 1) + '.';
|
||||
return nameSplit.join(' ');
|
||||
},
|
||||
currentUser: function() {
|
||||
var verifyEmail = Meteor.settings && Meteor.settings.public && Meteor.settings.public.verifyEmail || false;
|
||||
|
||||
if (!Meteor.user() || !Meteor.userId()) {
|
||||
return false;
|
||||
return;
|
||||
}
|
||||
|
||||
if (!verifyEmail) {
|
||||
@@ -17,18 +20,13 @@ Template.userAccountMenu.helpers({
|
||||
return true;
|
||||
}
|
||||
|
||||
if (Meteor.user().emails[0].verified) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
return !!Meteor.user().emails[0].verified;
|
||||
},
|
||||
showWorklistMenu: function() {
|
||||
var currentPath = Router.current().route.path(this);
|
||||
if (currentPath === '/' || currentPath === '/worklist') {
|
||||
return false;
|
||||
if (currentPath !== '/' && currentPath !== '/worklist') {
|
||||
return true;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
});
|
||||
|
||||
@@ -39,8 +37,62 @@ Template.userAccountMenu.events({
|
||||
'click #logoutButton': function() {
|
||||
// Remove reviewers info for the user
|
||||
Meteor.call('removeUserFromReviewers', Meteor.userId());
|
||||
|
||||
Meteor.logout(function() {
|
||||
Router.go('/entrySignIn');
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
Template.userAccountMenu.onRendered(function() {
|
||||
var oldUserId;
|
||||
var lastLoginModalInterval;
|
||||
|
||||
this.autorun(function() {
|
||||
// Hook login/logout
|
||||
var user = Meteor.user();
|
||||
var newUserId = user._id;
|
||||
|
||||
if (oldUserId === null && newUserId) {
|
||||
Session.set('showLastLoginModal', true);
|
||||
|
||||
// Log
|
||||
HipaaLogger.logEvent({
|
||||
eventType: 'init',
|
||||
userId: user._id,
|
||||
userName: user.fullName
|
||||
});
|
||||
|
||||
} else if (newUserId === null && oldUserId) {
|
||||
// Set showLastLoginModal as null
|
||||
Session.set('showLastLoginModal', null);
|
||||
|
||||
// Destroy interval for last login modal
|
||||
Meteor.clearInterval(lastLoginModalInterval);
|
||||
console.log('The user logged out');
|
||||
|
||||
// Log
|
||||
// TODO: eventType is not defined for logout in hipaa-audit-log
|
||||
/*HipaaLogger.logEvent({
|
||||
eventType: 'logout',
|
||||
userId: oldUserId,
|
||||
userName: userName
|
||||
});*/
|
||||
|
||||
// Remove the user from Reviewers
|
||||
Meteor.call('removeUserFromReviewers', oldUserId);
|
||||
}
|
||||
|
||||
oldUserId = Meteor.userId();
|
||||
|
||||
// Trigger last login date popup
|
||||
if (Session.get('showLastLoginModal')) {
|
||||
Modal.show('lastLoginModal');
|
||||
|
||||
lastLoginModalInterval = Meteor.setInterval(function() {
|
||||
Modal.hide('lastLoginModal');
|
||||
Session.set('showLastLoginModal', null);
|
||||
}, 3000);
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -1,34 +1,15 @@
|
||||
@import "{design}/app"
|
||||
|
||||
#userAccountMenu
|
||||
position: relative
|
||||
float: right
|
||||
padding: 5px
|
||||
height: 100%
|
||||
line-height: 40px
|
||||
z-index: 999
|
||||
|
||||
.list-group
|
||||
background-color: transparent
|
||||
color: white
|
||||
line-height: 35px
|
||||
margin-left: 10px
|
||||
.list-group-item
|
||||
padding: 5px 10px
|
||||
width: 200px
|
||||
a#userName
|
||||
padding: 0
|
||||
background-color: transparent
|
||||
color: white
|
||||
border: solid 1px transparent
|
||||
text-align: right
|
||||
display: inline-block
|
||||
width: auto
|
||||
#userMenu
|
||||
position: absolute
|
||||
right: 0
|
||||
.externalItem
|
||||
a
|
||||
color: #555
|
||||
.dropdown-toggle
|
||||
cursor: pointer
|
||||
text-decoration: none
|
||||
background-color: $primaryBackgroundColor
|
||||
color: $textSecondaryColor
|
||||
|
||||
.list-group-submenu
|
||||
.sub-sub-item
|
||||
padding-left: 20px
|
||||
padding-right: 20px
|
||||
ul.dropdown-menu
|
||||
li
|
||||
cursor: pointer
|
||||
@@ -0,0 +1,16 @@
|
||||
<template name="viewer">
|
||||
{{#if Template.subscriptionsReady}}
|
||||
<div id="viewer">
|
||||
{{>confirmDeleteDialog}}
|
||||
{{>lesionLocationDialog}}
|
||||
{{>nonTargetLesionDialog}}
|
||||
{{>nonTargetResponseDialog}}
|
||||
<!-- >conformanceCheckFeedback -->
|
||||
|
||||
{{ >toolbarSection }}
|
||||
{{ >flexboxLayout }}
|
||||
</div>
|
||||
{{else}}
|
||||
{{>loadingText}}
|
||||
{{/if}}
|
||||
</template>
|
||||
@@ -0,0 +1,309 @@
|
||||
Session.setDefault('activeViewport', false);
|
||||
Session.setDefault('studySidebarOpen', false);
|
||||
Session.setDefault('lesionSidebarOpen', false);
|
||||
Session.setDefault('additionalMeasurementsSidebarOpen', false);
|
||||
|
||||
Template.viewer.onCreated(function() {
|
||||
// Attach the Window resize listener
|
||||
$(window).on('resize', handleResize);
|
||||
|
||||
ValidationErrors.remove({});
|
||||
|
||||
var instance = this;
|
||||
instance.data.state = new ReactiveDict();
|
||||
instance.data.state.set('studySidebarOpen', Session.get('studySidebarOpen'));
|
||||
instance.data.state.set('lesionSidebarOpen', Session.get('lesionSidebarOpen'));
|
||||
instance.data.state.set('additionalMeasurementsSidebarOpen', Session.get('additionalMeasurementsSidebarOpen'));
|
||||
|
||||
var contentId = this.data.contentId;
|
||||
|
||||
OHIF = OHIF || window.OHIF || {
|
||||
viewer: {}
|
||||
};
|
||||
|
||||
OHIF.viewer.loadIndicatorDelay = 3000;
|
||||
OHIF.viewer.defaultTool = 'wwwc';
|
||||
OHIF.viewer.refLinesEnabled = true;
|
||||
OHIF.viewer.isPlaying = {};
|
||||
OHIF.viewer.cine = {
|
||||
framesPerSecond: 24,
|
||||
loop: true
|
||||
};
|
||||
|
||||
OHIF.viewer.functionList = {
|
||||
invert: function(element) {
|
||||
var viewport = cornerstone.getViewport(element);
|
||||
viewport.invert = !viewport.invert;
|
||||
cornerstone.setViewport(element, viewport);
|
||||
},
|
||||
resetViewport: function(element) {
|
||||
cornerstone.reset(element);
|
||||
},
|
||||
playClip: function(element) {
|
||||
var viewportIndex = $('.imageViewerViewport').index(element);
|
||||
var isPlaying = OHIF.viewer.isPlaying[viewportIndex] || false;
|
||||
if (isPlaying === true) {
|
||||
cornerstoneTools.stopClip(element);
|
||||
} else {
|
||||
cornerstoneTools.playClip(element);
|
||||
}
|
||||
|
||||
OHIF.viewer.isPlaying[viewportIndex] = !OHIF.viewer.isPlaying[viewportIndex];
|
||||
Session.set('UpdateCINE', Random.id());
|
||||
},
|
||||
toggleLesionTrackerTools: toggleLesionTrackerTools,
|
||||
clearTools: clearTools,
|
||||
bidirectional: function() {
|
||||
// Used for hotkeys
|
||||
toolManager.setActiveTool('bidirectional');
|
||||
},
|
||||
nonTarget: function() {
|
||||
// Used for hotkeys
|
||||
toolManager.setActiveTool('nonTarget');
|
||||
}
|
||||
};
|
||||
|
||||
// The hotkey can also be an array (e.g. ["NUMPAD0", "0"])
|
||||
OHIF.viewer.defaultHotkeys = OHIF.viewer.defaultHotkeys || {};
|
||||
OHIF.viewer.defaultHotkeys.toggleLesionTrackerTools = 'O';
|
||||
OHIF.viewer.defaultHotkeys.bidirectional = 'T'; // Target
|
||||
OHIF.viewer.defaultHotkeys.nonTarget = 'N'; // Non-target
|
||||
|
||||
if (isTouchDevice()) {
|
||||
OHIF.viewer.tooltipConfig = {
|
||||
trigger: 'manual'
|
||||
};
|
||||
} else {
|
||||
OHIF.viewer.tooltipConfig = {
|
||||
trigger: 'hover'
|
||||
};
|
||||
}
|
||||
|
||||
OHIF.viewer.updateImageSynchronizer = new cornerstoneTools.Synchronizer('CornerstoneNewImage', cornerstoneTools.updateImageSynchronizer);
|
||||
|
||||
if (ViewerData[contentId].loadedSeriesData) {
|
||||
log.info('Reloading previous loadedSeriesData');
|
||||
OHIF.viewer.loadedSeriesData = ViewerData[contentId].loadedSeriesData;
|
||||
|
||||
} else {
|
||||
log.info('Setting default ViewerData');
|
||||
OHIF.viewer.loadedSeriesData = {};
|
||||
|
||||
ViewerData[contentId].loadedSeriesData = OHIF.viewer.loadedSeriesData;
|
||||
|
||||
// Update the viewer data object
|
||||
if (!this.data.timepointIds || this.data.timepointIds.length <= 1) {
|
||||
// Update the viewer data object
|
||||
ViewerData[contentId].viewportColumns = 1;
|
||||
ViewerData[contentId].viewportRows = 1;
|
||||
} else if (this.data.timepointIds.length > 1) {
|
||||
ViewerData[contentId].viewportColumns = 2;
|
||||
ViewerData[contentId].viewportRows = 1;
|
||||
}
|
||||
|
||||
ViewerData[contentId].activeViewport = 0;
|
||||
Session.set('ViewerData', ViewerData);
|
||||
}
|
||||
|
||||
Session.set('activeViewport', ViewerData[contentId].activeViewport || false);
|
||||
|
||||
// Set lesion tool buttons as disabled if pixel spacing is not available for active element
|
||||
instance.autorun(pixelSpacingAutorunCheck);
|
||||
|
||||
// Update the ViewerStudies collection with the loaded studies
|
||||
ViewerStudies.remove({});
|
||||
|
||||
this.data.studies.forEach(function(study) {
|
||||
study.selected = true;
|
||||
ViewerStudies.insert(study);
|
||||
});
|
||||
|
||||
var patientId = this.data.studies[0].patientId;
|
||||
Session.set('patientId', patientId);
|
||||
|
||||
instance.autorun(function() {
|
||||
var dataContext = Template.currentData();
|
||||
instance.subscribe('singlePatientAssociatedStudies', dataContext.studies[0].patientId);
|
||||
instance.subscribe('singlePatientTimepoints', dataContext.studies[0].patientId);
|
||||
instance.subscribe('singlePatientMeasurements', dataContext.studies[0].patientId);
|
||||
instance.subscribe('singlePatientImageMeasurements', dataContext.studies[0].patientId);
|
||||
|
||||
var subscriptionsReady = instance.subscriptionsReady();
|
||||
log.info('autorun viewer.js. Ready: ' + subscriptionsReady);
|
||||
|
||||
if (subscriptionsReady) {
|
||||
// Set buttons as enabled/disabled when Timepoints collection is ready
|
||||
timepointAutoCheck(dataContext);
|
||||
|
||||
TrialResponseCriteria.validateAllDelayed();
|
||||
|
||||
ViewerStudies.find().observe({
|
||||
added: function(study) {
|
||||
// Find the relevant timepoint given the newly added study
|
||||
var timepoint = Timepoints.findOne({
|
||||
studyInstanceUids: {
|
||||
$in: [study.studyInstanceUid]
|
||||
}
|
||||
});
|
||||
|
||||
if (!timepoint) {
|
||||
log.warn('Study added to Viewer has not been associated!');
|
||||
return;
|
||||
}
|
||||
|
||||
// Update the added document with its related timepointId
|
||||
ViewerStudies.update(study._id, {
|
||||
$set: {
|
||||
timepointId: timepoint.timepointId
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
ImageMeasurements.find().observe({
|
||||
added: function(data) {
|
||||
if (data.clientId === ClientId) {
|
||||
return;
|
||||
}
|
||||
|
||||
syncImageMeasurementAndToolData(data);
|
||||
|
||||
// Update each displayed viewport
|
||||
updateAllViewports();
|
||||
},
|
||||
changed: function(data) {
|
||||
if (data.clientId === ClientId) {
|
||||
return;
|
||||
}
|
||||
|
||||
syncImageMeasurementAndToolData(data);
|
||||
|
||||
// Update each displayed viewport
|
||||
updateAllViewports();
|
||||
},
|
||||
removed: function(data) {
|
||||
if (data.clientId === ClientId) {
|
||||
return;
|
||||
}
|
||||
|
||||
removeToolDataWithMeasurementId(data.imageId, data.toolType, data.id);
|
||||
|
||||
// Update each displayed viewport
|
||||
updateAllViewports();
|
||||
}
|
||||
});
|
||||
|
||||
Measurements.find().observe({
|
||||
added: function(data) {
|
||||
if (data.clientId === ClientId) {
|
||||
TrialResponseCriteria.validateAllDelayed();
|
||||
return;
|
||||
}
|
||||
|
||||
log.info('Measurement added');
|
||||
|
||||
// This is used to re-add tools from the database into the
|
||||
// Cornerstone ToolData structure
|
||||
syncMeasurementAndToolData(data);
|
||||
|
||||
// Update each displayed viewport
|
||||
updateAllViewports();
|
||||
},
|
||||
changed: function(data) {
|
||||
if (data.clientId === ClientId) {
|
||||
TrialResponseCriteria.validateAllDelayed();
|
||||
return;
|
||||
}
|
||||
|
||||
log.info('Measurement changed');
|
||||
|
||||
// This is used to update changed tools from the database
|
||||
// in the Cornerstone ToolData structure
|
||||
syncMeasurementAndToolData(data);
|
||||
|
||||
// Update each displayed viewport
|
||||
updateAllViewports();
|
||||
|
||||
TrialResponseCriteria.validateAllDelayed();
|
||||
},
|
||||
removed: function(data) {
|
||||
log.info('Measurement removed');
|
||||
|
||||
// Check that this Measurement actually contains timepoint data
|
||||
if (!data || !data.timepoints) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Get the Measurement ID and relevant tool so we can remove
|
||||
// tool data for this Measurement
|
||||
var measurementId = data._id;
|
||||
var toolType = data.toolType;
|
||||
|
||||
// Remove the measurement from all the imageIds on which it exists
|
||||
// as toolData
|
||||
Object.keys(data.timepoints).forEach(function(timepointId) {
|
||||
// Clear the toolData for this timepoint
|
||||
var imageId = data.timepoints[timepointId].imageId;
|
||||
removeToolDataWithMeasurementId(imageId, toolType, measurementId);
|
||||
|
||||
// Set reviewer for this timepoint
|
||||
if (data.timepoints[timepointId].studyInstanceUid) {
|
||||
Meteor.call('setReviewer', data.timepoints[timepointId].studyInstanceUid);
|
||||
}
|
||||
});
|
||||
|
||||
// Sync database data with toolData for all the measurements
|
||||
// that have just been updated
|
||||
|
||||
// Note that here we need to use greater than and equals to
|
||||
// find the Measurements, whereas on the server it's
|
||||
// only "greater than", since inside this callback the
|
||||
// Measurements have already been decremented.
|
||||
var measurements = Measurements.find({
|
||||
patientId: data.patientId,
|
||||
lesionNumberAbsolute: {
|
||||
$gte: data.lesionNumberAbsolute
|
||||
}
|
||||
});
|
||||
|
||||
measurements.forEach(function(measurement) {
|
||||
syncMeasurementAndToolData(measurement);
|
||||
});
|
||||
|
||||
// Update each displayed viewport
|
||||
updateAllViewports();
|
||||
|
||||
ValidationErrors.remove({
|
||||
measurementId: data._id
|
||||
});
|
||||
|
||||
TrialResponseCriteria.validateAll();
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
Template.viewer.onRendered(function() {
|
||||
// Enable hotkeys
|
||||
enableHotkeys();
|
||||
});
|
||||
|
||||
Template.viewer.onDestroyed(function() {
|
||||
// Remove the Window resize listener
|
||||
$(window).off('resize', handleResize);
|
||||
|
||||
OHIF.viewer.updateImageSynchronizer.destroy();
|
||||
});
|
||||
|
||||
Template.viewer.events({
|
||||
'CornerstoneToolsMeasurementAdded .imageViewerViewport': function(e, template, eventData) {
|
||||
handleMeasurementAdded(e, eventData);
|
||||
},
|
||||
'CornerstoneToolsMeasurementModified .imageViewerViewport': function(e, template, eventData) {
|
||||
handleMeasurementModified(e, eventData);
|
||||
},
|
||||
'CornerstoneToolsMeasurementRemoved .imageViewerViewport': function(e, template, eventData) {
|
||||
handleMeasurementRemoved(e, eventData);
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,35 @@
|
||||
@import "{design}/app"
|
||||
|
||||
#viewer
|
||||
width: 100%
|
||||
height: 100%
|
||||
margin: 0
|
||||
padding: 0
|
||||
background-color: black
|
||||
|
||||
|
||||
//font awesome icons
|
||||
.fa-cr:before
|
||||
content: 'CR'
|
||||
font-weight: bold
|
||||
font-family: "Helvetica Neue", Helvetica, Arial, sans-serif
|
||||
|
||||
.fa-un:before
|
||||
content: 'UN'
|
||||
font-weight: bold
|
||||
font-family: "Helvetica Neue", Helvetica, Arial, sans-serif
|
||||
|
||||
.fa-ex:before
|
||||
content: 'EX'
|
||||
font-weight: bold
|
||||
font-family: "Helvetica Neue", Helvetica, Arial, sans-serif
|
||||
|
||||
#btnCollapse
|
||||
background-color: #333
|
||||
border-color: black
|
||||
|
||||
&.active
|
||||
border-color: #333
|
||||
|
||||
.btnCollapseIcon
|
||||
color: white
|
||||
@@ -0,0 +1,6 @@
|
||||
<template name="viewerMain">
|
||||
<div class="viewerMain">
|
||||
<div id='layoutManagerTarget'>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,10 @@
|
||||
Template.viewerMain.onRendered(function() {
|
||||
var instance = this;
|
||||
var studies = instance.data.studies;
|
||||
|
||||
var parentElement = instance.$("#layoutManagerTarget").get(0);
|
||||
window.layoutManager = new LayoutManager(parentElement, studies);
|
||||
|
||||
ProtocolEngine = new HP.ProtocolEngine(layoutManager, studies);
|
||||
HP.setEngine(ProtocolEngine);
|
||||
});
|
||||
@@ -0,0 +1,17 @@
|
||||
@import "{design}/app"
|
||||
|
||||
.viewerMain
|
||||
width: 100%
|
||||
height: 100%
|
||||
|
||||
#layoutManagerTarget
|
||||
width: 100%
|
||||
height: 100%
|
||||
|
||||
transition(all 0.3s ease)
|
||||
|
||||
#imageViewerViewports
|
||||
.viewportContainer:not(:last-child)
|
||||
.removable
|
||||
.imageViewerViewport
|
||||
border-right: $uiBorderThickness solid #436270
|
||||
Reference in new issue
Block a user