LT-259 New proactive UI (three options) for unsaved changes dialog + New API for unsaved changes module (now handlers can be attached to unsaved changes namespace) + Save measurements integration.

This commit is contained in:
Emanuel F. Oliveira 2016-11-20 23:56:54 -02:00
parent 4a3b01a4f6
commit 91bafb367c
10 changed files with 671 additions and 173 deletions

View File

@ -18,21 +18,37 @@ Template.app.onRendered(() => {
});
Template.app.events({
'click .js-toggle-studyList'() {
'click .js-toggle-studyList'(event) {
const contentId = Session.get('activeContentId');
OHIF.ui.unsavedChanges.checkBeforeAction('viewer.*', function(shouldProceed, hasChanges) {
if (shouldProceed) {
// Drop signaled unsaved changes if any...
if (hasChanges) {
OHIF.ui.unsavedChanges.clear('viewer.*');
OHIF.ui.unsavedChanges.presentProactiveDialog('viewer.*', (hasChanges, userChoice) => {
if (hasChanges) {
switch (userChoice) {
case 'abort-action':
return;
case 'save-changes':
OHIF.ui.unsavedChanges.trigger('viewer', 'save', false);
OHIF.ui.unsavedChanges.clear('viewer.*');
break;
case 'abandon-changes':
OHIF.ui.unsavedChanges.clear('viewer.*');
break;
}
if (contentId !== studylistContentId) {
switchToTab(studylistContentId);
} else {
switchToTab(viewerContentId);
}
}
if (contentId !== studylistContentId) {
switchToTab(studylistContentId);
} else {
switchToTab(viewerContentId);
}
}, {
position: {
x: event.clientX + 15,
y: event.clientY + 15
}
});

View File

@ -0,0 +1,35 @@
<template name="unsavedChangesDialog">
<div class="modal unsavedChangesDialog fade" tabindex="-1" role="dialog">
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header">
<button class="close" aria-label="Close" data-choice="abort-action" data-dismiss="modal">
<span aria-hidden="true">&times;</span>
</button>
<h4 class="modal-title">You have unsaved changes!</h4>
</div>
<div class="modal-footer">
{{#let label='Stay'}}
<button class="btn btn-default" aria-label="{{label}}" data-choice="abort-action" data-dismiss="modal">
{{label}}
</button>
{{/let}}
{{#let label='Leave without saving...'}}
<button class="btn btn-danger" aria-label="{{label}}" data-choice="abandon-changes" data-dismiss="modal">
{{label}}
</button>
{{/let}}
{{#let label='Save and Exit!'}}
<button class="btn btn-primary" aria-label="{{label}}" data-choice="save-changes" data-dismiss="modal">
{{label}}
</button>
{{/let}}
</div>
</div>
</div>
</div>
</template>

View File

@ -0,0 +1,133 @@
import { Blaze } from 'meteor/blaze';
import { Template } from 'meteor/templating';
import { OHIF } from 'meteor/ohif:core';
import { _ } from 'meteor/underscore';
import { $ } from 'meteor/jquery';
const MARGIN_RIGHT = 15;
const MARGIN_BOTTOM = 15;
Template.unsavedChangesDialog.onRendered(function() {
const instance = Template.instance();
const $modal = instance.$('.modal.unsavedChangesDialog');
// Routine which effectivelly displays the BS modal...
instance.displayModal = () => {
// Make modal options extensible...
const modalOptions = _.extend({
backdrop: 'static',
keyboard: false
}, instance.data.modalOptions);
// Set handler for "hidden" event... Simply remove the view!
$modal.on('hidden.bs.modal', () => {
Blaze.remove(instance.view);
});
// Create the bootstrap modal
$modal.modal(modalOptions);
};
// Routine which repositions the modal before display...
instance.displayModalWithPosition = (position) => {
// Preserve original CSS rules...
const origCSS = {
display: $modal.css('display'),
visibility: $modal.css('visibility')
};
// Make sure modal is propperly rendered before proceeding with math...
if (origCSS.display === 'none') {
$modal.css({
visibility: 'hidden',
display: 'block'
});
}
// Run presentation code on next tick...
setTimeout(() => {
let dimension;
const $dialog = $modal.find('.modal-dialog');
const dialogRect = {
position: {
x: parseInt(position.x) || 0,
y: parseInt(position.y) || 0
},
size: {
width: $dialog.outerWidth(),
height: $dialog.outerHeight()
}
};
const modalSize = {
width: $modal.width(),
height: $modal.height()
};
dimension = dialogRect.position.x + dialogRect.size.width + MARGIN_RIGHT;
if (dimension > modalSize.width) {
dialogRect.position.x -= dimension - modalSize.width;
}
if (dialogRect.position.x < 0) {
dialogRect.position.x = 0;
}
dimension = dialogRect.position.y + dialogRect.size.height + MARGIN_BOTTOM;
if (dimension > modalSize.height) {
dialogRect.position.y -= dimension - modalSize.height;
}
if (dialogRect.position.y < 0) {
dialogRect.position.y = 0;
}
// Restore original CSS...
$modal.css(origCSS);
// Set new position...
$dialog.css({
position: 'fixed',
margin: 0,
left: dialogRect.position.x,
top: dialogRect.position.y
});
instance.displayModal();
}, 0);
};
// Check if modal will be presented with custom positioning...
let position = instance.data.position;
if (position && 'x' in position && 'y' in position) {
instance.displayModalWithPosition(position);
} else {
instance.displayModal();
}
});
Template.unsavedChangesDialog.events({
'click button[data-choice]'(event) {
const instance = Template.instance();
const callback = instance.data.callback;
const choice = $(event.currentTarget).attr('data-choice') || '';
// if callback is a function, call it passing user choice...
if (typeof callback === 'function') {
callback.call(instance, choice);
}
}
});

View File

@ -0,0 +1,7 @@
.modal.unsavedChangesDialog
.modal-dialog
width: 400px
.modal-footer
border-top: 0 none

View File

@ -3,6 +3,8 @@ import './dialog/form.html';
import './dialog/form.js';
import './dialog/login.html';
import './dialog/login.js';
import './dialog/unsavedChangesDialog.html';
import './dialog/unsavedChangesDialog.js';
import './dropdown/backdrop.html';
import './dropdown/form.html';

View File

@ -0,0 +1,14 @@
import { Template } from 'meteor/templating';
import { Blaze } from 'meteor/blaze';
import { _ } from 'meteor/underscore';
import { OHIF } from 'meteor/ohif:core';
OHIF.ui.unsavedChangesDialog = function(callback, options) {
// Render the dialog with the given template passing the promise object and callbacks
const templateData = _.extend({}, options, {
callback: callback
});
Blaze.renderWithData(Template.unsavedChangesDialog, templateData, document.body);
};

View File

@ -1,6 +1,7 @@
import './bounded/bounded.js';
import './dialog/form.js';
import './dialog/spatial.js';
import './dialog/unsavedChangesDialog.js';
import './draggable/draggable.js';
import './dropdown/form.js';
import './resizable/resizable.js';

View File

@ -1,210 +1,476 @@
import { OHIF } from 'meteor/ohif:core';
import { _ } from 'meteor/underscore';
OHIF.ui.unsavedChanges = (function(OHIF, _) {
const FUNCTION = 'function';
const STRING = 'string';
const UNDEFINED = 'undefined';
const WILDCARD = '*'; // "*" is a special name which means "all children".
const SEPARATOR = '.';
// Root of the internal Namespace tree.
const rootTree = {};
/**
* Main Namespace Component Class
*/
// Create an unattached namespace node be later appended to the Namespace tree.
function createNode(name, hasChildren) {
class Node {
let node = { name };
constructor() {
this.value = 0;
this.children = {};
this.handlers = {};
}
if (hasChildren) {
node.type = 'tree';
node.children = {};
} else {
node.type = 'leaf';
node.value = 1;
getPathComponents(path) {
return typeof path === STRING ? path.split(SEPARATOR) : null;
}
getNodeUpToIndex(path, index) {
let node = this;
for (let i = 0; i < index; ++i) {
let item = path[i];
if (node.children.hasOwnProperty(item)) {
node = node.children[item];
} else {
node = null;
break;
}
}
return node;
}
// Attach a new node (identified by a namespace string) to the Namespace tree or increment its internal signal count.
function addNode(tree, path) {
append(name, value) {
let node,
result = false,
name = path.shift();
const children = this.children;
let node = null;
if (name !== '*') { // "*" is a special name which means "all children" and thus CANNOT be used.
if (name in tree) {
node = tree[name];
if (path.length > 0) {
result = node.type === 'tree' && addNode(node.children, path);
} else if (node.type === 'leaf') {
node.value++;
result = true;
}
} else {
node = createNode(name, path.length > 0);
if (node.type !== 'tree' || addNode(node.children, path)) {
tree[name] = node;
result = true;
if (children.hasOwnProperty(name)) {
node = children[name];
} else if (typeof name === STRING && name !== WILDCARD) {
node = new Node();
children[name] = node;
}
if (node !== null) {
node.value += value > 0 ? parseInt(value) : 0;
}
return node;
}
probe(recursively) {
let value = this.value;
// Calculate entire tree value recursively?
if (recursively === true) {
const children = this.children;
for (let item in children) {
if (children.hasOwnProperty(item)) {
value += children[item].probe(recursively);
}
}
}
return result;
return value;
}
// Detach a node (identified by a namespace string) from the internal Namespace tree consequently clearing its internal signal count.
// ... The supplied namespace can be a wildcard, in which case all sub-nodes are removed as well.
function removeNode(tree, path) {
clear(recursively) {
let result = false,
name = path.shift();
this.value = 0;
if (name === '*') {
for (name in tree) {
if (tree.hasOwnProperty(name)) {
delete tree[name];
// Clear entire tree recursively?
if (recursively === true) {
const children = this.children;
for (let item in children) {
if (children.hasOwnProperty(item)) {
children[item].clear(recursively);
}
}
}
}
appendPath(path, value) {
path = this.getPathComponents(path);
if (path !== null) {
const last = path.length - 1;
let node = this;
for (let i = 0; i < last; ++i) {
node = node.append(path[i], 0);
if (node === null) {
return false;
}
}
result = true;
} else if (name in tree) {
if (path.length === 0) {
delete tree[name];
return (node.append(path[last], value) !== null);
}
return false;
}
clearPath(path, recursively) {
path = this.getPathComponents(path);
if (path !== null) {
const last = path.length - 1;
let node = this.getNodeUpToIndex(path, last);
if (node !== null) {
let item = path[last];
if (item !== WILDCARD) {
if (node.children.hasOwnProperty(item)) {
node.children[item].clear(recursively);
return true;
}
} else {
const children = node.children;
for (item in children) {
if (children.hasOwnProperty(item)) {
children[item].clear(recursively);
}
}
return true;
}
}
}
return false;
}
probePath(path, recursively) {
path = this.getPathComponents(path);
if (path !== null) {
const last = path.length - 1;
let node = this.getNodeUpToIndex(path, last);
if (node !== null) {
let item = path[last];
if (item !== WILDCARD) {
if (node.children.hasOwnProperty(item)) {
return node.children[item].probe(recursively);
}
} else {
const children = node.children;
let value = 0;
for (item in children) {
if (children.hasOwnProperty(item)) {
value += children[item].probe(recursively);
}
}
return value;
}
}
}
return 0;
}
attachHandler(type, handler) {
let result = false;
if (typeof type === STRING && typeof handler === FUNCTION) {
const handlers = this.handlers;
const list = handlers.hasOwnProperty(type) ? handlers[type] : (handlers[type] = []);
const length = list.length;
let notFound = true;
for (let i = 0; i < length; ++i) {
if (handler === list[i]) {
notFound = false;
break;
}
}
if (notFound) {
list[length] = handler;
result = true;
} else {
let node = tree[name];
if (node.type === 'tree') {
result = removeNode(node.children, path);
}
}
}
return result;
}
// Count the amount of signals registered for a given node.
// ... The supplied namespace can be a wildcard, in which case the resulting value will consider the counter of all sub-nodes.
function probeNode(tree, path) {
removeHandler(type, handler) {
let node,
result = 0,
name = path.shift();
let result = false;
if (name === '*') {
for (name in tree) {
if (tree.hasOwnProperty(name)) {
node = tree[name];
if (node.type === 'leaf') {
result += node.value;
} else {
path.unshift('*');
result += probeNode(node.children, path);
if (typeof type === STRING && typeof handler === FUNCTION) {
const handlers = this.handlers;
if (handlers.hasOwnProperty(type)) {
const list = handlers[type];
const length = list.length;
for (let i = 0; i < length; ++i) {
if (handler === list[i]) {
list.splice(i, 1);
result = true;
break;
}
}
}
} else if (name in tree) {
node = tree[name];
if (node.type === 'tree') {
if (path.length === 0) {
path.unshift('*');
}
result = probeNode(node.children, path);
} else {
result = path.length === 0 ? node.value : 0;
}
}
return result;
}
// return the exposed interface of UnsavedChanges object
return {
trigger(type, nonRecursively) {
/**
* Signal an unsaved change for a given namespace.
* @param {String} name A string (e.g., "viewer.studyViewer.measurements.targets") that identifies the namespace of the signaled changes.
* @return {Boolean} Returns false if the signal could not be saved or the supplied namespace is invalid. Otherwise, true is returned.
*/
set: function(name) {
return typeof name === 'string' ? addNode(rootTree, name.split('.')) : false;
},
if (typeof type === STRING) {
/**
* Clear all signaled unsaved changes for a given namespace. If the supplied namespace is a wildcard, all signals below that namespace
* are cleared.
* @param {String} name A string that identifies the namespace of the signaled changes (e.g., "viewer.studyViewer.measurements.targets"
* for clearing the "targets" item of the "viewer.studyViewer.measurements" namespace or "viewer.studyViewer.*" to specify all signaled
* changes for the "viewer.studyViewer" namespace).
* @return {Boolean} Returns false if the signal could not be removed or the supplied namespace is invalid. Otherwise, true is returned.
*/
clear: function(name) {
return typeof name === 'string' ? removeNode(rootTree, name.split('.')) : false;
},
const handlers = this.handlers;
/**
* Count the amount of signaled unsaved changes for a given namespace. If the supplied namespace is a wildcard, all signals below that
* namespace will also be accounted.
* @param {String} name A string that identifies the namespace of the signaled changes (e.g., "viewer.studyViewer.measurements.targets"
* for counting the amount of signals for the "targets" item of the "viewer.studyViewer.measurements" namespace or "viewer.studyViewer.*"
* to count all signaled changes for the "viewer.studyViewer" namespace).
* @return {Number} Returns the amount of signaled changes for a given namespace. If the supplied namespace is a wildcard, the sum of all
* changes for that namespace are returned.
*/
probe: function(name) {
return typeof name === 'string' ? probeNode(rootTree, name.split('.')) : 0;
},
/**
* UI utility that presents a confirmation dialog to the user if any unsaved changes where sinaled for the given namespace.
* @param {String} name A string that identifies the namespace of the signaled changes (e.g., "viewer.studyViewer.measurements.targets"
* for considering only the signals for the "targets" item of the "viewer.studyViewer.measurements" namespace or "viewer.studyViewer.*"
* to consider all signaled changes for the "viewer.studyViewer" namespace).
* @param {Function} callback A callback function (e.g, function(shouldProceed, hasChanges) { ... }) that will be executed after assessment.
* Upon execution, the callback will receive two boolean arguments (shouldProceed and hasChanges) indicating if the action can be performed
* or not and if changes that need to be cleared exist.
* @param {Object} options (Optional) An object with UI presentation options.
* @param {String} options.title The string that will be used as a title for confirmation dialog.
* @param {String} options.message The string that will be used as a message for confirmation dialog.
* @return {void} No value is returned.
*/
checkBeforeAction: function(name, callback, options) {
let probe, hasChanges, shouldProceed;
if (typeof callback !== 'function') {
// nothing to do if no callback function is supplied...
return;
if (handlers.hasOwnProperty(type)) {
const list = handlers[type];
const length = list.length;
for (let i = 0; i < length; ++i) {
list[i].call(null);
}
}
probe = this.probe(name);
if (probe > 0) {
// Unsaved changes exist...
hasChanges = true;
let dialogOptions = _.extend({
title: 'You have unsaved changes!',
message: "Your changes will be lost if you don't save them before leaving the current page... Are you sure you want to proceed?"
}, options);
OHIF.ui.showFormDialog('dialogConfirm', dialogOptions).then(function() {
// Unsaved changes exist but user confirms action...
shouldProceed = true;
callback.call(null, shouldProceed, hasChanges);
}, function() {
// Unsaved changes exist and user does NOT confirm action...
shouldProceed = false;
callback.call(null, shouldProceed, hasChanges);
});
} else {
// No unsaved changes, action can be performed...
hasChanges = false;
shouldProceed = true;
callback.call(null, shouldProceed, hasChanges);
if (nonRecursively !== true) {
const children = this.children;
for (let item in children) {
if (children.hasOwnProperty(item)) {
children[item].trigger(type);
}
}
}
}
};
}
}(OHIF, _));
attachHandlerForPath(path, type, handler) {
path = this.getPathComponents(path);
if (path !== null) {
let node = this.getNodeUpToIndex(path, path.length);
if (node !== null) {
return node.attachHandler(type, handler);
}
}
return false;
}
removeHandlerForPath(path, type, handler) {
path = this.getPathComponents(path);
if (path !== null) {
let node = this.getNodeUpToIndex(path, path.length);
if (node !== null) {
return node.removeHandler(type, handler);
}
}
return false;
}
triggerHandlersForPath(path, type, nonRecursively) {
path = this.getPathComponents(path);
if (path !== null) {
let node = this.getNodeUpToIndex(path, path.length);
if (node !== null) {
node.trigger(type, nonRecursively);
}
}
}
}
/**
* Root Namespace Node and API
*/
const rootNode = new Node();
export const unsavedChanges = {
rootNode: rootNode,
/**
* Signal an unsaved change for a given namespace.
* @param {String} path A string (e.g., "viewer.studyViewer.measurements.targets") that identifies the namespace of the signaled changes.
* @return {Boolean} Returns false if the signal could not be saved or the supplied namespace is invalid. Otherwise, true is returned.
*/
set: function(path) {
return rootNode.appendPath(path, 1);
},
/**
* Clear all signaled unsaved changes for a given namespace. If the supplied namespace is a wildcard, all signals below that namespace
* are cleared.
* @param {String} path A string that identifies the namespace of the signaled changes (e.g., "viewer.studyViewer.measurements.targets"
* for clearing the "targets" item of the "viewer.studyViewer.measurements" namespace or "viewer.studyViewer.*" to specify all signaled
* changes for the "viewer.studyViewer" namespace).
* @param {Boolean} recursively Clear node and all its children recursively. If not specified defaults to true.
* @return {Boolean} Returns false if the signal could not be removed or the supplied namespace is invalid. Otherwise, true is returned.
*/
clear: function(path, recursively) {
return rootNode.clearPath(path, typeof recursively === UNDEFINED ? true : recursively);
},
/**
* Count the amount of signaled unsaved changes for a given namespace. If the supplied namespace is a wildcard, all signals below that
* namespace will also be accounted.
* @param {String} path A string that identifies the namespace of the signaled changes (e.g., "viewer.studyViewer.measurements.targets"
* for counting the amount of signals for the "targets" item of the "viewer.studyViewer.measurements" namespace or "viewer.studyViewer.*"
* to count all signaled changes for the "viewer.studyViewer" namespace).
* @param {Boolean} recursively Probe node and all its children recursively. If not specified defaults to true.
* @return {Number} Returns the amount of signaled changes for a given namespace. If the supplied namespace is a wildcard, the sum of all
* changes for that namespace are returned.
*/
probe: function(path, recursively) {
return rootNode.probePath(path, typeof recursively === UNDEFINED ? true : recursively);
},
/**
* Attach an event handler to the specified namespace.
* @param {String} name A string that identifies the namespace to which the event handler will be attached (e.g.,
* "viewer.studyViewer.measurements" to attach an event handler for that namespace).
* @param {String} type A string that identifies the event type to which the event handler will be attached.
* @param {Function} handler The handler that will be executed when the specifed event is triggered.
* @return {Boolean} Returns true on success and false on failure.
*/
attachHandler: function(path, type, handler) {
return (rootNode.appendPath(path, 0) && rootNode.attachHandlerForPath(path, type, handler));
},
/**
* Detach an event handler from the specified namespace.
* @param {String} name A string that identifies the namespace from which the event handler will be detached (e.g.,
* "viewer.studyViewer.measurements" to remove an event handler from that namespace).
* @param {String} type A string that identifies the event type to which the event handler was attached.
* @param {Function} handler The handler that will be removed from execution list.
* @return {Boolean} Returns true on success and false on failure.
*/
removeHandler: function(path, type, handler) {
return rootNode.removeHandlerForPath(path, type, handler);
},
/**
* Trigger all event handlers for the specified namespace and type.
* @param {String} name A string that identifies the namespace from which the event handler will be detached (e.g.,
* "viewer.studyViewer.measurements" to remove an event handler from that namespace).
* @param {String} type A string that identifies the event type which will be triggered.
* @param {Boolean} nonRecursively If set to true, prevents triggering event handlers from descending tree.
* @return {Void} No value is returned.
*/
trigger: function(path, type, nonRecursively) {
rootNode.triggerHandlersForPath(path, type, nonRecursively);
},
/**
* UI utility that presents a confirmation dialog to the user if any unsaved changes where signaled for the given namespace.
* @param {String} path A string that identifies the namespace of the signaled changes (e.g., "viewer.studyViewer.measurements.targets"
* for considering only the signals for the "targets" item of the "viewer.studyViewer.measurements" namespace or "viewer.studyViewer.*"
* to consider all signaled changes for the "viewer.studyViewer" namespace).
* @param {Function} callback A callback function (e.g, function(shouldProceed, hasChanges) { ... }) that will be executed after assessment.
* Upon execution, the callback will receive two boolean arguments (shouldProceed and hasChanges) indicating if the action can be performed
* or not and if changes that need to be cleared exist.
* @param {Object} options (Optional) An object with UI presentation options.
* @param {String} options.title The string that will be used as a title for confirmation dialog.
* @param {String} options.message The string that will be used as a message for confirmation dialog.
* @return {void} No value is returned.
*/
checkBeforeAction: function(path, callback, options) {
let probe, hasChanges, shouldProceed;
if (typeof callback !== 'function') {
// nothing to do if no callback function is supplied...
return;
}
probe = this.probe(path);
if (probe > 0) {
// Unsaved changes exist...
hasChanges = true;
let dialogOptions = _.extend({
title: 'You have unsaved changes!',
message: "Your changes will be lost if you don't save them before leaving the current page... Are you sure you want to proceed?"
}, options);
OHIF.ui.showFormDialog('dialogConfirm', dialogOptions).then(function() {
// Unsaved changes exist but user confirms action...
shouldProceed = true;
callback.call(null, shouldProceed, hasChanges);
}, function() {
// Unsaved changes exist and user does NOT confirm action...
shouldProceed = false;
callback.call(null, shouldProceed, hasChanges);
});
} else {
// No unsaved changes, action can be performed...
hasChanges = false;
shouldProceed = true;
callback.call(null, shouldProceed, hasChanges);
}
},
/**
* UI utility that presents a "proactive" dialog (with three options: stay, abandon-changes, save-changes) to the user if any unsaved changes where signaled for the given namespace.
* @param {String} path A string that identifies the namespace of the signaled changes (e.g., "viewer.studyViewer.measurements.targets"
* for considering only the signals for the "targets" item of the "viewer.studyViewer.measurements" namespace or "viewer.studyViewer.*"
* to consider all signaled changes for the "viewer.studyViewer" namespace).
* @param {Function} callback A callback function (e.g, function(hasChanges, userChoice) { ... }) that will be executed after assessment.
* Upon execution, the callback will receive two arguments: one boolean (hasChanges) indicating that unsaved changes exist and one string with the ID of the
* option picked by the user on the dialog ('abort-action', 'abandon-changes' and 'save-changes'). If no unsaved changes exist, the second argument is null.
* @param {Object} options (Optional) An object with UI presentation options.
* @param {Object} options.position An object with optimal position (e.g., { x: ..., y: ... }) for the dialog.
* @return {void} No value is returned.
*/
presentProactiveDialog: function(path, callback, options) {
let probe, hasChanges;
if (typeof callback !== 'function') {
// nothing to do if no callback function is supplied...
return;
}
probe = this.probe(path, true);
if (probe > 0) {
// Unsaved changes exist...
hasChanges = true;
OHIF.ui.unsavedChangesDialog(function(choice) {
callback.call(null, hasChanges, choice);
}, options);
} else {
// No unsaved changes, action can be performed...
hasChanges = false;
callback.call(null, hasChanges, null);
}
}
};
OHIF.ui.unsavedChanges = unsavedChanges;

View File

@ -25,6 +25,7 @@ Package.onUse(function(api) {
// UI Styles
api.addFiles([
'client/ui/resizable/resizable.styl',
'client/components/bootstrap/dialog/unsavedChangesDialog.styl',
'client/components/bootstrap/dropdown/dropdown.styl'
], 'client');

View File

@ -8,6 +8,36 @@ Template.caseProgress.onCreated(() => {
instance.progressPercent = new ReactiveVar();
instance.progressText = new ReactiveVar();
instance.isLocked = new ReactiveVar();
instance.saveData = () => {
const timepointApi = instance.data.timepointApi;
const timepoints = timepointApi.all();
OHIF.log.info('Saving Measurements for timepoints:');
OHIF.log.info(timepoints);
instance.data.measurementApi.storeMeasurements(timepoints);
// Clear signaled unsaved changes...
OHIF.ui.unsavedChanges.clear('viewer.studyViewer.measurements.*');
};
instance.unsavedChangesHandler = () => {
const isNotDisabled = !instance.$('.js-finish-case').hasClass('disabled');
if (isNotDisabled && instance.progressPercent.get() === 100) {
instance.saveData();
}
};
// Attach handler for unsaved changes dialog...
OHIF.ui.unsavedChanges.attachHandler('viewer.studyViewer.measurements', 'save', instance.unsavedChangesHandler);
});
Template.caseProgress.onDestroyed(() => {
const instance = Template.instance();
// Remove unsaved changes handler after this view has been destroyed...
OHIF.ui.unsavedChanges.removeHandler('viewer.studyViewer.measurements', 'save', instance.unsavedChangesHandler);
});
Template.caseProgress.onRendered(() => {
@ -132,15 +162,8 @@ Template.caseProgress.events({
return;
}
const timepointApi = instance.data.timepointApi;
const timepoints = timepointApi.all();
OHIF.log.info('Saving Measurements for timepoints:')
OHIF.log.info(timepoints);
instance.data.measurementApi.storeMeasurements(timepoints);
// Clear signaled unsaved changes...
OHIF.ui.unsavedChanges.clear('viewer.studyViewer.measurements.*');
instance.saveData();
switchToTab('studylistTab');
}
});