Skip to content
Permalink
Browse files

Delete confirmation & bulk delete for timelapses

Also introduced a new helper, a progress modal that can be used for
providing feedback about things such as bulk delete operations in the
background.

See #748 and discussion in #1807
  • Loading branch information...
foosel committed Mar 22, 2017
1 parent 675a54a commit 3ec2d7bd1440252a743be99d2fd2abcc10b14f15

Large diffs are not rendered by default.

@@ -162,6 +162,12 @@ function ItemListHelper(listType, supportedSorting, supportedFilters, defaultSor
return undefined;
};

self.resetPage = function() {
if (self.currentPage() > self.lastPage()) {
self.currentPage(self.lastPage());
}
};

//~~ searching

self.changeSearchFunction = function(searchFunction) {
@@ -670,10 +676,163 @@ function showConfirmationDialog(msg, onacknowledge, options) {
return modal;
}

/**
* Shows a progress modal depending on a supplied promise.
*
* Will listen to the supplied promise, update the progress on .progress events and
* enabling the close button and (optionally) closing the dialog on promise resolve.
*
* The calling code should call "notify" on the deferred backing the promise and supply
* two parameters: the text to display on the progress bar and the optional output field and
* a boolean value indicating whether the operation behind that update was successful or not.
* Non-successful progress updates will remove the barClassSuccess class from the progress bar and
* apply the barClassFailure class and also apply the outputClassFailure to the produced line
* in the output.
*
* To determine the progress, calling code should supply the prognosed maximum number of
* progress events. An internal counter will increment on each progress event and used together
* with the max value to calculate the percentage to display on the progress bar.
*
* If no max value is set, the progress bar will show a striped animation at 100% fill status
* to visualize "unknown but ongoing" status.
*
* Available options:
*
* * title: the title of the modal, defaults to "Progress"
* * message: the message of the modal, defaults to ""
* * buttonText: the text on the close button, defaults to "Close"
* * max: maximum number of expected progress events (when 100% will be reached), defaults
* to undefined
* * close: whether to close the dialog on completion, defaults to false
* * output: whether to display the progress texts in an output field, defaults to false
* * dialogClass: additional class to apply to the dialog div
* * barClassSuccess: additional class for the progress bar while all progress events are
* successful
* * barClassFailure: additional class for the progress bar when a progress event was
* unsuccessful
* * outputClassSuccess: additional class for successful output lines
* * outputClassFailure: additional class for unsuccessful output lines
*
* @param options modal options
* @param promise promise to monitor
* @returns {*|jQuery} the modal object
*/
function showProgressModal(options, promise) {
var title = options.title || gettext("Progress");
var message = options.message || "";
var buttonText = options.button || gettext("Close");
var max = options.max || undefined;
var close = options.close || false;
var output = options.output || false;

var dialogClass = options.dialogClass || "";
var barClassSuccess = options.barClassSuccess || "";
var barClassFailure = options.barClassFailure || "bar-danger";
var outputClassSuccess = options.outputClassSuccess || "";
var outputClassFailure = options.outputClassFailure || "text-error";

var modalHeader = $('<h3>' + title + '</h3>');
var paragraph = $('<p>' + message + '</p>');

var progress = $('<div class="progress progress-text-centered"></div>');
var progressBar = $('<div class="bar"></div>')
.addClass(barClassSuccess);
var progressTextBack = $('<span class="progress-text-back"></span>');
var progressTextFront = $('<span class="progress-text-front"></span>')
.width(progress.width());

if (max == undefined) {
progress.addClass("progress-striped active");
progressBar.width("100%");
}

progressBar
.append(progressTextFront);
progress
.append(progressTextBack)
.append(progressBar);

var button = $('<button class="btn">' + buttonText + '</button>')
.prop("disabled", true)
.attr("data-dismiss", "modal")
.attr("aria-hidden", "true");

var modalBody = $('<div></div>')
.addClass('modal-body')
.append(paragraph)
.append(progress);

var pre;
if (output) {
pre = $("<pre class='terminal pre-scrollable' style='height: 70px; font-size: 0.8em'></pre>");
modalBody.append(pre);
}

var modal = $('<div></div>')
.addClass('modal hide fade')
.addClass(dialogClass)
.append($('<div></div>').addClass('modal-header').append(modalHeader))
.append(modalBody)
.append($('<div></div>').addClass('modal-footer').append(button));
modal.modal({keyboard: false, backdrop: "static", show: true});

var counter = 0;
promise
.progress(function(text, success) {
var value;

if (max === undefined || max <= 0) {
value = 100;
} else {
counter++;
value = Math.max(Math.min(counter * 100 / max, 100), 0);
}

// update progress bar
progressBar.width(String(value) + "%");
progressTextFront.text(text);
progressTextBack.text(text);
progressTextFront.width(progress.width());

// if not successful, apply failure class
if (!success && !progressBar.hasClass(barClassFailure)) {
progressBar
.removeClass(barClassSuccess)
.addClass(barClassFailure);
}

if (output && pre) {
if (success) {
pre.append($("<span class='" + outputClassSuccess + "'>" + text + "</span><br>"));
} else {
pre.append($("<span class='" + outputClassFailure + "'>" + text + "</span><br>"));
}
pre.scrollTop(pre[0].scrollHeight - pre.height());
}
})
.done(function() {
button.prop("disabled", false);
if (close) {
modal.modal("hide");
}
})
.fail(function() {
button.prop("disabled", false);
});

return modal;
}

function showReloadOverlay() {
$("#reloadui_overlay").show();
}

function wrapPromiseWithAlways(p) {
var deferred = $.Deferred();
p.always(function() { deferred.resolve.apply(deferred, arguments); });
return deferred.promise();
}

function commentableLinesToArray(lines) {
return splitTextToArray(lines, "\n", true, function(item) {return !_.startsWith(item, "#")});
}
@@ -30,6 +30,9 @@ $(function() {
self.isReady = ko.observable(undefined);
self.isLoading = ko.observable(undefined);

self.markedForFileDeletion = ko.observableArray([]);
self.markedForUnrenderedDeletion = ko.observableArray([]);

self.isTemporary = ko.pureComputed(function() {
return self.isDirty() && !self.persist();
});
@@ -145,12 +148,17 @@ $(function() {
var config = response.config;
if (config === undefined) return;

self.timelapseType(config.type);
// timelapses & unrendered
self.listHelper.updateItems(response.files);
self.listHelper.resetPage();
if (response.unrendered) {
self.unrenderedListHelper.updateItems(response.unrendered);
self.unrenderedListHelper.resetPage();
}

// timelapse config
self.timelapseType(config.type);

if (config.type == "timed") {
if (config.interval != undefined && config.interval > 0) {
self.timelapseTimedInterval(config.interval);
@@ -207,14 +215,135 @@ $(function() {
self.isLoading(data.flags.loading);
};

self.markFilesOnPage = function() {
self.markedForFileDeletion(_.uniq(self.markedForFileDeletion().concat(_.map(self.listHelper.paginatedItems(), "name"))));
};

self.markAllFiles = function() {
self.markedForFileDeletion(_.map(self.listHelper.allItems, "name"));
};

self.clearMarkedFiles = function() {
self.markedForFileDeletion.removeAll();
};

self.removeFile = function(filename) {
OctoPrint.timelapse.delete(filename)
.done(self.requestData);
var perform = function() {
OctoPrint.timelapse.delete(filename)
.done(function() {
self.markedForFileDeletion.remove(filename);
self.requestData()
});
};

showConfirmationDialog(_.sprintf(gettext("You are about to delete timelapse file \"%(name)s\"."), {name: filename}),
perform)
};

self.removeMarkedFiles = function() {
var perform = function() {
self._bulkRemove(self.markedForFileDeletion(), "files")
.done(function() {
self.markedForFileDeletion.removeAll();
});
};

showConfirmationDialog(_.sprintf(gettext("You are about to delete %(count)d timelapse files."), {count: self.markedForFileDeletion().length}),
perform);
};

self.markUnrenderedOnPage = function() {
self.markedForUnrenderedDeletion(_.uniq(self.markedForUnrenderedDeletion().concat(_.map(self.unrenderedListHelper.paginatedItems(), "name"))));
};

self.markAllUnrendered = function() {
self.markedForUnrenderedDeletion(_.map(self.unrenderedListHelper.allItems, "name"));
};

self.clearMarkedUnrendered = function() {
self.markedForUnrenderedDeletion.removeAll();
};

self.removeUnrendered = function(name) {
OctoPrint.timelapse.deleteUnrendered(name)
.done(self.requestData);
var perform = function() {
OctoPrint.timelapse.deleteUnrendered(name)
.done(function() {
self.markedForUnrenderedDeletion.remove(name);
self.requestData();
});
};

showConfirmationDialog(_.sprintf(gettext("You are about to delete unrendered timelapse \"%(name)s\"."), {name: name}),
perform)
};

self.removeMarkedUnrendered = function() {
var perform = function() {
self._bulkRemove(self.markedForUnrenderedDeletion(), "unrendered")
.done(function() {
self.markedForUnrenderedDeletion.removeAll();
});
};

showConfirmationDialog(_.sprintf(gettext("You are about to delete %(count)d unrendered timelapses."), {count: self.markedForUnrenderedDeletion().length}),
perform);
};

self._bulkRemove = function(files, type) {
var title, message, handler;

if (type == "files") {
title = gettext("Deleting timelapse files");
message = _.sprintf(gettext("Deleting %(count)d timelapse files..."), {count: files.length});
handler = function(filename) {
return OctoPrint.timelapse.delete(filename)
.done(function() {
deferred.notify(_.sprintf(gettext("Deleted %(filename)s..."), {filename: filename}), true);
})
.fail(function() {
deferred.notify(_.sprintf(gettext("Deletion of %(filename)s failed, continuing..."), {filename: filename}), false);
});
}
} else if (type == "unrendered") {
title = gettext("Deleting unrendered timelapses");
message = _.sprintf(gettext("Deleting %(count)d unrendered timelapses..."), {count: files.length});
handler = function(filename) {
return OctoPrint.timelapse.deleteUnrendered(filename)
.done(function() {
deferred.notify(_.sprintf(gettext("Deleted %(filename)s..."), {filename: filename}), true);
})
.fail(function() {
deferred.notify(_.sprintf(gettext("Deletion of %(filename)s failed, continuing..."), {filename: filename}), false);
});
}
} else {
return;
}

var deferred = $.Deferred();

var promise = deferred.promise();

var options = {
title: title,
message: message,
max: files.length,
output: true
};
showProgressModal(options, promise);

var requests = [];
_.each(files, function(filename) {
var request = handler(filename);
requests.push(request)
});
$.when.apply($, _.map(requests, wrapPromiseWithAlways))
.done(function() {
deferred.resolve();
self.requestData();
});

return promise;
};

self.renderUnrendered = function(name) {
@@ -311,6 +311,16 @@ table {
}

// timelapse files
&.timelapse_files_checkbox,
&.timelapse_unrendered_checkbox {
text-align: center;
width: 10px;

input[type="checkbox"] {
margin-top: 0;
}
}

&.timelapse_files_name,
&.timelapse_unrendered_name {
text-overflow: ellipsis;
@@ -1021,6 +1031,15 @@ textarea.block {
text-decoration: underline;
}

.btn-mini .caret, .btn-small .caret {
margin-top: 8px;
}

.dropdown-menu-right {
right: 0;
left: auto;
}

/** Styles for Bootstrap Slider */

.slider {

0 comments on commit 3ec2d7b

Please sign in to comment.
You can’t perform that action at this time.