Work on refactoring measurements out of Lesion Tracker package
- Removed AssociatedStudies Collection - Measurement API is generated from configuration files - Data exchange methods are defined in configuration
This commit is contained in:
1 parent
c511edd419
commit
ab4d4c9008
537 files changed
+5122
-3545
No files matched your search
@@ -0,0 +1,19 @@
|
||||
<template name="progressDialog">
|
||||
<div id="progressDialog" tabindex="0">
|
||||
<div class="dialogHeader">
|
||||
<h5>{{progressDialogTitle}}</h5>
|
||||
</div>
|
||||
<div class="dialogContent">
|
||||
<div class="progress">
|
||||
<div class="progress-bar progress-bar-striped active"
|
||||
role="progressbar"
|
||||
style="width:{{progressStatus}}">
|
||||
{{formatNumberPrecision progressStatus 0}}%
|
||||
</div>
|
||||
</div>
|
||||
<div id="message">
|
||||
{{{progressMessage}}}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,69 @@
|
||||
progressDialog = {
|
||||
show: function(title, numberOfTotal) {
|
||||
Session.set('progressDialogSettings', {
|
||||
title: title,
|
||||
numberOfCompleted: 0,
|
||||
numberOfTotal: numberOfTotal
|
||||
});
|
||||
|
||||
$('#progressDialog').css('display', 'block');
|
||||
},
|
||||
update: function(numberOfCompleted) {
|
||||
var progressDialogSettings = Session.get('progressDialogSettings');
|
||||
progressDialogSettings.numberOfCompleted = numberOfCompleted;
|
||||
|
||||
Session.set('progressDialogSettings', progressDialogSettings);
|
||||
|
||||
if (progressDialogSettings.numberOfCompleted === progressDialogSettings.numberOfTotal) {
|
||||
progressDialog.close();
|
||||
}
|
||||
},
|
||||
close: function() {
|
||||
Session.set('progressDialogSettings', {
|
||||
title: '',
|
||||
numberOfCompleted: 0,
|
||||
numberOfTotal: 1
|
||||
});
|
||||
|
||||
$('#progressDialog').css('display', 'none');
|
||||
},
|
||||
setMessage: function(message) {
|
||||
let progressDialogSettings = Session.get('progressDialogSettings');
|
||||
progressDialogSettings.message = message;
|
||||
Session.set('progressDialogSettings', progressDialogSettings);
|
||||
}
|
||||
};
|
||||
|
||||
Template.progressDialog.helpers({
|
||||
progressDialogTitle() {
|
||||
var settings = Session.get('progressDialogSettings');
|
||||
if (!settings) {
|
||||
return;
|
||||
}
|
||||
|
||||
return settings.title;
|
||||
},
|
||||
|
||||
progressStatus() {
|
||||
const settings = Session.get('progressDialogSettings');
|
||||
if (!settings) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (settings.numberOfCompleted === undefined ||
|
||||
settings.numberOfTotal === undefined) {
|
||||
return;
|
||||
}
|
||||
|
||||
return settings.numberOfCompleted / settings.numberOfTotal;
|
||||
},
|
||||
|
||||
progressMessage() {
|
||||
var settings = Session.get('progressDialogSettings');
|
||||
if (!settings) {
|
||||
return;
|
||||
}
|
||||
|
||||
return settings.message;
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,25 @@
|
||||
#progressDialog
|
||||
display: none
|
||||
position: absolute
|
||||
top: 0
|
||||
bottom: 0
|
||||
left: 0
|
||||
right: 0
|
||||
margin: auto
|
||||
width: 20%
|
||||
height: 20%
|
||||
z-index: 100
|
||||
border-radius: 5px
|
||||
padding: 10px 20px 10px 20px
|
||||
background-color: rgba(255,255,255,1)
|
||||
outline: none
|
||||
|
||||
.dialogContent
|
||||
margin-bottom: 20px
|
||||
|
||||
#message
|
||||
width: 100%
|
||||
|
||||
span
|
||||
display: inline-block;
|
||||
width: 100%
|
||||
+104
@@ -0,0 +1,104 @@
|
||||
<template name="seriesDetailsTable">
|
||||
{{#unless selectedStudies.length}}
|
||||
<p align="center">Series details not found</p>
|
||||
{{/unless}}
|
||||
{{#each selectedStudies}}
|
||||
<div class="selectedStudyContainer">
|
||||
<table class="table table-responsive tblStudyDetails">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td class="center patientID">
|
||||
<div>
|
||||
<span>Patient ID</span>
|
||||
</div>
|
||||
<input type="text"
|
||||
class="form-control"
|
||||
id="patientID"
|
||||
placeholder="Patient ID"
|
||||
disabled="disabled"
|
||||
value="{{ patientId }}">
|
||||
</td>
|
||||
<td class="patientName">
|
||||
<div>
|
||||
<span>Patient Name</span>
|
||||
</div>
|
||||
<input type="text"
|
||||
class="form-control"
|
||||
id="patientName"
|
||||
placeholder="Patient Name"
|
||||
disabled="disabled"
|
||||
value="{{ formatPN patientName }}">
|
||||
</td>
|
||||
<td class="accessionNumber">
|
||||
<div>
|
||||
<span>Accession Number</span>
|
||||
</div>
|
||||
<input type="text"
|
||||
class="form-control"
|
||||
id="accessionNumber"
|
||||
placeholder="Accession Number"
|
||||
disabled="disabled"
|
||||
value="{{ accessionNumber }}">
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="studyDescription">
|
||||
<div>
|
||||
<span>Study Description</span>
|
||||
</div>
|
||||
<input type="text"
|
||||
class="form-control"
|
||||
id="studyDescription"
|
||||
placeholder="Study Description"
|
||||
disabled="disabled"
|
||||
value="{{ studyDescription }}">
|
||||
</td>
|
||||
<td class="studyDate">
|
||||
<div>
|
||||
<span>Study Date</span>
|
||||
</div>
|
||||
<input type="text"
|
||||
class="form-control"
|
||||
id="studyDate"
|
||||
placeholder="Study Date"
|
||||
disabled="disabled"
|
||||
value="{{ formatDA studyDate }}">
|
||||
</td>
|
||||
|
||||
<td></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<table class="table table-responsive tblSeriesDetails">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="header">Number</th>
|
||||
<th class="header">Modality</th>
|
||||
<th class="header">Description</th>
|
||||
<th class="header">Instances</th>
|
||||
</tr>
|
||||
</thead>
|
||||
|
||||
<tbody>
|
||||
{{#if displaySeriesLoadingText}}
|
||||
<tr>
|
||||
<td colspan="4" class="center">
|
||||
{{> loadingText }}
|
||||
</td>
|
||||
</tr>
|
||||
{{/if}}
|
||||
|
||||
{{#each seriesList}}
|
||||
<tr>
|
||||
<td class="center">{{ seriesNumber }}</td>
|
||||
<td class="center">{{ modality }}</td>
|
||||
<td class="center">{{ seriesDescription }}</td>
|
||||
<td class="center">{{ instances.length }}</td>
|
||||
</tr>
|
||||
{{/each}}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{{/each}}
|
||||
</template>
|
||||
@@ -0,0 +1,51 @@
|
||||
Template.seriesDetailsTable.onCreated(() => {
|
||||
const instance = Template.instance();
|
||||
|
||||
// Create selected studies
|
||||
instance.selectedStudies = new ReactiveDict();
|
||||
|
||||
let selectedStudiesData = instance.data.selectedStudies;
|
||||
|
||||
if (!selectedStudiesData) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Display loading text while getting series
|
||||
_.each(selectedStudiesData, selectedStudy => {
|
||||
|
||||
selectedStudy.displaySeriesLoadingText = true;
|
||||
});
|
||||
|
||||
// Set reactive selected studies
|
||||
instance.selectedStudies.set('studies', selectedStudiesData);
|
||||
});
|
||||
|
||||
Template.seriesDetailsTable.onRendered(() => {
|
||||
const instance = Template.instance();
|
||||
const studies = instance.selectedStudies.get('studies');
|
||||
|
||||
if (!studies) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Get series list for the study
|
||||
_.map(studies, (selectedStudy, index) => {
|
||||
studies[index].seriesList = [];
|
||||
getStudyMetadata(selectedStudy.studyInstanceUid, study => {
|
||||
|
||||
// Set series list
|
||||
studies[index].seriesList = study.seriesList;
|
||||
studies[index].displaySeriesLoadingText = false;
|
||||
|
||||
// Update selected studies
|
||||
instance.selectedStudies.set('studies', studies);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Template.seriesDetailsTable.helpers({
|
||||
selectedStudies() {
|
||||
const instance = Template.instance();
|
||||
return instance.selectedStudies.get('studies');
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,30 @@
|
||||
@import "{design}/app"
|
||||
|
||||
.selectedStudyContainer
|
||||
margin-bottom: 15px;
|
||||
|
||||
.tblStudyDetails
|
||||
tbody > tr > td > label
|
||||
color: $textPrimaryColor
|
||||
|
||||
tbody > tr > td > input[type="text"]
|
||||
height: 28px
|
||||
background-color: $primaryBackgroundColor
|
||||
color: $textPrimaryColor
|
||||
width: calc(100% - 10px)
|
||||
border-radius: 4px
|
||||
box-sizing: border-box
|
||||
|
||||
.tblSeriesDetails
|
||||
thead > tr > th
|
||||
text-align: center
|
||||
|
||||
tbody > tr
|
||||
|
||||
td
|
||||
text-align: center
|
||||
color: $textPrimaryColor
|
||||
|
||||
.loadingTextDiv
|
||||
font-size: 13px
|
||||
color: $textSecondaryColor
|
||||
+50
@@ -0,0 +1,50 @@
|
||||
<template name="serverInformationDicomWeb">
|
||||
<div class="row">
|
||||
<div class="col-lg-6">
|
||||
{{>inputText labelClass='form-group' key='wadoUriRoot'}}
|
||||
</div>
|
||||
<div class="col-lg-6">
|
||||
{{>inputText labelClass='form-group' key='wadoRoot'}}
|
||||
</div>
|
||||
<div class="col-lg-6">
|
||||
{{>inputSelect labelClass='form-group' key='imageRendering' hideSearch=true
|
||||
options=(clone placeholder='Select an option')}}
|
||||
</div>
|
||||
<div class="col-lg-6">
|
||||
{{>inputText labelClass='form-group' key='qidoRoot'}}
|
||||
</div>
|
||||
<div class="col-lg-6">
|
||||
<strong>QIDO options</strong>
|
||||
<div class="form-group">
|
||||
<div class="form-control">
|
||||
<div class="checkbox">
|
||||
{{>inputCheckbox key='qidoSupportsIncludeField'}}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<hr>
|
||||
<h4>Request options</h4>
|
||||
{{#group class='row' key='requestOptions'}}
|
||||
<div class="col-lg-6">
|
||||
{{>inputText labelClass='form-group' key='auth'}}
|
||||
</div>
|
||||
<div class="col-lg-6">
|
||||
<strong>Logging options</strong>
|
||||
<div class="form-group">
|
||||
<div class="form-control clearfix">
|
||||
<div class="checkbox pull-left">
|
||||
{{>inputCheckbox key='logRequests'}}
|
||||
</div>
|
||||
<div class="checkbox pull-left">
|
||||
{{>inputCheckbox key='logResponses'}}
|
||||
</div>
|
||||
<div class="checkbox pull-left">
|
||||
{{>inputCheckbox key='logTiming'}}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{{/group}}
|
||||
</template>
|
||||
+10
@@ -0,0 +1,10 @@
|
||||
Template.serverInformationDicomWeb.onRendered(() => {
|
||||
const instance = Template.instance();
|
||||
instance.autorun(function() {
|
||||
const mode = instance.data.mode.get();
|
||||
if (mode === 'edit') {
|
||||
const data = instance.data.currentItem.get();
|
||||
instance.data.form.value(data);
|
||||
}
|
||||
});
|
||||
});
|
||||
+60
@@ -0,0 +1,60 @@
|
||||
<template name="serverInformationDimse">
|
||||
<div class="row">
|
||||
<div class="col-lg-6">
|
||||
{{>inputText labelClass='form-group' key='wadoUriRoot'}}
|
||||
</div>
|
||||
</div>
|
||||
<div class="panel panel-default panel-body">
|
||||
<div class="clearfix">
|
||||
<h4 class="pull-left">Peer List</h4>
|
||||
{{#button class='btn btn-primary pull-right'
|
||||
action='newPeer' params=peer
|
||||
}}New peer{{/button}}
|
||||
</div>
|
||||
<hr>
|
||||
{{#group class='row' key='peers' arrayValues=true}}
|
||||
{{#each peer in instance.peers.get}}
|
||||
{{#group class='col-lg-6'}}
|
||||
<div class="panel panel-default panel-body">
|
||||
<div class="row">
|
||||
<div class="col-lg-6">
|
||||
{{>inputText class='form-control' key='aeTitle'}}
|
||||
</div>
|
||||
<div class="col-lg-6">
|
||||
{{>inputText class='form-control' key='hostAE'}}
|
||||
</div>
|
||||
<div class="col-lg-6">
|
||||
{{>inputText class='form-control' key='host'}}
|
||||
</div>
|
||||
<div class="col-lg-6">
|
||||
{{>inputText class='form-control' key='port'}}
|
||||
</div>
|
||||
<div class="col-lg-12">
|
||||
<strong>Settings</strong>
|
||||
<div class="form-group">
|
||||
<div class="form-control height-auto clearfix">
|
||||
<div class="checkbox pull-left">
|
||||
{{>inputCheckbox key='default'}}
|
||||
</div>
|
||||
<div class="checkbox pull-left">
|
||||
{{>inputCheckbox key='server'}}
|
||||
</div>
|
||||
<div class="checkbox pull-left">
|
||||
{{>inputCheckbox key='supportsInstanceRetrievalByStudyUid'}}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-lg-12 text-right">
|
||||
{{#button class='btn btn-sm btn-danger'
|
||||
action='removePeer' params=@index
|
||||
}}Remove{{/button}}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{{/group}}
|
||||
{{/each}}
|
||||
{{/group}}
|
||||
</div>
|
||||
<hr>
|
||||
</template>
|
||||
+38
@@ -0,0 +1,38 @@
|
||||
Template.serverInformationDimse.onCreated(() => {
|
||||
const instance = Template.instance();
|
||||
instance.peers = new ReactiveVar([]);
|
||||
instance.autorun(() => {
|
||||
const currentItem = instance.data.currentItem.get();
|
||||
if (currentItem) {
|
||||
instance.peers.set(currentItem.peers || []);
|
||||
}
|
||||
});
|
||||
const api = instance.data.api;
|
||||
_.extend(api, {
|
||||
newPeer() {
|
||||
const peers = instance.peers.get();
|
||||
peers.push({});
|
||||
instance.peers.set(peers);
|
||||
},
|
||||
removePeer(peerIndex) {
|
||||
const peers = instance.peers.get();
|
||||
peers.splice(peerIndex, 1);
|
||||
instance.peers.set(peers);
|
||||
|
||||
const data = instance.data.currentItem.get();
|
||||
data.peers = peers;
|
||||
Tracker.afterFlush(() => instance.data.form.value(data));
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
Template.serverInformationDimse.onRendered(() => {
|
||||
const instance = Template.instance();
|
||||
instance.autorun(() => {
|
||||
const mode = instance.data.mode.get();
|
||||
if (mode === 'edit') {
|
||||
const data = instance.data.currentItem.get();
|
||||
instance.data.form.value(data);
|
||||
}
|
||||
});
|
||||
});
|
||||
+27
@@ -0,0 +1,27 @@
|
||||
<template name="serverInformationForm">
|
||||
{{#form class='server-information-form' schema=instance.currentSchema api=this.api}}
|
||||
{{>inputHidden key='_id'}}
|
||||
<div class="row">
|
||||
<div class="col-lg-6">
|
||||
{{>inputText labelClass='form-group' key='name'}}
|
||||
</div>
|
||||
<div class="col-lg-6">
|
||||
{{>inputSelect labelClass='form-group' key='type'
|
||||
hideSearch=true disabled=(eq this.mode.get 'edit')
|
||||
options=(clone placeholder='Choose a server type')}}
|
||||
</div>
|
||||
</div>
|
||||
<hr>
|
||||
{{#if eq this.serverType.get 'dicomWeb'}}
|
||||
{{>serverInformationDicomWeb this}}
|
||||
{{/if}}
|
||||
{{#if eq this.serverType.get 'dimse'}}
|
||||
{{>serverInformationDimse this}}
|
||||
{{/if}}
|
||||
{{#if this.serverType.get}}
|
||||
<div>
|
||||
{{#button class='btn-primary' action='save'}}Save{{/button}}
|
||||
</div>
|
||||
{{/if}}
|
||||
{{/form}}
|
||||
</template>
|
||||
+72
@@ -0,0 +1,72 @@
|
||||
import { Template } from 'meteor/templating';
|
||||
import { ReactiveVar } from 'meteor/reactive-var';
|
||||
import { Tracker } from 'meteor/tracker';
|
||||
import { DICOMWebServer as dicomSchema } from 'meteor/ohif:study-list/both/schema/servers.js';
|
||||
import { DIMSEServer as dimseSchema } from 'meteor/ohif:study-list/both/schema/servers.js';
|
||||
|
||||
Template.serverInformationForm.onCreated(() => {
|
||||
const instance = Template.instance();
|
||||
|
||||
instance.data.api = {
|
||||
save() {
|
||||
// Stop here if the form validation fails
|
||||
if (!instance.data.form.validate()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Get the current form data
|
||||
const formData = instance.data.form.value();
|
||||
|
||||
// Call the save method
|
||||
Meteor.call('serverSave', formData, function(error) {
|
||||
if (error) {
|
||||
// TODO: check for errors: not-authorized, data-write
|
||||
}
|
||||
|
||||
instance.data.resetState();
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
instance.currentSchema = new ReactiveVar(dicomSchema);
|
||||
});
|
||||
|
||||
Template.serverInformationForm.onRendered(() => {
|
||||
const instance = Template.instance();
|
||||
|
||||
instance.data.$form = instance.$('form:first');
|
||||
instance.data.form = instance.data.$form.data('component');
|
||||
|
||||
// Handle the server type
|
||||
instance.autorun(() => {
|
||||
// Get the server type component
|
||||
const typeComponent = instance.$('[data-key=type] :input').data('component');
|
||||
|
||||
// Run this computation every time the user change the server type
|
||||
typeComponent.depend();
|
||||
|
||||
// Get the current server type value
|
||||
const type = typeComponent.value();
|
||||
|
||||
// Set the serverType reactive value
|
||||
instance.data.serverType.set(type);
|
||||
|
||||
// Change the schema based on the selected server type
|
||||
if (type === 'dimse') {
|
||||
instance.currentSchema.set(dimseSchema);
|
||||
} else {
|
||||
instance.currentSchema.set(dicomSchema);
|
||||
}
|
||||
});
|
||||
|
||||
// Handle the form mode (edit or add)
|
||||
instance.autorun(() => {
|
||||
const mode = instance.data.mode.get();
|
||||
|
||||
// Check if it is on edit mode and load the saved data
|
||||
if (mode === 'edit') {
|
||||
const data = instance.data.currentItem.get();
|
||||
instance.data.form.value(data);
|
||||
}
|
||||
});
|
||||
});
|
||||
+10
@@ -0,0 +1,10 @@
|
||||
<template name="serverInformationFormField">
|
||||
<div class="col-lg-6">
|
||||
<div class="form-group">
|
||||
<label class="wrapper">
|
||||
<span>{{this.label}}</span>
|
||||
<input type="text" name="{{this.name}}" class="form-control">
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
+45
@@ -0,0 +1,45 @@
|
||||
<template name="serverInformationList">
|
||||
{{#form api=instance.api}}
|
||||
<table id="tblServer" class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Name</th>
|
||||
<th>Type</th>
|
||||
<th class="text-right">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{{#each server in servers}}
|
||||
<tr>
|
||||
<td>{{server.name}}</td>
|
||||
<td>{{server.type}}</td>
|
||||
<td class="text-right">
|
||||
<div class="btn-group" role="group" aria-label="Actions">
|
||||
{{#button class='btn-sm btn-default' action='use' params=server title='Use this server' disabled=(isActive server)}}
|
||||
{{#if isActive server}}
|
||||
<i class="fa fa-check-square-o"></i>
|
||||
{{else}}
|
||||
<i class="fa fa-square-o"></i>
|
||||
{{/if}}
|
||||
{{/button}}
|
||||
{{#button class='btn-sm btn-default' action='edit' params=server title='Edit' disabled=(eq server.origin 'json')}}
|
||||
<i class="fa fa-pencil"></i>
|
||||
{{/button}}
|
||||
{{#button class='btn-sm btn-danger' action='delete' params=server title='Remove' disabled=(eq server.origin 'json')}}
|
||||
<i class="fa fa-trash"></i>
|
||||
{{/button}}
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
{{else}}
|
||||
<tr>
|
||||
<td class="text-center" colspan="3">No servers found</td>
|
||||
</tr>
|
||||
{{/each}}
|
||||
</tbody>
|
||||
</table>
|
||||
{{#button class='btn-sm btn-default' action='add'}}
|
||||
<i class="fa fa-plus"></i> Add a new server
|
||||
{{/button}}
|
||||
{{/form}}
|
||||
</template>
|
||||
+47
@@ -0,0 +1,47 @@
|
||||
Template.serverInformationList.onCreated(() => {
|
||||
const instance = Template.instance();
|
||||
|
||||
instance.api = {
|
||||
add: () => instance.data.mode.set('create'),
|
||||
edit(server) {
|
||||
instance.data.currentItem.set(server);
|
||||
instance.data.mode.set('edit');
|
||||
},
|
||||
delete(server) {
|
||||
// TODO: Replace this for confirmation dialog after LT-refactor is merged back to master
|
||||
if (!confirm('Are you sure you want to remove this peer?')) {
|
||||
return;
|
||||
}
|
||||
|
||||
Meteor.call('serverRemove', server._id, error => {
|
||||
// TODO: check for errors: data-write
|
||||
});
|
||||
},
|
||||
use(server) {
|
||||
Meteor.call('serverSetActive', server._id, error => {
|
||||
// TODO: check for errors: data-write
|
||||
});
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
Template.serverInformationList.helpers({
|
||||
isActive: function(server) {
|
||||
const currentServer = CurrentServer.findOne();
|
||||
if (!currentServer) {
|
||||
return;
|
||||
}
|
||||
|
||||
return server._id === currentServer.serverId;
|
||||
},
|
||||
servers: function() {
|
||||
const options = {
|
||||
sort: {
|
||||
origin: -1,
|
||||
type: 1,
|
||||
name: 1
|
||||
}
|
||||
};
|
||||
return Servers.find({}, options).fetch();
|
||||
}
|
||||
});
|
||||
+27
@@ -0,0 +1,27 @@
|
||||
<template name="serverInformationModal">
|
||||
<div class="modal" id="serverInformationModal" tabindex="-1" role="dialog" aria-labelledby="serverInformationModalLabel">
|
||||
<div class="modal-dialog modal-lg" role="document">
|
||||
<div class="modal-content">
|
||||
{{#let isList=(eq instance.container.mode.get "list")}}
|
||||
<div class="modal-header">
|
||||
<button type="button" class="close" data-dismiss="modal" aria-label="Close"><span aria-hidden="true">×</span></button>
|
||||
<h4 class="modal-title" id="serverInformationModalLabel">Server Information</h4>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
{{#if isList}}
|
||||
{{>serverInformationList instance.container}}
|
||||
{{else}}
|
||||
{{>serverInformationForm instance.container}}
|
||||
{{/if}}
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
{{#unless isList}}
|
||||
<button type="button" class="btn btn-default js-back">Back</button>
|
||||
{{/unless}}
|
||||
<button type="button" class="btn btn-default" data-dismiss="modal">Close</button>
|
||||
</div>
|
||||
{{/let}}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
+20
@@ -0,0 +1,20 @@
|
||||
Template.serverInformationModal.onCreated(function() {
|
||||
var instance = Template.instance();
|
||||
instance.container = {
|
||||
mode: new ReactiveVar('list'),
|
||||
serverType: new ReactiveVar(null),
|
||||
currentItem: new ReactiveVar(null),
|
||||
$form: null,
|
||||
resetState: function() {
|
||||
instance.container.mode.set('list');
|
||||
instance.container.serverType.set(null);
|
||||
instance.container.currentItem.set(null);
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
Template.serverInformationModal.events({
|
||||
'click .js-back, click [data-dismiss=modal]': function(event, instance) {
|
||||
instance.container.resetState();
|
||||
}
|
||||
});
|
||||
+5
@@ -0,0 +1,5 @@
|
||||
.server-information-form
|
||||
label.wrapper
|
||||
display: block
|
||||
.checkbox
|
||||
margin: 0 8px 0 0
|
||||
@@ -0,0 +1,25 @@
|
||||
<template name="studyContextMenu">
|
||||
{{#if uiSettings.studyListFunctionsEnabled}}
|
||||
<div id="studyContextMenu" class="studyContextMenu noselect"
|
||||
oncontextmenu='return false;'
|
||||
unselectable='on'
|
||||
onselectstart='return false;'>
|
||||
<ul>
|
||||
<li>
|
||||
<a id="viewSeriesDetails" type="button"
|
||||
title="View Series Details">
|
||||
View Series Details
|
||||
</a>
|
||||
<a class="disabled">Anonymize</a>
|
||||
<a class="disabled">Send</a>
|
||||
<hr/>
|
||||
<a class="disabled">Delete</a>
|
||||
<a id="exportSelectedStudies"
|
||||
title="Export Selected Studies">
|
||||
Export
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
{{/if}}
|
||||
</template>
|
||||
@@ -0,0 +1,105 @@
|
||||
function closeHandler(dialog) {
|
||||
// Hide the dialog
|
||||
$(dialog).css('display', 'none');
|
||||
|
||||
// Remove the backdrop
|
||||
$('.removableBackdrop').remove();
|
||||
}
|
||||
|
||||
/**
|
||||
* This function is used inside the StudyList package to define a right click callback
|
||||
*
|
||||
* @param e
|
||||
*/
|
||||
openStudyContextMenu = function(e) {
|
||||
StudyList.functions['exportSelectedStudies'] = exportSelectedStudies;
|
||||
StudyList.functions['viewSeriesDetails'] = viewSeriesDetails;
|
||||
|
||||
|
||||
Template.studyContextMenu.study = $(e.currentTarget);
|
||||
|
||||
var dialog = $('#studyContextMenu');
|
||||
|
||||
// Show the nonTargetLesion dialog above
|
||||
var dialogProperty = {
|
||||
display: 'block'
|
||||
};
|
||||
|
||||
var pageHeight = $(document).height();
|
||||
dialogProperty.top = Math.max(e.pageY, 0);
|
||||
dialogProperty.top = Math.min(dialogProperty.top, pageHeight - dialog.outerHeight());
|
||||
|
||||
var pageWidth = $(document).width();
|
||||
dialogProperty.left = Math.max(e.pageX, 0);
|
||||
dialogProperty.left = Math.min(dialogProperty.left, pageWidth - dialog.outerWidth());
|
||||
|
||||
// Device is touch device or not
|
||||
// If device is touch device, set position center of screen vertically and horizontally
|
||||
if (isTouchDevice()) {
|
||||
// add dialogMobile class to provide a black, transparent background
|
||||
dialog.addClass('dialogMobile');
|
||||
dialogProperty.top = 0;
|
||||
dialogProperty.left = 0;
|
||||
dialogProperty.right = 0;
|
||||
dialogProperty.bottom = 0;
|
||||
dialogProperty.margin = 'auto';
|
||||
}
|
||||
|
||||
dialog.css(dialogProperty);
|
||||
dialog.focus();
|
||||
|
||||
// Show the backdrop
|
||||
UI.render(Template.removableBackdrop, document.body);
|
||||
|
||||
// Make sure the context menu is closed when the user clicks away
|
||||
$('.removableBackdrop').one('mousedown touchstart', function() {
|
||||
closeHandler(dialog);
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Exports all selected studies on the studylist
|
||||
*/
|
||||
function exportSelectedStudies() {
|
||||
var selectedStudies = StudyListSelectedStudies.find({}, {
|
||||
sort: {
|
||||
studyDate: 1
|
||||
}
|
||||
}).fetch() || [];
|
||||
|
||||
exportStudies(selectedStudies);
|
||||
}
|
||||
|
||||
/**
|
||||
* Display series details of study in modal
|
||||
*/
|
||||
function viewSeriesDetails() {
|
||||
var selectedStudies = StudyListSelectedStudies.find({}, {
|
||||
sort: {
|
||||
studyDate: 1
|
||||
}
|
||||
}).fetch();
|
||||
|
||||
if (!selectedStudies) {
|
||||
return;
|
||||
}
|
||||
|
||||
Modal.show('viewSeriesDetailsModal', {
|
||||
selectedStudies: selectedStudies
|
||||
});
|
||||
}
|
||||
|
||||
Template.studyContextMenu.events({
|
||||
'click a': function(e) {
|
||||
var study = Template.studyContextMenu.study;
|
||||
var id = $(e.currentTarget).attr('id');
|
||||
|
||||
var fn = StudyList.functions[id];
|
||||
if (fn && typeof(fn) === 'function') {
|
||||
fn(study);
|
||||
}
|
||||
|
||||
var dialog = $('#studyContextMenu');
|
||||
closeHandler(dialog);
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,63 @@
|
||||
@import "{design}/app"
|
||||
|
||||
.studyContextMenu
|
||||
z-index: 10000
|
||||
display: none
|
||||
position: absolute
|
||||
color: #2f2f2f
|
||||
background-color: #f2f2f2
|
||||
|
||||
width: -moz-fit-content
|
||||
width: -webkit-fit-content
|
||||
width: fit-content
|
||||
width: 207px
|
||||
|
||||
height: -moz-fit-content
|
||||
height: -webkit-fit-content
|
||||
height: fit-content
|
||||
|
||||
min-width: 100px
|
||||
font-size: 14px
|
||||
text-align: left
|
||||
-webkit-background-clip: padding-box
|
||||
background-clip: padding-box
|
||||
border: 1px solid rgba(0, 0, 0, .15)
|
||||
border-radius: 3px
|
||||
-webkit-box-shadow: 0 6px 12px rgba(0, 0, 0, .175)
|
||||
box-shadow: 0 6px 12px rgba(0, 0, 0, .175)
|
||||
|
||||
hr
|
||||
border-top: solid 1px #d1d1d1
|
||||
margin: 2px
|
||||
|
||||
ul
|
||||
width: 100%
|
||||
margin: 5px 0
|
||||
list-style: none
|
||||
padding: 0
|
||||
|
||||
li
|
||||
clear: both
|
||||
white-space: nowrap
|
||||
|
||||
a
|
||||
padding: 3px 15px
|
||||
display: block
|
||||
text-decoration: none
|
||||
cursor: pointer
|
||||
font-size: 15px
|
||||
line-height: 1.7
|
||||
color: black
|
||||
|
||||
&:not(.disabled):hover
|
||||
theme('color', '$activeColor')
|
||||
text-decoration: none
|
||||
|
||||
&.disabled
|
||||
opacity: 0.4
|
||||
pointer-events: none
|
||||
cursor: default
|
||||
|
||||
.fa-lg
|
||||
width: 40px
|
||||
text-align: center
|
||||
@@ -0,0 +1,19 @@
|
||||
<template name="studylist">
|
||||
<div id="studylistTabs" class="tab-content">
|
||||
<div role="tabpanel" class="tab-pane active" id="studylistTab">
|
||||
<div class="studylistContainer">
|
||||
{{> studylistResult (clone this)}}
|
||||
</div>
|
||||
</div>
|
||||
<div role="tabpanel" class="tab-pane" id="viewerTab">
|
||||
<div class="viewerContainer">
|
||||
</div>
|
||||
</div>
|
||||
{{>loadingText}}
|
||||
</div>
|
||||
{{#each templateName in additionalTemplates}}
|
||||
{{> UI.dynamic template=templateName}}
|
||||
{{/each}}
|
||||
{{>studyContextMenu }}
|
||||
{{>progressDialog}}
|
||||
</template>
|
||||
@@ -0,0 +1,61 @@
|
||||
/**
|
||||
* Template: StudyList
|
||||
*
|
||||
* This is the main component of the StudyList package
|
||||
*/
|
||||
|
||||
// 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') || {};
|
||||
|
||||
// Define the StudyListStudies Collection
|
||||
// This is a client-side only Collection which
|
||||
// Stores the list of studies in the StudyList
|
||||
StudyListStudies = new Meteor.Collection(null);
|
||||
StudyListStudies._debugName = 'StudyListStudies';
|
||||
|
||||
Session.setDefault('activeContentId', 'studylistTab');
|
||||
|
||||
Template.studylist.onCreated(() => {
|
||||
const instance = Template.instance();
|
||||
|
||||
if (StudyList.subscriptions) {
|
||||
StudyList.subscriptions.forEach(subscriptionName => {
|
||||
instance.subscribe(subscriptionName);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
Template.studylist.onRendered(() => {
|
||||
const instance = Template.instance();
|
||||
if (instance.data && instance.data.studyInstanceUid) {
|
||||
const studyInstanceUid = instance.data.studyInstanceUid;
|
||||
openNewTab(studyInstanceUid, studyInstanceUid);
|
||||
} else {
|
||||
// If there is a tab set as active in the Session,
|
||||
// switch to that now.
|
||||
console.log('studylist onRendered');
|
||||
const contentId = Session.get('activeContentId');
|
||||
if (contentId !== 'studylistTab' && ViewerData && ViewerData[contentId]) {
|
||||
switchToTab(contentId);
|
||||
}
|
||||
}
|
||||
|
||||
Meteor.subscribe('hangingprotocols');
|
||||
});
|
||||
|
||||
Template.studylist.events({
|
||||
'click #tablist a[data-toggle="tab"]': function(e) {
|
||||
// If this tab is already active, do nothing
|
||||
const tabButton = $(e.currentTarget);
|
||||
const tabTitle = tabButton.parents('.tabTitle');
|
||||
if (tabTitle.hasClass('active')) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Otherwise, switch to the tab
|
||||
const contentId = tabButton.data('target').replace('#', '');
|
||||
switchToTab(contentId);
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,34 @@
|
||||
@import "{design}/app"
|
||||
|
||||
body
|
||||
theme('background-color', '$primaryBackgroundColor')
|
||||
|
||||
#tblStudyList
|
||||
tr
|
||||
height: 20px
|
||||
|
||||
#studylistTab
|
||||
theme('background-color', '$primaryBackgroundColor')
|
||||
|
||||
.studylistContainer
|
||||
theme('background-color', '$primaryBackgroundColor')
|
||||
margin: 0 auto
|
||||
color: white
|
||||
float: left;
|
||||
width: 100%;
|
||||
|
||||
.tab-content
|
||||
width: 100%
|
||||
height: calc(100% - 91px)
|
||||
|
||||
.loadingTextDiv
|
||||
theme('color', '$textSecondaryColor')
|
||||
font-size: 30px
|
||||
|
||||
.tab-pane
|
||||
width: 100%
|
||||
height: 100%
|
||||
|
||||
.viewerContainer
|
||||
height: 100%
|
||||
width: 100%
|
||||
+29
@@ -0,0 +1,29 @@
|
||||
<template name="studylistPagination">
|
||||
<div class="studylistPagination">
|
||||
<div class="row">
|
||||
<div class="col-xs-4 col-sm-3 col-md-3">
|
||||
{{#with recordCount}}
|
||||
<div class="form-inline form-group rows-per-page">
|
||||
<label>
|
||||
<span>Show</span>
|
||||
<select class="js-select-rows-per-page">
|
||||
<option value="25">25</option>
|
||||
<option value="50">50</option>
|
||||
<option value="100">100</option>
|
||||
</select>
|
||||
|
||||
<span class="rows-per-page-label">rows per page</span>
|
||||
</label>
|
||||
</div>
|
||||
{{/with}}
|
||||
</div>
|
||||
<div class="col-xs-8 col-sm-9 col-md-9">
|
||||
<div class="form-inline form-group page-number">
|
||||
<label>
|
||||
<ul id="pagination" class="no-margins"></ul>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,58 @@
|
||||
import { Template } from 'meteor/templating';
|
||||
|
||||
import './studylistPagination.html';
|
||||
|
||||
const visiblePages = 10;
|
||||
Template.studylistPagination.onCreated(function() {
|
||||
let instance = Template.instance();
|
||||
// replace parentVariable with the name of the instance variable
|
||||
instance.parent = this.parent(1);
|
||||
});
|
||||
|
||||
Template.studylistPagination.onRendered(function() {
|
||||
const instance = Template.instance();
|
||||
const $paginationControl = instance.$('#pagination');
|
||||
|
||||
// Track changes on recordCount and rowsPerPage
|
||||
instance.autorun(function() {
|
||||
const recordCount = instance.parent.recordCount.get();
|
||||
const rowsPerPage = instance.parent.rowsPerPage.get();
|
||||
|
||||
// Destroy plugin if exists
|
||||
if ($paginationControl.data().twbsPagination) {
|
||||
$paginationControl.twbsPagination('destroy');
|
||||
}
|
||||
|
||||
if (recordCount && rowsPerPage) {
|
||||
const totalPageNumber = Math.ceil(recordCount / rowsPerPage);
|
||||
|
||||
// Initialize plugin
|
||||
$paginationControl.twbsPagination({
|
||||
totalPages: totalPageNumber,
|
||||
visiblePages: visiblePages,
|
||||
onPageClick: function (event, page) {
|
||||
// Update currentPage
|
||||
// Decrease page by 1 to set currentPage
|
||||
// Since reactive table current page index starts by 0
|
||||
instance.parent.currentPage.set(page - 1);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
Template.studylistPagination.helpers({
|
||||
recordCount() {
|
||||
const instance = Template.instance();
|
||||
return instance.parent.recordCount.get();
|
||||
}
|
||||
});
|
||||
|
||||
Template.studylistPagination.events({
|
||||
'change .js-select-rows-per-page'(event, instance) {
|
||||
const rowsPerPage = $(event.currentTarget).val();
|
||||
|
||||
// Update rowsPerPage of parent
|
||||
instance.parent.rowsPerPage.set(parseInt(rowsPerPage, 10));
|
||||
}
|
||||
});
|
||||
+43
@@ -0,0 +1,43 @@
|
||||
@import "{design}/app"
|
||||
|
||||
.studylistPagination
|
||||
font-size: 13px
|
||||
font-weight: normal !important
|
||||
|
||||
.rows-per-page
|
||||
padding-top: 20px
|
||||
|
||||
label
|
||||
font-weight: normal
|
||||
|
||||
select
|
||||
theme('background-color', '$primaryBackgroundColor')
|
||||
color: white
|
||||
|
||||
.page-number
|
||||
text-align: right
|
||||
|
||||
label
|
||||
font-weight: normal
|
||||
|
||||
ul#pagination
|
||||
|
||||
li
|
||||
a
|
||||
padding: 4px 8px
|
||||
theme('background-color', '$primaryBackgroundColor')
|
||||
color: white
|
||||
|
||||
&:hover
|
||||
theme('color', '$activeColor')
|
||||
|
||||
.active
|
||||
a
|
||||
theme('background-color', '$uiGray')
|
||||
border-color: #ddd
|
||||
color: white
|
||||
|
||||
.disabled
|
||||
a:hover
|
||||
color: inherit
|
||||
|
||||
@@ -0,0 +1,107 @@
|
||||
<template name="studylistResult">
|
||||
<div class="studyListToolbar">
|
||||
<div class="header">
|
||||
Study list
|
||||
</div>
|
||||
<div class="studyCount pull-right">
|
||||
{{numberOfStudies}}
|
||||
</div>
|
||||
<div class="pull-right">
|
||||
{{>studylistToolbar}}
|
||||
</div>
|
||||
</div>
|
||||
<div class="theadBackground">
|
||||
</div>
|
||||
<div id="studyListContainer">
|
||||
<table id="tblStudyList" class="studylistResult table noselect">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="patientName">
|
||||
<div id="_patientName" class="sortingCell">
|
||||
<span>Patient Name</span>
|
||||
<i class="{{sortingColumnsIcons.patientName}}"> </i>
|
||||
</div>
|
||||
<input type="text"
|
||||
class="form-control studylist-search"
|
||||
id="patientName"
|
||||
placeholder="Patient Name">
|
||||
</th>
|
||||
<th class="patientId">
|
||||
<div id="_patientId" class="sortingCell">
|
||||
<span>MRN</span>
|
||||
<i class="{{sortingColumnsIcons.patientId}}"> </i>
|
||||
</div>
|
||||
<input type="text"
|
||||
class="form-control studylist-search"
|
||||
id="patientId"
|
||||
placeholder="MRN">
|
||||
</th>
|
||||
<th class="accessionNumber">
|
||||
<div id="_accessionNumber" class="sortingCell ">
|
||||
<span>Accession #</span>
|
||||
<i class="{{sortingColumnsIcons.accessionNumber}}"> </i>
|
||||
</div>
|
||||
<input type="text"
|
||||
class="form-control studylist-search"
|
||||
id="accessionNumber"
|
||||
placeholder="Accession #">
|
||||
</th>
|
||||
<th class="studyDate">
|
||||
<div id="_studyDate" class="sortingCell">
|
||||
<span>Study Date</span>
|
||||
<i class="{{sortingColumnsIcons.studyDate}}"> </i>
|
||||
</div>
|
||||
<input type="text"
|
||||
class="form-control studylist-search"
|
||||
name="daterange"
|
||||
id="studyDate"
|
||||
placeholder="Study Date">
|
||||
</th>
|
||||
<th class="modalities">
|
||||
<div id="_modalities" class="sortingCell">
|
||||
<span>Modality</span>
|
||||
<i class="{{sortingColumnsIcons.modalities}}"> </i>
|
||||
</div>
|
||||
<input type="text"
|
||||
class="form-control studylist-search"
|
||||
id="modality"
|
||||
placeholder="Modality">
|
||||
</th>
|
||||
<th class="studyDescription">
|
||||
<div id="_studyDescription" class="sortingCell">
|
||||
<span>Study Description</span>
|
||||
<i class="{{sortingColumnsIcons.studyDescription}}"> </i>
|
||||
</div>
|
||||
<input type="text"
|
||||
class="form-control studylist-search"
|
||||
id="studyDescription"
|
||||
placeholder="Study Description">
|
||||
</th>
|
||||
<th class="numberOfStudyRelatedInstances">
|
||||
<div id="_numberOfStudyRelatedInstances" class="sortingCell">
|
||||
<span># Images</span>
|
||||
<i class="{{sortingColumnsIcons.numberOfStudyRelatedInstances}}"> </i>
|
||||
</div>
|
||||
<input type="text" class="form-control studylist-search invisible"/>
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="studyListData">
|
||||
{{#each study in studies}}
|
||||
{{>studylistStudy (clone study this)}}
|
||||
{{/each}}
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<!-- Pagination -->
|
||||
{{> studylistPagination }}
|
||||
|
||||
{{#if session "showLoadingText"}}
|
||||
{{>loadingText}}
|
||||
{{ else }}
|
||||
{{#unless numberOfStudies}}
|
||||
<div class="notFound">No matching results</div>
|
||||
{{/unless}}
|
||||
{{/if}}
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,288 @@
|
||||
Session.setDefault('showLoadingText', true);
|
||||
|
||||
Template.studylistResult.helpers({
|
||||
/**
|
||||
* Returns a sorted instance of the StudyListStudies Collection
|
||||
* by Patient name and Study Date in Ascending order.
|
||||
*/
|
||||
studies() {
|
||||
const instance = Template.instance();
|
||||
let studies;
|
||||
let sortOption = {
|
||||
patientName: 1,
|
||||
studyDate: 1
|
||||
};
|
||||
|
||||
// Update sort option if session is defined
|
||||
if (Session.get('sortOption')) {
|
||||
sortOption = Session.get('sortOption');
|
||||
}
|
||||
|
||||
// Pagination parameters
|
||||
const rowsPerPage = instance.rowsPerPage.get();
|
||||
const currentPage = instance.currentPage.get();
|
||||
const offset = rowsPerPage * currentPage;
|
||||
const limit = offset + rowsPerPage;
|
||||
|
||||
studies = StudyListStudies.find({}, {
|
||||
sort: sortOption
|
||||
}).fetch();
|
||||
|
||||
if (!studies) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Update record count
|
||||
instance.recordCount.set(studies.length);
|
||||
|
||||
// Limit studies
|
||||
return studies.slice(offset, limit);
|
||||
},
|
||||
|
||||
numberOfStudies() {
|
||||
return StudyListStudies.find().count();
|
||||
},
|
||||
|
||||
sortingColumnsIcons() {
|
||||
const instance = Template.instance();
|
||||
|
||||
let sortingColumnsIcons = {};
|
||||
Object.keys(instance.sortingColumns.keys).forEach(key => {
|
||||
const value = instance.sortingColumns.get(key);
|
||||
|
||||
if (value === 1) {
|
||||
sortingColumnsIcons[key] = 'fa fa-fw fa-sort-up';
|
||||
} else if (value === -1) {
|
||||
sortingColumnsIcons[key] = 'fa fa-fw fa-sort-down';
|
||||
} else {
|
||||
// fa-fw is blank
|
||||
sortingColumnsIcons[key] = 'fa fa-fw';
|
||||
}
|
||||
});
|
||||
return sortingColumnsIcons;
|
||||
}
|
||||
});
|
||||
|
||||
var studyDateFrom;
|
||||
var studyDateTo;
|
||||
var filter;
|
||||
|
||||
/**
|
||||
* Transforms an input string into a search filter for
|
||||
* the StudyList Search call
|
||||
*
|
||||
* @param filter The input string to be searched for
|
||||
* @returns {*}
|
||||
*/
|
||||
function getFilter(filter) {
|
||||
if (filter && filter.length && filter.substr(filter.length - 1) !== '*') {
|
||||
filter += '*';
|
||||
}
|
||||
|
||||
return filter;
|
||||
}
|
||||
|
||||
/**
|
||||
* Search for a value in a string
|
||||
*/
|
||||
function isIndexOf(mainVal, searchVal) {
|
||||
if (mainVal === undefined || mainVal === '' || mainVal.indexOf(searchVal) > -1){
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Replace object if undefined
|
||||
*/
|
||||
function replaceUndefinedColumnValue(text) {
|
||||
if (text === undefined || text === 'undefined') {
|
||||
return '';
|
||||
} else {
|
||||
return text.toUpperCase();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
|
||||
/**
|
||||
* Convert string to study date
|
||||
*/
|
||||
function convertStringToStudyDate(dateStr) {
|
||||
const y = dateStr.substring(0, 4);
|
||||
const m = dateStr.substring(4, 6);
|
||||
const d = dateStr.substring(6, 8);
|
||||
const newDateStr = m + '/' + d + '/' + y;
|
||||
return new Date(newDateStr);
|
||||
}
|
||||
|
||||
/**
|
||||
* Runs a search for studies matching the studylist query parameters
|
||||
* Inserts the identified studies into the StudyListStudies Collection
|
||||
*/
|
||||
function search() {
|
||||
console.log('search()');
|
||||
|
||||
// Show loading message
|
||||
Session.set('showLoadingText', true);
|
||||
|
||||
// Create the filters to be used for the StudyList Search
|
||||
filter = {
|
||||
patientName: getFilter($('input#patientName').val()),
|
||||
patientId: getFilter($('input#patientId').val()),
|
||||
accessionNumber: getFilter($('input#accessionNumber').val()),
|
||||
studyDescription: getFilter($('input#studyDescription').val()),
|
||||
studyDateFrom: studyDateFrom,
|
||||
studyDateTo: studyDateTo,
|
||||
modalitiesInStudy: $('input#modality').val() ? $('input#modality').val() : ""
|
||||
};
|
||||
|
||||
// Make sure that modality has a reasonable value, since it is occasionally
|
||||
// returned as 'undefined'
|
||||
const modality = replaceUndefinedColumnValue($('input#modality').val());
|
||||
|
||||
// Clear all current studies
|
||||
StudyListStudies.remove({});
|
||||
|
||||
Meteor.call('StudyListSearch', filter, (error, studies) => {
|
||||
console.log('StudyListSearch');
|
||||
if (error) {
|
||||
log.warn(error);
|
||||
return;
|
||||
}
|
||||
|
||||
// Hide loading text
|
||||
Session.set('showLoadingText', false);
|
||||
|
||||
if (!studies) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Loop through all identified studies
|
||||
studies.forEach(study => {
|
||||
// Search the rest of the parameters that aren't done via the server call
|
||||
if (isIndexOf(study.modalities, modality) &&
|
||||
(new Date(studyDateFrom).setHours(0, 0, 0, 0) <= convertStringToStudyDate(study.studyDate) || !studyDateFrom || studyDateFrom === '') &&
|
||||
(convertStringToStudyDate(study.studyDate) <= new Date(studyDateTo).setHours(0, 0, 0, 0) || !studyDateTo || studyDateTo === '')) {
|
||||
|
||||
// Convert numberOfStudyRelatedInstance string into integer
|
||||
study.numberOfStudyRelatedInstances = !isNaN(study.numberOfStudyRelatedInstances) ? parseInt(study.numberOfStudyRelatedInstances) : undefined;
|
||||
|
||||
// Insert any matching studies into the StudyListStudies Collection
|
||||
StudyListStudies.insert(study);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
Template.studylistResult.onCreated(() => {
|
||||
let instance = Template.instance();
|
||||
instance.sortOption = new ReactiveVar();
|
||||
instance.sortingColumns = new ReactiveDict();
|
||||
|
||||
// Pagination parameters
|
||||
|
||||
// Rows per page is 25 as default
|
||||
instance.rowsPerPage = new ReactiveVar(25);
|
||||
|
||||
// Set currentPage indexed 0
|
||||
instance.currentPage = new ReactiveVar(0);
|
||||
instance.recordCount = new ReactiveVar();
|
||||
|
||||
// Set sortOption
|
||||
const sortOptionSession = Session.get('sortOption');
|
||||
if (sortOptionSession) {
|
||||
instance.sortingColumns.set(sortOptionSession);
|
||||
} else {
|
||||
instance.sortingColumns.set({
|
||||
patientName: 1,
|
||||
studyDate: 1,
|
||||
patientId: 0,
|
||||
accessionNumber: 0,
|
||||
studyDescription: 0,
|
||||
modality: 0,
|
||||
numberOfStudyRelatedInstances: 0
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
Template.studylistResult.onRendered(() => {
|
||||
const instance = Template.instance();
|
||||
|
||||
// Initialize daterangepicker
|
||||
const today = moment();
|
||||
const lastWeek = moment().subtract(6, 'days');
|
||||
const lastMonth = moment().subtract(29, 'days');
|
||||
const datePicker = instance.$('#studyDate').daterangepicker({
|
||||
maxDate: today,
|
||||
autoUpdateInput: true,
|
||||
ranges: {
|
||||
Today: [today, today],
|
||||
'Last 7 Days': [lastWeek, today],
|
||||
'Last 30 Days': [lastMonth, today]
|
||||
}
|
||||
}).data('daterangepicker');
|
||||
|
||||
// Retrieve all studies
|
||||
search();
|
||||
});
|
||||
|
||||
function resetSortingColumns(instance, sortingColumn) {
|
||||
Object.keys(instance.sortingColumns.keys).forEach(key => {
|
||||
if (key !== sortingColumn) {
|
||||
instance.sortingColumns.set(key, null);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
Template.studylistResult.events({
|
||||
'keydown input'(event) {
|
||||
if (event.which === 13) { // Enter
|
||||
search();
|
||||
}
|
||||
},
|
||||
|
||||
'onsearch input'() {
|
||||
search();
|
||||
},
|
||||
|
||||
'change #studyDate'(event) {
|
||||
let dateRange = $(event.currentTarget).val();
|
||||
|
||||
// Remove all space chars
|
||||
dateRange = dateRange.replace(/ /g, '');
|
||||
|
||||
// Split dateRange into subdates
|
||||
const dates = dateRange.split('-');
|
||||
studyDateFrom = dates[0];
|
||||
studyDateTo = dates[1];
|
||||
|
||||
if (dateRange !== '') {
|
||||
search();
|
||||
}
|
||||
},
|
||||
|
||||
'click div.sortingCell'(event, instance) {
|
||||
const elementId = event.currentTarget.id;
|
||||
|
||||
// Remove _ from id
|
||||
const columnName = elementId.replace('_', '');
|
||||
|
||||
let sortOption = {};
|
||||
resetSortingColumns(instance, columnName);
|
||||
|
||||
const columnObject = instance.sortingColumns.get(columnName);
|
||||
if (columnObject) {
|
||||
instance.sortingColumns.set(columnName, columnObject * -1);
|
||||
sortOption[columnName] = columnObject * -1;
|
||||
} else {
|
||||
instance.sortingColumns.set(columnName, 1);
|
||||
sortOption[columnName] = 1;
|
||||
}
|
||||
|
||||
instance.sortOption.set(sortOption);
|
||||
Session.set('sortOption', sortOption);
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,241 @@
|
||||
@import "{design}/app"
|
||||
|
||||
$tableHoverColor = #2c363f
|
||||
$tableTextPrimaryColor = white
|
||||
$tableTextSecondaryColor = #91b9cd
|
||||
|
||||
$inputBackgroundColor = #2c363f
|
||||
$inputPlaceholderColor = lightgray
|
||||
|
||||
$studyListToolbarHeight = 75px
|
||||
$tablePadding = 8%
|
||||
$bodyCellHeight = 40px
|
||||
|
||||
.studyListToolbar
|
||||
theme('background', '$uiGrayDarker')
|
||||
height: $studyListToolbarHeight
|
||||
border-bottom: 2px solid black
|
||||
padding: 0 30px
|
||||
|
||||
div
|
||||
display: inline-block
|
||||
|
||||
.header
|
||||
font-size: 22px
|
||||
font-weight: 300
|
||||
color: $tableTextSecondaryColor
|
||||
padding: 0 10px
|
||||
line-height: $studyListToolbarHeight
|
||||
|
||||
.studylistToolbar
|
||||
height: $studyListToolbarHeight
|
||||
line-height: $studyListToolbarHeight
|
||||
|
||||
.studyCount
|
||||
theme('color', '$uiSkyBlue')
|
||||
font-size: 40px
|
||||
font-weight: 100
|
||||
padding: 0 10px
|
||||
line-height: $studyListToolbarHeight
|
||||
|
||||
.theadBackground
|
||||
theme('background-color', '$uiGrayDarker')
|
||||
width: 100%
|
||||
height: 122px
|
||||
position: absolute
|
||||
z-index: 1
|
||||
|
||||
#studyListContainer
|
||||
background: black
|
||||
width: 100%
|
||||
padding: 0 $tablePadding
|
||||
position: absolute
|
||||
z-index: 2
|
||||
|
||||
.loadingTextDiv
|
||||
color: $tableTextSecondaryColor
|
||||
font-size: 30px
|
||||
|
||||
.notFound
|
||||
color: $tableTextSecondaryColor
|
||||
font-size: 30px
|
||||
font-weight: 200
|
||||
text-align: center
|
||||
|
||||
table#tblStudyList
|
||||
thead
|
||||
theme('background-color', '$uiGrayDarker')
|
||||
white-space: nowrap
|
||||
|
||||
tr
|
||||
th
|
||||
padding: 0
|
||||
border-bottom: solid 1px #6fbde2
|
||||
width: 100%
|
||||
|
||||
&:first-child
|
||||
padding-left: $tablePadding
|
||||
|
||||
&:last-child
|
||||
padding-right: $tablePadding
|
||||
|
||||
div.sortingCell
|
||||
display: inline-block
|
||||
cursor: pointer
|
||||
width: 100%
|
||||
min-width: 95px
|
||||
margin: 0 auto
|
||||
color: $tableTextPrimaryColor
|
||||
font-weight: 400
|
||||
padding: 20px
|
||||
|
||||
span
|
||||
font-size: 15px
|
||||
float: left
|
||||
|
||||
i
|
||||
margin: 0 5px
|
||||
|
||||
&:hover
|
||||
theme('color', '$activeColor')
|
||||
|
||||
&.active, &:active
|
||||
theme('color', '$activeColor')
|
||||
|
||||
input.studylist-search
|
||||
height: 34px
|
||||
margin: 0 5px 20px 5px
|
||||
padding: 20px
|
||||
cursor: pointer
|
||||
border: none
|
||||
background-color: $inputBackgroundColor
|
||||
color: $inputPlaceholderColor
|
||||
font-size: 10pt
|
||||
font-weight: normal
|
||||
width: calc(100% - 10px)
|
||||
border-radius: 4px
|
||||
box-sizing: border-box
|
||||
|
||||
transition(all 0.15s ease)
|
||||
placeholder-color($inputPlaceholderColor)
|
||||
|
||||
&.invisible
|
||||
visibility: hidden
|
||||
|
||||
&:active, &:hover
|
||||
background-color: $inputBackgroundColor
|
||||
|
||||
::-webkit-datetime-edit-year-field:not([aria-valuenow]),
|
||||
::-webkit-datetime-edit-month-field:not([aria-valuenow]),
|
||||
::-webkit-datetime-edit-day-field:not([aria-valuenow])
|
||||
color: transparent
|
||||
|
||||
tbody
|
||||
tr
|
||||
padding: 5px
|
||||
background-color: black
|
||||
|
||||
&:nth-child(even)
|
||||
theme('background-color', '$uiGrayDarker')
|
||||
|
||||
td
|
||||
height: $bodyCellHeight
|
||||
line-height: $bodyCellHeight
|
||||
color: $tableTextPrimaryColor
|
||||
font-weight: 300
|
||||
border-top: 1px solid #436270
|
||||
white-space: nowrap
|
||||
transition(all 0.1s ease)
|
||||
|
||||
&:first-child
|
||||
padding-left: $tablePadding
|
||||
|
||||
&:last-child
|
||||
padding-right: $tablePadding
|
||||
|
||||
&:hover, &:active, &.active
|
||||
background-color: $tableHoverColor
|
||||
color: white
|
||||
|
||||
td
|
||||
// This selector is necessary to override bootstrap's 'table' class
|
||||
border-top: 1px solid #436270
|
||||
border-bottom: 1px solid #436270
|
||||
background-color: $tableHoverColor
|
||||
|
||||
|
||||
@media only screen and (max-width: 1362px)
|
||||
$tablePadding = 5%
|
||||
|
||||
#studyListContainer
|
||||
padding: 0 $tablePadding
|
||||
|
||||
table#tblStudyList
|
||||
thead,
|
||||
tbody
|
||||
tr
|
||||
th,
|
||||
td
|
||||
&:first-child
|
||||
padding-left: $tablePadding
|
||||
|
||||
&:last-child
|
||||
padding-right: $tablePadding
|
||||
|
||||
@media only screen and (max-width: 1161px)
|
||||
$tablePadding = 3%
|
||||
|
||||
#studyListContainer
|
||||
padding: 0 $tablePadding
|
||||
|
||||
table#tblStudyList
|
||||
thead,
|
||||
tbody
|
||||
tr
|
||||
th,
|
||||
td
|
||||
&:first-child
|
||||
padding-left: $tablePadding
|
||||
|
||||
&:last-child
|
||||
padding-right: $tablePadding
|
||||
|
||||
@media only screen and (max-width: 1069px)
|
||||
$tablePaddingMediumScreen = 5px
|
||||
|
||||
.studyListToolbar
|
||||
padding: 0 $tablePaddingMediumScreen
|
||||
|
||||
#studyListContainer
|
||||
padding: 0
|
||||
|
||||
table#tblStudyList
|
||||
thead > tr > th
|
||||
|
||||
&:first-child
|
||||
padding-left: $tablePaddingMediumScreen
|
||||
|
||||
&:last-child
|
||||
padding-right: $tablePaddingMediumScreen
|
||||
|
||||
input.worklist-search
|
||||
padding: 10px
|
||||
|
||||
div.sortingCell
|
||||
padding: 10px 5px
|
||||
min-width: auto
|
||||
|
||||
i
|
||||
width: auto
|
||||
|
||||
tbody > tr > td
|
||||
&:first-child
|
||||
padding-left: $tablePaddingMediumScreen
|
||||
|
||||
&:last-child
|
||||
padding-right: $tablePaddingMediumScreen
|
||||
|
||||
.worklistPagination
|
||||
.row
|
||||
margin-left: 0
|
||||
margin-right: 0
|
||||
@@ -0,0 +1,25 @@
|
||||
<template name="studylistStudy">
|
||||
<tr class="studylistStudy noselect">
|
||||
<td class="patientName">
|
||||
{{formatPN patientName}}
|
||||
</td>
|
||||
<td class="patientId">
|
||||
{{patientId}}
|
||||
</td>
|
||||
<td class="accessionNumber">
|
||||
{{accessionNumber}}
|
||||
</td>
|
||||
<td class="studyDate">
|
||||
{{formatDA studyDate}}
|
||||
</td>
|
||||
<td class="modalities">
|
||||
{{modalities}}
|
||||
</td>
|
||||
<td class="studyDescription">
|
||||
{{studyDescription}}
|
||||
</td>
|
||||
<td class="numberOfStudyRelatedInstances">
|
||||
{{numberOfStudyRelatedInstances}}
|
||||
</td>
|
||||
</tr>
|
||||
</template>
|
||||
@@ -0,0 +1,166 @@
|
||||
// Maybe we should use regular StudyList collection?
|
||||
StudyListSelectedStudies = new Meteor.Collection(null);
|
||||
StudyListSelectedStudies._debugName = 'StudyListSelectedStudies';
|
||||
|
||||
function handleShiftClick(studyRow, data) {
|
||||
//log.info('shiftKey');
|
||||
var studyInstanceUid = studyRow.attr('studyInstanceUid');
|
||||
|
||||
// Select all rows in between these two rows
|
||||
if (StudyList.previouslySelected) {
|
||||
var previous = $(StudyList.previouslySelected);
|
||||
var rowsInBetween;
|
||||
if (previous.index() < studyRow.index()) {
|
||||
// The previously selected row is above (lower index) the
|
||||
// currently selected row.
|
||||
|
||||
// Fill in the rows upwards from the previously selected row
|
||||
rowsInBetween = previous.nextAll('tr');
|
||||
} else if (previous.index() > studyRow.index()) {
|
||||
// The previously selected row is below the currently
|
||||
// selected row.
|
||||
|
||||
// Fill in the rows upwards from the previously selected row
|
||||
rowsInBetween = previous.prevAll('tr');
|
||||
} else {
|
||||
// The rows are the same, deselect the current row.
|
||||
// TODO: CHECK THIS, pretty sure this is the wrong behaviour
|
||||
StudyListSelectedStudies.remove({});
|
||||
StudyList.previouslySelected = undefined;
|
||||
return;
|
||||
}
|
||||
|
||||
// Loop through the rows in between current and previous selected studies
|
||||
rowsInBetween.each(function() {
|
||||
var row = $(this);
|
||||
|
||||
if (row.hasClass('active')) {
|
||||
// If we find one that is already selected, do nothing
|
||||
return;
|
||||
}
|
||||
|
||||
// Get the relevant studyInstanceUid
|
||||
var studyInstanceUid = row.attr('studyInstanceUid');
|
||||
|
||||
// Retrieve the data context through Blaze
|
||||
var data = Blaze.getData(this);
|
||||
|
||||
// Set the current study as selected
|
||||
StudyListSelectedStudies.insert(data);
|
||||
row.addClass('active');
|
||||
|
||||
// When we reach the currently clicked-on row, stop the loop
|
||||
return !row.is(studyRow);
|
||||
});
|
||||
} else {
|
||||
// Set the current study as selected
|
||||
StudyListSelectedStudies.insert(data);
|
||||
studyRow.addClass('active');
|
||||
}
|
||||
}
|
||||
|
||||
function handleCtrlClick(studyRow, data) {
|
||||
//log.info('ctrlKey');
|
||||
var studyInstanceUid = studyRow.attr('studyInstanceUid');
|
||||
|
||||
if (studyRow.hasClass('active')) {
|
||||
studyRow.removeClass('active');
|
||||
|
||||
// Find the current studyInstanceUid in the stored list and remove it
|
||||
StudyListSelectedStudies.remove({
|
||||
studyInstanceUid: data.studyInstanceUid
|
||||
});
|
||||
} else {
|
||||
// Set the current study as selected
|
||||
StudyListSelectedStudies.insert(data);
|
||||
studyRow.addClass('active');
|
||||
|
||||
// Set this as the previously selected row, so the user can
|
||||
// use Shift to select from this point onwards
|
||||
StudyList.previouslySelected = studyRow;
|
||||
log.info('StudyList PreviouslySelected set: ' + studyRow.index());
|
||||
}
|
||||
}
|
||||
|
||||
Template.studylistStudy.onRendered(function() {
|
||||
var instance = this;
|
||||
var elem = instance.$('tr.studylistStudy').get(0);
|
||||
|
||||
// Enable HammerJS to allow touch support
|
||||
var mc = new Hammer.Manager(elem);
|
||||
var doubleTapRecognizer = new Hammer.Tap({
|
||||
event: 'doubletap',
|
||||
taps: 2,
|
||||
interval: 500,
|
||||
threshold: 30,
|
||||
posThreshold: 30
|
||||
});
|
||||
|
||||
mc.add(doubleTapRecognizer);
|
||||
});
|
||||
|
||||
Template.studylistStudy.events({
|
||||
'click tr.studylistStudy': function(e) {
|
||||
var studyRow = $(e.currentTarget);
|
||||
var data = this;
|
||||
|
||||
// Remove the ID so we can directly insert this into our client-side collection
|
||||
delete data._id;
|
||||
|
||||
if (e.shiftKey) {
|
||||
handleShiftClick(studyRow, data);
|
||||
} else if (e.ctrlKey || e.metaKey) {
|
||||
handleCtrlClick(studyRow, data);
|
||||
} else {
|
||||
// Select a single study
|
||||
//log.info('Regular click');
|
||||
|
||||
// Clear all selected studies
|
||||
StudyListSelectedStudies.remove({});
|
||||
$('tr.studylistStudy').removeClass('active');
|
||||
|
||||
// Set the previous study to the currently clicked-on study
|
||||
StudyList.previouslySelected = studyRow;
|
||||
//log.info('StudyList PreviouslySelected set: ' + studyRow.index());
|
||||
|
||||
// Set the current study as selected
|
||||
StudyListSelectedStudies.insert(data);
|
||||
studyRow.addClass('active');
|
||||
}
|
||||
},
|
||||
'mousedown tr.studylistStudy': function(e) {
|
||||
// This event handler is meant to handle middle-click on a study
|
||||
if (e.which !== 2) {
|
||||
return;
|
||||
}
|
||||
|
||||
var data = this;
|
||||
var middleClickOnStudy = StudyList.callbacks.middleClickOnStudy;
|
||||
if (middleClickOnStudy && typeof middleClickOnStudy === 'function') {
|
||||
middleClickOnStudy(data);
|
||||
}
|
||||
},
|
||||
'dblclick tr.studylistStudy, doubletap tr.studylistStudy': function(e) {
|
||||
if (e.which !== undefined && e.which !== 1) {
|
||||
return;
|
||||
}
|
||||
|
||||
var data = this;
|
||||
var dblClickOnStudy = StudyList.callbacks.dblClickOnStudy;
|
||||
|
||||
if (dblClickOnStudy && typeof dblClickOnStudy === 'function') {
|
||||
dblClickOnStudy(data);
|
||||
}
|
||||
},
|
||||
'contextmenu tr.studylistStudy, press tr.studylistStudy': function(e, template) {
|
||||
$(e.currentTarget).addClass('active');
|
||||
|
||||
if (openStudyContextMenu && typeof openStudyContextMenu === 'function') {
|
||||
e.preventDefault();
|
||||
|
||||
openStudyContextMenu(e, template);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
});
|
||||
@@ -0,0 +1,2 @@
|
||||
.studylistStudy
|
||||
cursor: pointer
|
||||
@@ -0,0 +1,12 @@
|
||||
<template name="studylistToolbar">
|
||||
<div class='studylistToolbar'>
|
||||
{{#if importSupported }}
|
||||
<div class="addNewStudy btn-file">
|
||||
<label for="btnImport">
|
||||
<i class="fa fa-plus"></i> Add new study
|
||||
</label>
|
||||
<input id="btnImport" class='js-import-files' type="file" webkitdirectory directory multiple>
|
||||
</div>
|
||||
{{/if }}
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,36 @@
|
||||
import { OHIF } from 'meteor/ohif:core';
|
||||
import { Template } from 'meteor/templating';
|
||||
import { Meteor } from 'meteor/meteor';
|
||||
import { Session } from 'meteor/session';
|
||||
import { $ } from 'meteor/jquery';
|
||||
|
||||
Template.studylistToolbar.onCreated(() => {
|
||||
Meteor.call('importSupported', (error, result) => {
|
||||
if (error || !result) {
|
||||
Session.set('importSupported', false);
|
||||
} else {
|
||||
Session.set('importSupported', true);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
Template.studylistToolbar.events({
|
||||
'change .js-import-files'(event) {
|
||||
// Get selected files located in the client machine
|
||||
const selectedFiles = $.map(event.currentTarget.files, value => value);
|
||||
|
||||
importStudies(selectedFiles);
|
||||
},
|
||||
|
||||
'click .js-import-files'(event) {
|
||||
// Reset file input
|
||||
$(event.currentTarget).val('');
|
||||
}
|
||||
});
|
||||
|
||||
Template.studylistToolbar.helpers({
|
||||
importSupported() {
|
||||
const importSupported = Session.get('importSupported');
|
||||
return (importSupported && OHIF.uiSettings.studyListFunctionsEnabled);
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,27 @@
|
||||
@import "{design}/app"
|
||||
|
||||
.studylistToolbar
|
||||
.addNewStudy
|
||||
margin: 0 10px
|
||||
|
||||
label
|
||||
theme('color', '$textSecondaryColor')
|
||||
font-weight: 400
|
||||
cursor: pointer
|
||||
|
||||
*
|
||||
pointer-events: none
|
||||
|
||||
&:hover
|
||||
theme('color', '$hoverColor')
|
||||
|
||||
&:active
|
||||
theme('color', '$activeColor')
|
||||
|
||||
input
|
||||
width: 0.1px;
|
||||
height: 0.1px
|
||||
opacity: 0
|
||||
overflow: hidden
|
||||
position: absolute
|
||||
z-index: -1
|
||||
@@ -0,0 +1,8 @@
|
||||
<template name="tabTitle">
|
||||
<li role="presentation" class="tabTitle">
|
||||
<a data-target="#{{contentid}}" role="tab" data-toggle="tab">
|
||||
{{title}}
|
||||
<button class="close">×</button>
|
||||
</a>
|
||||
</li>
|
||||
</template>
|
||||
@@ -0,0 +1,42 @@
|
||||
Template.tabTitle.events({
|
||||
/**
|
||||
* Closes a tab when the close button is pressed in the title
|
||||
* The next tab to the left is loaded if the current tab is closed
|
||||
*
|
||||
* @param e The click event used to close the tab
|
||||
*/
|
||||
'click .close': function(e) {
|
||||
console.log('click .close');
|
||||
|
||||
// Identify the tab title DOM node
|
||||
var tab = $(e.currentTarget).parents('a[data-toggle="tab"]').eq(0);
|
||||
|
||||
// Get the relevent contentId that this tab title represents
|
||||
// Replace any hash marks (#) that were required by Bootstrap's tab switching
|
||||
var contentId = tab.data('target').replace('#', '');
|
||||
|
||||
// Check if we are closing the active tab. If we are, prepare to switch
|
||||
// to the next tab to the left.
|
||||
var activeContentId = Session.get('activeContentId');
|
||||
if (activeContentId === contentId) {
|
||||
// Find the index of the tab that is being closed
|
||||
var tabIndex = tab.parent('li').index();
|
||||
|
||||
// Find the index the tab to its left
|
||||
var newActiveTabIndex = Math.max(tabIndex - 1, 0);
|
||||
|
||||
// Find the DOM node of the tab that will be activated
|
||||
var newActiveTab = $('.tabTitle').eq(newActiveTabIndex);
|
||||
|
||||
// Find the content ID of the tab that will be switched to
|
||||
var newActiveTabLink = newActiveTab.find('a[data-toggle=tab]');
|
||||
var newContentId = newActiveTabLink.data('target').replace('#', '');
|
||||
|
||||
// Switch to this tab
|
||||
switchToTab(newContentId);
|
||||
}
|
||||
|
||||
// Remove any stored data related to this tab from the global ViewerData structure
|
||||
delete ViewerData[contentId];
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,4 @@
|
||||
.tabTitle
|
||||
button.close
|
||||
// Prevent blue glow around button in Chrome
|
||||
outline: none
|
||||
+20
@@ -0,0 +1,20 @@
|
||||
<template name="viewSeriesDetailsModal">
|
||||
<div class="modal" id="viewSeriesDetailsModal" tabindex="-1" role="dialog" aria-labelledby="viewSeriesDetailsModalLabel">
|
||||
<div class="modal-dialog" role="document">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<button type="button" class="close" data-dismiss="modal" aria-label="Close"><span aria-hidden="true">×</span></button>
|
||||
<h4 class="modal-title" id="associationModalLabel">Series Details</h4>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
{{> seriesDetailsTable
|
||||
selectedStudies=selectedStudies
|
||||
}}
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-dismiss="modal" id='cancelAssociation'>Close</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
Reference in new issue
Block a user