From bc197d456c2501adee5628cc7b75c63951f50a1f Mon Sep 17 00:00:00 2001 From: Eleni Chappen Date: Mon, 9 Sep 2024 14:02:06 -0500 Subject: [PATCH 01/10] functional org roles overlay --- .../components/UserAccount/Username.test.js | 22 +----- .../UsersActions/UsersActionsOrgRoles.test.js | 2 +- .../components/UsersList/UsersList.test.js | 18 ++++- .../orgs/[orgId]/users/[userId]/layout.tsx | 7 +- src/assets/stylesheets/styles.scss | 31 +++++++- .../Overlays/OrgUserOrgRolesOverlay.tsx | 52 +++++++++++++ src/components/UserAccount/Username.tsx | 11 +-- .../UsersActions/UsersActionsOrgRoles.tsx | 29 +++++-- src/components/UsersList/UsersList.tsx | 75 ++++++++++++++++++- .../UsersList/UsersListOrgRoles.tsx | 20 +++-- src/components/uswds/Alert.tsx | 15 +++- src/components/uswds/Tag.tsx | 2 +- 12 files changed, 237 insertions(+), 47 deletions(-) create mode 100644 src/components/Overlays/OrgUserOrgRolesOverlay.tsx diff --git a/__tests__/components/UserAccount/Username.test.js b/__tests__/components/UserAccount/Username.test.js index 95af1cdb..2fa35662 100644 --- a/__tests__/components/UserAccount/Username.test.js +++ b/__tests__/components/UserAccount/Username.test.js @@ -7,14 +7,9 @@ import { render, screen } from '@testing-library/react'; import { Username } from '@/components/UserAccount/Username'; describe('Username', () => { - it('when given a user, displays the user name', () => { - // setup - const mockUser = { - guid: 'userguid', - username: 'User 1', - }; + it('when given a username, displays it', () => { // act - render(); + render(); const content = screen.getByText('User 1'); // assert expect(content).toBeInTheDocument(); @@ -23,16 +18,12 @@ describe('Username', () => { it('when given a service account user, displays the service account name', () => { // setup const guid = 'cafce0be-62dd-4f02-9770-d546c8714430'; - const mockUser = { - guid: 'userguid', - username: guid, - }; const mockAccount = { guid: guid, name: 'Auditor 1', }; // act - render(); + render(); const auditor = screen.getByText(/Auditor 1/); const svcAcct = screen.getByText(/service/); const username = screen.queryByText(guid); @@ -43,13 +34,8 @@ describe('Username', () => { }); it('when given a user without a username, displays default text', () => { - // setup - const mockUser = { - guid: 'userguid', - username: '', - }; // act - render(); + render(); const content = screen.getByText('Unnamed user'); // assert expect(content).toBeInTheDocument(); diff --git a/__tests__/components/UsersActions/UsersActionsOrgRoles.test.js b/__tests__/components/UsersActions/UsersActionsOrgRoles.test.js index 9d80a93a..eb8a90ef 100644 --- a/__tests__/components/UsersActions/UsersActionsOrgRoles.test.js +++ b/__tests__/components/UsersActions/UsersActionsOrgRoles.test.js @@ -76,7 +76,7 @@ describe('UsersActionsOrgRoles', () => { expect(screen.getByText(/Billing manager/)).toBeInTheDocument() ); // query - const submitBtn = screen.getByText('Save'); + const submitBtn = screen.getByText('Update roles'); // act fireEvent.click(submitBtn); // expect a success message diff --git a/__tests__/components/UsersList/UsersList.test.js b/__tests__/components/UsersList/UsersList.test.js index 8fa2e82a..21b40b0d 100644 --- a/__tests__/components/UsersList/UsersList.test.js +++ b/__tests__/components/UsersList/UsersList.test.js @@ -1,7 +1,7 @@ /** * @jest-environment jsdom */ -import { describe, expect, it } from '@jest/globals'; +import { describe, expect, it, beforeAll } from '@jest/globals'; import { render, screen } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import { UsersList } from '@/components/UsersList/UsersList'; @@ -68,6 +68,22 @@ const mockUserLogonTime = { }; describe('UsersList', () => { + beforeAll(() => { + /* global jest */ + /* eslint no-undef: "off" */ + HTMLDialogElement.prototype.show = jest.fn(function () { + this.open = true; + }); + + HTMLDialogElement.prototype.showModal = jest.fn(function () { + this.open = true; + }); + + HTMLDialogElement.prototype.close = jest.fn(function () { + this.open = false; + }); + /* eslint no-undef: "error" */ + }); it('sorts all users by username in alpha order', () => { // act render( diff --git a/src/app/orgs/[orgId]/users/[userId]/layout.tsx b/src/app/orgs/[orgId]/users/[userId]/layout.tsx index 68833031..473daed4 100644 --- a/src/app/orgs/[orgId]/users/[userId]/layout.tsx +++ b/src/app/orgs/[orgId]/users/[userId]/layout.tsx @@ -26,7 +26,12 @@ export default async function SpaceLayout({
} + heading={ + + } />
{children} diff --git a/src/assets/stylesheets/styles.scss b/src/assets/stylesheets/styles.scss index dcdd4377..372fa834 100644 --- a/src/assets/stylesheets/styles.scss +++ b/src/assets/stylesheets/styles.scss @@ -96,13 +96,22 @@ ), $background-color-palettes: ( - 'palette-color-system-mint-medium' // no trailing comma + 'palette-color-system-mint-medium', + 'palette-color-system-green-cool-vivid' // no trailing comma + ), + + $border-color-palettes: ( + 'palette-color-system-green-cool' // no trailing comma ), $top-palettes: ( 'palette-units-system-positive' ), + $bottom-palettes: ( + 'palette-units-system-positive' + ), + $right-palettes: ( 'palette-units-system-positive' ), @@ -312,3 +321,23 @@ dialog.overlayDrawer[open]::backdrop { background-color: rgb(0 0 0 / 0%); } } + +// USA Checkbox + +.usa-checkbox { + background: initial; // removes white bg provided by USWDS +} + +// USA Alerts + +$success-color-bright: 'green-cool-5v'; +$success-color-dark: 'green-cool-50'; + +.usa-alert--success { + @include u-bg($success-color-bright); + border-left-color: color($success-color-dark); + + .usa-alert__body { + @include u-bg($success-color-bright); + } +} diff --git a/src/components/Overlays/OrgUserOrgRolesOverlay.tsx b/src/components/Overlays/OrgUserOrgRolesOverlay.tsx new file mode 100644 index 00000000..c92508d3 --- /dev/null +++ b/src/components/Overlays/OrgUserOrgRolesOverlay.tsx @@ -0,0 +1,52 @@ +import { UsersActionsOrgRoles } from '@/components/UsersActions/UsersActionsOrgRoles'; +import { Username } from '@/components/UserAccount/Username'; +import { UserOrgPage } from '@/controllers/controller-types'; +import { ServiceCredentialBindingObj } from '@/api/cf/cloudfoundry-types'; +import { Tag } from '@/components/uswds/Tag'; + +export function OrgUserOrgRolesOverlay({ + orgGuid, + user, + onCancel, + onSuccess, + serviceAccount, +}: { + orgGuid: string; + user?: UserOrgPage | undefined | null; + onCancel: Function; + onSuccess: Function; + serviceAccount?: ServiceCredentialBindingObj | undefined | null; +}) { + if (!user) return null; + return ( + <> +

+ update organization roles +

+ {serviceAccount && ( + + )} +

+ +

+
+

+ By assigning specific roles, you can grant a user access to specific + information and features within a given organization. +

+ +
+ + ); +} diff --git a/src/components/UserAccount/Username.tsx b/src/components/UserAccount/Username.tsx index e95e771a..e2699f0f 100644 --- a/src/components/UserAccount/Username.tsx +++ b/src/components/UserAccount/Username.tsx @@ -1,16 +1,13 @@ 'use client'; -import { - ServiceCredentialBindingObj, - UserObj, -} from '@/api/cf/cloudfoundry-types'; +import { ServiceCredentialBindingObj } from '@/api/cf/cloudfoundry-types'; import { ServiceTag } from '@/components/ServiceTag'; export function Username({ - user, + username, serviceAccount, }: { - user: UserObj; + username?: string; serviceAccount?: ServiceCredentialBindingObj | undefined; }) { if (serviceAccount) { @@ -21,6 +18,6 @@ export function Username({ ); } else { - return <>{user.username ? user.username : 'Unnamed user'}; + return <>{username ? username : 'Unnamed user'}; } } diff --git a/src/components/UsersActions/UsersActionsOrgRoles.tsx b/src/components/UsersActions/UsersActionsOrgRoles.tsx index adc3c017..d3970bbc 100644 --- a/src/components/UsersActions/UsersActionsOrgRoles.tsx +++ b/src/components/UsersActions/UsersActionsOrgRoles.tsx @@ -60,11 +60,15 @@ const initialRoles = { export function UsersActionsOrgRoles({ orgGuid, userGuid, + onCancel, onCancelPath, + onSuccess, }: { orgGuid: string; userGuid: string; + onCancel?: Function; onCancelPath?: string; + onSuccess?: Function; }) { const [roles, setRoles] = useState(initialRoles as FormRoles); const [dataLoaded, setDataLoaded] = useState(false); @@ -127,6 +131,7 @@ export function UsersActionsOrgRoles({ setRoles(initialRoles); // reset data is needed to wipe the old guids from removed roles setFetchedRolesToState(result.payload.resources); setActionStatus('success' as ActionStatus); + onSuccess && onSuccess(userGuid); } if (result?.meta?.status === 'error') { result?.meta?.errors && setActionErrors(result.meta.errors); @@ -156,10 +161,10 @@ export function UsersActionsOrgRoles({ )}
- + Select org roles -
+
{Object.values(roles).map((role, i) => (
))}
-
+
+ {onCancelPath && ( )} - + {onCancel && ( + + )}
{actionStatus === 'pending' &&

submission in progress...

}
diff --git a/src/components/UsersList/UsersList.tsx b/src/components/UsersList/UsersList.tsx index a8802457..d2e5f16c 100644 --- a/src/components/UsersList/UsersList.tsx +++ b/src/components/UsersList/UsersList.tsx @@ -23,9 +23,14 @@ import { UsersListSpaceRoles } from '@/components/UsersList/UsersListSpaceRoles' import { UserAccountExpires } from '@/components/UserAccount/UserAccountExpires'; import { UserAccountLastLogin } from '@/components/UserAccount/UserAccountLastLogin'; import { UsersActionsRemoveFromOrg } from '@/components/UsersActions/UsersActionsRemoveFromOrg'; +import { OverlayDrawer } from '@/components/OverlayDrawer'; +import { OrgUserOrgRolesOverlay } from '@/components/Overlays/OrgUserOrgRolesOverlay'; +import { Button } from '@/components/uswds/Button'; type SortDirection = 'asc' | 'desc'; +type DialogType = 'org' | 'space'; + export function UsersList({ users, roles, @@ -45,6 +50,10 @@ export function UsersList({ const [searchValue, setSearchValue] = useState('' as string); const [sortParam, setSortParam] = useState('username' as string); const [sortDir, setSortDir] = useState('asc' as SortDirection); + const [dialogOpen, setDialogOpen] = useState(false); + const [dialogType, setDialogType] = useState('org' as DialogType); + const [currentMemberId, setCurrentMemberId] = useState(''); + const [successMsg, setSuccessMsg] = useState(''); // Searching/Filtering const onSearchAction = (value: string) => { @@ -96,7 +105,7 @@ export function UsersList({ } }; - // Modal + // Remove Modal const modalHeadingId = (name: string) => `removeUserSuccess-${name}`; function closeModal(): undefined { @@ -112,13 +121,71 @@ export function UsersList({ openModal(user); } + // Org roles overlay + + const openOrgRoles = (userId: string) => { + setDialogType('org'); + setCurrentMemberId(userId); + setDialogOpen(true); + }; + + const closeOrgRoles = () => { + setDialogOpen(false); + setCurrentMemberId(''); + }; + + // success message + + const dismissSuccessMsg = () => { + setSuccessMsg(''); + }; + const onOrgRolesSuccess = (userId: string) => { + const username = users.find((user) => user.guid === userId)?.username; + const usernameText = username ? `for ${username}` : ''; + const msg = `The organization roles ${usernameText} have been updated.`; + setSuccessMsg(msg); + closeOrgRoles(); + }; + // Helpers const currentUsers = usersSorted(usersFiltered(users)); const usersResultsText = currentUsers.length === 1 ? 'user' : 'users'; const spacesCount = Object.keys(spaces).length; + const currentMember = + users.find((user) => user.guid === currentMemberId) || null; return ( <> + closeOrgRoles()} + > + {dialogType === 'org' && ( + { + closeOrgRoles(); + }} + orgGuid={orgGuid} + user={currentMember} + serviceAccount={serviceAccounts[currentMember?.username || '']} + onSuccess={onOrgRolesSuccess} + /> + )} + + + {successMsg && ( + + {successMsg}{' '} + + + )} + @@ -205,8 +272,10 @@ export function UsersList({ sort={sortParam === 'orgRolesCount'} > { + openOrgRoles(user.guid); + }} /> diff --git a/src/components/UsersList/UsersListOrgRoles.tsx b/src/components/UsersList/UsersListOrgRoles.tsx index c206702f..3fb8750f 100644 --- a/src/components/UsersList/UsersListOrgRoles.tsx +++ b/src/components/UsersList/UsersListOrgRoles.tsx @@ -1,29 +1,35 @@ 'use client'; import React from 'react'; -import Link from 'next/link'; +import { Button } from '@/components/uswds/Button'; import { pluralize } from '@/helpers/text'; export function UsersListOrgRoles({ orgRolesCount, - href, + onClick, }: { orgRolesCount: number; - href: string; + onClick: Function; }) { if (orgRolesCount <= 0) { return ( <> None yet —{' '} - + ); } return ( - + ); } diff --git a/src/components/uswds/Alert.tsx b/src/components/uswds/Alert.tsx index dece5638..064b2569 100644 --- a/src/components/uswds/Alert.tsx +++ b/src/components/uswds/Alert.tsx @@ -42,8 +42,21 @@ export function Alert({ const Heading = headingLevel; + let role = 'region'; + if (type === 'success') { + role = 'status'; + } + if (type === 'error' || type === 'emergency') { + role = 'alert'; + } + return ( -
+
{heading && {heading}} {children && diff --git a/src/components/uswds/Tag.tsx b/src/components/uswds/Tag.tsx index f9855e01..077c9b15 100644 --- a/src/components/uswds/Tag.tsx +++ b/src/components/uswds/Tag.tsx @@ -7,6 +7,6 @@ export function Tag({ className?: string; label: string; }) { - const classes = classNames('usa-tag', className); + const classes = classNames('usa-tag radius-pill', className); return {label}; } From 94231c7c564dc04b611105da0d20883ca3f07068 Mon Sep 17 00:00:00 2001 From: Eleni Chappen Date: Mon, 9 Sep 2024 16:35:38 -0500 Subject: [PATCH 02/10] functional spaces roles overlay --- .../users/[userId]/org-roles/actions.test.js | 4 +- .../orgs/[orgId]/users/[userId]/actions.tsx | 2 +- .../users/[userId]/org-roles/actions.tsx | 2 +- src/assets/stylesheets/styles.scss | 47 ++++++++- src/components/GridList/GridListItem.tsx | 5 +- .../Overlays/OrgUserOrgRolesOverlay.tsx | 23 ++--- .../Overlays/OverlayHeaderUsername.tsx | 33 +++++++ src/components/Overlays/SpaceRolesOverlay.tsx | 35 +++++++ .../UsersActions/UsersActionsOrgRoles.tsx | 11 ++- .../UsersActionsSpaceRoles.tsx | 96 ++++++++++++------- src/components/UsersList/UsersList.tsx | 56 +++++++---- .../UsersList/UsersListSpaceRoles.tsx | 20 ++-- src/components/uswds/Alert.tsx | 6 +- 13 files changed, 247 insertions(+), 93 deletions(-) create mode 100644 src/components/Overlays/OverlayHeaderUsername.tsx create mode 100644 src/components/Overlays/SpaceRolesOverlay.tsx diff --git a/__tests__/app/orgs/[orgId]/users/[userId]/org-roles/actions.test.js b/__tests__/app/orgs/[orgId]/users/[userId]/org-roles/actions.test.js index c262ec9d..b0c87915 100644 --- a/__tests__/app/orgs/[orgId]/users/[userId]/org-roles/actions.test.js +++ b/__tests__/app/orgs/[orgId]/users/[userId]/org-roles/actions.test.js @@ -105,9 +105,7 @@ describe('edit org roles actions', () => { // act/expect expect(async () => { await updateOrgRolesForUser(userGuid, orgGuid, roles); - }).rejects.toThrow( - new Error('Unable to edit org role. Please try again.') - ); + }).rejects.toThrow(new Error('Try submitting your changes again.')); }); }); }); diff --git a/src/app/orgs/[orgId]/users/[userId]/actions.tsx b/src/app/orgs/[orgId]/users/[userId]/actions.tsx index 0668cf59..4a4e6b2e 100644 --- a/src/app/orgs/[orgId]/users/[userId]/actions.tsx +++ b/src/app/orgs/[orgId]/users/[userId]/actions.tsx @@ -40,7 +40,7 @@ export async function updateSpaceRolesForUser( logDevError( `api error on cf edit spaces page with http code ${response.status} for url: ${response.url}` ); - throw new Error('Unable to edit space role. Please try again.'); + throw new Error('Try submitting your changes again.'); } return response.headers.get('Location'); }); diff --git a/src/app/orgs/[orgId]/users/[userId]/org-roles/actions.tsx b/src/app/orgs/[orgId]/users/[userId]/org-roles/actions.tsx index 01d2ff50..8eb9f88a 100644 --- a/src/app/orgs/[orgId]/users/[userId]/org-roles/actions.tsx +++ b/src/app/orgs/[orgId]/users/[userId]/org-roles/actions.tsx @@ -41,7 +41,7 @@ export async function updateOrgRolesForUser( logDevError( `api error on cf edit org page with http code ${response.status} for url: ${response.url}` ); - throw new Error('Unable to edit org role. Please try again.'); + throw new Error('Try submitting your changes again.'); } return response.headers.get('Location'); }); diff --git a/src/assets/stylesheets/styles.scss b/src/assets/stylesheets/styles.scss index 372fa834..4f05fdba 100644 --- a/src/assets/stylesheets/styles.scss +++ b/src/assets/stylesheets/styles.scss @@ -97,11 +97,13 @@ $background-color-palettes: ( 'palette-color-system-mint-medium', - 'palette-color-system-green-cool-vivid' // no trailing comma + 'palette-color-system-green-cool-vivid', + 'palette-color-system-red-cool-vivid' // no trailing comma ), $border-color-palettes: ( - 'palette-color-system-green-cool' // no trailing comma + 'palette-color-system-green-cool', + 'palette-color-system-red-vivid' // no trailing comma ), $top-palettes: ( @@ -328,16 +330,51 @@ dialog.overlayDrawer[open]::backdrop { background: initial; // removes white bg provided by USWDS } +.usa-checkbox__label::before { + background: initial; + box-shadow: 0 0 0 2px color('primary'); +} + +// primary color #2C608A + // USA Alerts -$success-color-bright: 'green-cool-5v'; +$success-color-light: 'green-cool-5v'; $success-color-dark: 'green-cool-50'; +$error-color-light: 'red-cool-10v'; +$error-color-dark: 'red-40v'; + +.usa-alert .usa-alert__body { + max-width: none; +} + +.usa-alert .usa-alert__body h4 { + @include u-font-size('sans', 'md'); + margin-bottom: units(2px); +} .usa-alert--success { - @include u-bg($success-color-bright); + @include u-bg($success-color-light); border-left-color: color($success-color-dark); .usa-alert__body { - @include u-bg($success-color-bright); + @include u-bg($success-color-light); + + &:before { + @include u-bg($success-color-dark); // icon color + } + } +} + +.usa-alert--error { + @include u-bg($error-color-light); + border-left-color: color($error-color-dark); + + .usa-alert__body { + @include u-bg($error-color-light); + + &:before { + @include u-bg($error-color-dark); // icon color + } } } diff --git a/src/components/GridList/GridListItem.tsx b/src/components/GridList/GridListItem.tsx index 67c16865..7b6ed68b 100644 --- a/src/components/GridList/GridListItem.tsx +++ b/src/components/GridList/GridListItem.tsx @@ -2,10 +2,7 @@ import React from 'react'; export function GridListItem({ children }: { children: React.ReactNode }) { return ( -
+
{children}
); diff --git a/src/components/Overlays/OrgUserOrgRolesOverlay.tsx b/src/components/Overlays/OrgUserOrgRolesOverlay.tsx index c92508d3..8160a374 100644 --- a/src/components/Overlays/OrgUserOrgRolesOverlay.tsx +++ b/src/components/Overlays/OrgUserOrgRolesOverlay.tsx @@ -1,8 +1,7 @@ import { UsersActionsOrgRoles } from '@/components/UsersActions/UsersActionsOrgRoles'; -import { Username } from '@/components/UserAccount/Username'; import { UserOrgPage } from '@/controllers/controller-types'; import { ServiceCredentialBindingObj } from '@/api/cf/cloudfoundry-types'; -import { Tag } from '@/components/uswds/Tag'; +import { OverlayHeaderUsername } from './OverlayHeaderUsername'; export function OrgUserOrgRolesOverlay({ orgGuid, @@ -20,21 +19,11 @@ export function OrgUserOrgRolesOverlay({ if (!user) return null; return ( <> -

- update organization roles -

- {serviceAccount && ( - - )} -

- -

+

By assigning specific roles, you can grant a user access to specific diff --git a/src/components/Overlays/OverlayHeaderUsername.tsx b/src/components/Overlays/OverlayHeaderUsername.tsx new file mode 100644 index 00000000..44acd96d --- /dev/null +++ b/src/components/Overlays/OverlayHeaderUsername.tsx @@ -0,0 +1,33 @@ +import { ServiceCredentialBindingObj } from '@/api/cf/cloudfoundry-types'; +import { Username } from '@/components/UserAccount/Username'; +import { Tag } from '@/components/uswds/Tag'; + +export function OverlayHeaderUsername({ + header, + serviceAccount, + username, +}: { + header: string; + serviceAccount?: ServiceCredentialBindingObj | undefined | null; + username: string; +}) { + return ( + <> +

+ {header} +

+ {serviceAccount && ( + + )} +

+ +

+ + ); +} diff --git a/src/components/Overlays/SpaceRolesOverlay.tsx b/src/components/Overlays/SpaceRolesOverlay.tsx new file mode 100644 index 00000000..a6a73911 --- /dev/null +++ b/src/components/Overlays/SpaceRolesOverlay.tsx @@ -0,0 +1,35 @@ +import { UsersActionsSpaceRoles } from '@/components/UsersActions/UsersActionsSpaceRoles/UsersActionsSpaceRoles'; +import { UserOrgPage } from '@/controllers/controller-types'; +import { ServiceCredentialBindingObj } from '@/api/cf/cloudfoundry-types'; +import { OverlayHeaderUsername } from '@/components/Overlays/OverlayHeaderUsername'; + +export function SpaceRolesOverlay({ + onCancel, + onSuccess, + orgGuid, + serviceAccount, + user, +}: { + onCancel: Function; + onSuccess: Function; + orgGuid: string; + serviceAccount?: ServiceCredentialBindingObj | undefined | null; + user?: UserOrgPage | undefined | null; +}) { + if (!user) return; + return ( + <> + + + + ); +} diff --git a/src/components/UsersActions/UsersActionsOrgRoles.tsx b/src/components/UsersActions/UsersActionsOrgRoles.tsx index d3970bbc..31c01caf 100644 --- a/src/components/UsersActions/UsersActionsOrgRoles.tsx +++ b/src/components/UsersActions/UsersActionsOrgRoles.tsx @@ -157,7 +157,16 @@ export function UsersActionsOrgRoles({ Org roles have been saved! )} {actionStatus === 'error' && ( - {actionErrors.join(', ')} + + {actionErrors.join(', ')} If the error occurs again, please contact{' '} + + Cloud.gov support + + . + )}
diff --git a/src/components/UsersActions/UsersActionsSpaceRoles/UsersActionsSpaceRoles.tsx b/src/components/UsersActions/UsersActionsSpaceRoles/UsersActionsSpaceRoles.tsx index 1a1de11d..226ad817 100644 --- a/src/components/UsersActions/UsersActionsSpaceRoles/UsersActionsSpaceRoles.tsx +++ b/src/components/UsersActions/UsersActionsSpaceRoles/UsersActionsSpaceRoles.tsx @@ -1,6 +1,7 @@ 'use client'; import React from 'react'; +import Link from 'next/link'; import { getOrgUserSpacesPage } from '@/controllers/controllers'; import { ControllerResult, RolesState } from '@/controllers/controller-types'; import { defaultSpaceRoles } from '@/controllers/controller-helpers'; @@ -17,9 +18,13 @@ type ActionStatus = 'default' | 'pending' | 'success' | 'error'; type SpacesPayload = Array; export function UsersActionsSpaceRoles({ + onCancel, + onSuccess, orgGuid, userGuid, }: { + onCancel?: Function; + onSuccess?: Function; orgGuid: string; userGuid: string; }) { @@ -81,6 +86,7 @@ export function UsersActionsSpaceRoles({ if (meta.status === 'success') { setFetchedDataToState(payload); setActionStatus('success' as ActionStatus); + onSuccess && onSuccess(userGuid, 'space'); } if (meta.status === 'error') { meta?.errors && setActionErrors(meta.errors); @@ -101,47 +107,69 @@ export function UsersActionsSpaceRoles({ } return ( - {actionStatus === 'pending' && ( - Submission in progress... - )} - {actionStatus === 'success' && ( - Changes saved! - )} - {actionStatus === 'error' && ( - {actionErrors.join(', ')} - )}
-

Space roles

- Optional. By assigning additional roles, you can grant access to - space level information and features. + By assigning roles, you can grant a user access to specific + information and features within a given Space.

-
- -
- - {spaces.map((space: any) => ( -
Changes saved! + )} + {actionStatus === 'error' && ( + + {actionErrors.join(', ')} If the error occurs again, please contact{' '} + + Cloud.gov support + + . + + )} +
+ + {spaces.map((space: any) => ( +
+ + Select roles for space: {space.name} + + +
+ ))} +
+
+
+ + + {onCancel && ( +
- ))} -
+ Cancel + + )} +
+ {actionStatus === 'pending' &&

Submission in progress...

} ); } diff --git a/src/components/UsersList/UsersList.tsx b/src/components/UsersList/UsersList.tsx index d2e5f16c..378facbb 100644 --- a/src/components/UsersList/UsersList.tsx +++ b/src/components/UsersList/UsersList.tsx @@ -26,6 +26,7 @@ import { UsersActionsRemoveFromOrg } from '@/components/UsersActions/UsersAction import { OverlayDrawer } from '@/components/OverlayDrawer'; import { OrgUserOrgRolesOverlay } from '@/components/Overlays/OrgUserOrgRolesOverlay'; import { Button } from '@/components/uswds/Button'; +import { SpaceRolesOverlay } from '@/components/Overlays/SpaceRolesOverlay'; type SortDirection = 'asc' | 'desc'; @@ -121,30 +122,30 @@ export function UsersList({ openModal(user); } - // Org roles overlay - - const openOrgRoles = (userId: string) => { - setDialogType('org'); + // Overlays + const openOverlay = (userId: string, type: DialogType = 'org') => { + setDialogType(type); setCurrentMemberId(userId); setDialogOpen(true); }; - const closeOrgRoles = () => { + const closeOverlay = () => { setDialogOpen(false); setCurrentMemberId(''); }; - // success message - - const dismissSuccessMsg = () => { - setSuccessMsg(''); - }; - const onOrgRolesSuccess = (userId: string) => { + const onRolesEditSuccess = (userId: string, type: DialogType = 'org') => { const username = users.find((user) => user.guid === userId)?.username; const usernameText = username ? `for ${username}` : ''; - const msg = `The organization roles ${usernameText} have been updated.`; + const rolesText = type === 'org' ? 'organization' : 'space'; + const msg = `The ${rolesText} roles ${usernameText} have been updated.`; setSuccessMsg(msg); - closeOrgRoles(); + closeOverlay(); + }; + + // Success alert + const dismissSuccessMsg = () => { + setSuccessMsg(''); }; // Helpers @@ -159,23 +160,38 @@ export function UsersList({ closeOrgRoles()} + close={() => closeOverlay()} > {dialogType === 'org' && ( { - closeOrgRoles(); + closeOverlay(); }} orgGuid={orgGuid} user={currentMember} serviceAccount={serviceAccounts[currentMember?.username || '']} - onSuccess={onOrgRolesSuccess} + onSuccess={onRolesEditSuccess} + /> + )} + {dialogType === 'space' && ( + { + closeOverlay(); + }} + orgGuid={orgGuid} + serviceAccount={serviceAccounts[currentMember?.username || '']} + user={currentMember} + onSuccess={onRolesEditSuccess} /> )} {successMsg && ( - + {successMsg}{' '} ); } return ( - + ); } diff --git a/src/components/uswds/Alert.tsx b/src/components/uswds/Alert.tsx index 064b2569..669ca669 100644 --- a/src/components/uswds/Alert.tsx +++ b/src/components/uswds/Alert.tsx @@ -58,7 +58,11 @@ export function Alert({ aria-label={role === 'region' ? `${type} alert` : ''} >
- {heading && {heading}} + {heading && ( + + {heading} + + )} {children && (validation ? ( children From e782ab5d0dc8ba05ed5872902322b29e4dccad69 Mon Sep 17 00:00:00 2001 From: Eleni Chappen Date: Tue, 10 Sep 2024 09:15:18 -0500 Subject: [PATCH 03/10] remove some left padding --- src/components/UsersActions/UsersActionsOrgRoles.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/UsersActions/UsersActionsOrgRoles.tsx b/src/components/UsersActions/UsersActionsOrgRoles.tsx index 31c01caf..9d10ff16 100644 --- a/src/components/UsersActions/UsersActionsOrgRoles.tsx +++ b/src/components/UsersActions/UsersActionsOrgRoles.tsx @@ -173,7 +173,7 @@ export function UsersActionsOrgRoles({ Select org roles -
+
{Object.values(roles).map((role, i) => (
Date: Wed, 11 Sep 2024 09:04:35 -0500 Subject: [PATCH 04/10] add arial-label prop to overlay drawer dialog so that purpose of dialog is announced when open --- src/components/OverlayDrawer.tsx | 5 ++++- src/components/UsersList/UsersList.tsx | 30 ++++++++++++++------------ 2 files changed, 20 insertions(+), 15 deletions(-) diff --git a/src/components/OverlayDrawer.tsx b/src/components/OverlayDrawer.tsx index 6c09470a..4eae7c7b 100644 --- a/src/components/OverlayDrawer.tsx +++ b/src/components/OverlayDrawer.tsx @@ -5,11 +5,13 @@ import Image from 'next/image'; import closeIcon from '@/../public/img/uswds/usa-icons/close.svg'; export function OverlayDrawer({ + ariaLabel = 'dialog', // should announce the purpose of the dialog when opening children, close, // function for dialog close button that should change the isOpen prop from the parent id, isOpen = false, }: { + ariaLabel?: string; children: React.ReactNode; close: Function; id: string; @@ -43,8 +45,8 @@ export function OverlayDrawer({ id={id} className="overlayDrawer height-full maxh-none tablet-lg:width-tablet-lg maxw-none padding-y-10 tablet-lg:padding-y-15 padding-right-1 tablet-lg:padding-right-4 padding-left-3 tablet-lg:padding-left-10 bg-accent-warm-light border-accent-cool tablet-lg:border-accent-cool border-left-1 tablet-lg:border-left-105 border-right-0 border-top-0 border-bottom-0" ref={dialogRef} + aria-label={ariaLabel} > -
{children}
+
{children}
); } diff --git a/src/components/UsersList/UsersList.tsx b/src/components/UsersList/UsersList.tsx index 378facbb..fa45e114 100644 --- a/src/components/UsersList/UsersList.tsx +++ b/src/components/UsersList/UsersList.tsx @@ -30,7 +30,7 @@ import { SpaceRolesOverlay } from '@/components/Overlays/SpaceRolesOverlay'; type SortDirection = 'asc' | 'desc'; -type DialogType = 'org' | 'space'; +type OverlayType = 'org' | 'space'; export function UsersList({ users, @@ -51,8 +51,8 @@ export function UsersList({ const [searchValue, setSearchValue] = useState('' as string); const [sortParam, setSortParam] = useState('username' as string); const [sortDir, setSortDir] = useState('asc' as SortDirection); - const [dialogOpen, setDialogOpen] = useState(false); - const [dialogType, setDialogType] = useState('org' as DialogType); + const [overlayOpen, setOverlayOpen] = useState(false); + const [overlayType, setOverlayType] = useState('org' as OverlayType); const [currentMemberId, setCurrentMemberId] = useState(''); const [successMsg, setSuccessMsg] = useState(''); @@ -123,18 +123,18 @@ export function UsersList({ } // Overlays - const openOverlay = (userId: string, type: DialogType = 'org') => { - setDialogType(type); + const openOverlay = (userId: string, type: OverlayType = 'org') => { + setOverlayType(type); setCurrentMemberId(userId); - setDialogOpen(true); + setOverlayOpen(true); }; const closeOverlay = () => { - setDialogOpen(false); + setOverlayOpen(false); setCurrentMemberId(''); }; - const onRolesEditSuccess = (userId: string, type: DialogType = 'org') => { + const onRolesEditSuccess = (userId: string, type: OverlayType = 'org') => { const username = users.find((user) => user.guid === userId)?.username; const usernameText = username ? `for ${username}` : ''; const rolesText = type === 'org' ? 'organization' : 'space'; @@ -154,34 +154,36 @@ export function UsersList({ const spacesCount = Object.keys(spaces).length; const currentMember = users.find((user) => user.guid === currentMemberId) || null; + const overlayAriaLabel = `Edit ${overlayType === 'org' ? 'organization roles' : 'access permissions'} for ${currentMember ? currentMember?.username : 'this user'}`; return ( <> closeOverlay()} > - {dialogType === 'org' && ( + {overlayType === 'org' && ( { closeOverlay(); }} + onSuccess={onRolesEditSuccess} orgGuid={orgGuid} - user={currentMember} serviceAccount={serviceAccounts[currentMember?.username || '']} - onSuccess={onRolesEditSuccess} + user={currentMember} /> )} - {dialogType === 'space' && ( + {overlayType === 'space' && ( { closeOverlay(); }} + onSuccess={onRolesEditSuccess} orgGuid={orgGuid} serviceAccount={serviceAccounts[currentMember?.username || '']} user={currentMember} - onSuccess={onRolesEditSuccess} /> )} From e93f863b3a177ba46045f8adae500452924c2769 Mon Sep 17 00:00:00 2001 From: Eleni Chappen Date: Wed, 11 Sep 2024 11:17:08 -0500 Subject: [PATCH 05/10] clicking outside dialog will close it --- src/components/OverlayDrawer.tsx | 64 ++++++++++++++++++-------- src/components/UsersList/UsersList.tsx | 10 ++-- 2 files changed, 48 insertions(+), 26 deletions(-) diff --git a/src/components/OverlayDrawer.tsx b/src/components/OverlayDrawer.tsx index 4eae7c7b..35e897c2 100644 --- a/src/components/OverlayDrawer.tsx +++ b/src/components/OverlayDrawer.tsx @@ -8,13 +8,13 @@ export function OverlayDrawer({ ariaLabel = 'dialog', // should announce the purpose of the dialog when opening children, close, // function for dialog close button that should change the isOpen prop from the parent - id, + id = 'overlay-drawer', // helps distinguish which overlay drawer in case there are multiple on the page isOpen = false, }: { ariaLabel?: string; children: React.ReactNode; close: Function; - id: string; + id?: string; isOpen: boolean; }) { const dialogRef = useRef(null); @@ -33,35 +33,59 @@ export function OverlayDrawer({ close(); } }; - window.addEventListener('keydown', handleEscapeKeyPress); + // close when clicking outside dialog + const clicked = (e: MouseEvent) => { + const target = e.target as HTMLElement; + // Clicking #dialog-body will not trigger this, only the id of the dialog itself. + // #dialog-body must cover the full open dialog area for this to work. + if (isOpen && target?.id?.match(id)) { + close(); + } + }; + const addListeners = () => { + window.addEventListener('keydown', handleEscapeKeyPress); + window.addEventListener('click', clicked); + }; + const removeListeners = () => { + window.removeEventListener('keydown', handleEscapeKeyPress); + window.removeEventListener('click', clicked); + }; + if (isOpen) { + addListeners(); + } return () => { - window.removeEventListener('keydown', handleEscapeKeyPress); + removeListeners(); }; - }, [close, isOpen]); + }, [close, id, isOpen]); return ( - -
{children}
+ +
{children}
+
); } diff --git a/src/components/UsersList/UsersList.tsx b/src/components/UsersList/UsersList.tsx index fa45e114..ff83f6c6 100644 --- a/src/components/UsersList/UsersList.tsx +++ b/src/components/UsersList/UsersList.tsx @@ -151,6 +151,7 @@ export function UsersList({ // Helpers const currentUsers = usersSorted(usersFiltered(users)); const usersResultsText = currentUsers.length === 1 ? 'user' : 'users'; + const searchAriaLiveText = `${currentUsers.length} ${usersResultsText} found for ${searchValue}`; const spacesCount = Object.keys(spaces).length; const currentMember = users.find((user) => user.guid === currentMemberId) || null; @@ -160,7 +161,7 @@ export function UsersList({ <> closeOverlay()} > @@ -212,13 +213,10 @@ export function UsersList({ aria-live region needs to show up on initial page render. More info: https://tetralogical.com/blog/2024/05/01/why-are-my-live-regions-not-working/ */} -
+
{searchValue && (
- - {currentUsers.length} {usersResultsText} found for{' '} - {`"${searchValue}"`} - + {searchAriaLiveText}
)}
From 466e7a12812b9a64965f6afae4d73fb62233b162 Mon Sep 17 00:00:00 2001 From: Eleni Chappen Date: Wed, 11 Sep 2024 13:17:02 -0500 Subject: [PATCH 06/10] open up header --- src/components/Overlays/OverlayHeaderUsername.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/Overlays/OverlayHeaderUsername.tsx b/src/components/Overlays/OverlayHeaderUsername.tsx index 44acd96d..0eb3b968 100644 --- a/src/components/Overlays/OverlayHeaderUsername.tsx +++ b/src/components/Overlays/OverlayHeaderUsername.tsx @@ -25,7 +25,7 @@ export function OverlayHeaderUsername({ label="service" /> )} -

+

From 55d6cac530fda9d6020ed90bc7ca35de402ab401 Mon Sep 17 00:00:00 2001 From: Eleni Chappen Date: Wed, 11 Sep 2024 13:26:48 -0500 Subject: [PATCH 07/10] update overlay heading order --- src/components/Overlays/OverlayHeaderUsername.tsx | 10 +++++----- src/components/UsersActions/UsersActionsOrgRoles.tsx | 2 +- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/components/Overlays/OverlayHeaderUsername.tsx b/src/components/Overlays/OverlayHeaderUsername.tsx index 0eb3b968..63080479 100644 --- a/src/components/Overlays/OverlayHeaderUsername.tsx +++ b/src/components/Overlays/OverlayHeaderUsername.tsx @@ -13,21 +13,21 @@ export function OverlayHeaderUsername({ }) { return ( <> -

{header} -

+ {serviceAccount && ( )} -

+

-

+ ); } diff --git a/src/components/UsersActions/UsersActionsOrgRoles.tsx b/src/components/UsersActions/UsersActionsOrgRoles.tsx index 9d10ff16..4eb05d39 100644 --- a/src/components/UsersActions/UsersActionsOrgRoles.tsx +++ b/src/components/UsersActions/UsersActionsOrgRoles.tsx @@ -194,7 +194,7 @@ export function UsersActionsOrgRoles({