2017-08-22 10:53:26 +03:00
|
|
|
import Component from '@ember/component';
|
|
|
|
import {computed} from '@ember/object';
|
|
|
|
import {isBlank} from '@ember/utils';
|
|
|
|
import {reads} from '@ember/object/computed';
|
2017-04-14 18:16:52 +03:00
|
|
|
import {task, timeout} from 'ember-concurrency';
|
2016-08-11 09:58:38 +03:00
|
|
|
|
|
|
|
/**
|
|
|
|
* Task Button works exactly like Spin button, but with one major difference:
|
|
|
|
*
|
|
|
|
* Instead of passing a "submitting" parameter (which is bound to the parent object),
|
|
|
|
* you pass an ember-concurrency task. All of the "submitting" behavior is handled automatically.
|
|
|
|
*
|
|
|
|
* As another bonus, there's no need to handle canceling the promises when something
|
|
|
|
* like a controller changes. Because the only task running is handled through this
|
|
|
|
* component, all running promises will automatically be cancelled when this
|
|
|
|
* component is removed from the DOM
|
|
|
|
*/
|
2017-03-07 20:28:52 +03:00
|
|
|
const GhTaskButton = Component.extend({
|
2017-01-19 14:20:33 +03:00
|
|
|
tagName: 'button',
|
2017-04-19 19:21:47 +03:00
|
|
|
classNameBindings: [
|
|
|
|
'isRunning:appear-disabled',
|
|
|
|
'isIdleClass',
|
|
|
|
'isRunningClass',
|
|
|
|
'isSuccessClass',
|
|
|
|
'isFailureClass'
|
|
|
|
],
|
2018-09-17 18:03:58 +03:00
|
|
|
attributeBindings: ['disabled', 'form', 'type', 'tabindex'],
|
2016-08-24 16:14:36 +03:00
|
|
|
|
2016-08-11 09:58:38 +03:00
|
|
|
task: null,
|
2020-01-30 18:35:36 +03:00
|
|
|
taskArgs: undefined,
|
2016-08-24 16:14:36 +03:00
|
|
|
disabled: false,
|
2019-01-31 13:27:40 +03:00
|
|
|
defaultClick: false,
|
2017-03-07 20:28:52 +03:00
|
|
|
buttonText: 'Save',
|
2017-04-19 19:21:47 +03:00
|
|
|
idleClass: '',
|
|
|
|
runningClass: '',
|
2019-08-12 11:11:10 +03:00
|
|
|
showSuccess: true, // set to false if you want the spinner to show until a transition occurs
|
2017-03-07 20:28:52 +03:00
|
|
|
successText: 'Saved',
|
2017-03-10 00:48:54 +03:00
|
|
|
successClass: 'gh-btn-green',
|
2017-03-07 20:28:52 +03:00
|
|
|
failureText: 'Retry',
|
2017-03-10 00:48:54 +03:00
|
|
|
failureClass: 'gh-btn-red',
|
2016-08-11 09:58:38 +03:00
|
|
|
|
2018-03-20 17:57:59 +03:00
|
|
|
// Allowed actions
|
|
|
|
action: () => {},
|
|
|
|
|
2018-01-11 20:43:23 +03:00
|
|
|
runningText: reads('buttonText'),
|
2018-01-11 17:16:42 +03:00
|
|
|
|
2017-04-11 16:39:45 +03:00
|
|
|
// hasRun is needed so that a newly rendered button does not show the last
|
|
|
|
// state of the associated task
|
2018-01-11 17:16:42 +03:00
|
|
|
hasRun: computed('task.performCount', function () {
|
|
|
|
return this.get('task.performCount') > this._initialPerformCount;
|
|
|
|
}),
|
2017-01-19 14:20:33 +03:00
|
|
|
|
2017-04-19 19:21:47 +03:00
|
|
|
isIdleClass: computed('isIdle', function () {
|
2019-06-24 18:33:21 +03:00
|
|
|
return this.isIdle ? this.idleClass : '';
|
2017-04-19 19:21:47 +03:00
|
|
|
}),
|
|
|
|
|
2019-08-12 11:11:10 +03:00
|
|
|
isRunning: computed('task.last.isRunning', 'hasRun', 'showSuccess', function () {
|
|
|
|
let isRunning = this.get('task.last.isRunning');
|
|
|
|
|
|
|
|
if (this.hasRun && this.get('task.last.value') && !this.showSuccess) {
|
|
|
|
isRunning = true;
|
|
|
|
}
|
|
|
|
|
|
|
|
return isRunning;
|
|
|
|
}),
|
|
|
|
|
2017-04-19 19:21:47 +03:00
|
|
|
isRunningClass: computed('isRunning', function () {
|
2019-06-24 18:33:21 +03:00
|
|
|
return this.isRunning ? (this.runningClass || this.idleClass) : '';
|
2017-04-19 19:21:47 +03:00
|
|
|
}),
|
|
|
|
|
2017-04-11 16:39:45 +03:00
|
|
|
isSuccess: computed('hasRun', 'isRunning', 'task.last.value', function () {
|
2019-08-12 11:11:10 +03:00
|
|
|
if (!this.hasRun || this.isRunning || !this.showSuccess) {
|
2017-03-07 20:28:52 +03:00
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
let value = this.get('task.last.value');
|
|
|
|
return !isBlank(value) && value !== false;
|
|
|
|
}),
|
|
|
|
|
2017-03-10 00:48:54 +03:00
|
|
|
isSuccessClass: computed('isSuccess', function () {
|
2019-06-24 18:33:21 +03:00
|
|
|
return this.isSuccess ? this.successClass : '';
|
2017-03-10 00:48:54 +03:00
|
|
|
}),
|
|
|
|
|
2017-04-11 16:39:45 +03:00
|
|
|
isFailure: computed('hasRun', 'isRunning', 'isSuccess', 'task.last.error', function () {
|
2019-03-06 16:53:54 +03:00
|
|
|
if (!this.hasRun || this.isRunning || this.isSuccess) {
|
2017-03-07 20:28:52 +03:00
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
return this.get('task.last.error') !== undefined;
|
|
|
|
}),
|
|
|
|
|
2017-03-10 00:48:54 +03:00
|
|
|
isFailureClass: computed('isFailure', function () {
|
2019-06-24 18:33:21 +03:00
|
|
|
return this.isFailure ? this.failureClass : '';
|
2017-03-10 00:48:54 +03:00
|
|
|
}),
|
|
|
|
|
2017-03-07 20:28:52 +03:00
|
|
|
isIdle: computed('isRunning', 'isSuccess', 'isFailure', function () {
|
2019-03-06 16:53:54 +03:00
|
|
|
return !this.isRunning && !this.isSuccess && !this.isFailure;
|
2017-03-07 20:28:52 +03:00
|
|
|
}),
|
|
|
|
|
2018-01-11 20:43:23 +03:00
|
|
|
init() {
|
|
|
|
this._super(...arguments);
|
|
|
|
this._initialPerformCount = this.get('task.performCount');
|
|
|
|
},
|
|
|
|
|
2016-08-11 09:58:38 +03:00
|
|
|
click() {
|
2019-01-31 13:27:40 +03:00
|
|
|
// let the default click bubble if defaultClick===true - useful when
|
|
|
|
// you want to handle a form submit action rather than triggering a
|
|
|
|
// task directly
|
|
|
|
if (this.defaultClick) {
|
|
|
|
if (!this.isRunning) {
|
2019-03-06 16:53:54 +03:00
|
|
|
this._restartAnimation.perform();
|
2019-01-31 13:27:40 +03:00
|
|
|
}
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2017-01-19 14:20:33 +03:00
|
|
|
// do nothing if disabled externally
|
2019-03-06 16:53:54 +03:00
|
|
|
if (this.disabled) {
|
2017-01-19 14:20:33 +03:00
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
2019-03-06 16:53:54 +03:00
|
|
|
let task = this.task;
|
2016-08-24 16:14:36 +03:00
|
|
|
let taskName = this.get('task.name');
|
|
|
|
let lastTaskName = this.get('task.last.task.name');
|
|
|
|
|
2017-01-19 14:20:33 +03:00
|
|
|
// task-buttons are never disabled whilst running so that clicks when a
|
|
|
|
// taskGroup is running don't get dropped BUT that means we need to check
|
|
|
|
// here to avoid spamming actions from multiple clicks
|
2019-03-06 16:53:54 +03:00
|
|
|
if (this.isRunning && taskName === lastTaskName) {
|
2016-08-24 16:14:36 +03:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2018-03-20 17:57:59 +03:00
|
|
|
this.action();
|
2019-11-07 11:37:26 +03:00
|
|
|
task.perform(this.taskArgs);
|
2016-08-11 09:58:38 +03:00
|
|
|
|
2019-03-06 16:53:54 +03:00
|
|
|
this._restartAnimation.perform();
|
2017-04-14 18:16:52 +03:00
|
|
|
|
2017-03-10 00:48:54 +03:00
|
|
|
// prevent the click from bubbling and triggering form actions
|
|
|
|
return false;
|
2017-01-19 14:20:33 +03:00
|
|
|
},
|
|
|
|
|
2019-03-18 20:56:32 +03:00
|
|
|
// mouseDown can be prevented, this is useful for situations where we want
|
|
|
|
// to avoid on-blur events triggering before the button click
|
|
|
|
mouseDown(event) {
|
|
|
|
if (this.disableMouseDown) {
|
|
|
|
event.preventDefault();
|
|
|
|
}
|
|
|
|
},
|
|
|
|
|
2017-04-14 18:16:52 +03:00
|
|
|
// when local validation fails there's no transition from failed->running
|
|
|
|
// so we want to restart the retry spinner animation to show something
|
|
|
|
// has happened when the button is clicked
|
|
|
|
_restartAnimation: task(function* () {
|
2020-01-10 18:12:39 +03:00
|
|
|
let elem = this.element.querySelector('.retry-animated');
|
|
|
|
if (elem) {
|
2017-04-14 18:16:52 +03:00
|
|
|
elem.classList.remove('retry-animated');
|
|
|
|
yield timeout(10);
|
|
|
|
elem.classList.add('retry-animated');
|
|
|
|
}
|
2017-01-19 14:20:33 +03:00
|
|
|
})
|
2016-08-11 09:58:38 +03:00
|
|
|
});
|
2017-03-07 20:28:52 +03:00
|
|
|
|
|
|
|
export default GhTaskButton;
|