ICU-13883 add remove org modal (#2435)

* feat: 🎸 add remove org modal

✅ Closes: https://hashicorp.atlassian.net/browse/ICU-13883

---------

Co-authored-by: Cameron Perera <cameron.perera@hashicorp.com>
This commit is contained in:
Lisbet AlvarezandCameron Perera authored and GitHub committed 2024-08-15 16:04:46 -07:00
1 parent 0af81d9bc1
commit 79b06ee565
5 files changed
+320 -21

No files matched your search

@@ -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
@@ -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 @@
/>
</A.Footer>
</Hds::ApplicationState>
{{/if}}
{{#if this.selectedOrg}}
<Hds::Modal
@onClose={{this.toggleRemoveOrgModal}}
data-test-manage-scopes-remove-org-modal
as |M|
>
<M.Header>{{t 'resources.role.scope.remove-org.title'}}</M.Header>
<M.Body>
<Hds::Text::Body @tag='p' @color='primary'>
{{t
'resources.role.scope.remove-org.description'
orgDisplayName=this.orgDisplayName
}}
</Hds::Text::Body>
</M.Body>
<M.Footer as |F|>
<Hds::ButtonSet>
<Hds::Button
@text={{t 'resources.role.scope.actions.remove-org-and-projects'}}
{{on 'click' (fn this.removeProjects false)}}
/>
<Hds::Button
@color='secondary'
@text={{t 'resources.role.scope.actions.remove-org-only'}}
{{on 'click' F.close}}
/>
</Hds::ButtonSet>
</M.Footer>
</Hds::Modal>
{{/if}}
{{#if this.selectedOrgs}}
<Hds::Modal
@onClose={{this.toggleRemoveAllModal}}
data-test-manage-scopes-remove-all-orgs-modal
as |M|
>
<M.Header>{{t 'resources.role.scope.remove-all-orgs.title'}}</M.Header>
<M.Body>
<Hds::Text::Body @tag='p' @color='primary'>
{{t 'resources.role.scope.remove-all-orgs.description'}}
</Hds::Text::Body>
</M.Body>
<M.Footer as |F|>
<Hds::ButtonSet>
<Hds::Button
@text={{t 'resources.role.scope.actions.remove-orgs-and-projects'}}
{{on 'click' (fn this.removeProjects true)}}
/>
<Hds::Button
@color='secondary'
@text={{t 'resources.role.scope.actions.remove-orgs-only'}}
{{on 'click' F.close}}
/>
</Hds::ButtonSet>
</M.Footer>
</Hds::Modal>
{{/if}}
@@ -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 = [];
}
}
@@ -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++;
});
@@ -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) {