Ghost/ghost/admin/app/components/gh-task-button.js
Kevin Ansfield 502fcaba3c Added "Opt-in existing subscribers" option to newsletter creation
refs https://github.com/TryGhost/Team/issues/1528

- adds "opt-in existing" toggle to newsletter modal that's only shown when creating a newsletter
  - defaults to true
- updated newsletter save flow to show confirmation before creation
  - alters message to reflect auto-subscribe selection
  - count of existing subscribed members is not implemented as it's not yet supported by the API
- updated newsletter adapter and save flow to use auto opt-in selection
  - when option is checked the save URL is changed to `POST /members/?opt_in_existing=true`
- modified task button component to ignore a task return value of `canceled` so when it's received the buttons returns to the idle state instead of showing a saved or failed state
  - used by save routine when the "Back to edit" button is clicked in the create confirmation modal
2022-04-19 20:01:08 +01:00

210 lines
7.2 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.error', function () {
let taskName = this.get('task.name');
let lastTaskName = this.get('task.last.task.name');
if (!this.hasRun || this.isRunning || this.isSuccess) {
return false;
}
return (taskName === lastTaskName) && this.get('task.last.error') !== undefined && this.task.last?.error !== null;
}),
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;