From b5708aaf2dac0288240c424b8a7102f8f85984ef Mon Sep 17 00:00:00 2001 From: Lisbet Alvarez Date: Thu, 8 Aug 2024 17:57:59 -0700 Subject: [PATCH 1/9] =?UTF-8?q?feat:=20=F0=9F=8E=B8=20add=20remove=20org?= =?UTF-8?q?=20modal?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ✅ Closes: https://hashicorp.atlassian.net/browse/ICU-13883 --- addons/core/translations/resources/en-us.yaml | 5 +++ .../form/role/manage-custom-scopes/index.hbs | 30 +++++++++++++++- .../form/role/manage-custom-scopes/index.js | 34 +++++++++++++++++-- .../manage-scopes/manage-custom-scopes.js | 10 +++--- 4 files changed, 72 insertions(+), 7 deletions(-) diff --git a/addons/core/translations/resources/en-us.yaml b/addons/core/translations/resources/en-us.yaml index ff85c94f46..f8ceaf3aa5 100644 --- a/addons/core/translations/resources/en-us.yaml +++ b/addons/core/translations/resources/en-us.yaml @@ -615,6 +615,8 @@ 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 form: this: label: Add this scope @@ -627,6 +629,9 @@ 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 projects under this org? + description: Do you want to remove the projects under this org from this role? Or do you just want to remove the org from this role? 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 841c3a3d60..58aa61cfcb 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,32 @@ /> +{{/if}} + +{{#if this.showRemoveOrgModal}} + + {{t 'resources.role.scope.remove-org.title'}} + + + {{t 'resources.role.scope.remove-org.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 dc67d0b91f..929e369788 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,8 +5,14 @@ import Component from '@glimmer/component'; import { action } from '@ember/object'; +import { tracked } from '@glimmer/tracking'; export default class FormRoleManageCustomScopesIndexComponent extends Component { + // =attributes + + @tracked showRemoveOrgModal = false; + @tracked selectedOrg; + // =actions /** @@ -14,18 +20,42 @@ export default class FormRoleManageCustomScopesIndexComponent extends Component * @param {object} selectableRowsStates */ @action - selectionChange({ selectableRowsStates }) { - const { role } = this.args.model; + selectionChange({ selectableRowsStates, selectionKey }) { + const { role, projectTotals } = this.args.model; selectableRowsStates.forEach((row) => { const { isSelected, selectionKey: key } = row; const includesId = role.grant_scope_ids.includes(key); if (isSelected && !includesId) { + // Add the org id if it was selected and it doesn't exist as a grant scope. role.grant_scope_ids = [...role.grant_scope_ids, key]; } else if (!isSelected && includesId) { + // Remove org id if it was deselected and it does exist as a grant scope. role.grant_scope_ids = role.grant_scope_ids.filter( (item) => item !== key, ); + // If the org id was deselected and has selected projects then trigger the modal. + const selected = projectTotals[key]?.selected; + if (selectionKey === key && selected?.length) { + this.showRemoveOrgModal = true; + this.selectedOrg = key; + } } }); } + + @action + removeProjects() { + const { role, projectTotals } = this.args.model; + const { selected: projectIds, total } = projectTotals[this.selectedOrg]; + role.grant_scope_ids = role.grant_scope_ids.filter( + (item) => !projectIds.includes(item), + ); + projectTotals[this.selectedOrg] = { selected: [], total }; + this.showRemoveOrgModal = false; + } + + @action + toggleRemoveOrgModal() { + this.showRemoveOrgModal = false; + } } 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 075c785d5a..f7fc36f2c5 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 @@ -7,7 +7,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'; - +import { TrackedObject } from 'tracked-built-ins'; export default class ScopesScopeRolesRoleManageScopesManageCustomScopesRoute extends Route { // =attributes @@ -88,13 +88,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++; }); From bef3105eaae126a14174279eb5ac4285ba4b1021 Mon Sep 17 00:00:00 2001 From: Lisbet Alvarez Date: Mon, 12 Aug 2024 09:45:52 -0700 Subject: [PATCH 2/9] =?UTF-8?q?test:=20=F0=9F=92=8D=20add=20tests?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ✅ Closes: https://hashicorp.atlassian.net/browse/ICU-13883 --- .../acceptance/roles/global-scope-test.js | 71 +++++++++++++++++++ 1 file changed, 71 insertions(+) diff --git a/ui/admin/tests/acceptance/roles/global-scope-test.js b/ui/admin/tests/acceptance/roles/global-scope-test.js index 47ea17e27a..d985890d29 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 = '[data-test-manage-scopes-remove-org-modal]'; + const REMOVE_ORG_PROJECTS_BUTTON = + '[data-test-manage-scopes-remove-org-modal] .hds-button--color-primary'; + const REMOVE_ORG_ONLY_BUTTON = + '[data-test-manage-scopes-remove-org-modal] .hds-button--color-secondary'; const instances = { scopes: { @@ -509,6 +514,72 @@ module('Acceptance | roles | global-scope', function (hooks) { assert.strictEqual(currentURL(), urls.manageCustomScopes); }); + test('user can choose to only deselect 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).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.strictEqual(findAll(TABLE_ROW_SELECTOR).length, 1); + }); + + test('user can choose to deselect 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).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.strictEqual(findAll(TABLE_ROW_SELECTOR).length, 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).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.strictEqual(findAll(TABLE_ROW_SELECTOR).length, 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', From d9e850b5729c9978d6f04d2c2687726c012ecee0 Mon Sep 17 00:00:00 2001 From: Lisbet Alvarez <107949262+lisbet-alvarez@users.noreply.github.com> Date: Mon, 12 Aug 2024 19:14:03 -0700 Subject: [PATCH 3/9] Update ui/admin/tests/acceptance/roles/global-scope-test.js Co-authored-by: Cameron Perera --- ui/admin/tests/acceptance/roles/global-scope-test.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ui/admin/tests/acceptance/roles/global-scope-test.js b/ui/admin/tests/acceptance/roles/global-scope-test.js index d985890d29..48eaf8db41 100644 --- a/ui/admin/tests/acceptance/roles/global-scope-test.js +++ b/ui/admin/tests/acceptance/roles/global-scope-test.js @@ -577,7 +577,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, 0); + assert.dom(TABLE_ROW_SELECTOR).exist({ count: 0 }); }); test('user can search for a specific org scope by id on manage custom scopes page', async function (assert) { From 862749e9e2413a47d7e9bdc426996597a6f80ec8 Mon Sep 17 00:00:00 2001 From: Lisbet Alvarez Date: Mon, 12 Aug 2024 19:15:18 -0700 Subject: [PATCH 4/9] =?UTF-8?q?style:=20=F0=9F=92=84=20add=20newline?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ✅ Closes: https://hashicorp.atlassian.net/browse/ICU-13883 --- .../scope/roles/role/manage-scopes/manage-custom-scopes.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) 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 f7fc36f2c5..e5ed284d94 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,8 +6,9 @@ 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'; import { TrackedObject } from 'tracked-built-ins'; +import { TYPE_SCOPE_PROJECT } from 'api/models/scope'; + export default class ScopesScopeRolesRoleManageScopesManageCustomScopesRoute extends Route { // =attributes From 51666a00df2d03dd17c5fcffa9e321011827bea3 Mon Sep 17 00:00:00 2001 From: Lisbet Alvarez Date: Mon, 12 Aug 2024 19:18:27 -0700 Subject: [PATCH 5/9] =?UTF-8?q?test:=20=F0=9F=92=8D=20change=20asserts?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ✅ Closes: https://hashicorp.atlassian.net/browse/ICU-13883 --- .../acceptance/roles/global-scope-test.js | 22 +++++++++---------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/ui/admin/tests/acceptance/roles/global-scope-test.js b/ui/admin/tests/acceptance/roles/global-scope-test.js index 48eaf8db41..b9aef11a75 100644 --- a/ui/admin/tests/acceptance/roles/global-scope-test.js +++ b/ui/admin/tests/acceptance/roles/global-scope-test.js @@ -399,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); @@ -423,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) { @@ -432,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); @@ -452,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) { @@ -534,7 +534,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 choose to deselect org and projects using modal on manage custom scopes page', async function (assert) { @@ -557,7 +557,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, 0); + 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) { @@ -577,7 +577,7 @@ module('Acceptance | roles | global-scope', function (hooks) { await click(SAVE_BTN_SELECTOR); assert.strictEqual(currentURL(), urls.roleScopes); - assert.dom(TABLE_ROW_SELECTOR).exist({ count: 0 }); + 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) { @@ -635,7 +635,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); @@ -667,7 +667,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) { @@ -676,7 +676,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); @@ -706,7 +706,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) { From af66a63a849672a7869d94e7b4d2905c69bf6686 Mon Sep 17 00:00:00 2001 From: Lisbet Alvarez Date: Wed, 14 Aug 2024 19:52:23 -0700 Subject: [PATCH 6/9] =?UTF-8?q?refactor:=20=F0=9F=92=A1=20allow=20for=20se?= =?UTF-8?q?lecting=20and=20deselecting=20all=20projects=20and=20orgs?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ✅ Closes: https://hashicorp.atlassian.net/browse/ICU-13883 --- addons/core/translations/resources/en-us.yaml | 9 +- .../form/role/manage-custom-scopes/index.hbs | 37 +++++- .../form/role/manage-custom-scopes/index.js | 117 ++++++++++++++---- .../manage-scopes/manage-custom-scopes.js | 4 +- .../acceptance/roles/global-scope-test.js | 2 +- 5 files changed, 136 insertions(+), 33 deletions(-) diff --git a/addons/core/translations/resources/en-us.yaml b/addons/core/translations/resources/en-us.yaml index f8ceaf3aa5..c6b9cc28ea 100644 --- a/addons/core/translations/resources/en-us.yaml +++ b/addons/core/translations/resources/en-us.yaml @@ -617,6 +617,8 @@ role: 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 @@ -630,8 +632,11 @@ role: 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 projects under this org? - description: Do you want to remove the projects under this org from this role? Or do you just want to remove the org from this role? + 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 58aa61cfcb..67c4d53cbb 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 @@ -129,7 +129,7 @@ {{/if}} -{{#if this.showRemoveOrgModal}} +{{#if this.selectedOrg}} {{t 'resources.role.scope.remove-org.title'}} - {{t 'resources.role.scope.remove-org.description'}} + {{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 929e369788..1f070637b5 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 @@ -10,52 +10,117 @@ import { tracked } from '@glimmer/tracking'; export default class FormRoleManageCustomScopesIndexComponent extends Component { // =attributes - @tracked showRemoveOrgModal = false; - @tracked selectedOrg; + @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, selectionKey }) { const { role, projectTotals } = this.args.model; - selectableRowsStates.forEach((row) => { - const { isSelected, selectionKey: key } = row; - const includesId = role.grant_scope_ids.includes(key); - if (isSelected && !includesId) { - // Add the org id if it was selected and it doesn't exist as a grant scope. - role.grant_scope_ids = [...role.grant_scope_ids, key]; - } else if (!isSelected && includesId) { - // Remove org id if it was deselected and it does exist as a grant scope. + + const addOrRemoveValues = (add, remove, orgId) => { + let selectedOrg; + const includesId = role.grant_scope_ids.includes(orgId); + const selected = projectTotals[orgId]?.selected; + const remaining = projectTotals[orgId]?.remaining; + const total = projectTotals[orgId]?.total; + if (add && !includesId) { + if (remaining) { + role.grant_scope_ids = [...role.grant_scope_ids, ...remaining]; + projectTotals[orgId] = { + selected: [...selected, ...remaining], + total, + remaining: [], + }; + } + 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, ); - // If the org id was deselected and has selected projects then trigger the modal. - const selected = projectTotals[key]?.selected; - if (selectionKey === key && selected?.length) { - this.showRemoveOrgModal = true; - this.selectedOrg = key; - } } - }); + 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() { + removeProjects(toggleRemoveAllModal) { + const selectedOrgs = toggleRemoveAllModal + ? this.selectedOrgs + : [this.selectedOrg]; const { role, projectTotals } = this.args.model; - const { selected: projectIds, total } = projectTotals[this.selectedOrg]; - role.grant_scope_ids = role.grant_scope_ids.filter( - (item) => !projectIds.includes(item), - ); - projectTotals[this.selectedOrg] = { selected: [], total }; - this.showRemoveOrgModal = false; + selectedOrgs.forEach((orgId) => { + const { selected, total, remaining } = projectTotals[orgId]; + role.grant_scope_ids = role.grant_scope_ids.filter( + (item) => !selected.includes(item), + ); + projectTotals[orgId] = { + selected: [], + total, + remaining: [...selected, ...remaining], + }; + }); + if (toggleRemoveAllModal) { + this.toggleRemoveAllModal(); + } else { + this.toggleRemoveOrgModal(); + } } + /** + * Toggles the modal to remove an org and projects off. + */ @action toggleRemoveOrgModal() { - this.showRemoveOrgModal = false; + 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 e5ed284d94..eff6acf201 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 @@ -94,10 +94,12 @@ export default class ScopesScopeRolesRoleManageScopesManageCustomScopesRoute ext const projectTotals = new TrackedObject({}); projects.forEach(({ id, scope }) => { if (!projectTotals[scope.id]) { - projectTotals[scope.id] = { selected: [], total: 0 }; + projectTotals[scope.id] = { selected: [], total: 0, remaining: [] }; } if (projectIDs.includes(id)) { projectTotals[scope.id].selected.push(id); + } else { + projectTotals[scope.id].remaining.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 b9aef11a75..ec57794f56 100644 --- a/ui/admin/tests/acceptance/roles/global-scope-test.js +++ b/ui/admin/tests/acceptance/roles/global-scope-test.js @@ -423,7 +423,7 @@ module('Acceptance | roles | global-scope', function (hooks) { await click(SAVE_BTN_SELECTOR); assert.strictEqual(currentURL(), urls.roleScopes); - assert.dom(TABLE_ROW_SELECTOR).exists({ count: 1 }); + assert.dom(TABLE_ROW_SELECTOR).exists({ count: 2 }); }); test('user can cancel custom scopes to add on manage custom scopes page', async function (assert) { From 08cbcafcac4c9bbffa34eb18cc701cfdfe622bf6 Mon Sep 17 00:00:00 2001 From: Lisbet Alvarez Date: Thu, 15 Aug 2024 10:09:44 -0700 Subject: [PATCH 7/9] =?UTF-8?q?test:=20=F0=9F=92=8D=20woops,=20add=20missi?= =?UTF-8?q?ng=20tests?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ✅ Closes: https://hashicorp.atlassian.net/browse/ICU-13883 --- .../acceptance/roles/global-scope-test.js | 95 +++++++++++++++++-- 1 file changed, 85 insertions(+), 10 deletions(-) diff --git a/ui/admin/tests/acceptance/roles/global-scope-test.js b/ui/admin/tests/acceptance/roles/global-scope-test.js index ec57794f56..e12541f015 100644 --- a/ui/admin/tests/acceptance/roles/global-scope-test.js +++ b/ui/admin/tests/acceptance/roles/global-scope-test.js @@ -62,11 +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 = '[data-test-manage-scopes-remove-org-modal]'; - const REMOVE_ORG_PROJECTS_BUTTON = - '[data-test-manage-scopes-remove-org-modal] .hds-button--color-primary'; - const REMOVE_ORG_ONLY_BUTTON = - '[data-test-manage-scopes-remove-org-modal] .hds-button--color-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: { @@ -514,7 +514,7 @@ module('Acceptance | roles | global-scope', function (hooks) { assert.strictEqual(currentURL(), urls.manageCustomScopes); }); - test('user can choose to only deselect org using modal on manage custom scopes page', async function (assert) { + 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], }); @@ -523,7 +523,7 @@ module('Acceptance | roles | global-scope', function (hooks) { await click(`[href="${urls.manageCustomScopes}"]`); await click(SCOPE_CHECKBOX_SELECTOR('org', instances.scopes.org.id)); - assert.dom(REMOVE_ORG_MODAL).isVisible(); + assert.dom(REMOVE_ORG_MODAL('org')).isVisible(); await click(REMOVE_ORG_ONLY_BUTTON); await click(SAVE_BTN_SELECTOR); @@ -537,7 +537,7 @@ module('Acceptance | roles | global-scope', function (hooks) { assert.dom(TABLE_ROW_SELECTOR).exists({ count: 1 }); }); - test('user can choose to deselect org and projects using modal on manage custom scopes page', async function (assert) { + 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], }); @@ -546,7 +546,7 @@ module('Acceptance | roles | global-scope', function (hooks) { await click(`[href="${urls.manageCustomScopes}"]`); await click(SCOPE_CHECKBOX_SELECTOR('org', instances.scopes.org.id)); - assert.dom(REMOVE_ORG_MODAL).isVisible(); + assert.dom(REMOVE_ORG_MODAL('org')).isVisible(); await click(REMOVE_ORG_PROJECTS_BUTTON); await click(SAVE_BTN_SELECTOR); @@ -567,7 +567,82 @@ module('Acceptance | roles | global-scope', function (hooks) { await click(`[href="${urls.manageCustomScopes}"]`); await click(SCOPE_CHECKBOX_SELECTOR('org', instances.scopes.org.id)); - assert.dom(REMOVE_ORG_MODAL).doesNotExist(); + 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); From 0abcb094775003e2d34fc578b63755b41dd4284d Mon Sep 17 00:00:00 2001 From: Lisbet Alvarez Date: Thu, 15 Aug 2024 15:14:21 -0700 Subject: [PATCH 8/9] =?UTF-8?q?refactor:=20=F0=9F=92=A1=20remove=20autosel?= =?UTF-8?q?ecting?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ✅ Closes: https://hashicorp.atlassian.net/browse/ICU-13883 --- .../form/role/manage-custom-scopes/index.js | 18 ++---------------- .../role/manage-scopes/manage-custom-scopes.js | 4 +--- .../acceptance/roles/global-scope-test.js | 2 +- 3 files changed, 4 insertions(+), 20 deletions(-) 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 1f070637b5..08afe1d548 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 @@ -39,17 +39,7 @@ export default class FormRoleManageCustomScopesIndexComponent extends Component let selectedOrg; const includesId = role.grant_scope_ids.includes(orgId); const selected = projectTotals[orgId]?.selected; - const remaining = projectTotals[orgId]?.remaining; - const total = projectTotals[orgId]?.total; if (add && !includesId) { - if (remaining) { - role.grant_scope_ids = [...role.grant_scope_ids, ...remaining]; - projectTotals[orgId] = { - selected: [...selected, ...remaining], - total, - remaining: [], - }; - } role.grant_scope_ids = [...role.grant_scope_ids, orgId]; } else if (remove && includesId) { if (selected?.length) { @@ -91,15 +81,11 @@ export default class FormRoleManageCustomScopesIndexComponent extends Component : [this.selectedOrg]; const { role, projectTotals } = this.args.model; selectedOrgs.forEach((orgId) => { - const { selected, total, remaining } = projectTotals[orgId]; + const { selected, total } = projectTotals[orgId]; role.grant_scope_ids = role.grant_scope_ids.filter( (item) => !selected.includes(item), ); - projectTotals[orgId] = { - selected: [], - total, - remaining: [...selected, ...remaining], - }; + projectTotals[orgId] = { selected: [], total }; }); if (toggleRemoveAllModal) { this.toggleRemoveAllModal(); 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 eff6acf201..e5ed284d94 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 @@ -94,12 +94,10 @@ export default class ScopesScopeRolesRoleManageScopesManageCustomScopesRoute ext const projectTotals = new TrackedObject({}); projects.forEach(({ id, scope }) => { if (!projectTotals[scope.id]) { - projectTotals[scope.id] = { selected: [], total: 0, remaining: [] }; + projectTotals[scope.id] = { selected: [], total: 0 }; } if (projectIDs.includes(id)) { projectTotals[scope.id].selected.push(id); - } else { - projectTotals[scope.id].remaining.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 e12541f015..660286fcbb 100644 --- a/ui/admin/tests/acceptance/roles/global-scope-test.js +++ b/ui/admin/tests/acceptance/roles/global-scope-test.js @@ -423,7 +423,7 @@ module('Acceptance | roles | global-scope', function (hooks) { await click(SAVE_BTN_SELECTOR); assert.strictEqual(currentURL(), urls.roleScopes); - assert.dom(TABLE_ROW_SELECTOR).exists({ count: 2 }); + assert.dom(TABLE_ROW_SELECTOR).exists({ count: 1 }); }); test('user can cancel custom scopes to add on manage custom scopes page', async function (assert) { From 742468ecf1d808b7bf0e18f5d1566202a329fd86 Mon Sep 17 00:00:00 2001 From: Lisbet Alvarez Date: Thu, 15 Aug 2024 15:48:03 -0700 Subject: [PATCH 9/9] =?UTF-8?q?refactor:=20=F0=9F=92=A1=20change=20to=20em?= =?UTF-8?q?pty=20string?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ✅ Closes: https://hashicorp.atlassian.net/browse/ICU-13883 --- ui/admin/app/components/form/role/manage-custom-scopes/index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 08afe1d548..cce8f3aae3 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 @@ -99,7 +99,7 @@ export default class FormRoleManageCustomScopesIndexComponent extends Component */ @action toggleRemoveOrgModal() { - this.selectedOrg = []; + this.selectedOrg = ''; } /**