From 79b06ee565dcd820d9b6771d70f12f59f02b0b48 Mon Sep 17 00:00:00 2001 From: Lisbet Alvarez <107949262+lisbet-alvarez@users.noreply.github.com> Date: Thu, 15 Aug 2024 16:04:46 -0700 Subject: [PATCH] ICU-13883 add remove org modal (#2435) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: 🎸 add remove org modal ✅ Closes: https://hashicorp.atlassian.net/browse/ICU-13883 --------- Co-authored-by: Cameron Perera --- addons/core/translations/resources/en-us.yaml | 10 ++ .../form/role/manage-custom-scopes/index.hbs | 61 ++++++- .../form/role/manage-custom-scopes/index.js | 99 ++++++++++- .../manage-scopes/manage-custom-scopes.js | 9 +- .../acceptance/roles/global-scope-test.js | 162 +++++++++++++++++- 5 files changed, 320 insertions(+), 21 deletions(-) diff --git a/addons/core/translations/resources/en-us.yaml b/addons/core/translations/resources/en-us.yaml index ff85c94f4..c6b9cc28e 100644 --- a/addons/core/translations/resources/en-us.yaml +++ b/addons/core/translations/resources/en-us.yaml @@ -615,6 +615,10 @@ role: help: Customize which scopes are associated with this role. view-projects: '{selected}/{total} total projects' apply: Apply changes to org + remove-org-and-projects: Remove org and projects + remove-org-only: Remove just the org + remove-orgs-and-projects: Remove orgs and projects + remove-orgs-only: Remove just the orgs form: this: label: Add this scope @@ -627,6 +631,12 @@ role: descendants: label: Add all descendants help: Add all of the orgs and the projects in the orgs that are underneath this scope. + remove-org: + title: Remove this org? + description: You're about to remove {orgDisplayName}. Do you want to remove the projects under {orgDisplayName} as well? Or do you just want to remove {orgDisplayName} from this project? + remove-all-orgs: + title: Remove all selected orgs? + description: You're about to remove all the selected orgs. Do you want to remove the projects under these orgs as well? Or do you just want to remove just the orgs? target: title: Target title_plural: Targets 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 841c3a3d6..67c4d53cb 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 @@ -69,7 +69,7 @@ @iconPosition='trailing' @text={{t 'resources.role.scope.actions.view-projects' - selected=projectTotals.selected + selected=projectTotals.selected.length total=projectTotals.total }} @route='scopes.scope.roles.role.manage-scopes.manage-org-projects' @@ -127,4 +127,63 @@ /> +{{/if}} + +{{#if this.selectedOrg}} + + {{t 'resources.role.scope.remove-org.title'}} + + + {{t + 'resources.role.scope.remove-org.description' + orgDisplayName=this.orgDisplayName + }} + + + + + + + + + +{{/if}} + +{{#if this.selectedOrgs}} + + {{t 'resources.role.scope.remove-all-orgs.title'}} + + + {{t 'resources.role.scope.remove-all-orgs.description'}} + + + + + + + + + {{/if}} \ No newline at end of file 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 dc67d0b91..cce8f3aae 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,27 +5,108 @@ import Component from '@glimmer/component'; import { action } from '@ember/object'; +import { tracked } from '@glimmer/tracking'; export default class FormRoleManageCustomScopesIndexComponent extends Component { + // =attributes + + @tracked selectedOrgs = []; + @tracked selectedOrg = ''; + + /** + * Returns the display name of the selectedOrg. + * @type {string} + */ + get orgDisplayName() { + const org = this.args.model.orgScopes.find( + ({ id }) => id === this.selectedOrg, + ); + return org.displayName; + } + // =actions /** * Handles the selection changes for the paginated table. * @param {object} selectableRowsStates + * @param {object} selectionKey */ @action - selectionChange({ selectableRowsStates }) { - const { role } = this.args.model; - selectableRowsStates.forEach((row) => { - const { isSelected, selectionKey: key } = row; - const includesId = role.grant_scope_ids.includes(key); - if (isSelected && !includesId) { - role.grant_scope_ids = [...role.grant_scope_ids, key]; - } else if (!isSelected && includesId) { + selectionChange({ selectableRowsStates, selectionKey }) { + const { role, projectTotals } = this.args.model; + + const addOrRemoveValues = (add, remove, orgId) => { + let selectedOrg; + const includesId = role.grant_scope_ids.includes(orgId); + const selected = projectTotals[orgId]?.selected; + if (add && !includesId) { + role.grant_scope_ids = [...role.grant_scope_ids, orgId]; + } else if (remove && includesId) { + if (selected?.length) { + selectedOrg = orgId; + } role.grant_scope_ids = role.grant_scope_ids.filter( - (item) => item !== key, + (item) => item !== orgId, ); } + return selectedOrg; + }; + + if (selectionKey === 'all') { + const selectedOrgs = []; + selectableRowsStates.forEach(({ selectionKey, isSelected }) => { + const selectedOrg = addOrRemoveValues( + isSelected, + !isSelected, + selectionKey, + ); + if (selectedOrg) { + selectedOrgs.push(selectedOrg); + } + }); + this.selectedOrgs = selectedOrgs; + } else { + this.selectedOrg = addOrRemoveValues(true, true, selectionKey); + } + } + + /** + * Removes projects from an org that was deselected and toggles the correct modal off. + * @param {boolean} toggleRemoveAllModal + */ + @action + removeProjects(toggleRemoveAllModal) { + const selectedOrgs = toggleRemoveAllModal + ? this.selectedOrgs + : [this.selectedOrg]; + const { role, projectTotals } = this.args.model; + selectedOrgs.forEach((orgId) => { + const { selected, total } = projectTotals[orgId]; + role.grant_scope_ids = role.grant_scope_ids.filter( + (item) => !selected.includes(item), + ); + projectTotals[orgId] = { selected: [], total }; }); + if (toggleRemoveAllModal) { + this.toggleRemoveAllModal(); + } else { + this.toggleRemoveOrgModal(); + } + } + + /** + * Toggles the modal to remove an org and projects off. + */ + @action + toggleRemoveOrgModal() { + this.selectedOrg = ''; + } + + /** + * Toggles the modal to remove orgs and projects off. + */ + @action + toggleRemoveAllModal() { + this.selectedOrgs = []; } } 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 075c785d5..e5ed284d9 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 @@ -6,6 +6,7 @@ import Route from '@ember/routing/route'; import { action } from '@ember/object'; import { inject as service } from '@ember/service'; +import { TrackedObject } from 'tracked-built-ins'; import { TYPE_SCOPE_PROJECT } from 'api/models/scope'; export default class ScopesScopeRolesRoleManageScopesManageCustomScopesRoute extends Route { @@ -88,13 +89,15 @@ export default class ScopesScopeRolesRoleManageScopesManageCustomScopesRoute ext options, ); - const projectTotals = {}; + // We want this object to be tracked so that changes to this object + // cause the "Projects selected" column to re-render with updates. + const projectTotals = new TrackedObject({}); projects.forEach(({ id, scope }) => { if (!projectTotals[scope.id]) { - projectTotals[scope.id] = { selected: 0, total: 0 }; + projectTotals[scope.id] = { selected: [], total: 0 }; } if (projectIDs.includes(id)) { - projectTotals[scope.id].selected++; + projectTotals[scope.id].selected.push(id); } projectTotals[scope.id].total++; }); diff --git a/ui/admin/tests/acceptance/roles/global-scope-test.js b/ui/admin/tests/acceptance/roles/global-scope-test.js index 47ea17e27..660286fcb 100644 --- a/ui/admin/tests/acceptance/roles/global-scope-test.js +++ b/ui/admin/tests/acceptance/roles/global-scope-test.js @@ -62,6 +62,11 @@ module('Acceptance | roles | global-scope', function (hooks) { '.rose-dialog-footer .rose-button-primary'; const DISCARD_CHANGES_CANCEL_BUTTON = '.rose-dialog-footer .rose-button-secondary'; + const REMOVE_ORG_MODAL = (name) => + `[data-test-manage-scopes-remove-${name}-modal]`; + const REMOVE_ORG_PROJECTS_BUTTON = '.hds-modal .hds-button--color-primary'; + const REMOVE_ORG_ONLY_BUTTON = '.hds-modal .hds-button--color-secondary'; + const TABLE_ALL_CHECKBOX = 'thead tr [type="checkbox"]'; const instances = { scopes: { @@ -394,7 +399,7 @@ module('Acceptance | roles | global-scope', function (hooks) { await click(`[href="${urls.roleScopes}"]`); - assert.strictEqual(findAll(TABLE_ROW_SELECTOR).length, 0); + assert.dom(TABLE_ROW_SELECTOR).exists({ count: 0 }); await click(MANAGE_DROPDOWN_SELECTOR); await click(MANAGE_SCOPES_SELECTOR); @@ -418,7 +423,7 @@ module('Acceptance | roles | global-scope', function (hooks) { await click(SAVE_BTN_SELECTOR); assert.strictEqual(currentURL(), urls.roleScopes); - assert.strictEqual(findAll(TABLE_ROW_SELECTOR).length, 1); + assert.dom(TABLE_ROW_SELECTOR).exists({ count: 1 }); }); test('user can cancel custom scopes to add on manage custom scopes page', async function (assert) { @@ -427,7 +432,7 @@ module('Acceptance | roles | global-scope', function (hooks) { await click(`[href="${urls.roleScopes}"]`); - assert.strictEqual(findAll(TABLE_ROW_SELECTOR).length, 0); + assert.dom(TABLE_ROW_SELECTOR).exists({ count: 0 }); await click(MANAGE_DROPDOWN_SELECTOR); await click(MANAGE_SCOPES_SELECTOR); @@ -447,7 +452,7 @@ module('Acceptance | roles | global-scope', function (hooks) { await click(CANCEL_BTN_SELECTOR); assert.strictEqual(currentURL(), urls.roleScopes); - assert.strictEqual(findAll(TABLE_ROW_SELECTOR).length, 0); + assert.dom(TABLE_ROW_SELECTOR).exists({ count: 0 }); }); test('shows error message on custom scope save on manage custom scopes page', async function (assert) { @@ -509,6 +514,147 @@ module('Acceptance | roles | global-scope', function (hooks) { assert.strictEqual(currentURL(), urls.manageCustomScopes); }); + test('user can choose to only deselect an org using modal on manage custom scopes page', async function (assert) { + instances.role.update({ + grant_scope_ids: [instances.scopes.org.id, instances.scopes.project.id], + }); + await visit(urls.manageScopes); + + await click(`[href="${urls.manageCustomScopes}"]`); + await click(SCOPE_CHECKBOX_SELECTOR('org', instances.scopes.org.id)); + + assert.dom(REMOVE_ORG_MODAL('org')).isVisible(); + + await click(REMOVE_ORG_ONLY_BUTTON); + await click(SAVE_BTN_SELECTOR); + + assert.strictEqual(currentURL(), urls.manageScopes); + assert.dom(BUTTON_ICON_SELECTOR).isVisible(); + + await click(SAVE_BTN_SELECTOR); + + assert.strictEqual(currentURL(), urls.roleScopes); + assert.dom(TABLE_ROW_SELECTOR).exists({ count: 1 }); + }); + + test('user can choose to deselect an org and projects using modal on manage custom scopes page', async function (assert) { + instances.role.update({ + grant_scope_ids: [instances.scopes.org.id, instances.scopes.project.id], + }); + await visit(urls.manageScopes); + + await click(`[href="${urls.manageCustomScopes}"]`); + await click(SCOPE_CHECKBOX_SELECTOR('org', instances.scopes.org.id)); + + assert.dom(REMOVE_ORG_MODAL('org')).isVisible(); + + await click(REMOVE_ORG_PROJECTS_BUTTON); + await click(SAVE_BTN_SELECTOR); + + assert.strictEqual(currentURL(), urls.manageScopes); + assert.dom(BUTTON_ICON_SELECTOR).isVisible(); + + await click(SAVE_BTN_SELECTOR); + + assert.strictEqual(currentURL(), urls.roleScopes); + assert.dom(TABLE_ROW_SELECTOR).exists({ count: 0 }); + }); + + test('user cannot trigger modal when deselecting an org with no projects selected on manage custom scopes page', async function (assert) { + instances.role.update({ grant_scope_ids: [instances.scopes.org.id] }); + await visit(urls.manageScopes); + + await click(`[href="${urls.manageCustomScopes}"]`); + await click(SCOPE_CHECKBOX_SELECTOR('org', instances.scopes.org.id)); + + assert.dom(REMOVE_ORG_MODAL('org')).doesNotExist(); + + await click(SAVE_BTN_SELECTOR); + + assert.strictEqual(currentURL(), urls.manageScopes); + assert.dom(BUTTON_ICON_SELECTOR).isVisible(); + + await click(SAVE_BTN_SELECTOR); + + assert.strictEqual(currentURL(), urls.roleScopes); + assert.dom(TABLE_ROW_SELECTOR).exists({ count: 0 }); + }); + + test('user can choose to only deselect all orgs using modal on manage custom scopes page', async function (assert) { + instances.role.update({ + grant_scope_ids: [instances.scopes.org.id, instances.scopes.project.id], + }); + await visit(urls.manageScopes); + + await click(`[href="${urls.manageCustomScopes}"]`); + // Click once to select all + await click(TABLE_ALL_CHECKBOX); + // Click once more to deselect all + await click(TABLE_ALL_CHECKBOX); + + assert.dom(REMOVE_ORG_MODAL('all-orgs')).isVisible(); + + await click(REMOVE_ORG_ONLY_BUTTON); + await click(SAVE_BTN_SELECTOR); + + assert.strictEqual(currentURL(), urls.manageScopes); + assert.dom(BUTTON_ICON_SELECTOR).isVisible(); + + await click(SAVE_BTN_SELECTOR); + + assert.strictEqual(currentURL(), urls.roleScopes); + assert.dom(TABLE_ROW_SELECTOR).exists({ count: 1 }); + }); + + test('user can choose to deselect all orgs and projects using modal on manage custom scopes page', async function (assert) { + instances.role.update({ + grant_scope_ids: [instances.scopes.org.id, instances.scopes.project.id], + }); + await visit(urls.manageScopes); + + await click(`[href="${urls.manageCustomScopes}"]`); + // Click once to select all + await click(TABLE_ALL_CHECKBOX); + // Click once more to deselect all + await click(TABLE_ALL_CHECKBOX); + + assert.dom(REMOVE_ORG_MODAL('all-orgs')).isVisible(); + + await click(REMOVE_ORG_PROJECTS_BUTTON); + await click(SAVE_BTN_SELECTOR); + + assert.strictEqual(currentURL(), urls.manageScopes); + assert.dom(BUTTON_ICON_SELECTOR).isVisible(); + + await click(SAVE_BTN_SELECTOR); + + assert.strictEqual(currentURL(), urls.roleScopes); + assert.dom(TABLE_ROW_SELECTOR).exists({ count: 0 }); + }); + + test('user cannot trigger modal when deselecting all orgs with no projects selected on manage custom scopes page', async function (assert) { + instances.role.update({ grant_scope_ids: [instances.scopes.org.id] }); + await visit(urls.manageScopes); + + await click(`[href="${urls.manageCustomScopes}"]`); + // Click once to select all + await click(TABLE_ALL_CHECKBOX); + // Click once more to deselect all + await click(TABLE_ALL_CHECKBOX); + + assert.dom(REMOVE_ORG_MODAL('all-orgs')).doesNotExist(); + + await click(SAVE_BTN_SELECTOR); + + assert.strictEqual(currentURL(), urls.manageScopes); + assert.dom(BUTTON_ICON_SELECTOR).isVisible(); + + await click(SAVE_BTN_SELECTOR); + + assert.strictEqual(currentURL(), urls.roleScopes); + assert.dom(TABLE_ROW_SELECTOR).exists({ count: 0 }); + }); + 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', @@ -564,7 +710,7 @@ module('Acceptance | roles | global-scope', function (hooks) { await click(`[href="${urls.roleScopes}"]`); - assert.strictEqual(findAll(TABLE_ROW_SELECTOR).length, 0); + assert.dom(TABLE_ROW_SELECTOR).exists({ count: 0 }); await click(MANAGE_DROPDOWN_SELECTOR); await click(MANAGE_SCOPES_SELECTOR); @@ -596,7 +742,7 @@ module('Acceptance | roles | global-scope', function (hooks) { await click(SAVE_BTN_SELECTOR); assert.strictEqual(currentURL(), urls.roleScopes); - assert.strictEqual(findAll(TABLE_ROW_SELECTOR).length, 1); + assert.dom(TABLE_ROW_SELECTOR).exists({ count: 1 }); }); test('user can cancel custom scopes to add on manage org projects page', async function (assert) { @@ -605,7 +751,7 @@ module('Acceptance | roles | global-scope', function (hooks) { await click(`[href="${urls.roleScopes}"]`); - assert.strictEqual(findAll(TABLE_ROW_SELECTOR).length, 0); + assert.dom(TABLE_ROW_SELECTOR).exists({ count: 0 }); await click(MANAGE_DROPDOWN_SELECTOR); await click(MANAGE_SCOPES_SELECTOR); @@ -635,7 +781,7 @@ module('Acceptance | roles | global-scope', function (hooks) { await click(CANCEL_BTN_SELECTOR); assert.strictEqual(currentURL(), urls.roleScopes); - assert.strictEqual(findAll(TABLE_ROW_SELECTOR).length, 0); + assert.dom(TABLE_ROW_SELECTOR).exists({ count: 0 }); }); test('shows error message on custom scope save on manage org projects page', async function (assert) {