diff --git a/ui/core/app/routes/scopes/scope/projects.js b/ui/core/app/routes/scopes/scope/projects.js index efd9d71f8..77f57f12d 100644 --- a/ui/core/app/routes/scopes/scope/projects.js +++ b/ui/core/app/routes/scopes/scope/projects.js @@ -2,6 +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 '../../../utilities/confirm'; export default class ScopesScopeProjectsRoute extends Route { // =services @@ -9,6 +10,7 @@ export default class ScopesScopeProjectsRoute extends Route { @service intl; @service notify; @service session; + @service confirm; // =methods @@ -84,12 +86,13 @@ export default class ScopesScopeProjectsRoute extends Route { */ @action @loading + @confirm('questions.delete-confirm') async delete(project) { try { await project.destroyRecord(); + await this.replaceWith('scopes.scope.projects'); this.refresh(); this.notify.success(this.intl.t('notifications.delete-success')); - this.transitionTo('scopes.scope.projects'); } catch (error) { // TODO: replace with translated strings this.notify.error(error.message, { closeAfter: null }); diff --git a/ui/core/app/services/confirm.js b/ui/core/app/services/confirm.js new file mode 100644 index 000000000..d3c0e4fe8 --- /dev/null +++ b/ui/core/app/services/confirm.js @@ -0,0 +1,53 @@ +import Service from '@ember/service'; +import { getOwner } from '@ember/application'; +import { bind } from '@ember/runloop'; +import { Promise, resolve } from 'rsvp'; +import config from '../config/environment'; + +export default class ConfirmService extends Service { + + // =attributes + + /** + * When the confirm service is disabled, it always returns resolving promises. + * @type {boolean} + */ + enabled = config.enableConfirmService; + + /** + * The window's confirm function, bound to the window scope. + * @type {Function} + */ + get confirmFn() { + // The Ember way of accessing globals... + const document = + getOwner(this).lookup('service:-document').documentElement; + // defaultView === window, but without using globals directly + const windowRef = document.parentNode.defaultView; + const { confirm } = windowRef; + return bind(windowRef, confirm); + } + + // =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 + * @return {Promise} + */ + confirm(text) { + if (this.enabled) { + return new Promise((resolve, reject) => { + if (this.confirmFn(text)) { + return resolve(); + } else { + return reject(); + } + }); + } else { + return resolve(); + } + } +} diff --git a/ui/core/app/utilities/confirm.js b/ui/core/app/utilities/confirm.js new file mode 100644 index 000000000..755ff2f59 --- /dev/null +++ b/ui/core/app/utilities/confirm.js @@ -0,0 +1,23 @@ +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(() => {}); + }; + } +} diff --git a/ui/core/config/environment.js b/ui/core/config/environment.js index f2c6362f9..a6f794d02 100644 --- a/ui/core/config/environment.js +++ b/ui/core/config/environment.js @@ -34,6 +34,7 @@ module.exports = function (environment) { appName: APP_NAME, notifyTimeout: 4000, + enableConfirmService: true, contentSecurityPolicyHeader: 'Content-Security-Policy', contentSecurityPolicyMeta: true, @@ -98,6 +99,7 @@ module.exports = function (environment) { // Notification timeout should be 0 for fast tests ENV.notifyTimeout = 0; + ENV.enableConfirmService = false; } if (environment === 'production') { diff --git a/ui/core/tests/unit/services/confirm-test.js b/ui/core/tests/unit/services/confirm-test.js new file mode 100644 index 000000000..c573fc2ed --- /dev/null +++ b/ui/core/tests/unit/services/confirm-test.js @@ -0,0 +1,12 @@ +import { module, test } from 'qunit'; +import { setupTest } from 'ember-qunit'; + +module('Unit | Service | confirm', function(hooks) { + setupTest(hooks); + + // Replace this with your real tests. + test('it exists', function(assert) { + let service = this.owner.lookup('service:confirm'); + assert.ok(service); + }); +}); diff --git a/ui/core/translations/en-us.yaml b/ui/core/translations/en-us.yaml index a86a36263..413f0bae4 100644 --- a/ui/core/translations/en-us.yaml +++ b/ui/core/translations/en-us.yaml @@ -17,4 +17,7 @@ titles: global: Global actions: Actions descriptions: - empty-set: There are no items to display yet. You may be able to add items or try back later. \ No newline at end of file + empty-set: There are no items to display yet. You may be able to add items or try back later. +questions: + delete-confirm: Are you sure you want to delete this resource? + remove-confirm: Are you sure you want to remove this association?