fix: Move content into to frontend directory

Doing this BEFORE the merge prevents a lot of merge conflicts.

Signed-off-by: Tilman Vatteroth <git@tilmanvatteroth.de>
This commit is contained in:
Tilman Vatteroth 2022-11-11 11:16:18 +01:00
parent 4e18ce38f3
commit 762a0a850e
No known key found for this signature in database
GPG key ID: B97799103358209B
1051 changed files with 0 additions and 35 deletions

View file

@ -0,0 +1,55 @@
/*
* SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import React from 'react'
import { cypressId } from '../../../utils/cypress-attribute'
import { Button, Modal } from 'react-bootstrap'
import { Trans } from 'react-i18next'
import { CopyableField } from '../../common/copyable/copyable-field/copyable-field'
import type { ModalVisibilityProps } from '../../common/modals/common-modal'
import { CommonModal } from '../../common/modals/common-modal'
import type { AccessTokenWithSecret } from '../../../api/tokens/types'
export interface AccessTokenCreatedModalProps extends ModalVisibilityProps {
tokenWithSecret?: AccessTokenWithSecret
}
/**
* Modal that shows the secret of a freshly created access token.
*
* @param show True when the modal should be shown, false otherwise.
* @param onHide Callback that gets called when the modal should be dismissed.
* @param tokenWithSecret The token altogether with its secret.
*/
export const AccessTokenCreatedModal: React.FC<AccessTokenCreatedModalProps> = ({ show, onHide, tokenWithSecret }) => {
if (!tokenWithSecret) {
return null
}
return (
<CommonModal
show={show}
onHide={onHide}
title='profile.modal.addedAccessToken.title'
{...cypressId('access-token-modal-add')}>
<Modal.Body>
<Trans
i18nKey='profile.modal.addedAccessToken.message'
values={{
label: tokenWithSecret.label
}}
/>
<br />
<CopyableField content={tokenWithSecret.secret} />
</Modal.Body>
<Modal.Footer>
<Button variant='primary' onClick={onHide}>
<Trans i18nKey='common.close' />
</Button>
</Modal.Footer>
</CommonModal>
)
}

View file

@ -0,0 +1,49 @@
/*
* SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import type { ChangeEvent } from 'react'
import React from 'react'
import { Form } from 'react-bootstrap'
import { Trans, useTranslation } from 'react-i18next'
import { cypressId } from '../../../../utils/cypress-attribute'
import { useExpiryDates } from './hooks/use-expiry-dates'
interface AccessTokenCreationFormExpiryFieldProps extends AccessTokenCreationFormFieldProps {
onChangeExpiry: (event: ChangeEvent<HTMLInputElement>) => void
}
/**
* Input field for expiry of a new token.
*
* @param formValues The values of the stored form values.
* @param onChangeExpiry Callback that updates the stored expiry form value.
*/
export const AccessTokenCreationFormExpiryField: React.FC<AccessTokenCreationFormExpiryFieldProps> = ({
onChangeExpiry,
formValues
}) => {
useTranslation()
const minMaxDefaultDates = useExpiryDates()
return (
<Form.Group>
<Form.Label>
<Trans i18nKey={'profile.accessTokens.expiry'} />
</Form.Label>
<Form.Control
type='date'
size='sm'
value={formValues.expiryDate}
className='bg-dark text-light'
onChange={onChangeExpiry}
min={minMaxDefaultDates.min}
max={minMaxDefaultDates.max}
required
{...cypressId('access-token-add-input-expiry')}
/>
</Form.Group>
)
}

View file

@ -0,0 +1,12 @@
/*
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
interface AccessTokenCreationFormFieldProps {
formValues: {
expiryDate: string
label: string
}
}

View file

@ -0,0 +1,51 @@
/*
* SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import type { ChangeEvent } from 'react'
import React, { useMemo } from 'react'
import { Form } from 'react-bootstrap'
import { Trans, useTranslation } from 'react-i18next'
import { cypressId } from '../../../../utils/cypress-attribute'
interface AccessTokenCreationFormLabelFieldProps extends AccessTokenCreationFormFieldProps {
onChangeLabel: (event: ChangeEvent<HTMLInputElement>) => void
}
/**
* Input field for the label of a new token.
*
* @param onChangeLabel Callback for updating the stored label form value.
* @param formValues The stored form values.
*/
export const AccessTokenCreationFormLabelField: React.FC<AccessTokenCreationFormLabelFieldProps> = ({
onChangeLabel,
formValues
}) => {
const { t } = useTranslation()
const labelValid = useMemo(() => {
return formValues.label.trim() !== ''
}, [formValues])
return (
<Form.Group>
<Form.Label>
<Trans i18nKey={'profile.accessTokens.label'} />
</Form.Label>
<Form.Control
type='text'
size='sm'
placeholder={t('profile.accessTokens.label') ?? undefined}
value={formValues.label}
className='bg-dark text-light'
onChange={onChangeLabel}
isValid={labelValid}
required
{...cypressId('access-token-add-input-label')}
/>
</Form.Group>
)
}

View file

@ -0,0 +1,30 @@
/*
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import React, { useMemo } from 'react'
import { cypressId } from '../../../../utils/cypress-attribute'
import { Trans } from 'react-i18next'
import { Button } from 'react-bootstrap'
/**
* Submit button for creating a new access token.
*/
export const AccessTokenCreationFormSubmitButton: React.FC<AccessTokenCreationFormFieldProps> = ({ formValues }) => {
const validFormValues = useMemo(() => {
return formValues.label.trim() !== '' && formValues.expiryDate.trim() !== ''
}, [formValues])
return (
<Button
type='submit'
variant='primary'
size='sm'
disabled={!validFormValues}
{...cypressId('access-token-add-button')}>
<Trans i18nKey='profile.accessTokens.createToken' />
</Button>
)
}

View file

@ -0,0 +1,87 @@
/*
* SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import type { ChangeEvent } from 'react'
import React, { Fragment, useCallback, useMemo, useState } from 'react'
import { Form } from 'react-bootstrap'
import { Trans, useTranslation } from 'react-i18next'
import { AccessTokenCreatedModal } from '../access-token-created-modal'
import type { AccessTokenWithSecret } from '../../../../api/tokens/types'
import { AccessTokenCreationFormLabelField } from './access-token-creation-form-label-field'
import { AccessTokenCreationFormExpiryField } from './access-token-creation-form-expiry-field'
import { AccessTokenCreationFormSubmitButton } from './access-token-creation-form-submit-button'
import { useExpiryDates } from './hooks/use-expiry-dates'
import { useOnCreateToken } from './hooks/use-on-create-token'
import type { AccessTokenUpdateProps } from '../profile-access-tokens'
interface NewTokenFormValues {
label: string
expiryDate: string
}
/**
* Form for creating a new access token.
*
* @param onUpdateList Callback that is fired when a token was created to update the list.
*/
export const AccessTokenCreationForm: React.FC<AccessTokenUpdateProps> = ({ onUpdateList }) => {
useTranslation()
const expiryDates = useExpiryDates()
const formValuesInitialState = useMemo(() => {
return {
expiryDate: expiryDates.default,
label: ''
}
}, [expiryDates])
const [formValues, setFormValues] = useState<NewTokenFormValues>(() => formValuesInitialState)
const [newTokenWithSecret, setNewTokenWithSecret] = useState<AccessTokenWithSecret>()
const onHideCreatedModal = useCallback(() => {
setFormValues(formValuesInitialState)
setNewTokenWithSecret(undefined)
onUpdateList()
}, [formValuesInitialState, onUpdateList])
const onCreateToken = useOnCreateToken(formValues.label, formValues.expiryDate, setNewTokenWithSecret)
const onChangeExpiry = useCallback((event: ChangeEvent<HTMLInputElement>) => {
setFormValues((previousValues) => {
return {
...previousValues,
expiryDate: event.target.value
}
})
}, [])
const onChangeLabel = useCallback((event: ChangeEvent<HTMLInputElement>) => {
setFormValues((previousValues) => {
return {
...previousValues,
label: event.target.value
}
})
}, [])
return (
<Fragment>
<h5>
<Trans i18nKey={'profile.accessTokens.createToken'} />
</h5>
<Form onSubmit={onCreateToken} className='text-start'>
<AccessTokenCreationFormLabelField onChangeLabel={onChangeLabel} formValues={formValues} />
<AccessTokenCreationFormExpiryField onChangeExpiry={onChangeExpiry} formValues={formValues} />
<AccessTokenCreationFormSubmitButton formValues={formValues} />
</Form>
<AccessTokenCreatedModal
tokenWithSecret={newTokenWithSecret}
show={!!newTokenWithSecret}
onHide={onHideCreatedModal}
/>
</Fragment>
)
}

View file

@ -0,0 +1,38 @@
/*
* SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { useMemo } from 'react'
import { DateTime } from 'luxon'
interface ExpiryDates {
default: string
min: string
max: string
}
/**
* Returns the minimal, maximal and default expiry date for new access tokens.
*
* @return Memoized expiry dates.
*/
export const useExpiryDates = (): ExpiryDates => {
return useMemo(() => {
const today = DateTime.now()
return {
min: today.toISODate(),
max: today
.plus({
year: 2
})
.toISODate(),
default: today
.plus({
year: 1
})
.toISODate()
}
}, [])
}

View file

@ -0,0 +1,41 @@
/*
* SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { DateTime } from 'luxon'
import type { FormEvent } from 'react'
import { useCallback } from 'react'
import { postNewAccessToken } from '../../../../../api/tokens'
import type { AccessTokenWithSecret } from '../../../../../api/tokens/types'
import { useUiNotifications } from '../../../../notifications/ui-notification-boundary'
/**
* Callback for requesting a new access token from the API and returning the response token and secret.
*
* @param label The label for the new access token.
* @param expiryDate The expiry date of the new access token.
* @param setNewTokenWithSecret Callback to set the new access token with the secret from the API.
* @return Callback that can be called when the new access token should be requested.
*/
export const useOnCreateToken = (
label: string,
expiryDate: string,
setNewTokenWithSecret: (token: AccessTokenWithSecret) => void
): ((event: FormEvent) => void) => {
const { showErrorNotification } = useUiNotifications()
return useCallback(
(event: FormEvent) => {
event.preventDefault()
const expiryInMillis = DateTime.fromFormat(expiryDate, 'yyyy-MM-dd').toMillis()
postNewAccessToken(label, expiryInMillis)
.then((tokenWithSecret) => {
setNewTokenWithSecret(tokenWithSecret)
})
.catch(showErrorNotification('profile.accessTokens.creationFailed'))
},
[expiryDate, label, setNewTokenWithSecret, showErrorNotification]
)
}

View file

@ -0,0 +1,65 @@
/*
* SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import React, { useCallback } from 'react'
import type { ModalVisibilityProps } from '../../common/modals/common-modal'
import { CommonModal } from '../../common/modals/common-modal'
import { cypressId } from '../../../utils/cypress-attribute'
import { Button, Modal } from 'react-bootstrap'
import { Trans, useTranslation } from 'react-i18next'
import type { AccessToken } from '../../../api/tokens/types'
import { deleteAccessToken } from '../../../api/tokens'
import { useUiNotifications } from '../../notifications/ui-notification-boundary'
export interface AccessTokenDeletionModalProps extends ModalVisibilityProps {
token: AccessToken
}
/**
* Modal that asks for confirmation when deleting an access token.
*
* @param show True when the deletion modal should be shown, false otherwise.
* @param token The access token to delete.
* @param onHide Callback that is fired when the modal is closed.
*/
export const AccessTokenDeletionModal: React.FC<AccessTokenDeletionModalProps> = ({ show, token, onHide }) => {
useTranslation()
const { showErrorNotification, dispatchUiNotification } = useUiNotifications()
const onConfirmDelete = useCallback(() => {
deleteAccessToken(token.keyId)
.then(() => {
return dispatchUiNotification(
'profile.modal.deleteAccessToken.notificationTitle',
'profile.modal.deleteAccessToken.notificationText',
{
contentI18nOptions: {
label: token.label
}
}
)
})
.catch(showErrorNotification('profile.modal.deleteAccessToken.failed'))
.finally(() => onHide?.())
}, [token.keyId, token.label, showErrorNotification, dispatchUiNotification, onHide])
return (
<CommonModal
show={show}
onHide={onHide}
title={'profile.modal.deleteAccessToken.title'}
{...cypressId('access-token-modal-delete')}>
<Modal.Body>
<Trans i18nKey='profile.modal.deleteAccessToken.message' values={{ label: token.label }} />
</Modal.Body>
<Modal.Footer>
<Button variant='danger' onClick={onConfirmDelete}>
<Trans i18nKey={'common.delete'} />
</Button>
</Modal.Footer>
</CommonModal>
)
}

View file

@ -0,0 +1,75 @@
/*
* SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import React, { useCallback, useMemo } from 'react'
import { Col, ListGroup, Row } from 'react-bootstrap'
import { cypressId } from '../../../utils/cypress-attribute'
import { Trans, useTranslation } from 'react-i18next'
import { DateTime } from 'luxon'
import { IconButton } from '../../common/icon-button/icon-button'
import type { AccessToken } from '../../../api/tokens/types'
import { AccessTokenDeletionModal } from './access-token-deletion-modal'
import type { AccessTokenUpdateProps } from './profile-access-tokens'
import { useBooleanState } from '../../../hooks/common/use-boolean-state'
export interface AccessTokenListEntryProps {
token: AccessToken
}
/**
* List entry that represents an access token with the possibility to delete it.
*
* @param token The access token.
* @param onUpdateList Callback that is fired when the deletion modal is closed to update the token list.
*/
export const AccessTokenListEntry: React.FC<AccessTokenListEntryProps & AccessTokenUpdateProps> = ({
token,
onUpdateList
}) => {
useTranslation()
const [modalVisibility, showModal, closeModal] = useBooleanState()
const onHideDeletionModal = useCallback(() => {
closeModal()
onUpdateList()
}, [closeModal, onUpdateList])
const lastUsed = useMemo(() => {
if (!token.lastUsedAt) {
return <Trans i18nKey={'profile.accessTokens.neverUsed'} />
}
return (
<Trans
i18nKey='profile.accessTokens.lastUsed'
values={{
time: DateTime.fromISO(token.lastUsedAt).toRelative({
style: 'short'
})
}}
/>
)
}, [token.lastUsedAt])
return (
<ListGroup.Item className='bg-dark'>
<Row>
<Col className='text-start' {...cypressId('access-token-label')}>
{token.label}
</Col>
<Col className='text-start text-white-50'>{lastUsed}</Col>
<Col xs='auto'>
<IconButton
icon='trash-o'
variant='danger'
onClick={showModal}
{...cypressId('access-token-delete-button')}
/>
</Col>
</Row>
<AccessTokenDeletionModal token={token} show={modalVisibility} onHide={onHideDeletionModal} />
</ListGroup.Item>
)
}

View file

@ -0,0 +1,72 @@
/*
* SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import React, { useCallback, useEffect, useMemo, useState } from 'react'
import { Card, ListGroup } from 'react-bootstrap'
import { Trans, useTranslation } from 'react-i18next'
import { getAccessTokenList } from '../../../api/tokens'
import type { AccessToken } from '../../../api/tokens/types'
import { ShowIf } from '../../common/show-if/show-if'
import { AccessTokenListEntry } from './access-token-list-entry'
import { AccessTokenCreationForm } from './access-token-creation-form/access-token-creation-form'
import { useUiNotifications } from '../../notifications/ui-notification-boundary'
export interface AccessTokenUpdateProps {
onUpdateList: () => void
}
/**
* Profile page section that shows the user's access tokens and allows to manage them.
*/
export const ProfileAccessTokens: React.FC = () => {
useTranslation()
const [accessTokens, setAccessTokens] = useState<AccessToken[]>([])
const { showErrorNotification } = useUiNotifications()
const refreshAccessTokens = useCallback(() => {
getAccessTokenList()
.then((tokens) => {
setAccessTokens(tokens)
})
.catch(showErrorNotification('profile.accessTokens.loadingFailed'))
}, [showErrorNotification])
useEffect(() => {
refreshAccessTokens()
}, [refreshAccessTokens])
const tokensDom = useMemo(
() =>
accessTokens.map((token) => (
<AccessTokenListEntry token={token} key={token.keyId} onUpdateList={refreshAccessTokens} />
)),
[accessTokens, refreshAccessTokens]
)
return (
<Card className='bg-dark mb-4 access-tokens'>
<Card.Body>
<Card.Title>
<Trans i18nKey='profile.accessTokens.title' />
</Card.Title>
<p className='text-start'>
<Trans i18nKey='profile.accessTokens.info' />
</p>
<p className='text-start small'>
<Trans i18nKey='profile.accessTokens.infoDev' />
</p>
<hr />
<ShowIf condition={accessTokens.length === 0}>
<Trans i18nKey='profile.accessTokens.noTokens' />
</ShowIf>
<ListGroup>{tokensDom}</ListGroup>
<hr />
<ShowIf condition={accessTokens.length < 200}>
<AccessTokenCreationForm onUpdateList={refreshAccessTokens} />
</ShowIf>
</Card.Body>
</Card>
)
}

View file

@ -0,0 +1,60 @@
/*
* SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import React, { useCallback } from 'react'
import type { ModalVisibilityProps } from '../../common/modals/common-modal'
import { CommonModal } from '../../common/modals/common-modal'
import { Trans, useTranslation } from 'react-i18next'
import { Button, Modal } from 'react-bootstrap'
import { CountdownButton } from '../../common/countdown-button/countdown-button'
import { deleteUser } from '../../../api/me'
import { clearUser } from '../../../redux/user/methods'
import { useUiNotifications } from '../../notifications/ui-notification-boundary'
/**
* Confirmation modal for deleting your account.
*
* @param show True if the modal should be shown, false otherwise.
* @param onHide Callback that is fired when the modal is closed.
*/
export const AccountDeletionModal: React.FC<ModalVisibilityProps> = ({ show, onHide }) => {
useTranslation()
const { showErrorNotification, dispatchUiNotification } = useUiNotifications()
const deleteUserAccount = useCallback(() => {
deleteUser()
.then(() => {
clearUser()
return dispatchUiNotification(
'profile.modal.deleteUser.notificationTitle',
'profile.modal.deleteUser.notificationText',
{}
)
})
.catch(showErrorNotification('profile.modal.deleteUser.failed'))
.finally(() => {
if (onHide) {
onHide()
}
})
}, [dispatchUiNotification, onHide, showErrorNotification])
return (
<CommonModal show={show} title={'profile.modal.deleteUser.message'} onHide={onHide} showCloseButton={true}>
<Modal.Body>
<Trans i18nKey='profile.modal.deleteUser.subMessage' />
</Modal.Body>
<Modal.Footer>
<Button variant='secondary' onClick={onHide}>
<Trans i18nKey='common.close' />
</Button>
<CountdownButton variant='danger' onClick={deleteUserAccount} countdownStartSeconds={10}>
<Trans i18nKey={'profile.modal.deleteUser.title'} />
</CountdownButton>
</Modal.Footer>
</CommonModal>
)
}

View file

@ -0,0 +1,45 @@
/*
* SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import React, { Fragment } from 'react'
import { Button, Card, Row } from 'react-bootstrap'
import { Trans, useTranslation } from 'react-i18next'
import { ForkAwesomeIcon } from '../../common/fork-awesome/fork-awesome-icon'
import { AccountDeletionModal } from './account-deletion-modal'
import { useBooleanState } from '../../../hooks/common/use-boolean-state'
/**
* Profile page section that allows to export all data from the account or to delete the account.
*/
export const ProfileAccountManagement: React.FC = () => {
useTranslation()
const [modalVisibility, showModal, closeModal] = useBooleanState()
return (
<Fragment>
<Card className='bg-dark mb-4'>
<Card.Body>
<Card.Title>
<Trans i18nKey='profile.accountManagement' />
</Card.Title>
<Row>
<Button variant='secondary' href={'me/export'} className='mb-2'>
<ForkAwesomeIcon icon='cloud-download' fixedWidth={true} className='mx-2' />
<Trans i18nKey='profile.exportUserData' />
</Button>
</Row>
<Row>
<Button variant='danger' onClick={showModal}>
<ForkAwesomeIcon icon='trash' fixedWidth={true} className='mx-2' />
<Trans i18nKey='profile.deleteUser' />
</Button>
</Row>
</Card.Body>
</Card>
<AccountDeletionModal show={modalVisibility} onHide={closeModal} />
</Fragment>
)
}

View file

@ -0,0 +1,83 @@
/*
* SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import type { FormEvent } from 'react'
import React, { useCallback, useMemo, useRef, useState } from 'react'
import { Button, Card, Form } from 'react-bootstrap'
import { Trans, useTranslation } from 'react-i18next'
import { doLocalPasswordChange } from '../../../api/auth/local'
import { useOnInputChange } from '../../../hooks/common/use-on-input-change'
import { NewPasswordField } from '../../common/fields/new-password-field'
import { PasswordAgainField } from '../../common/fields/password-again-field'
import { CurrentPasswordField } from '../../common/fields/current-password-field'
import { useUiNotifications } from '../../notifications/ui-notification-boundary'
/**
* Profile page section for changing the password when using internal login.
*/
export const ProfileChangePassword: React.FC = () => {
useTranslation()
const { showErrorNotification, dispatchUiNotification } = useUiNotifications()
const [oldPassword, setOldPassword] = useState('')
const [newPassword, setNewPassword] = useState('')
const [newPasswordAgain, setNewPasswordAgain] = useState('')
const formRef = useRef<HTMLFormElement>(null)
const onChangeOldPassword = useOnInputChange(setOldPassword)
const onChangeNewPassword = useOnInputChange(setNewPassword)
const onChangeNewPasswordAgain = useOnInputChange(setNewPasswordAgain)
const onSubmitPasswordChange = useCallback(
(event: FormEvent) => {
event.preventDefault()
doLocalPasswordChange(oldPassword, newPassword)
.then(() =>
dispatchUiNotification('profile.changePassword.successTitle', 'profile.changePassword.successText', {
icon: 'check'
})
)
.catch(showErrorNotification('profile.changePassword.failed'))
.finally(() => {
if (formRef.current) {
formRef.current.reset()
}
setOldPassword('')
setNewPassword('')
setNewPasswordAgain('')
})
},
[oldPassword, newPassword, showErrorNotification, dispatchUiNotification]
)
const ready = useMemo(() => {
return (
oldPassword.trim() !== '' &&
newPassword.trim() !== '' &&
newPasswordAgain.trim() !== '' &&
newPassword === newPasswordAgain
)
}, [oldPassword, newPassword, newPasswordAgain])
return (
<Card className='bg-dark mb-4'>
<Card.Body>
<Card.Title>
<Trans i18nKey='profile.changePassword.title' />
</Card.Title>
<Form onSubmit={onSubmitPasswordChange} className='text-left' ref={formRef}>
<CurrentPasswordField onChange={onChangeOldPassword} value={oldPassword} />
<NewPasswordField onChange={onChangeNewPassword} value={newPassword} />
<PasswordAgainField password={newPassword} onChange={onChangeNewPasswordAgain} value={newPasswordAgain} />
<Button type='submit' variant='primary' disabled={!ready}>
<Trans i18nKey='common.save' />
</Button>
</Form>
</Card.Body>
</Card>
)
}

View file

@ -0,0 +1,58 @@
/*
* SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import type { FormEvent } from 'react'
import React, { useCallback, useMemo, useState } from 'react'
import { Button, Card, Form } from 'react-bootstrap'
import { Trans, useTranslation } from 'react-i18next'
import { updateDisplayName } from '../../../api/me'
import { fetchAndSetUser } from '../../login-page/auth/utils'
import { useApplicationState } from '../../../hooks/common/use-application-state'
import { DisplayNameField } from '../../common/fields/display-name-field'
import { useOnInputChange } from '../../../hooks/common/use-on-input-change'
import { useUiNotifications } from '../../notifications/ui-notification-boundary'
/**
* Profile page section for changing the current display name.
*/
export const ProfileDisplayName: React.FC = () => {
useTranslation()
const userName = useApplicationState((state) => state.user?.displayName)
const [displayName, setDisplayName] = useState(userName ?? '')
const { showErrorNotification } = useUiNotifications()
const onChangeDisplayName = useOnInputChange(setDisplayName)
const onSubmitNameChange = useCallback(
(event: FormEvent) => {
event.preventDefault()
updateDisplayName(displayName)
.then(fetchAndSetUser)
.catch(showErrorNotification('profile.changeDisplayNameFailed'))
},
[displayName, showErrorNotification]
)
const formSubmittable = useMemo(() => {
return displayName.trim() !== '' && displayName !== userName
}, [displayName, userName])
return (
<Card className='bg-dark mb-4'>
<Card.Body>
<Card.Title>
<Trans i18nKey='profile.userProfile' />
</Card.Title>
<Form onSubmit={onSubmitNameChange} className='text-left'>
<DisplayNameField onChange={onChangeDisplayName} value={displayName} initialValue={userName} />
<Button type='submit' variant='primary' disabled={!formSubmittable}>
<Trans i18nKey='common.save' />
</Button>
</Form>
</Card.Body>
</Card>
)
}