Add boolean state hook

Signed-off-by: Tilman Vatteroth <git@tilmanvatteroth.de>
This commit is contained in:
Tilman Vatteroth 2022-07-08 20:30:11 +02:00
parent 311d37b16f
commit 50d2dee9d2
15 changed files with 114 additions and 118 deletions

View file

@ -1,20 +1,20 @@
/* /*
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file) * SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file)
* *
* SPDX-License-Identifier: AGPL-3.0-only * SPDX-License-Identifier: AGPL-3.0-only
*/ */
import React, { Fragment, useCallback, useState } from 'react' import React, { Fragment } from 'react'
import { Button } from 'react-bootstrap' import { Button } from 'react-bootstrap'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { ForkAwesomeIcon } from '../../../common/fork-awesome/fork-awesome-icon' import { ForkAwesomeIcon } from '../../../common/fork-awesome/fork-awesome-icon'
import { HelpModal } from './help-modal' import { HelpModal } from './help-modal'
import { cypressId } from '../../../../utils/cypress-attribute' import { cypressId } from '../../../../utils/cypress-attribute'
import { useBooleanState } from '../../../../hooks/common/use-boolean-state'
export const HelpButton: React.FC = () => { export const HelpButton: React.FC = () => {
const { t } = useTranslation() const { t } = useTranslation()
const [show, setShow] = useState(false) const [modalVisibility, showModal, closeModal] = useBooleanState()
const onHide = useCallback(() => setShow(false), [])
return ( return (
<Fragment> <Fragment>
@ -24,10 +24,10 @@ export const HelpButton: React.FC = () => {
className='ml-2 text-secondary' className='ml-2 text-secondary'
size='sm' size='sm'
variant='outline-light' variant='outline-light'
onClick={() => setShow(true)}> onClick={showModal}>
<ForkAwesomeIcon icon='question-circle' /> <ForkAwesomeIcon icon='question-circle' />
</Button> </Button>
<HelpModal show={show} onHide={onHide} /> <HelpModal show={modalVisibility} onHide={closeModal} />
</Fragment> </Fragment>
) )
} }

View file

@ -1,33 +1,33 @@
/* /*
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file) * SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file)
* *
* SPDX-License-Identifier: AGPL-3.0-only * SPDX-License-Identifier: AGPL-3.0-only
*/ */
import React, { useCallback, useEffect, useRef, useState } from 'react' import React, { useEffect, useRef } from 'react'
import { MaxLengthWarningModal } from './max-length-warning-modal' import { MaxLengthWarningModal } from './max-length-warning-modal'
import { useNoteMarkdownContent } from '../../../../hooks/common/use-note-markdown-content' import { useNoteMarkdownContent } from '../../../../hooks/common/use-note-markdown-content'
import { useApplicationState } from '../../../../hooks/common/use-application-state' import { useApplicationState } from '../../../../hooks/common/use-application-state'
import { useBooleanState } from '../../../../hooks/common/use-boolean-state'
/** /**
* Watches the length of the document and shows a warning modal to the user if the document length exceeds the configured value. * Watches the length of the document and shows a warning modal to the user if the document length exceeds the configured value.
*/ */
export const MaxLengthWarning: React.FC = () => { export const MaxLengthWarning: React.FC = () => {
const [showMaxLengthWarningModal, setShowMaxLengthWarningModal] = useState(false) const [modalVisibility, showModal, closeModal] = useBooleanState()
const maxLengthWarningAlreadyShown = useRef(false) const maxLengthWarningAlreadyShown = useRef(false)
const maxDocumentLength = useApplicationState((state) => state.config.maxDocumentLength) const maxDocumentLength = useApplicationState((state) => state.config.maxDocumentLength)
const hideWarning = useCallback(() => setShowMaxLengthWarningModal(false), [])
const markdownContent = useNoteMarkdownContent() const markdownContent = useNoteMarkdownContent()
useEffect(() => { useEffect(() => {
if (markdownContent.length > maxDocumentLength && !maxLengthWarningAlreadyShown.current) { if (markdownContent.length > maxDocumentLength && !maxLengthWarningAlreadyShown.current) {
setShowMaxLengthWarningModal(true) showModal()
maxLengthWarningAlreadyShown.current = true maxLengthWarningAlreadyShown.current = true
} }
if (markdownContent.length <= maxDocumentLength) { if (markdownContent.length <= maxDocumentLength) {
maxLengthWarningAlreadyShown.current = false maxLengthWarningAlreadyShown.current = false
} }
}, [markdownContent, maxDocumentLength]) }, [markdownContent, maxDocumentLength, showModal])
return <MaxLengthWarningModal show={showMaxLengthWarningModal} onHide={hideWarning} /> return <MaxLengthWarningModal show={modalVisibility} onHide={closeModal} />
} }

View file

@ -1,5 +1,5 @@
/* /*
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file) * SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file)
* *
* SPDX-License-Identifier: AGPL-3.0-only * SPDX-License-Identifier: AGPL-3.0-only
*/ */
@ -12,30 +12,27 @@ import type {
} from '../../render-page/window-post-message-communicator/rendering-message' } from '../../render-page/window-post-message-communicator/rendering-message'
import { CommunicationMessageType } from '../../render-page/window-post-message-communicator/rendering-message' import { CommunicationMessageType } from '../../render-page/window-post-message-communicator/rendering-message'
import { useEditorReceiveHandler } from '../../render-page/window-post-message-communicator/hooks/use-editor-receive-handler' import { useEditorReceiveHandler } from '../../render-page/window-post-message-communicator/hooks/use-editor-receive-handler'
import { useBooleanState } from '../../../hooks/common/use-boolean-state'
export const CommunicatorImageLightbox: React.FC = () => { export const CommunicatorImageLightbox: React.FC = () => {
const [lightboxDetails, setLightboxDetails] = useState<ImageDetails | undefined>(undefined) const [lightboxDetails, setLightboxDetails] = useState<ImageDetails | undefined>(undefined)
const [show, setShow] = useState<boolean>(false) const [modalVisibility, showModal, closeModal] = useBooleanState()
useEditorReceiveHandler( useEditorReceiveHandler(
CommunicationMessageType.IMAGE_CLICKED, CommunicationMessageType.IMAGE_CLICKED,
useCallback( useCallback(
(values: ImageClickedMessage) => { (values: ImageClickedMessage) => {
setLightboxDetails?.(values.details) setLightboxDetails?.(values.details)
setShow(true) showModal()
}, },
[setLightboxDetails] [showModal]
) )
) )
const hideLightbox = useCallback(() => {
setShow(false)
}, [])
return ( return (
<ImageLightboxModal <ImageLightboxModal
show={show} show={modalVisibility}
onHide={hideLightbox} onHide={closeModal}
src={lightboxDetails?.src} src={lightboxDetails?.src}
alt={lightboxDetails?.alt} alt={lightboxDetails?.alt}
title={lightboxDetails?.title} title={lightboxDetails?.title}

View file

@ -1,11 +1,11 @@
/* /*
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file) * SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file)
* *
* SPDX-License-Identifier: AGPL-3.0-only * SPDX-License-Identifier: AGPL-3.0-only
*/ */
import type { PropsWithChildren } from 'react' import type { PropsWithChildren } from 'react'
import React, { Fragment, useCallback, useState } from 'react' import React, { Fragment, useCallback } from 'react'
import { Trans, useTranslation } from 'react-i18next' import { Trans, useTranslation } from 'react-i18next'
import { SidebarButton } from '../sidebar-button/sidebar-button' import { SidebarButton } from '../sidebar-button/sidebar-button'
import type { SpecificSidebarEntryProps } from '../types' import type { SpecificSidebarEntryProps } from '../types'
@ -14,7 +14,7 @@ import { cypressId } from '../../../../utils/cypress-attribute'
import { showErrorNotification } from '../../../../redux/ui-notifications/methods' import { showErrorNotification } from '../../../../redux/ui-notifications/methods'
import { deleteNote } from '../../../../api/notes' import { deleteNote } from '../../../../api/notes'
import { DeleteNoteModal } from './delete-note-modal' import { DeleteNoteModal } from './delete-note-modal'
import { ShowIf } from '../../../common/show-if/show-if' import { useBooleanState } from '../../../../hooks/common/use-boolean-state'
/** /**
* Sidebar entry that can be used to delete the current note. * Sidebar entry that can be used to delete the current note.
@ -24,15 +24,11 @@ import { ShowIf } from '../../../common/show-if/show-if'
*/ */
export const DeleteNoteSidebarEntry: React.FC<PropsWithChildren<SpecificSidebarEntryProps>> = ({ hide, className }) => { export const DeleteNoteSidebarEntry: React.FC<PropsWithChildren<SpecificSidebarEntryProps>> = ({ hide, className }) => {
useTranslation() useTranslation()
const [showDialog, setShowDialog] = useState(false)
const noteId = useApplicationState((state) => state.noteDetails.id) const noteId = useApplicationState((state) => state.noteDetails.id)
const openDialog = useCallback(() => setShowDialog(true), []) const [modalVisibility, showModal, closeModal] = useBooleanState()
const closeDialog = useCallback(() => setShowDialog(false), [])
const deleteNoteAndCloseDialog = useCallback(() => { const deleteNoteAndCloseDialog = useCallback(() => {
deleteNote(noteId) deleteNote(noteId).catch(showErrorNotification('landing.history.error.deleteNote.text')).finally(closeModal)
.catch(showErrorNotification('landing.history.error.deleteNote.text')) }, [closeModal, noteId])
.finally(() => setShowDialog(false))
}, [noteId])
return ( return (
<Fragment> <Fragment>
@ -41,12 +37,10 @@ export const DeleteNoteSidebarEntry: React.FC<PropsWithChildren<SpecificSidebarE
icon={'trash'} icon={'trash'}
className={className} className={className}
hide={hide} hide={hide}
onClick={openDialog}> onClick={showModal}>
<Trans i18nKey={'landing.history.menu.deleteNote'} /> <Trans i18nKey={'landing.history.menu.deleteNote'} />
</SidebarButton> </SidebarButton>
<ShowIf condition={showDialog}> <DeleteNoteModal onHide={closeModal} onConfirm={deleteNoteAndCloseDialog} show={modalVisibility} />
<DeleteNoteModal onHide={closeDialog} onConfirm={deleteNoteAndCloseDialog} show={showDialog} />
</ShowIf>
</Fragment> </Fragment>
) )
} }

View file

@ -1,18 +1,19 @@
/* /*
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file) * SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file)
* *
* SPDX-License-Identifier: AGPL-3.0-only * SPDX-License-Identifier: AGPL-3.0-only
*/ */
import React, { Fragment, useState } from 'react' import React, { Fragment } from 'react'
import { Trans, useTranslation } from 'react-i18next' import { Trans, useTranslation } from 'react-i18next'
import { NoteInfoModal } from '../../document-bar/note-info/note-info-modal' import { NoteInfoModal } from '../../document-bar/note-info/note-info-modal'
import { SidebarButton } from '../sidebar-button/sidebar-button' import { SidebarButton } from '../sidebar-button/sidebar-button'
import type { SpecificSidebarEntryProps } from '../types' import type { SpecificSidebarEntryProps } from '../types'
import { cypressId } from '../../../../utils/cypress-attribute' import { cypressId } from '../../../../utils/cypress-attribute'
import { useBooleanState } from '../../../../hooks/common/use-boolean-state'
export const NoteInfoSidebarEntry: React.FC<SpecificSidebarEntryProps> = ({ className, hide }) => { export const NoteInfoSidebarEntry: React.FC<SpecificSidebarEntryProps> = ({ className, hide }) => {
const [showModal, setShowModal] = useState(false) const [modalVisibility, showModal, closeModal] = useBooleanState()
useTranslation() useTranslation()
return ( return (
@ -21,11 +22,11 @@ export const NoteInfoSidebarEntry: React.FC<SpecificSidebarEntryProps> = ({ clas
hide={hide} hide={hide}
className={className} className={className}
icon={'line-chart'} icon={'line-chart'}
onClick={() => setShowModal(true)} onClick={showModal}
{...cypressId('sidebar-btn-document-info')}> {...cypressId('sidebar-btn-document-info')}>
<Trans i18nKey={'editor.modal.documentInfo.title'} /> <Trans i18nKey={'editor.modal.documentInfo.title'} />
</SidebarButton> </SidebarButton>
<NoteInfoModal show={showModal} onHide={() => setShowModal(false)} /> <NoteInfoModal show={modalVisibility} onHide={closeModal} />
</Fragment> </Fragment>
) )
} }

View file

@ -1,25 +1,26 @@
/* /*
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file) * SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file)
* *
* SPDX-License-Identifier: AGPL-3.0-only * SPDX-License-Identifier: AGPL-3.0-only
*/ */
import React, { Fragment, useState } from 'react' import React, { Fragment } from 'react'
import { Trans, useTranslation } from 'react-i18next' import { Trans, useTranslation } from 'react-i18next'
import { PermissionModal } from '../../document-bar/permissions/permission-modal' import { PermissionModal } from '../../document-bar/permissions/permission-modal'
import { SidebarButton } from '../sidebar-button/sidebar-button' import { SidebarButton } from '../sidebar-button/sidebar-button'
import type { SpecificSidebarEntryProps } from '../types' import type { SpecificSidebarEntryProps } from '../types'
import { useBooleanState } from '../../../../hooks/common/use-boolean-state'
export const PermissionsSidebarEntry: React.FC<SpecificSidebarEntryProps> = ({ className, hide }) => { export const PermissionsSidebarEntry: React.FC<SpecificSidebarEntryProps> = ({ className, hide }) => {
const [showModal, setShowModal] = useState(false) const [modalVisibility, showModal, closeModal] = useBooleanState()
useTranslation() useTranslation()
return ( return (
<Fragment> <Fragment>
<SidebarButton hide={hide} className={className} icon={'lock'} onClick={() => setShowModal(true)}> <SidebarButton hide={hide} className={className} icon={'lock'} onClick={showModal}>
<Trans i18nKey={'editor.modal.permissions.title'} /> <Trans i18nKey={'editor.modal.permissions.title'} />
</SidebarButton> </SidebarButton>
<PermissionModal show={showModal} onHide={() => setShowModal(false)} /> <PermissionModal show={modalVisibility} onHide={closeModal} />
</Fragment> </Fragment>
) )
} }

View file

@ -1,30 +1,25 @@
/* /*
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file) * SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file)
* *
* SPDX-License-Identifier: AGPL-3.0-only * SPDX-License-Identifier: AGPL-3.0-only
*/ */
import React, { Fragment, useCallback, useState } from 'react' import React, { Fragment } from 'react'
import { Trans } from 'react-i18next' import { Trans } from 'react-i18next'
import { RevisionModal } from '../../document-bar/revisions/revision-modal' import { RevisionModal } from '../../document-bar/revisions/revision-modal'
import { SidebarButton } from '../sidebar-button/sidebar-button' import { SidebarButton } from '../sidebar-button/sidebar-button'
import type { SpecificSidebarEntryProps } from '../types' import type { SpecificSidebarEntryProps } from '../types'
import { useBooleanState } from '../../../../hooks/common/use-boolean-state'
export const RevisionSidebarEntry: React.FC<SpecificSidebarEntryProps> = ({ className, hide }) => { export const RevisionSidebarEntry: React.FC<SpecificSidebarEntryProps> = ({ className, hide }) => {
const [showModal, setShowModal] = useState(false) const [modalVisibility, showModal, closeModal] = useBooleanState()
const onHide = useCallback(() => {
setShowModal(false)
}, [])
const onShow = useCallback(() => {
setShowModal(true)
}, [])
return ( return (
<Fragment> <Fragment>
<SidebarButton hide={hide} className={className} icon={'history'} onClick={onShow}> <SidebarButton hide={hide} className={className} icon={'history'} onClick={showModal}>
<Trans i18nKey={'editor.modal.revision.title'} /> <Trans i18nKey={'editor.modal.revision.title'} />
</SidebarButton> </SidebarButton>
<RevisionModal show={showModal} onHide={onHide} /> <RevisionModal show={modalVisibility} onHide={closeModal} />
</Fragment> </Fragment>
) )
} }

View file

@ -1,25 +1,26 @@
/* /*
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file) * SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file)
* *
* SPDX-License-Identifier: AGPL-3.0-only * SPDX-License-Identifier: AGPL-3.0-only
*/ */
import React, { Fragment, useState } from 'react' import React, { Fragment } from 'react'
import { Trans, useTranslation } from 'react-i18next' import { Trans, useTranslation } from 'react-i18next'
import { ShareModal } from '../../document-bar/share/share-modal' import { ShareModal } from '../../document-bar/share/share-modal'
import { SidebarButton } from '../sidebar-button/sidebar-button' import { SidebarButton } from '../sidebar-button/sidebar-button'
import type { SpecificSidebarEntryProps } from '../types' import type { SpecificSidebarEntryProps } from '../types'
import { useBooleanState } from '../../../../hooks/common/use-boolean-state'
export const ShareSidebarEntry: React.FC<SpecificSidebarEntryProps> = ({ className, hide }) => { export const ShareSidebarEntry: React.FC<SpecificSidebarEntryProps> = ({ className, hide }) => {
const [showModal, setShowModal] = useState(false) const [modalVisibility, showModal, closeModal] = useBooleanState()
useTranslation() useTranslation()
return ( return (
<Fragment> <Fragment>
<SidebarButton hide={hide} className={className} icon={'share'} onClick={() => setShowModal(true)}> <SidebarButton hide={hide} className={className} icon={'share'} onClick={showModal}>
<Trans i18nKey={'editor.modal.shareLink.title'} /> <Trans i18nKey={'editor.modal.shareLink.title'} />
</SidebarButton> </SidebarButton>
<ShareModal show={showModal} onHide={() => setShowModal(false)} /> <ShareModal show={modalVisibility} onHide={closeModal} />
</Fragment> </Fragment>
) )
} }

View file

@ -1,16 +1,17 @@
/* /*
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file) * SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file)
* *
* SPDX-License-Identifier: AGPL-3.0-only * SPDX-License-Identifier: AGPL-3.0-only
*/ */
import React, { Fragment, useCallback, useState } from 'react' import React, { Fragment, useCallback } from 'react'
import { Dropdown } from 'react-bootstrap' import { Dropdown } from 'react-bootstrap'
import { Trans, useTranslation } from 'react-i18next' import { Trans, useTranslation } from 'react-i18next'
import { ForkAwesomeIcon } from '../../common/fork-awesome/fork-awesome-icon' import { ForkAwesomeIcon } from '../../common/fork-awesome/fork-awesome-icon'
import type { IconName } from '../../common/fork-awesome/types' import type { IconName } from '../../common/fork-awesome/types'
import type { DeleteHistoryNoteModalProps } from '../../editor-page/sidebar/delete-note-sidebar-entry/delete-note-modal' import type { DeleteHistoryNoteModalProps } from '../../editor-page/sidebar/delete-note-sidebar-entry/delete-note-modal'
import { DeleteNoteModal } from '../../editor-page/sidebar/delete-note-sidebar-entry/delete-note-modal' import { DeleteNoteModal } from '../../editor-page/sidebar/delete-note-sidebar-entry/delete-note-modal'
import { useBooleanState } from '../../../hooks/common/use-boolean-state'
export interface DropdownItemWithDeletionModalProps { export interface DropdownItemWithDeletionModalProps {
onConfirm: () => void onConfirm: () => void
@ -47,24 +48,24 @@ export const DropdownItemWithDeletionModal: React.FC<
className className
}) => { }) => {
useTranslation() useTranslation()
const [showDialog, setShowDialog] = useState(false) const [modalVisibility, showModal, closeModal] = useBooleanState()
const handleConfirm = useCallback(() => { const handleConfirm = useCallback(() => {
setShowDialog(false) closeModal()
onConfirm() onConfirm()
}, [onConfirm]) }, [closeModal, onConfirm])
const onHide = useCallback(() => setShowDialog(false), [])
return ( return (
<Fragment> <Fragment>
<Dropdown.Item onClick={() => setShowDialog(true)} className={className}> <Dropdown.Item onClick={showModal} className={className}>
<ForkAwesomeIcon icon={modalIcon} fixedWidth={true} className='mx-2' /> <ForkAwesomeIcon icon={modalIcon} fixedWidth={true} className='mx-2' />
<Trans i18nKey={itemI18nKey} /> <Trans i18nKey={itemI18nKey} />
</Dropdown.Item> </Dropdown.Item>
<DeleteNoteModal <DeleteNoteModal
optionalNoteTitle={noteTitle} optionalNoteTitle={noteTitle}
onConfirm={handleConfirm} onConfirm={handleConfirm}
show={showDialog} show={modalVisibility}
onHide={onHide} onHide={closeModal}
modalTitleI18nKey={modalTitleI18nKey} modalTitleI18nKey={modalTitleI18nKey}
modalButtonI18nKey={modalButtonI18nKey} modalButtonI18nKey={modalButtonI18nKey}
modalQuestionI18nKey={modalQuestionI18nKey} modalQuestionI18nKey={modalQuestionI18nKey}

View file

@ -1,10 +1,10 @@
/* /*
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file) * SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file)
* *
* SPDX-License-Identifier: AGPL-3.0-only * SPDX-License-Identifier: AGPL-3.0-only
*/ */
import React, { Fragment, useCallback, useState } from 'react' import React, { Fragment, useCallback } from 'react'
import { Button } from 'react-bootstrap' import { Button } from 'react-bootstrap'
import { Trans, useTranslation } from 'react-i18next' import { Trans, useTranslation } from 'react-i18next'
import { ForkAwesomeIcon } from '../../common/fork-awesome/fork-awesome-icon' import { ForkAwesomeIcon } from '../../common/fork-awesome/fork-awesome-icon'
@ -12,36 +12,34 @@ import { DeletionModal } from '../../common/modals/deletion-modal'
import { deleteAllHistoryEntries, safeRefreshHistoryState } from '../../../redux/history/methods' import { deleteAllHistoryEntries, safeRefreshHistoryState } from '../../../redux/history/methods'
import { showErrorNotification } from '../../../redux/ui-notifications/methods' import { showErrorNotification } from '../../../redux/ui-notifications/methods'
import { cypressId } from '../../../utils/cypress-attribute' import { cypressId } from '../../../utils/cypress-attribute'
import { useBooleanState } from '../../../hooks/common/use-boolean-state'
export const ClearHistoryButton: React.FC = () => { export const ClearHistoryButton: React.FC = () => {
const { t } = useTranslation() const { t } = useTranslation()
const [show, setShow] = useState(false) const [modalVisibility, showModal, closeModal] = useBooleanState()
const handleShow = () => setShow(true)
const handleClose = () => setShow(false)
const onConfirm = useCallback(() => { const onConfirm = useCallback(() => {
deleteAllHistoryEntries().catch((error: Error) => { deleteAllHistoryEntries().catch((error: Error) => {
showErrorNotification('landing.history.error.deleteEntry.text')(error) showErrorNotification('landing.history.error.deleteEntry.text')(error)
safeRefreshHistoryState() safeRefreshHistoryState()
}) })
handleClose() closeModal()
}, []) }, [closeModal])
return ( return (
<Fragment> <Fragment>
<Button <Button
variant={'light'} variant={'light'}
title={t('landing.history.toolbar.clear')} title={t('landing.history.toolbar.clear')}
onClick={handleShow} onClick={showModal}
{...cypressId('history-clear-button')}> {...cypressId('history-clear-button')}>
<ForkAwesomeIcon icon={'trash'} /> <ForkAwesomeIcon icon={'trash'} />
</Button> </Button>
<DeletionModal <DeletionModal
onConfirm={onConfirm} onConfirm={onConfirm}
deletionButtonI18nKey={'landing.history.toolbar.clear'} deletionButtonI18nKey={'landing.history.toolbar.clear'}
show={show} show={modalVisibility}
onHide={handleClose} onHide={closeModal}
title={'landing.history.modal.clearHistory.title'}> title={'landing.history.modal.clearHistory.title'}>
<h5> <h5>
<Trans i18nKey={'landing.history.modal.clearHistory.question'} /> <Trans i18nKey={'landing.history.modal.clearHistory.question'} />

View file

@ -1,25 +1,24 @@
/* /*
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file) * SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file)
* *
* SPDX-License-Identifier: AGPL-3.0-only * SPDX-License-Identifier: AGPL-3.0-only
*/ */
import React, { Fragment, useCallback, useState } from 'react' import React, { Fragment } from 'react'
import { Trans } from 'react-i18next' import { Trans } from 'react-i18next'
import { VersionInfoModal } from './version-info-modal' import { VersionInfoModal } from './version-info-modal'
import { cypressId } from '../../../../utils/cypress-attribute' import { cypressId } from '../../../../utils/cypress-attribute'
import { useBooleanState } from '../../../../hooks/common/use-boolean-state'
export const VersionInfoLink: React.FC = () => { export const VersionInfoLink: React.FC = () => {
const [show, setShow] = useState(false) const [modalVisibility, showModal, closeModal] = useBooleanState()
const closeModal = useCallback(() => setShow(false), [])
const showModal = useCallback(() => setShow(true), [])
return ( return (
<Fragment> <Fragment>
<a {...cypressId('show-version-modal')} href={'#'} className={'text-light'} onClick={showModal}> <a {...cypressId('show-version-modal')} href={'#'} className={'text-light'} onClick={showModal}>
<Trans i18nKey={'landing.versionInfo.versionInfo'} /> <Trans i18nKey={'landing.versionInfo.versionInfo'} />
</a> </a>
<VersionInfoModal onHide={closeModal} show={show} /> <VersionInfoModal onHide={closeModal} show={modalVisibility} />
</Fragment> </Fragment>
) )
} }

View file

@ -37,11 +37,7 @@ export const AccessTokenDeletionModal: React.FC<AccessTokenDeletionModalProps> =
) )
}) })
.catch(showErrorNotification('profile.modal.deleteAccessToken.failed')) .catch(showErrorNotification('profile.modal.deleteAccessToken.failed'))
.finally(() => { .finally(() => onHide?.())
if (onHide) {
onHide()
}
})
}, [token, onHide]) }, [token, onHide])
return ( return (

View file

@ -1,10 +1,10 @@
/* /*
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file) * SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file)
* *
* SPDX-License-Identifier: AGPL-3.0-only * SPDX-License-Identifier: AGPL-3.0-only
*/ */
import React, { useCallback, useMemo, useState } from 'react' import React, { useCallback, useMemo } from 'react'
import { Col, ListGroup, Row } from 'react-bootstrap' import { Col, ListGroup, Row } from 'react-bootstrap'
import { cypressId } from '../../../utils/cypress-attribute' import { cypressId } from '../../../utils/cypress-attribute'
import { Trans, useTranslation } from 'react-i18next' import { Trans, useTranslation } from 'react-i18next'
@ -13,6 +13,7 @@ import { IconButton } from '../../common/icon-button/icon-button'
import type { AccessToken } from '../../../api/tokens/types' import type { AccessToken } from '../../../api/tokens/types'
import { AccessTokenDeletionModal } from './access-token-deletion-modal' import { AccessTokenDeletionModal } from './access-token-deletion-modal'
import type { AccessTokenUpdateProps } from './profile-access-tokens' import type { AccessTokenUpdateProps } from './profile-access-tokens'
import { useBooleanState } from '../../../hooks/common/use-boolean-state'
export interface AccessTokenListEntryProps { export interface AccessTokenListEntryProps {
token: AccessToken token: AccessToken
@ -28,16 +29,12 @@ export const AccessTokenListEntry: React.FC<AccessTokenListEntryProps & AccessTo
onUpdateList onUpdateList
}) => { }) => {
useTranslation() useTranslation()
const [showDeletionModal, setShowDeletionModal] = useState(false) const [modalVisibility, showModal, closeModal] = useBooleanState()
const onShowDeletionModal = useCallback(() => {
setShowDeletionModal(true)
}, [])
const onHideDeletionModal = useCallback(() => { const onHideDeletionModal = useCallback(() => {
setShowDeletionModal(false) closeModal()
onUpdateList() onUpdateList()
}, [onUpdateList]) }, [closeModal, onUpdateList])
const lastUsed = useMemo(() => { const lastUsed = useMemo(() => {
if (!token.lastUsedAt) { if (!token.lastUsedAt) {
@ -66,12 +63,12 @@ export const AccessTokenListEntry: React.FC<AccessTokenListEntryProps & AccessTo
<IconButton <IconButton
icon='trash-o' icon='trash-o'
variant='danger' variant='danger'
onClick={onShowDeletionModal} onClick={showModal}
{...cypressId('access-token-delete-button')} {...cypressId('access-token-delete-button')}
/> />
</Col> </Col>
</Row> </Row>
<AccessTokenDeletionModal token={token} show={showDeletionModal} onHide={onHideDeletionModal} /> <AccessTokenDeletionModal token={token} show={modalVisibility} onHide={onHideDeletionModal} />
</ListGroup.Item> </ListGroup.Item>
) )
} }

View file

@ -1,30 +1,23 @@
/* /*
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file) * SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file)
* *
* SPDX-License-Identifier: AGPL-3.0-only * SPDX-License-Identifier: AGPL-3.0-only
*/ */
import React, { Fragment, useCallback, useState } from 'react' import React, { Fragment } from 'react'
import { Button, Card } from 'react-bootstrap' import { Button, Card } from 'react-bootstrap'
import { Trans, useTranslation } from 'react-i18next' import { Trans, useTranslation } from 'react-i18next'
import { ForkAwesomeIcon } from '../../common/fork-awesome/fork-awesome-icon' import { ForkAwesomeIcon } from '../../common/fork-awesome/fork-awesome-icon'
import { AccountDeletionModal } from './account-deletion-modal' import { AccountDeletionModal } from './account-deletion-modal'
import { apiUrl } from '../../../utils/api-url' import { apiUrl } from '../../../utils/api-url'
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. * Profile page section that allows to export all data from the account or to delete the account.
*/ */
export const ProfileAccountManagement: React.FC = () => { export const ProfileAccountManagement: React.FC = () => {
useTranslation() useTranslation()
const [showDeleteModal, setShowDeleteModal] = useState(false) const [modalVisibility, showModal, closeModal] = useBooleanState()
const onShowDeletionModal = useCallback(() => {
setShowDeleteModal(true)
}, [])
const onHideDeletionModal = useCallback(() => {
setShowDeleteModal(false)
}, [])
return ( return (
<Fragment> <Fragment>
@ -37,13 +30,13 @@ export const ProfileAccountManagement: React.FC = () => {
<ForkAwesomeIcon icon='cloud-download' fixedWidth={true} className='mx-2' /> <ForkAwesomeIcon icon='cloud-download' fixedWidth={true} className='mx-2' />
<Trans i18nKey='profile.exportUserData' /> <Trans i18nKey='profile.exportUserData' />
</Button> </Button>
<Button variant='danger' block onClick={onShowDeletionModal}> <Button variant='danger' block onClick={showModal}>
<ForkAwesomeIcon icon='trash' fixedWidth={true} className='mx-2' /> <ForkAwesomeIcon icon='trash' fixedWidth={true} className='mx-2' />
<Trans i18nKey='profile.deleteUser' /> <Trans i18nKey='profile.deleteUser' />
</Button> </Button>
</Card.Body> </Card.Body>
</Card> </Card>
<AccountDeletionModal show={showDeleteModal} onHide={onHideDeletionModal} /> <AccountDeletionModal show={modalVisibility} onHide={closeModal} />
</Fragment> </Fragment>
) )
} }

View file

@ -0,0 +1,23 @@
/*
* SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { useCallback, useState } from 'react'
/**
* Provides a boolean state and two functions that set the boolean to true or false.
*
* @param initialState The initial value of the state
* @return An array containing the state, and two functions that set the state value to true or false.
*/
export const useBooleanState = (
initialState: boolean | (() => boolean) = false
): [state: boolean, setToTrue: () => void, setToFalse: () => void] => {
const [state, setState] = useState(initialState)
const setToFalse = useCallback(() => setState(false), [])
const setToTrue = useCallback(() => setState(true), [])
return [state, setToTrue, setToFalse]
}