mirror of
https://github.com/hedgedoc/hedgedoc.git
synced 2025-05-23 03:27:05 -04:00
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:
parent
4e18ce38f3
commit
762a0a850e
1051 changed files with 0 additions and 35 deletions
|
@ -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>
|
||||
)
|
||||
}
|
|
@ -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>
|
||||
)
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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>
|
||||
)
|
||||
}
|
|
@ -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>
|
||||
)
|
||||
}
|
|
@ -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>
|
||||
)
|
||||
}
|
|
@ -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()
|
||||
}
|
||||
}, [])
|
||||
}
|
|
@ -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]
|
||||
)
|
||||
}
|
|
@ -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>
|
||||
)
|
||||
}
|
|
@ -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>
|
||||
)
|
||||
}
|
|
@ -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>
|
||||
)
|
||||
}
|
|
@ -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>
|
||||
)
|
||||
}
|
|
@ -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>
|
||||
)
|
||||
}
|
|
@ -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>
|
||||
)
|
||||
}
|
|
@ -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>
|
||||
)
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue