MDL-59845 tool_lp: Add js tracking around UI refresh
[moodle.git] / admin / tool / lp / amd / src / planactions.js
1 // This file is part of Moodle - http://moodle.org/
2 //
3 // Moodle is free software: you can redistribute it and/or modify
4 // it under the terms of the GNU General Public License as published by
5 // the Free Software Foundation, either version 3 of the License, or
6 // (at your option) any later version.
7 //
8 // Moodle is distributed in the hope that it will be useful,
9 // but WITHOUT ANY WARRANTY; without even the implied warranty of
10 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
11 // GNU General Public License for more details.
12 //
13 // You should have received a copy of the GNU General Public License
14 // along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
16 /**
17  * Plan actions via ajax.
18  *
19  * @module     tool_lp/planactions
20  * @package    tool_lp
21  * @copyright  2015 David Monllao
22  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
23  */
24 define(['jquery',
25         'core/templates',
26         'core/ajax',
27         'core/notification',
28         'core/str',
29         'tool_lp/menubar',
30         'tool_lp/dialogue'],
31         function($, templates, ajax, notification, str, Menubar, Dialogue) {
33     /**
34      * PlanActions class.
35      *
36      * Note that presently this cannot be instantiated more than once per page.
37      *
38      * @param {String} type The type of page we're in.
39      */
40     var PlanActions = function(type) {
41         this._type = type;
43         if (type === 'plan') {
44             // This is the page to view one plan.
45             this._region = '[data-region="plan-page"]';
46             this._planNode = '[data-region="plan-page"]';
47             this._template = 'tool_lp/plan_page';
48             this._contextMethod = 'tool_lp_data_for_plan_page';
50         } else if (type === 'plans') {
51             // This is the page to view a list of plans.
52             this._region = '[data-region="plans"]';
53             this._planNode = '[data-region="plan-node"]';
54             this._template = 'tool_lp/plans_page';
55             this._contextMethod = 'tool_lp_data_for_plans_page';
57         } else {
58             throw new TypeError('Unexpected type.');
59         }
60     };
62     /** @type {String} Ajax method to fetch the page data from. */
63     PlanActions.prototype._contextMethod = null;
64     /** @type {String} Selector to find the node describing the plan. */
65     PlanActions.prototype._planNode = null;
66     /** @type {String} Selector mapping to the region to update. Usually similar to wrapper. */
67     PlanActions.prototype._region = null;
68     /** @type {String} Name of the template used to render the region. */
69     PlanActions.prototype._template = null;
70     /** @type {String} Type of page/region we're in. */
71     PlanActions.prototype._type = null;
73     /**
74      * Resolve the arguments to refresh the region.
75      *
76      * @param  {Object} planData Plan data from plan node.
77      * @return {Object} List of arguments.
78      */
79     PlanActions.prototype._getContextArgs = function(planData) {
80         var self = this,
81             args = {};
83         if (self._type === 'plan') {
84             args = {
85                 planid: planData.id
86             };
88         } else if (self._type === 'plans') {
89             args = {
90                 userid: planData.userid
91             };
92         }
94         return args;
95     };
97     /**
98      * Refresh the plan view.
99      *
100      * This is useful when you only want to refresh the view.
101      *
102      * @param  {String} selector The node to search the plan data from.
103      */
104     PlanActions.prototype.refresh = function(selector) {
105         var planData = this._findPlanData($(selector));
106         this._callAndRefresh([], planData);
107     };
109     /**
110      * Callback to render the region template.
111      *
112      * @param {Object} context The context for the template.
113      * @return {Promise}
114      */
115     PlanActions.prototype._renderView = function(context) {
116         var self = this;
117         return templates.render(self._template, context)
118             .then(function(newhtml, newjs) {
119                 $(self._region).replaceWith(newhtml);
120                 templates.runTemplateJS(newjs);
121                 return;
122             });
123     };
125     /**
126      * Call multiple ajax methods, and refresh.
127      *
128      * @param  {Array}  calls    List of Ajax calls.
129      * @param  {Object} planData Plan data from plan node.
130      * @return {Promise}
131      */
132     PlanActions.prototype._callAndRefresh = function(calls, planData) {
133         // Because this function causes a refresh, we must track the JS completion from start to finish to prevent
134         // stale reference issues in Behat.
135         var callKey = 'tool_lp/planactions:_callAndRefresh-' + Math.floor(Math.random() * Math.floor(1000));
136         M.util.js_pending(callKey);
138         var self = this;
139         calls.push({
140             methodname: self._contextMethod,
141             args: self._getContextArgs(planData)
142         });
144         // Apply all the promises, and refresh when the last one is resolved.
145         return $.when.apply($, ajax.call(calls))
146             .then(function() {
147                 return self._renderView(arguments[arguments.length - 1]);
148             })
149             .fail(notification.exception)
150             .always(function() {
151                 return M.util.js_complete(callKey);
152             });
153     };
155     /**
156      * Delete a plan and reload the region.
157      *
158      * @param  {Object} planData Plan data from plan node.
159      */
160     PlanActions.prototype._doDelete = function(planData) {
161         var self = this,
162             calls = [{
163                 methodname: 'core_competency_delete_plan',
164                 args: {id: planData.id}
165             }];
166         self._callAndRefresh(calls, planData);
167     };
169     /**
170      * Delete a plan.
171      *
172      * @param  {Object} planData Plan data from plan node.
173      */
174     PlanActions.prototype.deletePlan = function(planData) {
175         var self = this,
176             requests;
178         requests = ajax.call([{
179             methodname: 'core_competency_read_plan',
180             args: {id: planData.id}
181         }]);
183         requests[0].done(function(plan) {
184             str.get_strings([
185                 {key: 'confirm', component: 'moodle'},
186                 {key: 'deleteplan', component: 'tool_lp', param: plan.name},
187                 {key: 'delete', component: 'moodle'},
188                 {key: 'cancel', component: 'moodle'}
189             ]).done(function(strings) {
190                 notification.confirm(
191                     strings[0], // Confirm.
192                     strings[1], // Delete plan X?
193                     strings[2], // Delete.
194                     strings[3], // Cancel.
195                     function() {
196                         self._doDelete(planData);
197                     }
198                 );
199             }).fail(notification.exception);
200         }).fail(notification.exception);
202     };
204     /**
205      * Reopen plan and reload the region.
206      *
207      * @param  {Object} planData Plan data from plan node.
208      */
209     PlanActions.prototype._doReopenPlan = function(planData) {
210         var self = this,
211             calls = [{
212                 methodname: 'core_competency_reopen_plan',
213                 args: {planid: planData.id}
214             }];
215         self._callAndRefresh(calls, planData);
216     };
218     /**
219      * Reopen a plan.
220      *
221      * @param  {Object} planData Plan data from plan node.
222      */
223     PlanActions.prototype.reopenPlan = function(planData) {
224         var self = this,
225             requests = ajax.call([{
226                 methodname: 'core_competency_read_plan',
227                 args: {id: planData.id}
228             }]);
230         requests[0].done(function(plan) {
231             str.get_strings([
232                 {key: 'confirm', component: 'moodle'},
233                 {key: 'reopenplanconfirm', component: 'tool_lp', param: plan.name},
234                 {key: 'reopenplan', component: 'tool_lp'},
235                 {key: 'cancel', component: 'moodle'}
236             ]).done(function(strings) {
237                 notification.confirm(
238                     strings[0], // Confirm.
239                     strings[1], // Reopen plan X?
240                     strings[2], // Reopen.
241                     strings[3], // Cancel.
242                     function() {
243                         self._doReopenPlan(planData);
244                     }
245                 );
246             }).fail(notification.exception);
247         }).fail(notification.exception);
249     };
251     /**
252      * Complete plan and reload the region.
253      *
254      * @param  {Object} planData Plan data from plan node.
255      */
256     PlanActions.prototype._doCompletePlan = function(planData) {
257         var self = this,
258             calls = [{
259                 methodname: 'core_competency_complete_plan',
260                 args: {planid: planData.id}
261             }];
262         self._callAndRefresh(calls, planData);
263     };
265     /**
266      * Complete a plan process.
267      *
268      * @param  {Object} planData Plan data from plan node.
269      */
270     PlanActions.prototype.completePlan = function(planData) {
271         var self = this,
272             requests = ajax.call([{
273                 methodname: 'core_competency_read_plan',
274                 args: {id: planData.id}
275             }]);
277         requests[0].done(function(plan) {
278             str.get_strings([
279                 {key: 'confirm', component: 'moodle'},
280                 {key: 'completeplanconfirm', component: 'tool_lp', param: plan.name},
281                 {key: 'completeplan', component: 'tool_lp'},
282                 {key: 'cancel', component: 'moodle'}
283             ]).done(function(strings) {
284                 notification.confirm(
285                     strings[0], // Confirm.
286                     strings[1], // Complete plan X?
287                     strings[2], // Complete.
288                     strings[3], // Cancel.
289                     function() {
290                         self._doCompletePlan(planData);
291                     }
292                 );
293             }).fail(notification.exception);
294         }).fail(notification.exception);
295     };
297     /**
298      * Unlink plan and reload the region.
299      *
300      * @param  {Object} planData Plan data from plan node.
301      */
302     PlanActions.prototype._doUnlinkPlan = function(planData) {
303         var self = this,
304             calls = [{
305                 methodname: 'core_competency_unlink_plan_from_template',
306                 args: {planid: planData.id}
307             }];
308         self._callAndRefresh(calls, planData);
309     };
311     /**
312      * Unlink a plan process.
313      *
314      * @param  {Object} planData Plan data from plan node.
315      */
316     PlanActions.prototype.unlinkPlan = function(planData) {
317         var self = this,
318             requests = ajax.call([{
319                 methodname: 'core_competency_read_plan',
320                 args: {id: planData.id}
321             }]);
323         requests[0].done(function(plan) {
324             str.get_strings([
325                 {key: 'confirm', component: 'moodle'},
326                 {key: 'unlinkplantemplateconfirm', component: 'tool_lp', param: plan.name},
327                 {key: 'unlinkplantemplate', component: 'tool_lp'},
328                 {key: 'cancel', component: 'moodle'}
329             ]).done(function(strings) {
330                 notification.confirm(
331                     strings[0], // Confirm.
332                     strings[1], // Unlink plan X?
333                     strings[2], // Unlink.
334                     strings[3], // Cancel.
335                     function() {
336                         self._doUnlinkPlan(planData);
337                     }
338                 );
339             }).fail(notification.exception);
340         }).fail(notification.exception);
341     };
343     /**
344      * Request review of a plan.
345      *
346      * @param  {Object} planData Plan data from plan node.
347      * @method _doRequestReview
348      */
349     PlanActions.prototype._doRequestReview = function(planData) {
350         var calls = [{
351             methodname: 'core_competency_plan_request_review',
352             args: {
353                 id: planData.id
354             }
355         }];
356         this._callAndRefresh(calls, planData);
357     };
359     /**
360      * Request review of a plan.
361      *
362      * @param  {Object} planData Plan data from plan node.
363      * @method requestReview
364      */
365     PlanActions.prototype.requestReview = function(planData) {
366         this._doRequestReview(planData);
367     };
369     /**
370      * Cancel review request of a plan.
371      *
372      * @param  {Object} planData Plan data from plan node.
373      * @method _doCancelReviewRequest
374      */
375     PlanActions.prototype._doCancelReviewRequest = function(planData) {
376         var calls = [{
377             methodname: 'core_competency_plan_cancel_review_request',
378             args: {
379                 id: planData.id
380             }
381         }];
382         this._callAndRefresh(calls, planData);
383     };
385     /**
386      * Cancel review request of a plan.
387      *
388      * @param  {Object} planData Plan data from plan node.
389      * @method cancelReviewRequest
390      */
391     PlanActions.prototype.cancelReviewRequest = function(planData) {
392         this._doCancelReviewRequest(planData);
393     };
395     /**
396      * Start review of a plan.
397      *
398      * @param  {Object} planData Plan data from plan node.
399      * @method _doStartReview
400      */
401     PlanActions.prototype._doStartReview = function(planData) {
402         var calls = [{
403             methodname: 'core_competency_plan_start_review',
404             args: {
405                 id: planData.id
406             }
407         }];
408         this._callAndRefresh(calls, planData);
409     };
411     /**
412      * Start review of a plan.
413      *
414      * @param  {Object} planData Plan data from plan node.
415      * @method startReview
416      */
417     PlanActions.prototype.startReview = function(planData) {
418         this._doStartReview(planData);
419     };
421     /**
422      * Stop review of a plan.
423      *
424      * @param  {Object} planData Plan data from plan node.
425      * @method _doStopReview
426      */
427     PlanActions.prototype._doStopReview = function(planData) {
428         var calls = [{
429             methodname: 'core_competency_plan_stop_review',
430             args: {
431                 id: planData.id
432             }
433         }];
434         this._callAndRefresh(calls, planData);
435     };
437     /**
438      * Stop review of a plan.
439      *
440      * @param  {Object} planData Plan data from plan node.
441      * @method stopReview
442      */
443     PlanActions.prototype.stopReview = function(planData) {
444         this._doStopReview(planData);
445     };
447     /**
448      * Approve a plan.
449      *
450      * @param  {Object} planData Plan data from plan node.
451      * @method _doApprove
452      */
453     PlanActions.prototype._doApprove = function(planData) {
454         var calls = [{
455             methodname: 'core_competency_approve_plan',
456             args: {
457                 id: planData.id
458             }
459         }];
460         this._callAndRefresh(calls, planData);
461     };
463     /**
464      * Approve a plan.
465      *
466      * @param  {Object} planData Plan data from plan node.
467      * @method approve
468      */
469     PlanActions.prototype.approve = function(planData) {
470         this._doApprove(planData);
471     };
473     /**
474      * Unapprove a plan.
475      *
476      * @param  {Object} planData Plan data from plan node.
477      * @method _doUnapprove
478      */
479     PlanActions.prototype._doUnapprove = function(planData) {
480         var calls = [{
481             methodname: 'core_competency_unapprove_plan',
482             args: {
483                 id: planData.id
484             }
485         }];
486         this._callAndRefresh(calls, planData);
487     };
489     /**
490      * Unapprove a plan.
491      *
492      * @param  {Object} planData Plan data from plan node.
493      * @method unapprove
494      */
495     PlanActions.prototype.unapprove = function(planData) {
496         this._doUnapprove(planData);
497     };
499     /**
500      * Display list of linked courses on a modal dialogue.
501      *
502      * @param  {Event} e The event.
503      */
504     PlanActions.prototype._showLinkedCoursesHandler = function(e) {
505         e.preventDefault();
507         var competencyid = $(e.target).data('id');
508         var requests = ajax.call([{
509             methodname: 'tool_lp_list_courses_using_competency',
510             args: {id: competencyid}
511         }]);
513         requests[0].done(function(courses) {
514             var context = {
515                 courses: courses
516             };
517             templates.render('tool_lp/linked_courses_summary', context).done(function(html) {
518                 str.get_string('linkedcourses', 'tool_lp').done(function(linkedcourses) {
519                     new Dialogue(
520                         linkedcourses, // Title.
521                         html // The linked courses.
522                     );
523                 }).fail(notification.exception);
524             }).fail(notification.exception);
525         }).fail(notification.exception);
526     };
528     /**
529      * Plan event handler.
530      *
531      * @param  {String} method The method to call.
532      * @param  {Event} e The event.
533      * @method _eventHandler
534      */
535     PlanActions.prototype._eventHandler = function(method, e) {
536         e.preventDefault();
537         var data = this._findPlanData($(e.target));
538         this[method](data);
539     };
541     /**
542      * Find the plan data from the plan node.
543      *
544      * @param  {Node} node The node to search from.
545      * @return {Object} Plan data.
546      */
547     PlanActions.prototype._findPlanData = function(node) {
548         var parent = node.parentsUntil($(this._region).parent(), this._planNode),
549             data;
551         if (parent.length != 1) {
552             throw new Error('The plan node was not located.');
553         }
555         data = parent.data();
556         if (typeof data === 'undefined' || typeof data.id === 'undefined') {
557             throw new Error('Plan data could not be found.');
558         }
560         return data;
561     };
563     /**
564      * Enhance a menu bar.
565      *
566      * @param  {String} selector Menubar selector.
567      */
568     PlanActions.prototype.enhanceMenubar = function(selector) {
569         Menubar.enhance(selector, {
570             '[data-action="plan-delete"]': this._eventHandler.bind(this, 'deletePlan'),
571             '[data-action="plan-complete"]': this._eventHandler.bind(this, 'completePlan'),
572             '[data-action="plan-reopen"]': this._eventHandler.bind(this, 'reopenPlan'),
573             '[data-action="plan-unlink"]': this._eventHandler.bind(this, 'unlinkPlan'),
574             '[data-action="plan-request-review"]': this._eventHandler.bind(this, 'requestReview'),
575             '[data-action="plan-cancel-review-request"]': this._eventHandler.bind(this, 'cancelReviewRequest'),
576             '[data-action="plan-start-review"]': this._eventHandler.bind(this, 'startReview'),
577             '[data-action="plan-stop-review"]': this._eventHandler.bind(this, 'stopReview'),
578             '[data-action="plan-approve"]': this._eventHandler.bind(this, 'approve'),
579             '[data-action="plan-unapprove"]': this._eventHandler.bind(this, 'unapprove'),
580         });
581     };
583     /**
584      * Register the events in the region.
585      *
586      * At this stage this cannot be used with enhanceMenubar or multiple handlers
587      * will be added to the same node.
588      */
589     PlanActions.prototype.registerEvents = function() {
590         var wrapper = $(this._region);
592         wrapper.find('[data-action="plan-delete"]').click(this._eventHandler.bind(this, 'deletePlan'));
593         wrapper.find('[data-action="plan-complete"]').click(this._eventHandler.bind(this, 'completePlan'));
594         wrapper.find('[data-action="plan-reopen"]').click(this._eventHandler.bind(this, 'reopenPlan'));
595         wrapper.find('[data-action="plan-unlink"]').click(this._eventHandler.bind(this, 'unlinkPlan'));
597         wrapper.find('[data-action="plan-request-review"]').click(this._eventHandler.bind(this, 'requestReview'));
598         wrapper.find('[data-action="plan-cancel-review-request"]').click(this._eventHandler.bind(this, 'cancelReviewRequest'));
599         wrapper.find('[data-action="plan-start-review"]').click(this._eventHandler.bind(this, 'startReview'));
600         wrapper.find('[data-action="plan-stop-review"]').click(this._eventHandler.bind(this, 'stopReview'));
601         wrapper.find('[data-action="plan-approve"]').click(this._eventHandler.bind(this, 'approve'));
602         wrapper.find('[data-action="plan-unapprove"]').click(this._eventHandler.bind(this, 'unapprove'));
604         wrapper.find('[data-action="find-courses-link"]').click(this._showLinkedCoursesHandler.bind(this));
605     };
607     return PlanActions;
608 });