refactor: 💡 port confirmation service and components into core
This commit is contained in:
26 files changed
+59
-17
No files matched your search
@@ -1,6 +0,0 @@
|
||||
{{#each this.confirm.pending as |confirmation|}}
|
||||
{{yield
|
||||
confirmation
|
||||
(fn this.accept confirmation)
|
||||
(fn this.deny confirmation)}}
|
||||
{{/each}}
|
||||
@@ -1,38 +0,0 @@
|
||||
import Component from '@glimmer/component';
|
||||
import { inject as service } from '@ember/service';
|
||||
import { action } from '@ember/object';
|
||||
|
||||
/**
|
||||
* A component that yields for each pending confirmation in the confirmations
|
||||
* service, passing the confirmation instance, a confirm action, and a dismiss
|
||||
* action. Useful for rendering confirm modals and awaiting user action.
|
||||
*/
|
||||
export default class PendingConfirmationsComponent extends Component {
|
||||
// =services
|
||||
|
||||
@service confirm;
|
||||
|
||||
// =actions
|
||||
|
||||
/**
|
||||
* Accepts the passed confirmation.
|
||||
* @param {Confirmation} confirmation
|
||||
* @return {Confirmation}
|
||||
*/
|
||||
@action
|
||||
accept(confirmation) {
|
||||
confirmation.confirm();
|
||||
return confirmation;
|
||||
}
|
||||
|
||||
/**
|
||||
* Denies the passed confirmation.
|
||||
* @param {Confirmation} confirmation
|
||||
* @return {Confirmation}
|
||||
*/
|
||||
@action
|
||||
deny(confirmation) {
|
||||
confirmation.dismiss();
|
||||
return confirmation;
|
||||
}
|
||||
}
|
||||
@@ -1,26 +0,0 @@
|
||||
import { getOwner } from '@ember/application';
|
||||
|
||||
/**
|
||||
* Decorates a method whereby calls to the method will first request
|
||||
* confirmation via the confirmation service. If the user accepts, the
|
||||
* original method is executed. If the user denies, it is not.
|
||||
*/
|
||||
export function confirm(confirmationTextKey) {
|
||||
return function (_target, _propertyKey, desc) {
|
||||
const method = desc.value;
|
||||
desc.value = function () {
|
||||
const owner = getOwner(this);
|
||||
const confirmService = owner.lookup('service:confirm');
|
||||
const intl = owner.lookup('service:intl');
|
||||
const text = intl.t(confirmationTextKey);
|
||||
return (
|
||||
confirmService
|
||||
.confirm(text)
|
||||
// If user confirmed, execute the decorated method
|
||||
.then(() => method.apply(this, arguments))
|
||||
// If the user denied, gracefully handle promise rejection with no-op
|
||||
.catch((e) => e)
|
||||
);
|
||||
};
|
||||
};
|
||||
}
|
||||
@@ -3,7 +3,7 @@ import { inject as service } from '@ember/service';
|
||||
import { A } from '@ember/array';
|
||||
import { action } from '@ember/object';
|
||||
import loading from 'ember-loading/decorator';
|
||||
import { confirm } from '../../decorators/confirm';
|
||||
import { confirm } from 'core/decorators/confirm';
|
||||
import { notifySuccess, notifyError } from '../../decorators/notify';
|
||||
|
||||
export default class ScopesScopeRoute extends Route {
|
||||
|
||||
@@ -2,7 +2,7 @@ import Route from '@ember/routing/route';
|
||||
import { inject as service } from '@ember/service';
|
||||
import { action } from '@ember/object';
|
||||
import loading from 'ember-loading/decorator';
|
||||
import { confirm } from '../../../decorators/confirm';
|
||||
import { confirm } from 'core/decorators/confirm';
|
||||
import { notifySuccess, notifyError } from '../../../decorators/notify';
|
||||
|
||||
export default class ScopesScopeAuthMethodsRoute extends Route {
|
||||
|
||||
@@ -2,7 +2,7 @@ import Route from '@ember/routing/route';
|
||||
import { action } from '@ember/object';
|
||||
import { inject as service } from '@ember/service';
|
||||
import loading from 'ember-loading/decorator';
|
||||
import { confirm } from '../../../../../decorators/confirm';
|
||||
import { confirm } from 'core/decorators/confirm';
|
||||
import { notifySuccess, notifyError } from '../../../../../decorators/notify';
|
||||
|
||||
export default class ScopesScopeAuthMethodsAuthMethodAccountsRoute extends Route {
|
||||
|
||||
@@ -2,7 +2,7 @@ import Route from '@ember/routing/route';
|
||||
import { inject as service } from '@ember/service';
|
||||
import { action } from '@ember/object';
|
||||
import loading from 'ember-loading/decorator';
|
||||
import { confirm } from '../../../decorators/confirm';
|
||||
import { confirm } from 'core/decorators/confirm';
|
||||
import { notifySuccess, notifyError } from '../../../decorators/notify';
|
||||
|
||||
export default class ScopesScopeGroupsRoute extends Route {
|
||||
|
||||
@@ -3,7 +3,7 @@ import { action } from '@ember/object';
|
||||
import { inject as service } from '@ember/service';
|
||||
import { hash, all } from 'rsvp';
|
||||
import loading from 'ember-loading/decorator';
|
||||
import { confirm } from '../../../../../decorators/confirm';
|
||||
import { confirm } from 'core/decorators/confirm';
|
||||
import { notifySuccess, notifyError } from '../../../../../decorators/notify';
|
||||
|
||||
export default class ScopesScopeGroupsGroupMembersRoute extends Route {
|
||||
|
||||
@@ -2,7 +2,7 @@ import Route from '@ember/routing/route';
|
||||
import { inject as service } from '@ember/service';
|
||||
import { action } from '@ember/object';
|
||||
import loading from 'ember-loading/decorator';
|
||||
import { confirm } from '../../../decorators/confirm';
|
||||
import { confirm } from 'core/decorators/confirm';
|
||||
import { notifySuccess, notifyError } from '../../../decorators/notify';
|
||||
|
||||
export default class ScopesScopeHostCatalogsRoute extends Route {
|
||||
|
||||
@@ -2,7 +2,7 @@ import Route from '@ember/routing/route';
|
||||
import { inject as service } from '@ember/service';
|
||||
import { action } from '@ember/object';
|
||||
import loading from 'ember-loading/decorator';
|
||||
import { confirm } from '../../../../../decorators/confirm';
|
||||
import { confirm } from 'core/decorators/confirm';
|
||||
import { notifySuccess, notifyError } from '../../../../../decorators/notify';
|
||||
|
||||
export default class ScopesScopeHostCatalogsHostCatalogHostSetsRoute extends Route {
|
||||
|
||||
+1
-1
@@ -3,7 +3,7 @@ import { hash, all } from 'rsvp';
|
||||
import { action } from '@ember/object';
|
||||
import { inject as service } from '@ember/service';
|
||||
import loading from 'ember-loading/decorator';
|
||||
import { confirm } from '../../../../../../../decorators/confirm';
|
||||
import { confirm } from 'core/decorators/confirm';
|
||||
import {
|
||||
notifySuccess,
|
||||
notifyError,
|
||||
|
||||
@@ -2,7 +2,7 @@ import Route from '@ember/routing/route';
|
||||
import { inject as service } from '@ember/service';
|
||||
import { action } from '@ember/object';
|
||||
import loading from 'ember-loading/decorator';
|
||||
import { confirm } from '../../../../../decorators/confirm';
|
||||
import { confirm } from 'core/decorators/confirm';
|
||||
import { notifySuccess, notifyError } from '../../../../../decorators/notify';
|
||||
|
||||
export default class ScopesScopeHostCatalogsHostCatalogHostsRoute extends Route {
|
||||
|
||||
@@ -2,7 +2,7 @@ import Route from '@ember/routing/route';
|
||||
import { inject as service } from '@ember/service';
|
||||
import { action } from '@ember/object';
|
||||
import loading from 'ember-loading/decorator';
|
||||
import { confirm } from '../../../decorators/confirm';
|
||||
import { confirm } from 'core/decorators/confirm';
|
||||
import { notifySuccess, notifyError } from '../../../decorators/notify';
|
||||
|
||||
export default class ScopesScopeRolesRoute extends Route {
|
||||
|
||||
@@ -3,7 +3,7 @@ import { all, hash } from 'rsvp';
|
||||
import { action } from '@ember/object';
|
||||
import { inject as service } from '@ember/service';
|
||||
import loading from 'ember-loading/decorator';
|
||||
import { confirm } from '../../../../../decorators/confirm';
|
||||
import { confirm } from 'core/decorators/confirm';
|
||||
import { notifySuccess, notifyError } from '../../../../../decorators/notify';
|
||||
|
||||
export default class ScopesScopeRolesRolePrincipalsRoute extends Route {
|
||||
|
||||
@@ -2,7 +2,7 @@ import Route from '@ember/routing/route';
|
||||
import { inject as service } from '@ember/service';
|
||||
import { action } from '@ember/object';
|
||||
import loading from 'ember-loading/decorator';
|
||||
import { confirm } from '../../../decorators/confirm';
|
||||
import { confirm } from 'core/decorators/confirm';
|
||||
import { notifySuccess, notifyError } from '../../../decorators/notify';
|
||||
|
||||
export default class ScopesScopeTargetsRoute extends Route {
|
||||
|
||||
@@ -3,7 +3,7 @@ import { all, hash } from 'rsvp';
|
||||
import { action } from '@ember/object';
|
||||
import { inject as service } from '@ember/service';
|
||||
import loading from 'ember-loading/decorator';
|
||||
import { confirm } from '../../../../../decorators/confirm';
|
||||
import { confirm } from 'core/decorators/confirm';
|
||||
import { notifySuccess, notifyError } from '../../../../../decorators/notify';
|
||||
|
||||
export default class ScopesScopeTargetsTargetHostSetsRoute extends Route {
|
||||
|
||||
@@ -2,7 +2,7 @@ import Route from '@ember/routing/route';
|
||||
import { inject as service } from '@ember/service';
|
||||
import { action } from '@ember/object';
|
||||
import loading from 'ember-loading/decorator';
|
||||
import { confirm } from '../../../decorators/confirm';
|
||||
import { confirm } from 'core/decorators/confirm';
|
||||
import { notifySuccess, notifyError } from '../../../decorators/notify';
|
||||
|
||||
export default class ScopesScopeUsersRoute extends Route {
|
||||
|
||||
@@ -1,146 +0,0 @@
|
||||
import Service from '@ember/service';
|
||||
import { A } from '@ember/array';
|
||||
import { tracked } from '@glimmer/tracking';
|
||||
import { defer, resolve } from 'rsvp';
|
||||
import { filterBy } from '@ember/object/computed';
|
||||
import config from '../config/environment';
|
||||
|
||||
/**
|
||||
* A simple service that emits Confirmation instances and
|
||||
* exposes a `pending` property to discover pending
|
||||
* confirmations.
|
||||
* @augments Service
|
||||
*
|
||||
* @example
|
||||
* const confirmation = confirmationService.confirm('Text', { type: 'discard' });
|
||||
*/
|
||||
export default class ConfirmationsService extends Service {
|
||||
// =attributes
|
||||
|
||||
/**
|
||||
* When the confirm service is disabled, it always returns resolving promises.
|
||||
* @type {boolean}
|
||||
*/
|
||||
enabled = config.enableConfirmService;
|
||||
|
||||
/**
|
||||
* @type {Array}
|
||||
*/
|
||||
confirmations = A();
|
||||
|
||||
/**
|
||||
* An array of pending confirmations (those where `done === false`).
|
||||
* @type {Array}
|
||||
*/
|
||||
@filterBy('confirmations', 'done', false) pending;
|
||||
|
||||
// =methods
|
||||
|
||||
/**
|
||||
* When the service is enabled, returns a promise that resolves if the user
|
||||
* accepts the confirmation, otherwise rejects. If the service is disabled,
|
||||
* this method always returns a resolving promise.
|
||||
* @param {string} text
|
||||
* @param {object} options
|
||||
* @return {Promise}
|
||||
*/
|
||||
confirm(text, options) {
|
||||
if (this.enabled) {
|
||||
const confirmation = new Confirmation(text, options);
|
||||
this.confirmations.addObject(confirmation);
|
||||
return confirmation;
|
||||
} else {
|
||||
return resolve();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A promise-like object that is either "confirmed" or "dismissed", represented
|
||||
* internally by a promise that resolves or rejects. Confirmations may be used
|
||||
* as promises via `then`, `catch`, and `finally`.
|
||||
*
|
||||
* Not intended for instantiation directly. Use the confirmation service
|
||||
* to create confirmation instances.
|
||||
*
|
||||
* @example
|
||||
* const confirmation = new Confirmation('Text', { type: 'discard' });
|
||||
* confirmation.confirm();
|
||||
* // confirmation.done now equals true
|
||||
* confirmation
|
||||
* .then(() => { }) // do something
|
||||
* .catch(() => { }) // not called, since the confirmation is confirmed
|
||||
* .finally(() => { }); // do something
|
||||
*
|
||||
* @example
|
||||
* const confirmation = new Confirmation('Text', { type: 'discard' });
|
||||
* confirmation.dismiss();
|
||||
* // confirmation.done now equals true
|
||||
* confirmation
|
||||
* .then(() => { }) // not called, since the confirmation is dismissed
|
||||
* .catch(() => { }) // do something
|
||||
* .finally(() => { }); // do something
|
||||
*/
|
||||
class Confirmation {
|
||||
// =properties
|
||||
|
||||
/**
|
||||
* Indicates whether the confirmation has been confirmed or dismissed.
|
||||
* Automatically set to `true` after `confirm` or `dismiss` are called.
|
||||
* @type {Boolean}
|
||||
*/
|
||||
@tracked done = false;
|
||||
|
||||
#deferred = null;
|
||||
|
||||
/**
|
||||
* Sets the type field to the specified type and sets up a defer.
|
||||
* @param {String} type An optional type annotation for convenience.
|
||||
* Has no effect on confirmation.
|
||||
*/
|
||||
constructor(text, options = {}) {
|
||||
this.text = text;
|
||||
this.options = options;
|
||||
this.#deferred = defer();
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the confirmation to done and resolves the defer.
|
||||
*/
|
||||
confirm() {
|
||||
this.done = true;
|
||||
this.#deferred.resolve();
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the confirmation to done and rejects the defer.
|
||||
*/
|
||||
dismiss() {
|
||||
this.done = true;
|
||||
this.#deferred.reject();
|
||||
}
|
||||
|
||||
/**
|
||||
* Delegates to the internal defer promise, allowing the
|
||||
* confirmation to be treated like a promise.
|
||||
*/
|
||||
then() {
|
||||
return this.#deferred.promise.then(...arguments);
|
||||
}
|
||||
|
||||
/**
|
||||
* Delegates to the internal defer promise, allowing the
|
||||
* confirmation to be treated like a promise.
|
||||
*/
|
||||
catch() {
|
||||
return this.#deferred.promise.catch(...arguments);
|
||||
}
|
||||
|
||||
/**
|
||||
* Delegates to the internal defer promise, allowing the
|
||||
* confirmation to be treated like a promise.
|
||||
*/
|
||||
finally() {
|
||||
return this.#deferred.promise.finally(...arguments);
|
||||
}
|
||||
}
|
||||
@@ -1,59 +0,0 @@
|
||||
import { module, test } from 'qunit';
|
||||
import { setupRenderingTest } from 'ember-qunit';
|
||||
import { render, findAll, click } from '@ember/test-helpers';
|
||||
import { hbs } from 'ember-cli-htmlbars';
|
||||
|
||||
module('Integration | Component | pending-confirmations', function (hooks) {
|
||||
setupRenderingTest(hooks);
|
||||
|
||||
test('it renders pending confirmations', async function (assert) {
|
||||
assert.expect(2);
|
||||
const service = this.owner.lookup('service:confirm');
|
||||
service.enabled = true;
|
||||
await render(hbs`
|
||||
<PendingConfirmations as |confirmations|>
|
||||
<div class="pending">Pending</div>
|
||||
</PendingConfirmations>
|
||||
`);
|
||||
assert.equal(findAll('.pending').length, 0, 'No pending confirmations');
|
||||
service.confirm();
|
||||
await render(hbs`
|
||||
<PendingConfirmations as |confirmations|>
|
||||
<div class="pending">Pending</div>
|
||||
</PendingConfirmations>
|
||||
`);
|
||||
assert.equal(findAll('.pending').length, 1, 'One pending confirmations');
|
||||
});
|
||||
|
||||
test('it exposes a confirm action that confirms the confirmation', async function (assert) {
|
||||
assert.expect(3);
|
||||
const service = this.owner.lookup('service:confirm');
|
||||
service.enabled = true;
|
||||
const confirmation = service.confirm();
|
||||
confirmation.then(() => assert.ok(true, 'confirmation.then was called'));
|
||||
await render(hbs`
|
||||
<PendingConfirmations as |confirmations confirm|>
|
||||
<button {{on "click" confirm}}>Confirm</button>
|
||||
</PendingConfirmations>
|
||||
`);
|
||||
assert.notOk(confirmation.done, 'Confirmation is not done');
|
||||
await click('button');
|
||||
assert.ok(confirmation.done, 'Confirmation is done');
|
||||
});
|
||||
|
||||
test('it exposes a dismiss action that dismisses the confirmation', async function (assert) {
|
||||
assert.expect(3);
|
||||
const service = this.owner.lookup('service:confirm');
|
||||
service.enabled = true;
|
||||
const confirmation = service.confirm();
|
||||
confirmation.catch(() => assert.ok(true, 'confirmation.catch was called'));
|
||||
await render(hbs`
|
||||
<PendingConfirmations as |confirmations confirm dismiss|>
|
||||
<button {{on "click" dismiss}}>Dismiss</button>
|
||||
</PendingConfirmations>
|
||||
`);
|
||||
assert.notOk(confirmation.done, 'Confirmation is not done');
|
||||
await click('button');
|
||||
assert.ok(confirmation.done, 'Confirmation is done');
|
||||
});
|
||||
});
|
||||
@@ -1,71 +0,0 @@
|
||||
import { module, test } from 'qunit';
|
||||
import { setupTest } from 'ember-qunit';
|
||||
|
||||
module('Unit | Service | confirm', function (hooks) {
|
||||
setupTest(hooks);
|
||||
|
||||
test('`confirm()` emits promise-like instances', function (assert) {
|
||||
assert.expect(3);
|
||||
const service = this.owner.lookup('service:confirm');
|
||||
service.enabled = true;
|
||||
const confirmation = service.confirm();
|
||||
assert.ok(confirmation.then, 'confirmation has a then method');
|
||||
assert.ok(confirmation.catch, 'confirmation has a catch method');
|
||||
assert.ok(confirmation.finally, 'confirmation has a finally method');
|
||||
});
|
||||
|
||||
test('`pending` is an array of pending confirmations', function (assert) {
|
||||
assert.expect(3);
|
||||
const service = this.owner.lookup('service:confirm');
|
||||
service.enabled = true;
|
||||
assert.equal(service.pending.length, 0, 'No pending confirmations yet');
|
||||
const confirmation = service.confirm();
|
||||
assert.equal(service.pending.length, 1, 'One pending confirmation created');
|
||||
confirmation.confirm();
|
||||
assert.equal(service.pending.length, 0, 'Confirmation was confirmed');
|
||||
});
|
||||
|
||||
test('confirmations may be confirmed or dismissed', function (assert) {
|
||||
assert.expect(4);
|
||||
const service = this.owner.lookup('service:confirm');
|
||||
service.enabled = true;
|
||||
let confirmation = service.confirm();
|
||||
assert.notOk(confirmation.done, 'Confirmation is pending');
|
||||
confirmation.confirm();
|
||||
assert.ok(confirmation.done, 'Confirmation is done via confirm');
|
||||
confirmation = service.confirm();
|
||||
assert.notOk(confirmation.done, 'Confirmation is pending');
|
||||
confirmation.dismiss();
|
||||
assert.ok(confirmation.done, 'Confirmation is done via dismiss');
|
||||
});
|
||||
|
||||
test('confirmations resolve on confirm', function (assert) {
|
||||
assert.expect(1);
|
||||
const service = this.owner.lookup('service:confirm');
|
||||
service.enabled = true;
|
||||
const confirmation = service.confirm();
|
||||
confirmation.then(() => assert.ok(true, 'Confirmation confirmed'));
|
||||
confirmation.confirm();
|
||||
});
|
||||
|
||||
test('confirmations reject on dismiss', function (assert) {
|
||||
assert.expect(1);
|
||||
const service = this.owner.lookup('service:confirm');
|
||||
service.enabled = true;
|
||||
const confirmation = service.confirm();
|
||||
confirmation.catch(() => assert.ok(true, 'Confirmation dismissed'));
|
||||
confirmation.dismiss();
|
||||
});
|
||||
|
||||
test('confirmations have a finally method', function (assert) {
|
||||
assert.expect(2);
|
||||
const service = this.owner.lookup('service:confirm');
|
||||
service.enabled = true;
|
||||
let confirmation = service.confirm();
|
||||
confirmation.finally(() => assert.ok(true, 'Confirmation finally called'));
|
||||
confirmation.confirm();
|
||||
confirmation = service.confirm();
|
||||
confirmation.finally(() => assert.ok(true, 'Confirmation finally called'));
|
||||
confirmation.dismiss();
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user