diff --git a/addons/rose/addon/components/rose/form/index.js b/addons/rose/addon/components/rose/form/index.js index 249cd9fbd..2a9f102e3 100644 --- a/addons/rose/addon/components/rose/form/index.js +++ b/addons/rose/addon/components/rose/form/index.js @@ -40,13 +40,27 @@ export default class RoseFormComponent extends Component { /** * Calls the passed `onSubmit` function while disabling the default form * submit behavior. + * + * Submit handlers _may optionally return a promise_. If they do, the promise + * is assumed to reflect the state of the submission operation, + * i.e. if the promise resolves the submission was successful and if it + * rejects, the submission failed. * @param {Event} e */ @action handleSubmit(e) { e.preventDefault(); e.stopPropagation(); - this.args.onSubmit(); + const submitResult = this.args.onSubmit(); + // If the submit handler returns a promise, we want to handle success + // by re-enabling read-only mode on the form (if applicable). + // Since submit is allowed to fail in this context and we don't want + // unhandled rejection warnings, we have a no-op catch. + if (submitResult?.then) { + submitResult + .then(() => this.isEditable = false) + .catch(() => {}); + } return false; } diff --git a/addons/rose/tests/integration/components/rose/form-test.js b/addons/rose/tests/integration/components/rose/form-test.js index 7c33aa290..a8103ca56 100644 --- a/addons/rose/tests/integration/components/rose/form-test.js +++ b/addons/rose/tests/integration/components/rose/form-test.js @@ -2,6 +2,7 @@ import { module, test } from 'qunit'; import { setupRenderingTest } from 'ember-qunit'; import { render, find, findAll, click } from '@ember/test-helpers'; import hbs from 'htmlbars-inline-precompile'; +import { resolve, reject } from 'rsvp'; module('Integration | Component | rose/form', function (hooks) { setupRenderingTest(hooks); @@ -70,7 +71,7 @@ module('Integration | Component | rose/form', function (hooks) { this.cancel = () => { assert.ok(true, 'cancel function may still be passed even for @showEditToggle'); }; - this.onSubmit = () => {}; + this.submit = () => {}; await render(hbs` {}; + await render(hbs` + + + + + `); + // Before enabling edit mode, fields are disabled and the edit mode button is displayed + assert.ok(find('.rose-form')); + assert.equal(findAll('input[disabled]').length, 1); + assert.equal(findAll('button').length, 1); + assert.equal(find('button').textContent.trim(), 'Edit'); + // After entering edit mode, fields are enabled and save/cancel buttons are displayed + await click('button'); + assert.equal(findAll('input[disabled]').length, 0); + assert.equal(findAll('button').length, 2); + assert.equal(find('[type="submit"]').textContent.trim(), 'Save'); + assert.equal(find('button:not([type="submit"])').textContent.trim(), 'Cancel'); + // After saving with failure, fields are enabled and save/cancel buttons are displayed + this.submit = () => reject(); + assert.equal(findAll('input[disabled]').length, 0); + assert.equal(findAll('button').length, 2); + assert.equal(find('[type="submit"]').textContent.trim(), 'Save'); + assert.equal(find('button:not([type="submit"])').textContent.trim(), 'Cancel'); + // After saving with success, fields are disabled again and the edit mode button is displayed + this.submit = () => resolve(); + await click('button[type="submit"]'); + assert.equal(findAll('input[disabled]').length, 1); + assert.equal(findAll('button').length, 1); + assert.equal(find('button').textContent.trim(), 'Edit'); + }); }); diff --git a/ui/core/app/components/form/account/password/index.js b/ui/core/app/components/form/account/password/index.js index e5d4a264b..4a7727c5f 100644 --- a/ui/core/app/components/form/account/password/index.js +++ b/ui/core/app/components/form/account/password/index.js @@ -32,6 +32,6 @@ export default class FormAccountPasswordIndexComponent extends Component { submit(fn) { const password = this.password; next(() => this.resetPassword()); - this.args.model.isNew ? fn(password) : fn(); + return this.args.model.isNew ? fn(password) : fn(); } } diff --git a/ui/core/app/routes/scopes/scope/auth-methods.js b/ui/core/app/routes/scopes/scope/auth-methods.js index d68b2833b..f7bbe65c5 100644 --- a/ui/core/app/routes/scopes/scope/auth-methods.js +++ b/ui/core/app/routes/scopes/scope/auth-methods.js @@ -61,6 +61,7 @@ export default class ScopesScopeAuthMethodsRoute extends Route { } catch (error) { //TODO: replace with translated strings this.notify.error(error.message, { closeAfter: null }); + throw error; } } diff --git a/ui/core/app/routes/scopes/scope/auth-methods/auth-method/accounts.js b/ui/core/app/routes/scopes/scope/auth-methods/auth-method/accounts.js index 752e063f0..fbd28041a 100644 --- a/ui/core/app/routes/scopes/scope/auth-methods/auth-method/accounts.js +++ b/ui/core/app/routes/scopes/scope/auth-methods/auth-method/accounts.js @@ -61,6 +61,7 @@ export default class ScopesScopeAuthMethodsAuthMethodAccountsRoute extends Route } catch (error) { // TODO: replace with translated strings this.notify.error(error.message, { closeAfter: null }); + throw error; } } diff --git a/ui/core/app/routes/scopes/scope/groups.js b/ui/core/app/routes/scopes/scope/groups.js index e8e607967..66ac9d8bd 100644 --- a/ui/core/app/routes/scopes/scope/groups.js +++ b/ui/core/app/routes/scopes/scope/groups.js @@ -50,14 +50,15 @@ export default class ScopesScopeGroupsRoute extends Route { const { isNew } = group; try { await group.save(); + await this.transitionTo('scopes.scope.groups.group', group); this.refresh(); this.notify.success( this.intl.t(isNew ? 'notifications.create-success' : 'notifications.save-success') ); - this.transitionTo('scopes.scope.groups.group', group); } catch (error) { //TODO: replace with translated strings this.notify.error(error.message, { closeAfter: null }); + throw error; } } @@ -72,7 +73,7 @@ export default class ScopesScopeGroupsRoute extends Route { await group.destroyRecord(); this.refresh(); this.notify.success(this.intl.t('notifications.delete-success')); - this.transitionTo('scopes.scope.groups'); + await this.transitionTo('scopes.scope.groups'); } catch (error) { //TODO: replace with translated strings this.notify.error(error.message, { closeAfter: null }); diff --git a/ui/core/app/routes/scopes/scope/orgs.js b/ui/core/app/routes/scopes/scope/orgs.js index a1e85bb9e..76ff7b208 100644 --- a/ui/core/app/routes/scopes/scope/orgs.js +++ b/ui/core/app/routes/scopes/scope/orgs.js @@ -54,14 +54,15 @@ export default class ScopesScopeOrgsRoute extends Route { const { isNew } = org; try { await org.save(); + await this.transitionTo('scopes.scope', org); this.refresh(); this.notify.success( this.intl.t(isNew ? 'notifications.create-success' : 'notifications.save-success') ); - this.transitionTo('scopes.scope', org); } catch (error) { // TODO: replace with translated strings this.notify.error(error.message, { closeAfter: null }); + throw error; } } } diff --git a/ui/core/app/routes/scopes/scope/projects.js b/ui/core/app/routes/scopes/scope/projects.js index 51650f447..efd9d71f8 100644 --- a/ui/core/app/routes/scopes/scope/projects.js +++ b/ui/core/app/routes/scopes/scope/projects.js @@ -66,14 +66,15 @@ export default class ScopesScopeProjectsRoute extends Route { const { isNew } = project; try { await project.save(); + await this.transitionTo('scopes.scope.projects.project', project); this.refresh(); this.notify.success( this.intl.t(isNew ? 'notifications.create-success' : 'notifications.save-success') ); - this.transitionTo('scopes.scope.projects.project', project); } catch (error) { // TODO: replace with translated strings this.notify.error(error.message, { closeAfter: null }); + throw error; } } diff --git a/ui/core/app/routes/scopes/scope/projects/project/host-catalogs.js b/ui/core/app/routes/scopes/scope/projects/project/host-catalogs.js index 31618cf93..3cc9e212d 100644 --- a/ui/core/app/routes/scopes/scope/projects/project/host-catalogs.js +++ b/ui/core/app/routes/scopes/scope/projects/project/host-catalogs.js @@ -44,17 +44,18 @@ export default class ScopesScopeProjectsProjectHostCatalogsRoute extends Route { const { isNew } = hostCatalog; try { await hostCatalog.save(); + await this.transitionTo( + 'scopes.scope.projects.project.host-catalogs.host-catalog', + hostCatalog + ); this.refresh(); this.notify.success( this.intl.t(isNew ? 'notifications.create-success' : 'notifications.save-success') ); - this.transitionTo( - 'scopes.scope.projects.project.host-catalogs.host-catalog', - hostCatalog - ); } catch (error) { // TODO: replace with translated strings this.notify.error(error.message, { closeAfter: null }); + throw error; } } diff --git a/ui/core/app/routes/scopes/scope/projects/project/host-catalogs/host-catalog/host-sets.js b/ui/core/app/routes/scopes/scope/projects/project/host-catalogs/host-catalog/host-sets.js index 008429df4..affdabbe0 100644 --- a/ui/core/app/routes/scopes/scope/projects/project/host-catalogs/host-catalog/host-sets.js +++ b/ui/core/app/routes/scopes/scope/projects/project/host-catalogs/host-catalog/host-sets.js @@ -53,13 +53,14 @@ export default class ScopesScopeProjectsProjectHostCatalogsHostCatalogHostSetsRo 'scopes.scope.projects.project.host-catalogs.host-catalog.host-sets.host-set', hostSet ); - await this.refresh(); + this.refresh(); this.notify.success( this.intl.t(isNew ? 'notifications.create-success' : 'notifications.save-success') ); } catch (error) { // TODO: replace with translated strings this.notify.error(error.message, { closeAfter: null }); + throw error; } } diff --git a/ui/core/app/routes/scopes/scope/projects/project/host-catalogs/host-catalog/hosts.js b/ui/core/app/routes/scopes/scope/projects/project/host-catalogs/host-catalog/hosts.js index b0ce74d38..031975c46 100644 --- a/ui/core/app/routes/scopes/scope/projects/project/host-catalogs/host-catalog/hosts.js +++ b/ui/core/app/routes/scopes/scope/projects/project/host-catalogs/host-catalog/hosts.js @@ -53,13 +53,14 @@ export default class ScopesScopeProjectsProjectHostCatalogsHostCatalogHostsRoute 'scopes.scope.projects.project.host-catalogs.host-catalog.hosts.host', host ); - await this.refresh(); + this.refresh(); this.notify.success( this.intl.t(isNew ? 'notifications.create-success' : 'notifications.save-success') ); } catch (error) { // TODO: replace with translated strings this.notify.error(error.message, { closeAfter: null }); + throw error; } } diff --git a/ui/core/app/routes/scopes/scope/projects/project/targets.js b/ui/core/app/routes/scopes/scope/projects/project/targets.js index 501343e8d..fa019bdc7 100644 --- a/ui/core/app/routes/scopes/scope/projects/project/targets.js +++ b/ui/core/app/routes/scopes/scope/projects/project/targets.js @@ -44,14 +44,15 @@ export default class ScopesScopeProjectsProjectTargetsRoute extends Route { const { isNew } = target; try { await target.save(); + await this.transitionTo('scopes.scope.projects.project.targets.target', target); this.refresh(); this.notify.success( this.intl.t(isNew ? 'notifications.create-success' : 'notifications.save-success') ); - this.transitionTo('scopes.scope.projects.project.targets.target', target); } catch (error) { // TODO: replace with translated strings this.notify.error(error.message, { closeAfter: null }); + throw error; } } diff --git a/ui/core/app/routes/scopes/scope/roles.js b/ui/core/app/routes/scopes/scope/roles.js index 84c6e7a8d..c9e907069 100644 --- a/ui/core/app/routes/scopes/scope/roles.js +++ b/ui/core/app/routes/scopes/scope/roles.js @@ -50,14 +50,15 @@ export default class ScopesScopeRolesRoute extends Route { const { isNew } = role; try { await role.save(); + await this.transitionTo('scopes.scope.roles.role', role); this.refresh(); this.notify.success( this.intl.t(isNew ? 'notifications.create-success' : 'notifications.save-success') ); - this.transitionTo('scopes.scope.roles.role', role); } catch (error) { // TODO: replace with translated strings this.notify.error(error.message, { closeAfter: null }); + throw error; } } diff --git a/ui/core/app/routes/scopes/scope/users.js b/ui/core/app/routes/scopes/scope/users.js index bc6b6cf02..563cf13d8 100644 --- a/ui/core/app/routes/scopes/scope/users.js +++ b/ui/core/app/routes/scopes/scope/users.js @@ -51,14 +51,15 @@ export default class ScopesScopeUsersRoute extends Route { const { isNew } = user; try { await user.save(); + await this.transitionTo('scopes.scope.users.user', user); this.refresh(); this.notify.success( this.intl.t(isNew ? 'notifications.create-success' : 'notifications.save-success') ); - this.transitionTo('scopes.scope.users.user', user); } catch (error) { // TODO: replace with translated strings this.notify.error(error.message, { closeAfter: null }); + throw error; } }