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:
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) {
|
||||
|
||||
Reference in new issue
Block a user