apostrophe
Version:
The Apostrophe Content Management System.
962 lines (838 loc) • 33 kB
JavaScript
/*
This is a moog modal for the reorganize pages feature
*/
apos.define('apostrophe-pages-reorganize', {
extend: 'apostrophe-modal',
source: 'reorganize',
// options to be POSTed to get-jqtree, empty in the base class
jqtree: {},
construct: function(self, options) {
self.choices = [];
self.beforeShow = function(callback) {
self.$tree = self.$el.find('[data-tree]');
self.$tree.tree({
data: [],
autoOpen: 0,
openFolderDelay: 2500,
dragAndDrop: true,
onCanMoveTo: function(movedNode, targetNode, position) {
// Cannot create peers of root
if ((targetNode.slug === '/') && (position !== 'inside')) {
return false;
}
return true;
},
onCreateLi: function(node, $li) {
self.enhanceNode(node, $li);
}
});
self.$tree.on('click', '[data-visit]', function() {
self.visit($(this));
return false;
});
self.$tree.on('click', '[data-edit]', function() {
self.edit($(this));
self.updateVirtualTrashcans();
return false;
});
self.$tree.on('click', '[data-delete]', function() {
self.delete($(this));
return false;
});
self.$tree.on('click', '[data-delete-from-trash]', function() {
self.deleteFromTrash($(this));
return false;
});
self.$tree.on('tree.click', function(e) {
// Disable single selection, which we don't use for anything right now
e.preventDefault();
});
self.$tree.on('tree.move', function(e) {
self.move(e);
return false;
});
self.enableCheckboxEvents();
self.enableSelectAll();
self.$batch = self.$el.find('[data-batch]');
self.$batchOperation = self.$batch.find('[name="batch-operation"]');
return async.series([ self.enableBatchOperations, self.reload ], callback);
};
// Enhance a node in the tree with additional elements.
// jqtree doesn't template these, and running nunjucks for
// each one would be a big perf hit with big trees anyway.
// So we add controls directly to `$li` as needed.
self.enhanceNode = function(node, $li) {
// Identify the root trashcan and add a class to its li so that we
// can hide inappropriate controls within the trash
// TODO: do we want to make this slug a constant forever?
if (node.type === 'trash') {
$li.addClass('apos-trash');
} else if (node.virtualTrashcan) {
$li.addClass('apos-trash');
}
self.configureTitleContainer(node, $li);
self.indentLevel(node, $li);
if (!node.virtualTrashcan) {
self.addControlGroupToNode(node, $li);
// Add a checkbox for selection.
var $checkbox = $('<input type="checkbox" name="selected" value="' + node.id + '" />');
$li.find('.jqtree-element .apos-reorganize-title').before($checkbox);
var $link;
$link = $('<a class="apos-edit" target="_blank"></a>');
$link.attr('data-node-id', node.id);
$link.attr('data-edit', '1');
$link.append('Settings');
// add published indicator
$li.find('.jqtree-element .apos-reorganize-controls--published').append('<span class="apos-reorganize-published apos-reorganize-published--' + node.published + '"></span>');
$link.attr('href', '#');
$li.find('.jqtree-element .apos-reorganize-controls--edit').append($link);
self.addVisitLink(node, $li);
// node.publish means we CAN PUBLISH IT, not that it IS PUBLISHED.
// See node.published. -Tom
if (node.publish) {
// Regular "to the trash" button. CSS hides when
// already in the trash
$link = $('<a class="apos-delete"></a>');
$link.attr('data-node-id', node.id);
$link.attr('data-delete', '1');
$link.attr('href', '#');
$link.append('Move to Trash');
$li.find('.jqtree-element .apos-reorganize-controls--trash').append($link);
if (self.options.deleteFromTrash) {
// "Destroy forever" button. CSS reveals when
// already in the trash
$link = $('<a class="apos-delete-from-trash"></a>');
$link.attr('data-node-id', node.id);
$link.attr('data-delete-from-trash', '1');
$link.attr('href', '#');
$link.append('Delete from Trash');
$li.find('.jqtree-element .apos-reorganize-controls--trash').append($link);
}
}
}
};
self.addControlGroupToNode = function(node, $li) {
$li.find('.jqtree-element').append($('<span class="apos-reorganize-controls apos-reorganize-controls--published"></span>'));
$li.find('.jqtree-element').append($('<span class="apos-reorganize-controls apos-reorganize-controls--edit"></span>'));
$li.find('.jqtree-element').append($('<span class="apos-reorganize-controls apos-reorganize-controls--link"></span>'));
$li.find('.jqtree-element').append($('<span class="apos-reorganize-controls apos-reorganize-controls--trash"></span>'));
};
self.indentLevel = function(node, $li) {
// add page depth as data attr
$li.attr('data-apos-reorganize-depth', node.getLevel());
if (node.getLevel() !== 1) {
$li.find('.apos-reorganize-title').css('padding-left', 15 * node.getLevel() + 'px');
}
};
self.configureTitleContainer = function(node, $li) {
var $titleContainer = $('<span class="apos-reorganize-title"></span>');
$titleContainer.append($li.find('.jqtree-title'));
if ($li.find('.jqtree-toggler').length === 0) {
$titleContainer.addClass('apos-reorganize-title--no-toggle');
} else {
$titleContainer.prepend($li.find('.jqtree-toggler'));
}
$li.find('.jqtree-element').prepend($titleContainer);
};
// Add button used to visit the page
self.addVisitLink = function(node, $li) {
// Append a link to the jqtree-element div.
// The link has a url '#node-[id]' and a data property 'node-id'.
var $link = $('<a class="apos-visit" target="_blank"></a>');
$link.attr('data-node-id', node.id);
$link.attr('data-visit', '1');
$link.attr('href', '#');
// link.text('»');
// $link.append('<i class="fa fa-external-link"></i>');
$link.append('Link');
$li.find('.jqtree-element .apos-reorganize-controls--link').append($link);
};
// After a reorg the page URL may have changed, be prepared to
// navigate there or to the home page or just refresh to reflect
// possible new tabs
self.afterHide = function() {
var page = apos.pages.page;
var _id = page._id;
self.api('info', { _id: _id }, function(data) {
if (data.status !== 'ok') {
// WTF, go home
apos.ui.redirect('/');
}
var newPathname = data.page.slug.replace(/^\/\//, '/');
apos.ui.redirect(newPathname);
}, function() {
// If the page no longer exists, navigate away to home page
apos.ui.redirect('/');
});
};
self.visit = function($node) {
var nodeId = $node.attr('data-node-id');
var node = self.$tree.tree('getNodeById', nodeId);
var tab = window.open(apos.prefix + node.slug, '_blank');
tab.focus();
};
self.edit = function($node) {
var nodeId = $node.attr('data-node-id');
var node = self.$tree.tree('getNodeById', nodeId);
var isTrash = node.trash;
apos.create('apostrophe-pages-editor-update', {
action: self.options.action,
page: { type: node.type, _id: node.id },
afterSave: function(page, callback) {
// Update the node and reflect any change in trash status.
var node = self.$tree.tree('getNodeById', page._id);
if (node) {
var newNode = _.pick(node, 'children', 'id');
_.assign(newNode, _.pick(page, 'slug', 'type', 'trash'));
newNode.label = page.title;
self.$tree.tree('updateNode', node, newNode);
if (isTrash && (!newNode.trash)) {
// Ceased to be trash, move it out of virtual trashcan
self.$tree.tree('moveNode', node, node.parent.parent, 'inside');
} else if (newNode.trash && (!isTrash)) {
// Became trash, move it into virtual trashcan
var parent = node.parent;
var virtualTrashcan = _.find(parent.children, { virtualTrashcan: true });
self.$tree.tree('moveNode', node, virtualTrashcan, 'inside');
}
}
return callback(null);
},
redirectIfSamePage: false
});
};
self.delete = function($node) {
if (!confirm('Are you sure you want to move this page to the trash?')) {
return false;
}
var nodeId = $node.attr('data-node-id');
var node = self.$tree.tree('getNodeById', nodeId);
var parent = node.parent;
// Find the trashcan so we can mirror what happened on the server
var trash;
if (apos.docs.trashInSchema) {
trash = parent.children[parent.children.length - 1];
} else {
_.each(self.$tree.tree('getTree').children[0].children, function(node) {
if (node.trash) {
trash = node;
}
});
}
if (!trash) {
apos.notify('No trashcan.', { type: 'error', dismiss: true });
return false;
}
apos.pages.trash(node.id, function(err, parentSlug, changed) {
if (err) {
apos.notify('An error occurred.', { type: 'error', dismiss: true });
return;
}
if (apos.docs.trashInSchema) {
self.$tree.tree('moveNode', node, trash, 'inside');
self.updateVirtualTrashcans();
} else {
self.$tree.tree('moveNode', node, trash, 'inside');
_.each(changed, function(info) {
var node = self.$tree.tree('getNodeById', info.id);
if (node) {
node.slug = info.slug;
}
});
}
});
return false;
};
self.deleteFromTrash = function($node) {
var nodeId = $node.attr('data-node-id');
var node = self.$tree.tree('getNodeById', nodeId);
var warning = 'Are you sure you want to DESTROY THIS PAGE FOREVER? This CANNOT BE UNDONE.';
if (node.children && node.children.length) {
warning = 'Are you sure you want to DESTROY THIS PAGE AND ALL OF ITS CHILD PAGES FOREVER? This CANNOT BE UNDONE.';
}
if (!confirm(warning)) {
return false;
}
apos.pages.deleteFromTrash(node.id, function(err, parentSlug) {
if (err) {
apos.notify('An error occurred.', { type: 'error', dismiss: true });
return;
}
self.$tree.tree('removeNode', node);
self.updateVirtualTrashcans();
});
return false;
};
self.move = function(e) {
var trashing;
e.preventDefault();
self.$el.find('.apos-reorganize-progress').fadeIn();
var data = {
movedId: e.move_info.moved_node.id,
targetId: e.move_info.target_node.id,
position: e.move_info.position
};
// Refuse requests to move something before the
// home page, or after it (as a peer). Inside it is fine
var target = e.move_info.target_node;
if ((!target.parent.parent) && (e.move_info.position !== 'inside')) {
return;
}
// You also can't move something after the conventional trashcan
if ((target.type === 'trash') && (!target.virtualTrashcan) && (e.move_info.position === 'after')) {
return;
}
if (apos.docs.trashInSchema) {
if (e.move_info.target_node.virtualTrashcan) {
if ((e.move_info.position === 'before') || (e.move_info.position === 'after')) {
var after = e.move_info.target_node.getPreviousSibling();
if (after) {
data.position = 'after';
data.targetId = after.id;
} else {
data.position = 'inside';
data.targetId = e.move_info.target_node.parent.id;
}
} else {
// "inside" case
// Actually moves into the parent, but also needs to get marked as trash
data.targetId = e.move_info.target_node.parent.id;
trashing = true;
}
} else if (e.move_info.moved_node.trash && (!e.move_info.target_node.trash)) {
// Moving out of a virtual trashcan
trashing = false;
}
}
var changed = [];
return async.series([
move,
trash
], function(err) {
if (err) {
self.errorOnMove();
}
e.move_info.do_move();
_.each(changed, function(doc) {
var node = self.$tree.tree('getNodeById', doc.id);
if (node) {
node.slug = doc.slug;
node.trash = doc.trash;
}
});
self.updateVirtualTrashcans();
self.$el.find('.apos-reorganize-progress').fadeOut();
});
function move(callback) {
if ((data.position === 'inside') && (data.targetId === trueParentId(e.move_info.moved_node))) {
// It's just a change of trash status
return callback(null);
}
return self.api('move', data, function(data) {
if (data.status === 'ok') {
changed = changed.concat(data.changed);
return callback(null);
} else {
return callback(data.status);
}
}, function(err) {
return callback(err);
});
}
function trash(callback) {
if (trashing === undefined) {
return callback(null);
}
var node = e.move_info.moved_node;
if (trashing) {
return apos.pages.trash(node.id, function(err, parentSlug, _changed) {
changed = changed.concat(_changed);
return callback(err);
});
} else {
return apos.pages.rescue(node.id, function(err, parentSlug, _changed) {
changed = changed.concat(_changed);
return callback(err);
});
}
}
function trueParentId(node) {
if (!node.parent) {
return;
}
if (!node.parent.virtualTrashcan) {
return node.parent.id;
}
return trueParentId(node.parent);
}
};
self.reload = function(callback) {
self.api('get-jqtree', options.jqtree, function(data) {
if (data.status === 'ok') {
if (apos.docs.trashInSchema) {
// Remove the standard Apostrophe trash can from the displayed tree,
// unless it already has content
var trashIndex = _.findIndex(data.tree[0].children, { slug: '/trash' });
if (trashIndex !== -1) {
var trash = data.tree[0].children[trashIndex];
if (!trash.children.length) {
data.tree[0].children.splice(trashIndex, 1);
}
}
addVirtualTrashcan(data.tree[0]);
}
self.$tree.tree('loadData', data.tree);
self.updateVirtualTrashcans();
if (callback) {
return callback();
}
} else {
self.errorOnReload();
}
}, function() {
self.errorOnReload();
});
function addVirtualTrashcan(node) {
var trashed = _.filter(node.children || [], { trash: true });
var regular = _.filter(node.children || [], function(node) {
return !node.trash;
});
if (trashed.length || regular.length) {
var trashcan = {
label: 'Trash',
id: apos.utils.generateId(),
virtualTrashcan: true,
children: trashed
};
node.children = regular.concat([ trashcan ]);
}
_.each(regular, addVirtualTrashcan);
}
};
self.updateVirtualTrashcans = function() {
self.updateVirtualTrashcan(self.$tree.tree('getTree').children[0]);
};
self.updateVirtualTrashcan = function(node) {
if (!apos.docs.trashInSchema) {
return;
}
var trashcan = _.find(node.children || [], { virtualTrashcan: true });
var trashed = _.filter(node.children || [], { trash: true });
var regular = _.filter(node.children || [], function(node) {
return !(node.trash || node.virtualTrashcan);
});
if (trashcan && (!trashcan.children.length) && (!regular.length)) {
self.$tree.tree('removeNode', trashcan);
} else if ((!trashcan) && (trashed.length || regular.length)) {
trashcan = {
label: 'Trash',
id: apos.utils.generateId(),
virtualTrashcan: true
};
self.$tree.tree('appendNode', trashcan, node);
trashcan = self.$tree.tree('getNodeById', trashcan.id);
// Reverse the array, otherwise "inside" will put the
// last first
trashed.reverse();
_.each(trashed, function(child) {
self.$tree.tree('moveNode', child, trashcan, 'inside');
});
}
_.each(node.children || [], function(node) {
if (node.virtualTrashcan) {
return;
}
self.updateVirtualTrashcan(node);
});
};
self.errorOnReload = function() {
apos.notify('The server did not respond or you do not have the appropriate privileges.', { type: 'error' });
self.hide();
};
self.errorOnMove = function() {
apos.notify('You may only move pages you are allowed to publish. If you move a page to a new parent, you must be allowed to edit the new parent.', { type: 'error' });
setImmediate(function() {
self.reload(function() {
self.$el.find('.apos-reorganize-progress').fadeOut();
});
});
};
self.decorate = function() {
if (options.decorate) {
options.decorate(self, options);
}
};
// Currently called by chooser, later perhaps used
// to manage pages too
self.enableCheckboxEvents = function() {
self.$el.on('change', 'input[name="selected"][type="checkbox"]', function(e) {
var $box = $(this);
var id = $box.attr('value');
if ($box.prop('checked')) {
self.addChoice(id);
} else {
self.removeChoice(id);
}
});
// Add ability to select multiple checkboxes (Using Left Shift)
var lastChecked;
self.$el.on('click', 'input[name="selected"][type="checkbox"]', function (e) {
// Store a variable called lastchecked to point to the last checked checkbox. If it is undefined it's the first checkbox that's selected.
if (!lastChecked) {
lastChecked = this;
return;
}
// If shift key is pressed and the checkbox is checked.
if (e.shiftKey && this.checked) {
// Get the siblings for the checkboxes that are being checked.
var $checkboxesInScope = $(this).closest('ul.jqtree_common').find('input') || [];
// Get the Index of the currently selected checkbox. (The one checked with holiding shift)
var startIndex = $checkboxesInScope.index(this);
// Get the index of the previously selected checkbox.
var endIndex = $checkboxesInScope.index(lastChecked);
// Get a list of all checkboxes inbetween both the indexes and make them checked.
$checkboxesInScope.slice(Math.min(startIndex, endIndex), Math.max(startIndex, endIndex) + 1).each(function (i, el) {
$(el).prop('checked', true);
$(el).trigger('change');
});
}
lastChecked = this;
});
};
self.addChoice = function(id) {
self.addChoiceToState(id);
self.reflectChoiceInCheckbox(id);
};
self.addChoiceToState = function(id) {
if (!_.contains(self.choices, id)) {
self.choices.push(id);
}
self.reflectChoiceCount();
};
self.removeChoice = function(id) {
self.removeChoiceFromState(id);
self.reflectChoiceInCheckbox(id);
};
self.removeChoiceFromState = function(id) {
self.choices = _.filter(self.choices, function(_id) {
return id !== _id;
});
self.reflectChoiceCount();
};
// Return just the ids of the choices. Subclasses
// might need to extend this to avoid returning
// other data associated with a choice. Unlike get()
// this does not require a callback
self.getIds = function() {
return self.choices;
};
self.clearChoices = function() {
self.choices = [];
self.reflectChoiceCount();
};
// Reflect existing choices in checkboxes.
self.reflectChoicesInCheckboxes = function() {
_.each(self.getVisibleIds(), function(id) {
// Trigger click to do the right thing if progressive
// enhancement is in play.
self.reflectChoiceInCheckbox(id);
});
};
self.getVisibleIds = function() {
var ids = [];
self.$el.find('input[name="selected"][type="checkbox"]').each(function() {
ids.push($(this).attr('value'));
});
return ids;
};
self.enableSelectAll = function() {
self.$el.on('change', '[name="select-all"]', function() {
var checked = $(this).prop('checked');
var ids = self.getVisibleIds();
if (checked) {
_.each(ids, function(id) {
self.addChoiceToState(id);
});
} else {
self.clearChoices();
}
self.reflectChoicesInCheckboxes();
});
};
self.reflectSelectAll = function() {
var $selectAll = self.$el.find('[name="select-all"]');
if (self.getVisibleIds().length === self.choices.length) {
$selectAll.prop('checked', true);
} else {
$selectAll.prop('checked', false);
}
};
// Reflect the current selection state of the given id
// by checking or unchecking the relevant box, if
// currently visible
self.reflectChoiceInCheckbox = function(id) {
var state = _.includes(self.getIds(), id);
self.displayChoiceInCheckbox(id, state);
};
// Return a jquery object referencing the checkbox for the given piece id
self.getCheckbox = function(id) {
return self.$el.find('input[type="checkbox"][value="' + id + '"]');
};
// Set the display state of the given checkbox. returns
// a jQuery object referencing the checkbox, for the convenience
// of subclasses that extend this
self.displayChoiceInCheckbox = function(id, checked) {
var $checkbox = self.getCheckbox(id);
$checkbox.prop('checked', checked);
return $checkbox;
};
self.reflectChoiceCount = _.debounce(function() {
// indirection so overrides work
self.reflectChoiceCountBody();
});
self.reflectChoiceCountBody = function() {
self.reflectBatchOperation();
self.reflectSelectAll();
};
// Enables batch operations, such as moving every selected
// item to the trash. Maps the operations found in options.batchOperations
// to methods, for instance `{ name: 'trash'}` maps to
// a call to `self.batchTrash()`. Also implements the UI for
// selecting and invoking a batch operation.
self.enableBatchOperations = function(callback) {
self.$batchOperationTemplate = self.$batchOperation.clone();
self.batchOperations = self.options.batchOperations;
self.reflectBatchOperation();
self.$batchOperation.on('change', function() {
self.reflectBatchOperation();
});
self.link('apos-batch-operation', function($el, action) {
self.batchOperations[action].handler();
return false;
});
return async.eachSeries(self.batchOperations, function(batchOperation, callback) {
return self.enableBatchOperation(batchOperation, callback);
}, callback);
};
// Invoked when a new batch operation is chosen to reflect it in the UI
// by displaying the appropriate button and, where relevant, the
// appropriate string field. Also invoked when the manage view is refreshed,
// so that filters can impact which operations are currently enabled.
self.reflectBatchOperation = function() {
if (!self.$batchOperation.length) {
// Batch operations not present
return;
}
// We just want to hide the options you can't pick right now,
// but that's not possible with option elements, so we have to
// rebuild the list each time this is an issue and then remove
// the inappropriate items. What a PITA.
var val = self.$batchOperation.val();
// TODO bring back code from manager-modal that determines which
var $selected = self.$batchOperation.find('[value="' + val + '"]');
if ($selected.length) {
self.$batchOperation.val(val);
} else {
self.$batchOperation[0].selectedIndex = 0;
val = self.$batchOperation.val();
}
self.$batch.find('[data-apos-batch-operation-form]').removeClass('apos-active');
self.$batch.find('[data-apos-batch-operation-form="' + val + '"]').addClass('apos-active');
self.$batch.find('[data-apos-batch-operation]').addClass('apos-hidden');
self.$batch.find('[data-apos-batch-operation="' + val + '"]').removeClass('apos-hidden');
// Reflect current count of selected items
var count = self.getIds().length;
self.$batch.find('[name="batch-operation"] option').each(function() {
var $option = $(this);
$option.text($option.text().replace(/\([\d]+\)/, '(' + count + ')'));
});
// Availability based on whether there is a selection
var $buttons = self.$batch.find('.apos-button');
if (count) {
$buttons.removeClass('apos-button--disabled');
} else {
$buttons.addClass('apos-button--disabled');
}
};
self.batchOperations = {};
// Preps for supporting a single batch operation, matching the operation name
// to a method name such as `batchTrash` via the `name` property.
// Also populates the subform for it, if any. Requires callback.
// Invoked for you by `enableBatchOperations`. Do not invoke directly.
self.enableBatchOperation = function(batchOperation, callback) {
self.batchOperations[batchOperation.name] = batchOperation;
batchOperation.handler = self['batch' + apos.utils.capitalizeFirst(apos.utils.camelName(batchOperation.name))];
if (!batchOperation.schema) {
return setImmediate(callback);
}
var data = apos.schemas.newInstance(batchOperation.schema);
return apos.schemas.populate(
self.$batch.find('[data-apos-batch-operation-form="' + batchOperation.name + '"]'),
batchOperation.schema,
data,
callback
);
};
// Moves all selected items (`self.choices`) to the trash, after
// asking for user confirmation.
self.batchTrash = function() {
if (self.choices.length > 0) {
return self.batchSimple(
'trash',
'Are you sure you want to trash ' + self.choices.length + ' item' + (self.choices.length !== 1 ? 's' : '') + '?',
{}
);
}
};
// Rescues all selected items (`self.choices`) from the trash, after
// asking for user confirmation.
self.batchRescue = function() {
if (self.choices.length > 0) {
return self.batchSimple(
'rescue',
'Are you sure you want to rescue ' + self.choices.length + ' item' + (self.choices.length !== 1 ? 's' : '') + ' from the trash?',
{}
);
}
};
// Publishes all selected items (`self.choices`), after asking for
// user confirmation.
self.batchPublish = function() {
if (self.choices.length > 0) {
return self.batchSimple(
'publish',
'Are you sure you want to publish ' + self.choices.length + ' item' + (self.choices.length !== 1 ? 's' : '') + '?',
{}
);
}
};
// Unpublishes all selected items (`self.choices`), after asking for
// user confirmation.
self.batchUnpublish = function() {
if (self.choices.length > 0) {
return self.batchSimple(
'unpublish',
'Are you sure you want to unpublish ' + self.choices.length + ' item' + (self.choices.length !== 1 ? 's' : '') + '?',
{}
);
}
};
// Tags all selected items (`self.choices`), after asking for
// user confirmation.
self.batchTag = function() {
if (self.choices.length > 0) {
return self.batchSimple(
'tag',
'Are you sure you want to tag ' + self.choices.length + ' item' + (self.choices.length !== 1 ? 's' : '') + '?',
{}
);
}
};
// Untags all selected items (`self.choices`), after asking for
// user confirmation.
self.batchUntag = function() {
if (self.choices.length > 0) {
return self.batchSimple(
'untag',
'Are you sure you want to untag ' + self.choices.length + ' item' + (self.choices.length !== 1 ? 's' : '') + '?',
{}
);
}
};
// Carry out a named batch operation, such as `trash`, displaying the
// provided prompt and, if confirmed by the user, invoking the
// corresponding verb in this module's API.
//
// `options.dataSource` can be used to specify a function
// to be invoked to gather more input before calling the API.
// It receives `(data, callback)`, where `data.ids` and any
// input gathered from the schema are already present, and
// should update `data` and invoke `callback` with
// null on success or with an error on failure.
//
// `options.success` is invoked only if the operation
// succeeds. It receives `(result, callback)` where
// `result` is the response from the API and `callback`
// *must* be invoked by the success function after
// completing its additional operations, even if the user
// chooses to cancel or skip those operations.
self.batchSimple = function(operationName, confirmationPrompt, options) {
var operation = self.batchOperations[operationName];
if (!confirm(confirmationPrompt)) {
return;
}
var data = {
ids: self.choices,
job: true
};
// So we don't still say "unpublish (4)" when there are
// now 0 visible things after unpublishing all 4
self.clearChoices();
return async.series([ convert, dataSource, save ], function(err) {
if (err) {
if (Array.isArray(err)) {
// Schemas module already highlighted it
return;
}
apos.notify(err, { type: 'error' });
self.reflectChoicesInCheckboxes();
self.reload();
return;
}
self.reflectChoicesInCheckboxes();
self.reload();
});
function convert(callback) {
if (!operation.schema) {
return callback(null);
}
return apos.schemas.convert(
self.$batch.find('[data-apos-batch-operation-form="' + operationName + '"]'),
operation.schema,
data,
{},
callback
);
}
function dataSource(callback) {
if (!options.dataSource) {
return callback(null);
}
return options.dataSource(data, callback);
}
function save(callback) {
apos.ui.globalBusy(true);
return self.api(operation.route || operationName, data, function(result) {
apos.ui.globalBusy(false);
if (result.status !== 'ok') {
return callback('An error occurred. Please try again.');
}
if (result.jobId) {
var jobs = apos.modules['apostrophe-jobs'];
return jobs.progress(result.jobId, {
success: function(result) {
if (options.success) {
return options.success(result, callback);
} else {
return callback(null);
}
},
change: 'apostrophe-page'
});
}
if (options.success) {
return options.success(result, callback);
} else {
return callback(null);
}
}, function() {
apos.ui.globalBusy(false);
return callback('An error occurred. Please try again.');
});
}
};
// Decorate at the end of the construct method, so that we can override
// methods that were added by the decorator in subclasses.
self.decorate();
}
});