* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
+
import { LargeCenteredLayout, PageContentFontWrapper, TopBar } from 'design-system';
import * as React from 'react';
+import { createPortal } from 'react-dom';
import { Helmet } from 'react-helmet-async';
import { Outlet } from 'react-router-dom';
import { useCurrentLoginUser } from '../../app/components/current-user/CurrentUserContext';
export default function Account() {
const currentUser = useCurrentLoginUser();
+ const [portalAnchor, setPortalAnchor] = React.useState<Element | null>(null);
+
+ // Set portal anchor on mount
+ React.useEffect(() => {
+ setPortalAnchor(document.getElementById('component-nav-portal'));
+ }, []);
const title = translate('my_account.page');
+
return (
<div id="account-page">
- <header>
- <TopBar>
- <div className="sw-flex sw-items-center sw-gap-2 sw-pb-4">
- <UserCard user={currentUser} />
- </div>
- <Nav />
- </TopBar>
- </header>
+ {portalAnchor &&
+ createPortal(
+ <header>
+ <TopBar>
+ <div className="sw-flex sw-items-center sw-gap-2 sw-pb-4">
+ <UserCard user={currentUser} />
+ </div>
+
+ <Nav />
+ </TopBar>
+ </header>,
+ portalAnchor,
+ )}
<LargeCenteredLayout as="main">
<PageContentFontWrapper className="sw-body-sm sw-py-8">
<Suggestions suggestions="account" />
+
<Helmet
defaultTitle={title}
defer={false}
translate('my_account.page'),
)}
/>
+
<A11ySkipTarget anchor="account_main" />
+
<Outlet />
</PageContentFontWrapper>
</LargeCenteredLayout>
import { screen, waitFor, within } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { UserEvent } from '@testing-library/user-event/dist/types/setup/setup';
+import React from 'react';
+import { Outlet, Route } from 'react-router-dom';
import selectEvent from 'react-select-event';
import { getMyProjects, getScannableProjects } from '../../../api/components';
import NotificationsMock from '../../../api/mocks/NotificationsMock';
expect(screen.getByText(name)).toBeInTheDocument();
- expect(screen.getByRole('heading', { name: 'my_account.profile' })).toBeInTheDocument();
+ expect(screen.getByText('my_account.profile')).toBeInTheDocument();
expect(screen.getByText('my_account.security')).toBeInTheDocument();
expect(screen.getByText('my_account.notifications')).toBeInTheDocument();
expect(screen.getByText('my_account.projects')).toBeInTheDocument();
securityPagePath,
);
- expect(await screen.findByText('users.tokens')).toBeInTheDocument();
+ expect(await screen.findByText('users.tokens.generate')).toBeInTheDocument();
await waitFor(() => expect(screen.getAllByRole('row')).toHaveLength(3)); // 2 tokens + header
// Add the token
securityPagePath,
);
- expect(await screen.findByText('users.tokens')).toBeInTheDocument();
+ expect(await screen.findByText('users.tokens.generate')).toBeInTheDocument();
// expired token is flagged as such
const expiredTokenRow = await screen.findByRole('row', { name: /expired token/ });
}
function renderAccountApp(currentUser: CurrentUser, navigateTo?: string) {
- renderAppRoutes('account', routes, { currentUser, navigateTo });
+ renderAppRoutes(
+ 'account',
+ () => (
+ <Route
+ path="/"
+ element={
+ <>
+ <div id="component-nav-portal" />
+
+ <Outlet />
+ </>
+ }
+ >
+ {routes()}
+ </Route>
+ ),
+ { currentUser, navigateTo },
+ );
}
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
+
+import { ButtonPrimary, FlagMessage, FormField, InputField } from 'design-system';
import * as React from 'react';
import { changePassword } from '../../api/users';
-import { SubmitButton } from '../../components/controls/buttons';
-import { Alert } from '../../components/ui/Alert';
-import MandatoryFieldMarker from '../../components/ui/MandatoryFieldMarker';
import MandatoryFieldsExplanation from '../../components/ui/MandatoryFieldsExplanation';
import { translate } from '../../helpers/l10n';
import { ChangePasswordResults, LoggedInUser } from '../../types/users';
interface Props {
className?: string;
- user: LoggedInUser;
onPasswordChange?: () => void;
+ user: LoggedInUser;
}
-interface State {
- errors?: string[];
- success: boolean;
-}
+export default function ResetPasswordForm({
+ className,
+ onPasswordChange,
+ user: { login },
+}: Readonly<Props>) {
+ const [error, setError] = React.useState<string | undefined>(undefined);
+ const [oldPassword, setOldPassword] = React.useState('');
+ const [password, setPassword] = React.useState('');
+ const [passwordConfirmation, setPasswordConfirmation] = React.useState('');
+ const [success, setSuccess] = React.useState(false);
-export default class ResetPasswordForm extends React.Component<Props, State> {
- oldPassword: HTMLInputElement | null = null;
- password: HTMLInputElement | null = null;
- passwordConfirmation: HTMLInputElement | null = null;
- state: State = {
- success: false,
- };
+ const handleSuccessfulChange = () => {
+ setOldPassword('');
+ setPassword('');
+ setPasswordConfirmation('');
+ setSuccess(true);
- handleSuccessfulChange = () => {
- if (!this.oldPassword || !this.password || !this.passwordConfirmation) {
- return;
- }
- this.oldPassword.value = '';
- this.password.value = '';
- this.passwordConfirmation.value = '';
- this.setState({ success: true, errors: undefined });
- if (this.props.onPasswordChange) {
- this.props.onPasswordChange();
- }
- };
-
- setErrors = (errors: string[]) => {
- this.setState({ success: false, errors });
+ onPasswordChange?.();
};
- handleChangePassword = (event: React.FormEvent) => {
+ const handleChangePassword = (event: React.FormEvent) => {
event.preventDefault();
- if (!this.oldPassword || !this.password || !this.passwordConfirmation) {
- return;
- }
- const { user } = this.props;
- const previousPassword = this.oldPassword.value;
- const password = this.password.value;
- const passwordConfirmation = this.passwordConfirmation.value;
+
+ setError(undefined);
+ setSuccess(false);
if (password !== passwordConfirmation) {
- this.password.focus();
- this.setErrors([translate('user.password_doesnt_match_confirmation')]);
+ setError(translate('user.password_doesnt_match_confirmation'));
} else {
- changePassword({ login: user.login, password, previousPassword })
- .then(this.handleSuccessfulChange)
+ changePassword({ login, password, previousPassword: oldPassword })
+ .then(handleSuccessfulChange)
.catch((result: ChangePasswordResults) => {
if (result === ChangePasswordResults.OldPasswordIncorrect) {
- this.setErrors([translate('user.old_password_incorrect')]);
+ setError(translate('user.old_password_incorrect'));
} else if (result === ChangePasswordResults.NewPasswordSameAsOld) {
- this.setErrors([translate('user.new_password_same_as_old')]);
+ setError(translate('user.new_password_same_as_old'));
}
});
}
};
- render() {
- const { className } = this.props;
- const { success, errors } = this.state;
-
- return (
- <form className={className} onSubmit={this.handleChangePassword}>
- {success && <Alert variant="success">{translate('my_profile.password.changed')}</Alert>}
+ return (
+ <form className={className} onSubmit={handleChangePassword}>
+ {success && (
+ <div className="sw-pb-4">
+ <FlagMessage variant="success">{translate('my_profile.password.changed')}</FlagMessage>
+ </div>
+ )}
- {errors &&
- errors.map((e) => (
- <Alert key={e} variant="error">
- {e}
- </Alert>
- ))}
+ {error !== undefined && (
+ <div className="sw-pb-4">
+ <FlagMessage variant="error">{error}</FlagMessage>
+ </div>
+ )}
- <MandatoryFieldsExplanation className="form-field" />
+ <MandatoryFieldsExplanation className="sw-block sw-clear-both sw-pb-4" />
- <div className="form-field">
- <label htmlFor="old_password">
- {translate('my_profile.password.old')}
- <MandatoryFieldMarker />
- </label>
- <input
+ <div className="sw-pb-4">
+ <FormField htmlFor="old_password" label={translate('my_profile.password.old')} required>
+ <InputField
autoComplete="off"
id="old_password"
name="old_password"
- ref={(elem) => (this.oldPassword = elem)}
+ onChange={(e: React.ChangeEvent<HTMLInputElement>) => setOldPassword(e.target.value)}
required
type="password"
+ value={oldPassword}
/>
- </div>
- <div className="form-field">
- <label htmlFor="password">
- {translate('my_profile.password.new')}
- <MandatoryFieldMarker />
- </label>
- <input
+ </FormField>
+ </div>
+
+ <div className="sw-pb-4">
+ <FormField htmlFor="password" label={translate('my_profile.password.new')} required>
+ <InputField
autoComplete="off"
id="password"
name="password"
- ref={(elem) => (this.password = elem)}
+ onChange={(e: React.ChangeEvent<HTMLInputElement>) => setPassword(e.target.value)}
required
type="password"
+ value={password}
/>
- </div>
- <div className="form-field">
- <label htmlFor="password_confirmation">
- {translate('my_profile.password.confirm')}
- <MandatoryFieldMarker />
- </label>
- <input
+ </FormField>
+ </div>
+
+ <div className="sw-pb-4">
+ <FormField
+ htmlFor="password_confirmation"
+ label={translate('my_profile.password.confirm')}
+ required
+ >
+ <InputField
autoComplete="off"
id="password_confirmation"
name="password_confirmation"
- ref={(elem) => (this.passwordConfirmation = elem)}
+ onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
+ setPasswordConfirmation(e.target.value)
+ }
required
type="password"
+ value={passwordConfirmation}
/>
- </div>
- <div className="form-field">
- <SubmitButton id="change-password">{translate('update_verb')}</SubmitButton>
- </div>
- </form>
- );
- }
+ </FormField>
+ </div>
+
+ <div className="sw-py-3">
+ <ButtonPrimary id="change-password" type="submit">
+ {translate('update_verb')}
+ </ButtonPrimary>
+ </div>
+ </form>
+ );
}