Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Functional users list overlays #483

Merged
merged 10 commits into from
Sep 13, 2024
Merged
1 change: 1 addition & 0 deletions .env.example.local
Original file line number Diff line number Diff line change
Expand Up @@ -39,3 +39,4 @@ CF_API_TOKEN=
# Prefixing a variable with NEXT_PUBLIC_ will make it available to the browser:
# https://nextjs.org/docs/app/building-your-application/configuring/environment-variables#bundling-environment-variables-for-the-browser
NEXT_PUBLIC_USER_INVITE_URL=https://account.dev.us-gov-west-1.aws-us-gov.cloud.gov/invite
NEXT_PUBLIC_CLOUD_SUPPORT_URL="mailto:[email protected]?body=What+is+your+question%3F%0D%0A%0D%0APlease+provide+your+application+name+or+URL.+Do+not+include+any+sensitive+information+about+your+platform+in+this+email."
Original file line number Diff line number Diff line change
Expand Up @@ -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.'));
});
});
});
Expand Down
22 changes: 4 additions & 18 deletions __tests__/components/UserAccount/Username.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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(<Username user={mockUser} />);
render(<Username username="User 1" />);
const content = screen.getByText('User 1');
// assert
expect(content).toBeInTheDocument();
Expand All @@ -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(<Username user={mockUser} serviceAccount={mockAccount} />);
render(<Username username="foo user" serviceAccount={mockAccount} />);
const auditor = screen.getByText(/Auditor 1/);
const svcAcct = screen.getByText(/service/);
const username = screen.queryByText(guid);
Expand All @@ -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(<Username user={mockUser} />);
render(<Username username="" />);
const content = screen.getByText('Unnamed user');
// assert
expect(content).toBeInTheDocument();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
18 changes: 17 additions & 1 deletion __tests__/components/UsersList/UsersList.test.js
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -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(
Expand Down
2 changes: 1 addition & 1 deletion src/app/orgs/[orgId]/users/[userId]/actions.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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');
});
Expand Down
7 changes: 6 additions & 1 deletion src/app/orgs/[orgId]/users/[userId]/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,12 @@ export default async function SpaceLayout({
</div>
<div className="margin-top-3">
<PageHeader
heading={<Username user={user} serviceAccount={serviceAccount} />}
heading={
<Username
username={user.username}
serviceAccount={serviceAccount}
/>
}
/>
</div>
{children}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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');
});
Expand Down
68 changes: 67 additions & 1 deletion src/assets/stylesheets/styles.scss
Original file line number Diff line number Diff line change
Expand Up @@ -96,13 +96,24 @@
),

$background-color-palettes: (
'palette-color-system-mint-medium' // no trailing comma
'palette-color-system-mint-medium',
'palette-color-system-green-cool-vivid',
'palette-color-system-red-cool-vivid' // no trailing comma
),

$border-color-palettes: (
'palette-color-system-green-cool',
'palette-color-system-red-vivid' // no trailing comma
),

$top-palettes: (
'palette-units-system-positive'
),

$bottom-palettes: (
'palette-units-system-positive'
),

$right-palettes: (
'palette-units-system-positive'
),
Expand Down Expand Up @@ -312,3 +323,58 @@ dialog.overlayDrawer[open]::backdrop {
background-color: rgb(0 0 0 / 0%);
}
}

// USA Checkbox

.usa-checkbox {
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-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-light);
border-left-color: color($success-color-dark);

.usa-alert__body {
@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
}
}
}
5 changes: 1 addition & 4 deletions src/components/GridList/GridListItem.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,7 @@ import React from 'react';

export function GridListItem({ children }: { children: React.ReactNode }) {
return (
<div
role="listitem"
className="border padding-x-5 padding-y-3 radius-md border-base-lighter bg-white"
>
<div role="listitem" className="padding-y-3">
{children}
</div>
);
Expand Down
67 changes: 47 additions & 20 deletions src/components/OverlayDrawer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,16 @@ import Image from 'next/image';
import closeIcon from '@/../public/img/uswds/usa-icons/close.svg';

export function OverlayDrawer({
beepdotgov marked this conversation as resolved.
Show resolved Hide resolved
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<HTMLDialogElement>(null);
Expand All @@ -31,34 +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 (
<dialog
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"
className="overlayDrawer height-full maxh-none tablet-lg:width-tablet-lg maxw-none border-0 padding-0"
ref={dialogRef}
aria-label={ariaLabel}
>
<div style={{ overscrollBehavior: 'contain' }}>{children}</div>
<button
type="button"
className="usa-button usa-modal__close position-fixed top-7 right-4"
aria-label="Close this dialog"
onClick={() => close()}
<div
id="dialog-body"
className="minh-full 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"
>
<Image
unoptimized
src={closeIcon}
alt="Close this dialog"
width="32"
height="32"
/>
</button>
<button
type="button"
className="usa-button usa-modal__close position-fixed top-7 right-4"
aria-label="Close this dialog"
onClick={() => close()}
>
<Image
unoptimized
src={closeIcon}
alt="Close this dialog"
width="32"
height="32"
/>
</button>
<div>{children}</div>
</div>
</dialog>
);
}
41 changes: 41 additions & 0 deletions src/components/Overlays/OrgUserOrgRolesOverlay.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import { UsersActionsOrgRoles } from '@/components/UsersActions/UsersActionsOrgRoles';
import { UserOrgPage } from '@/controllers/controller-types';
import { ServiceCredentialBindingObj } from '@/api/cf/cloudfoundry-types';
import { OverlayHeaderUsername } from './OverlayHeaderUsername';

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 (
<>
<OverlayHeaderUsername
header="update organization roles"
serviceAccount={serviceAccount}
username={user.username}
/>
<div className="usa-prose">
<p>
By assigning specific roles, you can grant a user access to specific
information and features within a given organization.
echappen marked this conversation as resolved.
Show resolved Hide resolved
</p>
<UsersActionsOrgRoles
orgGuid={orgGuid}
userGuid={user.guid}
onCancel={onCancel}
onSuccess={onSuccess}
/>
</div>
</>
);
}
Loading