From 7ac904a31f8ec274fe3ddde28f72390a4c743663 Mon Sep 17 00:00:00 2001 From: Lisbet Alvarez <107949262+lisbet-alvarez@users.noreply.github.com> Date: Thu, 8 Aug 2024 10:46:39 -0700 Subject: [PATCH] ICU-14496 add discard unsaved changes modal to manage scopes (#2421) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * refactor: 💡 add discard unsaved changes modal ✅ Closes: https://hashicorp.atlassian.net/browse/ICU-14496 --- .../form/role/manage-custom-scopes/index.hbs | 8 +- .../form/role/manage-custom-scopes/index.js | 28 ++-- .../form/role/manage-org-projects/index.hbs | 8 +- .../form/role/manage-org-projects/index.js | 29 ++--- .../form/role/manage-scopes/index.hbs | 8 +- .../form/role/manage-scopes/index.js | 16 ++- .../scope/roles/role/manage-scopes/index.js | 9 +- .../manage-scopes/manage-custom-scopes.js | 13 +- .../role/manage-scopes/manage-org-projects.js | 7 +- ui/admin/app/routes/application.js | 3 +- .../manage-scopes/manage-custom-scopes.js | 16 +++ .../role/manage-scopes/manage-org-projects.js | 39 ++++-- .../scope/roles/role/manage-scopes/index.hbs | 2 +- .../manage-scopes/manage-custom-scopes.hbs | 2 +- .../manage-scopes/manage-org-projects.hbs | 1 + .../acceptance/roles/global-scope-test.js | 122 ++++++++++++++++++ .../tests/acceptance/roles/org-scope-test.js | 80 ++++++++++++ .../roles/role/manage-scopes/index-test.js | 13 +- .../manage-custom-scopes-test.js | 12 +- .../manage-scopes/manage-org-projects-test.js | 13 +- 20 files changed, 331 insertions(+), 98 deletions(-) diff --git a/ui/admin/app/components/form/role/manage-custom-scopes/index.hbs b/ui/admin/app/components/form/role/manage-custom-scopes/index.hbs index 5faf3ffff..841c3a3d6 100644 --- a/ui/admin/app/components/form/role/manage-custom-scopes/index.hbs +++ b/ui/admin/app/components/form/role/manage-custom-scopes/index.hbs @@ -6,7 +6,7 @@ {{#if @model.totalItemsCount}} {{#if @model.orgScopes}} - {{#if this.selectedItems.length}} + {{#if @model.role.grantScopeOrgIDs.length}}
{{t 'resources.role.scope.titles.selected' - items=this.selectedItems.length + items=@model.role.grantScopeOrgIDs.length total=@model.totalItemsCount }} @@ -46,7 +46,7 @@ <:body as |B|> diff --git a/ui/admin/app/components/form/role/manage-custom-scopes/index.js b/ui/admin/app/components/form/role/manage-custom-scopes/index.js index c8db41f32..dc67d0b91 100644 --- a/ui/admin/app/components/form/role/manage-custom-scopes/index.js +++ b/ui/admin/app/components/form/role/manage-custom-scopes/index.js @@ -5,21 +5,8 @@ import Component from '@glimmer/component'; import { action } from '@ember/object'; -import { tracked } from '@glimmer/tracking'; export default class FormRoleManageCustomScopesIndexComponent extends Component { - // =attributes - - @tracked selectedItems = [...this.args.model.role.grantScopeOrgIDs]; - - get allGrantScopes() { - return [ - ...this.args.model.role.grantScopeKeywords, - ...this.args.model.role.grantScopeProjectIDs, - ...this.selectedItems, - ]; - } - // =actions /** @@ -28,15 +15,16 @@ export default class FormRoleManageCustomScopesIndexComponent extends Component */ @action selectionChange({ selectableRowsStates }) { + const { role } = this.args.model; selectableRowsStates.forEach((row) => { const { isSelected, selectionKey: key } = row; - const includesId = this.selectedItems.includes(key); - if (isSelected) { - if (!includesId) this.selectedItems = [...this.selectedItems, key]; - } else if (includesId) { - this.selectedItems = this.selectedItems.filter( - (item) => item !== key, - ); + const includesId = role.grant_scope_ids.includes(key); + if (isSelected && !includesId) { + role.grant_scope_ids = [...role.grant_scope_ids, key]; + } else if (!isSelected && includesId) { + role.grant_scope_ids = role.grant_scope_ids.filter( + (item) => item !== key, + ); } }); } diff --git a/ui/admin/app/components/form/role/manage-org-projects/index.hbs b/ui/admin/app/components/form/role/manage-org-projects/index.hbs index 743c71398..8e078fbce 100644 --- a/ui/admin/app/components/form/role/manage-org-projects/index.hbs +++ b/ui/admin/app/components/form/role/manage-org-projects/index.hbs @@ -6,7 +6,7 @@ {{#if @model.totalItemsCount}} {{#if @model.projectScopes}} - {{#if this.selectedItems.length}} + {{#if this.selectedProjectsCount}}
{{t 'resources.role.scope.titles.selected' - items=this.selectedItems.length + items=this.selectedProjectsCount total=@model.totalItemsCount }} @@ -45,7 +45,7 @@ <:body as |B|> diff --git a/ui/admin/app/components/form/role/manage-org-projects/index.js b/ui/admin/app/components/form/role/manage-org-projects/index.js index 2ef0c9d93..6fcc16138 100644 --- a/ui/admin/app/components/form/role/manage-org-projects/index.js +++ b/ui/admin/app/components/form/role/manage-org-projects/index.js @@ -5,20 +5,13 @@ import Component from '@glimmer/component'; import { action } from '@ember/object'; -import { tracked } from '@glimmer/tracking'; export default class FormRoleManageOrgProjectsIndexComponent extends Component { // =attributes - @tracked selectedItems = [...this.args.model.selectedProjectIDs]; - - get allGrantScopes() { - return [ - ...this.args.model.role.grantScopeKeywords, - ...this.args.model.role.grantScopeOrgIDs, - ...this.args.model.remainingProjectIDs, - ...this.selectedItems, - ]; + get selectedProjectsCount() { + const { role, remainingProjectIDs } = this.args.model; + return role.grantScopeProjectIDs.length - remainingProjectIDs.length; } // =actions @@ -29,16 +22,16 @@ export default class FormRoleManageOrgProjectsIndexComponent extends Component { */ @action selectionChange({ selectableRowsStates }) { + const { role } = this.args.model; selectableRowsStates.forEach((row) => { const { isSelected, selectionKey: key } = row; - const includesId = this.selectedItems.includes(key); - if (isSelected) { - if (!includesId) this.selectedItems = [...this.selectedItems, key]; - } else { - if (includesId) - this.selectedItems = this.selectedItems.filter( - (item) => item !== key, - ); + const includesId = role.grant_scope_ids.includes(key); + if (isSelected && !includesId) { + role.grant_scope_ids = [...role.grant_scope_ids, key]; + } else if (!isSelected && includesId) { + role.grant_scope_ids = role.grant_scope_ids.filter( + (item) => item !== key, + ); } }); } diff --git a/ui/admin/app/components/form/role/manage-scopes/index.hbs b/ui/admin/app/components/form/role/manage-scopes/index.hbs index fb745d512..3eca81364 100644 --- a/ui/admin/app/components/form/role/manage-scopes/index.hbs +++ b/ui/admin/app/components/form/role/manage-scopes/index.hbs @@ -5,7 +5,7 @@ { - this.selectedItems = this.selectedItems.filter((item) => item !== value); + this.args.model.grant_scope_ids = this.args.model.grant_scope_ids.filter( + (item) => item !== value, + ); }; if (checked) { - this.selectedItems = [...this.selectedItems, value]; + this.args.model.grant_scope_ids = [ + ...this.args.model.grant_scope_ids, + value, + ]; if (value === GRANT_SCOPE_CHILDREN) { removeValue(GRANT_SCOPE_DESCENDANTS); } diff --git a/ui/admin/app/controllers/scopes/scope/roles/role/manage-scopes/index.js b/ui/admin/app/controllers/scopes/scope/roles/role/manage-scopes/index.js index ef7f9f2b4..9b52238ad 100644 --- a/ui/admin/app/controllers/scopes/scope/roles/role/manage-scopes/index.js +++ b/ui/admin/app/controllers/scopes/scope/roles/role/manage-scopes/index.js @@ -23,22 +23,23 @@ export default class ScopesScopeRolesRoleManageScopesIndexController extends Con /** * Save grant scope IDs to current role via the API. * @param {RoleModel} role - * @param {[string]} grantScopeIDs */ @action @loading @notifyError(({ message }) => message, { catch: true }) @notifySuccess('resources.role.scope.messages.manage-scopes.success') - async setGrantScopes(role, grantScopeIDs) { - await role.setGrantScopes(grantScopeIDs); + async setGrantScopes(role) { + await role.setGrantScopes(role.grant_scope_ids); this.router.replaceWith('scopes.scope.roles.role.scopes'); } /** * Redirect to role scopes as if nothing ever happened. + * @param {RoleModel} role */ @action - cancel() { + cancel(role) { + role.rollbackAttributes(); this.router.replaceWith('scopes.scope.roles.role.scopes'); } } diff --git a/ui/admin/app/controllers/scopes/scope/roles/role/manage-scopes/manage-custom-scopes.js b/ui/admin/app/controllers/scopes/scope/roles/role/manage-scopes/manage-custom-scopes.js index 66940700a..598bc2645 100644 --- a/ui/admin/app/controllers/scopes/scope/roles/role/manage-scopes/manage-custom-scopes.js +++ b/ui/admin/app/controllers/scopes/scope/roles/role/manage-scopes/manage-custom-scopes.js @@ -43,23 +43,24 @@ export default class ScopesScopeRolesRoleManageScopesManageCustomScopesControlle /** * Save grant scope IDs to current role via the API. * @param {RoleModel} role - * @param {[string]} grantScopeIDs */ @action @loading @notifyError(({ message }) => message, { catch: true }) @notifySuccess('resources.role.scope.messages.manage-custom-scopes.success') - async setGrantScopes(role, grantScopeIDs) { - await role.setGrantScopes(grantScopeIDs); + async setGrantScopes(role) { + await role.setGrantScopes(role.grant_scope_ids); this.manageScopes.showCheckIcon = true; - await this.router.replaceWith('scopes.scope.roles.role.manage-scopes'); + this.router.replaceWith('scopes.scope.roles.role.manage-scopes'); } /** * Redirect to manage scopes as if nothing ever happened. + * @param {RoleModel} role */ @action - async cancel() { - await this.router.replaceWith('scopes.scope.roles.role.manage-scopes'); + cancel(role) { + role.rollbackAttributes(); + this.router.replaceWith('scopes.scope.roles.role.manage-scopes'); } } diff --git a/ui/admin/app/controllers/scopes/scope/roles/role/manage-scopes/manage-org-projects.js b/ui/admin/app/controllers/scopes/scope/roles/role/manage-scopes/manage-org-projects.js index 18c179154..466d3d17e 100644 --- a/ui/admin/app/controllers/scopes/scope/roles/role/manage-scopes/manage-org-projects.js +++ b/ui/admin/app/controllers/scopes/scope/roles/role/manage-scopes/manage-org-projects.js @@ -43,14 +43,13 @@ export default class ScopesScopeRolesRoleManageScopesManageOrgProjectsController /** * Save grant scope IDs to current role via the API. * @param {RoleModel} role - * @param {[string]} grantScopeIDs */ @action @loading @notifyError(({ message }) => message, { catch: true }) @notifySuccess('resources.role.scope.messages.manage-org-projects.success') - async setGrantScopes(role, grantScopeIDs) { - await role.setGrantScopes(grantScopeIDs); + async setGrantScopes(role) { + await role.setGrantScopes(role.grant_scope_ids); this.manageScopes.showCheckIcon = true; if (role.scope.isGlobal) { this.router.replaceWith( @@ -63,9 +62,11 @@ export default class ScopesScopeRolesRoleManageScopesManageOrgProjectsController /** * Redirect to previous as if nothing ever happened. + * @param {RoleModel} role */ @action cancel(role) { + role.rollbackAttributes(); if (role.scope.isGlobal) { this.router.replaceWith( 'scopes.scope.roles.role.manage-scopes.manage-custom-scopes', diff --git a/ui/admin/app/routes/application.js b/ui/admin/app/routes/application.js index 4187b03c4..4cb0dc6ac 100644 --- a/ui/admin/app/routes/application.js +++ b/ui/admin/app/routes/application.js @@ -43,7 +43,8 @@ export default class ApplicationRoute extends Route { this.router.on('routeWillChange', async (transition) => { const fromName = transition?.from?.name; const toName = transition?.to?.name; - const maybeModel = transition?.from?.attributes; + const maybeModel = + transition?.data?.model ?? transition?.from?.attributes; if (fromName !== toName && maybeModel?.hasDirtyAttributes) { transition.abort(); try { diff --git a/ui/admin/app/routes/scopes/scope/roles/role/manage-scopes/manage-custom-scopes.js b/ui/admin/app/routes/scopes/scope/roles/role/manage-scopes/manage-custom-scopes.js index c6821407b..075c785d5 100644 --- a/ui/admin/app/routes/scopes/scope/roles/role/manage-scopes/manage-custom-scopes.js +++ b/ui/admin/app/routes/scopes/scope/roles/role/manage-scopes/manage-custom-scopes.js @@ -4,6 +4,7 @@ */ import Route from '@ember/routing/route'; +import { action } from '@ember/object'; import { inject as service } from '@ember/service'; import { TYPE_SCOPE_PROJECT } from 'api/models/scope'; @@ -26,6 +27,8 @@ export default class ScopesScopeRolesRoleManageScopesManageCustomScopesRoute ext // =services @service store; + @service intl; + @service confirm; // =methods @@ -128,4 +131,17 @@ export default class ScopesScopeRolesRoleManageScopesManageCustomScopesRoute ext ); return scopes.meta?.totalItems; } + + // =actions + + /** + * Stores the role model in the transition data property so that the application level hook + * can check for dirty attributes and trigger the confirm service. + * @param {object} transition + */ + @action + async willTransition(transition) { + const { role } = transition.from.attributes; + transition.data = { model: role }; + } } diff --git a/ui/admin/app/routes/scopes/scope/roles/role/manage-scopes/manage-org-projects.js b/ui/admin/app/routes/scopes/scope/roles/role/manage-scopes/manage-org-projects.js index 64e89984c..66978e5b8 100644 --- a/ui/admin/app/routes/scopes/scope/roles/role/manage-scopes/manage-org-projects.js +++ b/ui/admin/app/routes/scopes/scope/roles/role/manage-scopes/manage-org-projects.js @@ -4,6 +4,7 @@ */ import Route from '@ember/routing/route'; +import { action } from '@ember/object'; import { inject as service } from '@ember/service'; export default class ScopesScopeRolesRoleManageScopesManageOrgProjectsRoute extends Route { @@ -25,12 +26,14 @@ export default class ScopesScopeRolesRoleManageScopesManageOrgProjectsRoute exte // =services @service store; + @service intl; + @service confirm; // =methods /** * Loads projects for current org scope. - * @return {Promise<{role: RoleModel, orgScope: ScopeModel, projectScopes: [ScopeModel], totalItems: number, totalItemsCount: number, selectedProjectIDs: [string], remainingProjectIDs: [string]}> } + * @return {Promise<{role: RoleModel, orgScope: ScopeModel, projectScopes: [ScopeModel], totalItems: number, totalItemsCount: number, remainingProjectIDs: [string]}> } */ async model({ org_id, search, page, pageSize }) { const role = this.modelFor('scopes.scope.roles.role'); @@ -53,8 +56,7 @@ export default class ScopesScopeRolesRoleManageScopesManageOrgProjectsRoute exte totalItems, ); - const { selectedProjectIDs, remainingProjectIDs } = - await this.getSelectedProjects(role, org_id); + const remainingProjectIDs = await this.getRemainingProjects(role, org_id); return { role, @@ -62,7 +64,6 @@ export default class ScopesScopeRolesRoleManageScopesManageOrgProjectsRoute exte projectScopes, totalItems, totalItemsCount, - selectedProjectIDs, remainingProjectIDs, }; } @@ -98,12 +99,12 @@ export default class ScopesScopeRolesRoleManageScopesManageOrgProjectsRoute exte } /** - * Extract project type grant scope ids that belong to this org scope. + * Extract project type grant scope ids that don't belong to this org scope. * @param {RoleModel} role * @param {string} org_id - * @returns {object} + * @returns {Promise<[string]>} remainingProjectIDs */ - async getSelectedProjects(role, org_id) { + async getRemainingProjects(role, org_id) { let selectedProjectIDs = role.grantScopeProjectIDs; let remainingProjectIDs = []; if (role.scope.isGlobal && selectedProjectIDs.length) { @@ -115,15 +116,25 @@ export default class ScopesScopeRolesRoleManageScopesManageOrgProjectsRoute exte { scope_id: 'global', query: { filters: { id } } }, options, ); - selectedProjectIDs = projects.reduce((selectedIDs, project) => { - if (project.scope.id === org_id) { - selectedIDs.push(project.id); - } else { + projects.forEach((project) => { + if (project.scope.id !== org_id) { remainingProjectIDs.push(project.id); } - return selectedIDs; - }, []); + }); } - return { selectedProjectIDs, remainingProjectIDs }; + return remainingProjectIDs; + } + + // =actions + + /** + * Stores the role model in the transition data property so that the application level hook + * can check for dirty attributes and trigger the confirm service. + * @param {object} transition + */ + @action + async willTransition(transition) { + const { role } = transition.from.attributes; + transition.data = { model: role }; } } diff --git a/ui/admin/app/templates/scopes/scope/roles/role/manage-scopes/index.hbs b/ui/admin/app/templates/scopes/scope/roles/role/manage-scopes/index.hbs index a6bd4fd8a..88215f172 100644 --- a/ui/admin/app/templates/scopes/scope/roles/role/manage-scopes/index.hbs +++ b/ui/admin/app/templates/scopes/scope/roles/role/manage-scopes/index.hbs @@ -22,7 +22,7 @@ @model={{@model}} @showCheckIcon={{this.showCheckIcon}} @submit={{fn this.setGrantScopes @model}} - @cancel={{this.cancel}} + @cancel={{fn this.cancel @model}} /> diff --git a/ui/admin/app/templates/scopes/scope/roles/role/manage-scopes/manage-custom-scopes.hbs b/ui/admin/app/templates/scopes/scope/roles/role/manage-scopes/manage-custom-scopes.hbs index d4e46fa69..041f8ef6a 100644 --- a/ui/admin/app/templates/scopes/scope/roles/role/manage-scopes/manage-custom-scopes.hbs +++ b/ui/admin/app/templates/scopes/scope/roles/role/manage-scopes/manage-custom-scopes.hbs @@ -33,7 +33,7 @@ @pageSize={{this.pageSize}} @handleSearchInput={{this.handleSearchInput}} @submit={{fn this.setGrantScopes @model.role}} - @cancel={{this.cancel}} + @cancel={{fn this.cancel @model.role}} /> diff --git a/ui/admin/app/templates/scopes/scope/roles/role/manage-scopes/manage-org-projects.hbs b/ui/admin/app/templates/scopes/scope/roles/role/manage-scopes/manage-org-projects.hbs index c4b221a15..baed52292 100644 --- a/ui/admin/app/templates/scopes/scope/roles/role/manage-scopes/manage-org-projects.hbs +++ b/ui/admin/app/templates/scopes/scope/roles/role/manage-scopes/manage-org-projects.hbs @@ -47,6 +47,7 @@ @search={{this.search}} @page={{this.page}} @pageSize={{this.pageSize}} + @selectedItems={{this.selectedItems}} @handleSearchInput={{this.handleSearchInput}} @submit={{fn this.setGrantScopes @model.role}} @cancel={{fn this.cancel @model.role}} diff --git a/ui/admin/tests/acceptance/roles/global-scope-test.js b/ui/admin/tests/acceptance/roles/global-scope-test.js index a05d829ce..47ea17e27 100644 --- a/ui/admin/tests/acceptance/roles/global-scope-test.js +++ b/ui/admin/tests/acceptance/roles/global-scope-test.js @@ -29,6 +29,8 @@ module('Acceptance | roles | global-scope', function (hooks) { setupMirage(hooks); setupIndexedDb(hooks); + let confirmService; + const SCOPE_TOGGLE_SELECTOR = (name) => `.hds-form-toggle input[name="${name}"]`; const SCOPE_CHECKBOX_SELECTOR = (type, id) => @@ -55,6 +57,11 @@ module('Acceptance | roles | global-scope', function (hooks) { const BUTTON_ICON_SELECTOR = '.hds-button__icon [data-test-icon="check-circle"]'; const PAGINATION_SELECTOR = '.hds-pagination'; + const DISCARD_CHANGES_DIALOG = '.rose-dialog'; + const DISCARD_CHANGES_DISCARD_BUTTON = + '.rose-dialog-footer .rose-button-primary'; + const DISCARD_CHANGES_CANCEL_BUTTON = + '.rose-dialog-footer .rose-button-secondary'; const instances = { scopes: { @@ -75,6 +82,8 @@ module('Acceptance | roles | global-scope', function (hooks) { hooks.beforeEach(function () { authenticateSession({}); + confirmService = this.owner.lookup('service:confirm'); + instances.scopes.global = this.server.create('scope', { id: 'global' }); instances.scopes.org = this.server.create('scope', { type: 'org', @@ -345,6 +354,40 @@ module('Acceptance | roles | global-scope', function (hooks) { assert.dom(TOAST_SELECTOR).isVisible(); }); + test('user is prompted to confirm exit when there are unsaved changes on manage scopes page', async function (assert) { + instances.role.update({ grant_scope_ids: [] }); + confirmService.enabled = true; + await visit(urls.role); + + await click(MANAGE_DROPDOWN_SELECTOR); + await click(MANAGE_SCOPES_SELECTOR); + await click(SCOPE_TOGGLE_SELECTOR(GRANT_SCOPE_THIS)); + await click(`[href="${urls.roles}"]`); + + assert.dom(DISCARD_CHANGES_DIALOG).isVisible(); + + await click(DISCARD_CHANGES_DISCARD_BUTTON); + + assert.strictEqual(currentURL(), urls.roles); + }); + + test('user user can cancel transition when there are unsaved changes on manage scopes page', async function (assert) { + instances.role.update({ grant_scope_ids: [] }); + confirmService.enabled = true; + await visit(urls.role); + + await click(MANAGE_DROPDOWN_SELECTOR); + await click(MANAGE_SCOPES_SELECTOR); + await click(SCOPE_TOGGLE_SELECTOR(GRANT_SCOPE_THIS)); + await click(`[href="${urls.roles}"]`); + + assert.dom(DISCARD_CHANGES_DIALOG).isVisible(); + + await click(DISCARD_CHANGES_CANCEL_BUTTON); + + assert.strictEqual(currentURL(), urls.manageScopes); + }); + test('user can save custom scopes to add on manage custom scopes page', async function (assert) { instances.role.update({ grant_scope_ids: [] }); await visit(urls.role); @@ -432,6 +475,40 @@ module('Acceptance | roles | global-scope', function (hooks) { assert.dom(TOAST_SELECTOR).isVisible(); }); + test('user is prompted to confirm exit when there are unsaved changes on manage custom scopes page', async function (assert) { + instances.role.update({ grant_scope_ids: [] }); + confirmService.enabled = true; + await visit(urls.manageScopes); + + await click(`[href="${urls.manageCustomScopes}"]`); + await click(SCOPE_CHECKBOX_SELECTOR('org', instances.scopes.org.id)); + + await click(`[href="${urls.roles}"]`); + + assert.dom(DISCARD_CHANGES_DIALOG).isVisible(); + + await click(DISCARD_CHANGES_DISCARD_BUTTON); + + assert.strictEqual(currentURL(), urls.roles); + }); + + test('user user can cancel transition when there are unsaved changes on manage custom scopes page', async function (assert) { + instances.role.update({ grant_scope_ids: [] }); + confirmService.enabled = true; + await visit(urls.manageScopes); + + await click(`[href="${urls.manageCustomScopes}"]`); + await click(SCOPE_CHECKBOX_SELECTOR('org', instances.scopes.org.id)); + + await click(`[href="${urls.roles}"]`); + + assert.dom(DISCARD_CHANGES_DIALOG).isVisible(); + + await click(DISCARD_CHANGES_CANCEL_BUTTON); + + assert.strictEqual(currentURL(), urls.manageCustomScopes); + }); + test('user can search for a specific org scope by id on manage custom scopes page', async function (assert) { const anotherOrg = this.server.create('scope', { type: 'org', @@ -591,6 +668,51 @@ module('Acceptance | roles | global-scope', function (hooks) { assert.dom(TOAST_SELECTOR).isVisible(); }); + test('user is prompted to confirm exit when there are unsaved changes on manage org projects page', async function (assert) { + instances.role.update({ grant_scope_ids: [] }); + confirmService.enabled = true; + await visit(urls.manageScopes); + + await click(`[href="${urls.manageCustomScopes}"]`); + await click( + `tbody [href="${urls.manageScopes}/${instances.scopes.org.id}"]`, + ); + await click( + SCOPE_CHECKBOX_SELECTOR('project', instances.scopes.project.id), + ); + await click(`[href="${urls.roles}"]`); + + assert.dom(DISCARD_CHANGES_DIALOG).isVisible(); + + await click(DISCARD_CHANGES_DISCARD_BUTTON); + + assert.strictEqual(currentURL(), urls.roles); + }); + + test('user user can cancel transition when there are unsaved changes on manage org projects page', async function (assert) { + instances.role.update({ grant_scope_ids: [] }); + confirmService.enabled = true; + await visit(urls.manageScopes); + + await click(`[href="${urls.manageCustomScopes}"]`); + await click( + `tbody [href="${urls.manageScopes}/${instances.scopes.org.id}"]`, + ); + await click( + SCOPE_CHECKBOX_SELECTOR('project', instances.scopes.project.id), + ); + await click(`[href="${urls.roles}"]`); + + assert.dom(DISCARD_CHANGES_DIALOG).isVisible(); + + await click(DISCARD_CHANGES_CANCEL_BUTTON); + + assert.strictEqual( + currentURL(), + `${urls.manageScopes}/${instances.scopes.org.id}`, + ); + }); + test('user can search for a specific project scope by id on manage org projects page', async function (assert) { const anotherProject = this.server.create('scope', { type: 'project', diff --git a/ui/admin/tests/acceptance/roles/org-scope-test.js b/ui/admin/tests/acceptance/roles/org-scope-test.js index 451dafc8d..b3c236365 100644 --- a/ui/admin/tests/acceptance/roles/org-scope-test.js +++ b/ui/admin/tests/acceptance/roles/org-scope-test.js @@ -28,6 +28,8 @@ module('Acceptance | roles | org-scope', function (hooks) { setupMirage(hooks); setupIndexedDb(hooks); + let confirmService; + const SCOPE_TOGGLE_SELECTOR = (name) => `.hds-form-toggle input[name="${name}"]`; const SCOPE_CHECKBOX_SELECTOR = (type, id) => @@ -52,6 +54,11 @@ module('Acceptance | roles | org-scope', function (hooks) { const BUTTON_ICON_SELECTOR = '.hds-button__icon [data-test-icon="check-circle"]'; const PAGINATION_SELECTOR = '.hds-pagination'; + const DISCARD_CHANGES_DIALOG = '.rose-dialog'; + const DISCARD_CHANGES_DISCARD_BUTTON = + '.rose-dialog-footer .rose-button-primary'; + const DISCARD_CHANGES_CANCEL_BUTTON = + '.rose-dialog-footer .rose-button-secondary'; const instances = { scopes: { @@ -61,6 +68,7 @@ module('Acceptance | roles | org-scope', function (hooks) { }, role: null, }; + const urls = { orgScope: null, roles: null, @@ -72,6 +80,8 @@ module('Acceptance | roles | org-scope', function (hooks) { hooks.beforeEach(function () { authenticateSession({}); + confirmService = this.owner.lookup('service:confirm'); + instances.scopes.global = this.server.create('scope', { id: 'global' }); instances.scopes.org = this.server.create('scope', { type: 'org', @@ -300,6 +310,40 @@ module('Acceptance | roles | org-scope', function (hooks) { assert.dom(TOAST_SELECTOR).isVisible(); }); + test('user is prompted to confirm exit when there are unsaved changes on manage scopes page', async function (assert) { + instances.role.update({ grant_scope_ids: [] }); + confirmService.enabled = true; + await visit(urls.role); + + await click(MANAGE_DROPDOWN_SELECTOR); + await click(MANAGE_SCOPES_SELECTOR); + await click(SCOPE_TOGGLE_SELECTOR(GRANT_SCOPE_THIS)); + await click(`[href="${urls.roles}"]`); + + assert.dom(DISCARD_CHANGES_DIALOG).isVisible(); + + await click(DISCARD_CHANGES_DISCARD_BUTTON); + + assert.strictEqual(currentURL(), urls.roles); + }); + + test('user user can cancel transition when there are unsaved changes on manage scopes page', async function (assert) { + instances.role.update({ grant_scope_ids: [] }); + confirmService.enabled = true; + await visit(urls.role); + + await click(MANAGE_DROPDOWN_SELECTOR); + await click(MANAGE_SCOPES_SELECTOR); + await click(SCOPE_TOGGLE_SELECTOR(GRANT_SCOPE_THIS)); + await click(`[href="${urls.roles}"]`); + + assert.dom(DISCARD_CHANGES_DIALOG).isVisible(); + + await click(DISCARD_CHANGES_CANCEL_BUTTON); + + assert.strictEqual(currentURL(), urls.manageScopes); + }); + test('user can save custom scopes to add on manage org projects page', async function (assert) { instances.role.update({ grant_scope_ids: [] }); await visit(urls.role); @@ -394,6 +438,42 @@ module('Acceptance | roles | org-scope', function (hooks) { assert.dom(TOAST_SELECTOR).isVisible(); }); + test('user is prompted to confirm exit when there are unsaved changes on manage org projects page', async function (assert) { + instances.role.update({ grant_scope_ids: [] }); + confirmService.enabled = true; + await visit(urls.manageScopes); + + await click(`[href="${urls.manageOrgProjects}"]`); + await click( + SCOPE_CHECKBOX_SELECTOR('project', instances.scopes.project.id), + ); + await click(`[href="${urls.roles}"]`); + + assert.dom(DISCARD_CHANGES_DIALOG).isVisible(); + + await click(DISCARD_CHANGES_DISCARD_BUTTON); + + assert.strictEqual(currentURL(), urls.roles); + }); + + test('user user can cancel transition when there are unsaved changes on manage org projects page', async function (assert) { + instances.role.update({ grant_scope_ids: [] }); + confirmService.enabled = true; + await visit(urls.manageScopes); + + await click(`[href="${urls.manageOrgProjects}"]`); + await click( + SCOPE_CHECKBOX_SELECTOR('project', instances.scopes.project.id), + ); + await click(`[href="${urls.roles}"]`); + + assert.dom(DISCARD_CHANGES_DIALOG).isVisible(); + + await click(DISCARD_CHANGES_CANCEL_BUTTON); + + assert.strictEqual(currentURL(), urls.manageOrgProjects); + }); + test('user can search for a specific project scope by id on manage org projects page', async function (assert) { const anotherProject = this.server.create('scope', { type: 'project', diff --git a/ui/admin/tests/unit/controllers/scopes/scope/roles/role/manage-scopes/index-test.js b/ui/admin/tests/unit/controllers/scopes/scope/roles/role/manage-scopes/index-test.js index b056556bc..91b5b55cc 100644 --- a/ui/admin/tests/unit/controllers/scopes/scope/roles/role/manage-scopes/index-test.js +++ b/ui/admin/tests/unit/controllers/scopes/scope/roles/role/manage-scopes/index-test.js @@ -8,6 +8,7 @@ import { setupTest } from 'ember-qunit'; import { visit, currentURL, waitUntil } from '@ember/test-helpers'; import setupMirage from 'ember-cli-mirage/test-support/setup-mirage'; import { authenticateSession } from 'ember-simple-auth/test-support'; +import { GRANT_SCOPE_THIS } from 'api/models/role'; module( 'Unit | Controller | scopes/scope/roles/role/manage-scopes/index', @@ -59,17 +60,23 @@ module( assert.deepEqual(role.grant_scope_ids, []); - await controller.setGrantScopes(role, ['this']); + role.grant_scope_ids = [GRANT_SCOPE_THIS]; + await controller.setGrantScopes(role); - assert.deepEqual(role.grant_scope_ids, ['this']); + assert.deepEqual(role.grant_scope_ids, [GRANT_SCOPE_THIS]); }); test('cancel action causes transition to expected route', async function (assert) { await visit(urls.manageScopes); + const role = await store.findRecord('role', instances.role.id); - controller.cancel(); + assert.deepEqual(role.grant_scope_ids, []); + + role.grant_scope_ids = [GRANT_SCOPE_THIS]; + controller.cancel(role); await waitUntil(() => currentURL() === urls.scopes); + assert.deepEqual(role.grant_scope_ids, []); assert.strictEqual(currentURL(), urls.scopes); }); }, diff --git a/ui/admin/tests/unit/controllers/scopes/scope/roles/role/manage-scopes/manage-custom-scopes-test.js b/ui/admin/tests/unit/controllers/scopes/scope/roles/role/manage-scopes/manage-custom-scopes-test.js index 76d398462..ac0501ad0 100644 --- a/ui/admin/tests/unit/controllers/scopes/scope/roles/role/manage-scopes/manage-custom-scopes-test.js +++ b/ui/admin/tests/unit/controllers/scopes/scope/roles/role/manage-scopes/manage-custom-scopes-test.js @@ -75,16 +75,22 @@ module( assert.deepEqual(role.grant_scope_ids, []); - await controller.setGrantScopes(role, [instances.scopes.org.id]); + role.grant_scope_ids = [instances.scopes.org.id]; + await controller.setGrantScopes(role); assert.deepEqual(role.grant_scope_ids, [instances.scopes.org.id]); }); - test('cancel action causes transition to expected route', async function (assert) { + test('cancel action rolls back changes causes transition to expected route', async function (assert) { await visit(urls.manageCustomScopes); + const role = await store.findRecord('role', instances.role.id); - await controller.cancel(); + assert.deepEqual(role.grant_scope_ids, []); + role.grant_scope_ids = [instances.scopes.org.id]; + await controller.cancel(role); + + assert.deepEqual(role.grant_scope_ids, []); assert.strictEqual(currentURL(), urls.manageScopes); }); }, diff --git a/ui/admin/tests/unit/controllers/scopes/scope/roles/role/manage-scopes/manage-org-projects-test.js b/ui/admin/tests/unit/controllers/scopes/scope/roles/role/manage-scopes/manage-org-projects-test.js index 3390e78d7..8e8402a19 100644 --- a/ui/admin/tests/unit/controllers/scopes/scope/roles/role/manage-scopes/manage-org-projects-test.js +++ b/ui/admin/tests/unit/controllers/scopes/scope/roles/role/manage-scopes/manage-org-projects-test.js @@ -83,22 +83,25 @@ module( assert.deepEqual(role.grant_scope_ids, []); - await controller.setGrantScopes(role, [instances.scopes.project.id]); - - assert.deepEqual(role.grant_scope_ids, [instances.scopes.project.id]); - + role.grant_scope_ids = [instances.scopes.project.id]; + await controller.setGrantScopes(role); await waitUntil(() => currentURL() === urls.manageCustomScopes); + assert.deepEqual(role.grant_scope_ids, [instances.scopes.project.id]); assert.strictEqual(currentURL(), urls.manageCustomScopes); }); - test('cancel action causes transition to expected route', async function (assert) { + test('cancel action rolls back changes causes transition to expected route', async function (assert) { await visit(urls.manageOrgProjects); const role = await store.findRecord('role', instances.role.id); + assert.deepEqual(role.grant_scope_ids, []); + + role.grant_scope_ids = [instances.scopes.project.id]; await controller.cancel(role); await waitUntil(() => currentURL() === urls.manageCustomScopes); + assert.deepEqual(role.grant_scope_ids, []); assert.strictEqual(currentURL(), urls.manageCustomScopes); }); },