mirror of
https://github.com/TryGhost/Ghost.git
synced 2024-12-01 22:02:11 +03:00
8e120a74d6
refs 502fcaba3c
- the conditional for `isFailure` was wrong meaning we were showing the idle state unintentionally
- fixed the conditional so it checks for the return value rather than a `null` error
- updated the template with test selectors so it's easier to determine state where class names are not sufficient to differentiate
211 lines
7.3 KiB
JavaScript
211 lines
7.3 KiB
JavaScript
import Component from '@ember/component';
|
|
import config from 'ghost-admin/config/environment';
|
|
import {action, computed} from '@ember/object';
|
|
import {isBlank} from '@ember/utils';
|
|
import {reads} from '@ember/object/computed';
|
|
import {task, timeout} from 'ember-concurrency';
|
|
|
|
/**
|
|
* 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
|
|
*/
|
|
const GhTaskButton = Component.extend({
|
|
tagName: 'button',
|
|
classNameBindings: [
|
|
'isRunning:appear-disabled',
|
|
'isIdleClass',
|
|
'isRunningClass',
|
|
'isSuccessClass',
|
|
'isFailureClass'
|
|
],
|
|
attributeBindings: ['disabled', 'form', 'type', 'tabindex', 'data-test-button'],
|
|
|
|
task: null,
|
|
taskArgs: undefined,
|
|
disabled: false,
|
|
defaultClick: false,
|
|
buttonText: 'Save',
|
|
idleClass: '',
|
|
runningClass: '',
|
|
showSuccess: true, // set to false if you want the spinner to show until a transition occurs
|
|
autoReset: true, // set to false if you want don't want task button to reset after timeout
|
|
successText: 'Saved',
|
|
successClass: 'gh-btn-green',
|
|
failureText: 'Retry',
|
|
failureClass: 'gh-btn-red',
|
|
unlinkedTask: false,
|
|
|
|
isTesting: undefined,
|
|
|
|
// Allowed actions
|
|
action: () => {},
|
|
|
|
runningText: reads('buttonText'),
|
|
|
|
// hasRun is needed so that a newly rendered button does not show the last
|
|
// state of the associated task
|
|
hasRun: computed('task.performCount', function () {
|
|
return this.get('task.performCount') > this._initialPerformCount;
|
|
}),
|
|
|
|
isIdleClass: computed('isIdle', function () {
|
|
return this.isIdle ? this.idleClass : '';
|
|
}),
|
|
|
|
isRunning: computed('task.last.isRunning', 'hasRun', 'showSuccess', function () {
|
|
let taskName = this.get('task.name');
|
|
let lastTaskName = this.get('task.last.task.name');
|
|
|
|
let isRunning = (taskName === lastTaskName) && this.get('task.last.isRunning');
|
|
if (this.hasRun && (taskName === lastTaskName) && this.get('task.last.value') && !this.showSuccess) {
|
|
isRunning = true;
|
|
}
|
|
|
|
return isRunning;
|
|
}),
|
|
|
|
isRunningClass: computed('isRunning', function () {
|
|
return this.isRunning ? (this.runningClass || this.idleClass) : '';
|
|
}),
|
|
|
|
isSuccess: computed('hasRun', 'isRunning', 'task.last.value', function () {
|
|
let taskName = this.get('task.name');
|
|
let lastTaskName = this.get('task.last.task.name');
|
|
|
|
if (!this.hasRun || this.isRunning || !this.showSuccess) {
|
|
return false;
|
|
}
|
|
|
|
let value = this.get('task.last.value');
|
|
return (taskName === lastTaskName) && !isBlank(value) && value !== false && value !== 'canceled';
|
|
}),
|
|
|
|
isSuccessClass: computed('isSuccess', function () {
|
|
return this.isSuccess ? this.successClass : '';
|
|
}),
|
|
|
|
isFailure: computed('hasRun', 'isRunning', 'isSuccess', 'task.last.{value,error}', function () {
|
|
let taskName = this.get('task.name');
|
|
let lastTaskName = this.get('task.last.task.name');
|
|
const lastTaskValue = this.task?.last?.value;
|
|
|
|
if (!this.hasRun || this.isRunning || this.isSuccess) {
|
|
return false;
|
|
}
|
|
|
|
return (taskName === lastTaskName) && this.get('task.last.error') !== undefined && lastTaskValue !== 'canceled';
|
|
}),
|
|
|
|
isFailureClass: computed('isFailure', function () {
|
|
return this.isFailure ? this.failureClass : '';
|
|
}),
|
|
|
|
isIdle: computed('isRunning', 'isSuccess', 'isFailure', function () {
|
|
return !this.isRunning && !this.isSuccess && !this.isFailure;
|
|
}),
|
|
|
|
init() {
|
|
this._super(...arguments);
|
|
this._initialPerformCount = this.get('task.performCount');
|
|
if (this.isTesting === undefined) {
|
|
this.isTesting = config.environment === 'test';
|
|
}
|
|
},
|
|
|
|
click() {
|
|
// 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) {
|
|
this._restartAnimation.perform();
|
|
}
|
|
return;
|
|
}
|
|
|
|
// do nothing if disabled externally
|
|
if (this.disabled) {
|
|
return false;
|
|
}
|
|
|
|
let taskName = this.get('task.name');
|
|
let lastTaskName = this.get('task.last.task.name');
|
|
|
|
// 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
|
|
if (this.isRunning && taskName === lastTaskName) {
|
|
return;
|
|
}
|
|
this.action();
|
|
this._handleMainTask.perform();
|
|
|
|
this._restartAnimation.perform();
|
|
|
|
// prevent the click from bubbling and triggering form actions
|
|
return false;
|
|
},
|
|
|
|
// 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();
|
|
}
|
|
},
|
|
|
|
handleReset: action(function () {
|
|
const isTaskSuccess = this.get('task.last.isSuccessful') && this.get('task.last.value');
|
|
if (this.autoReset && this.showSuccess && isTaskSuccess) {
|
|
this._resetButtonState.perform();
|
|
}
|
|
}),
|
|
|
|
// 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* () {
|
|
let elem = this.element.querySelector('.retry-animated');
|
|
if (elem) {
|
|
elem.classList.remove('retry-animated');
|
|
yield timeout(10);
|
|
elem.classList.add('retry-animated');
|
|
}
|
|
}),
|
|
|
|
_handleMainTask: task(function* () {
|
|
this._resetButtonState.cancelAll();
|
|
|
|
// if the task button will be removed by the result of the task then
|
|
// it needs to be marked as unlinked to ensure it runs to completion
|
|
// and ember-concurrency doesn't output self-cancel warnings
|
|
if (this.unlinkedTask) {
|
|
yield this.task.unlinked().perform(this.taskArgs);
|
|
} else {
|
|
yield this.task.perform(this.taskArgs);
|
|
}
|
|
|
|
const isTaskSuccess = this.get('task.last.isSuccessful') && this.get('task.last.value');
|
|
if (this.autoReset && this.showSuccess && isTaskSuccess) {
|
|
this._resetButtonState.perform();
|
|
}
|
|
}),
|
|
|
|
_resetButtonState: task(function* () {
|
|
yield timeout(this.isTesting ? 50 : 2500);
|
|
if (!this.get('task.last.isRunning')) {
|
|
// Reset last task to bring button back to idle state
|
|
yield this.set('task.last', null);
|
|
}
|
|
}).restartable()
|
|
});
|
|
|
|
export default GhTaskButton;
|