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
*/
import React, { Fragment, useCallback, useState } from 'react'
import React, { Fragment } from 'react'
import { Button } from 'react-bootstrap'
import { useTranslation } from 'react-i18next'
import { ForkAwesomeIcon } from '../../../common/fork-awesome/fork-awesome-icon'
import { HelpModal } from './help-modal'
import { cypressId } from '../../../../utils/cypress-attribute'
import { useBooleanState } from '../../../../hooks/common/use-boolean-state'
export const HelpButton: React.FC = () => {
const { t } = useTranslation()
const [show, setShow] = useState(false)
const onHide = useCallback(() => setShow(false), [])
const [modalVisibility, showModal, closeModal] = useBooleanState()
return (
<Fragment>
@ -24,10 +24,10 @@ export const HelpButton: React.FC = () => {
className='ml-2 text-secondary'
size='sm'
variant='outline-light'
onClick={() => setShow(true)}>
onClick={showModal}>
<ForkAwesomeIcon icon='question-circle' />
</Button>
<HelpModal show={show} onHide={onHide} />
<HelpModal show={modalVisibility} onHide={closeModal} />
</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
*/
import React, { useCallback, useEffect, useRef, useState } from 'react'
import React, { useEffect, useRef } from 'react'
import { MaxLengthWarningModal } from './max-length-warning-modal'
import { useNoteMarkdownContent } from '../../../../hooks/common/use-note-markdown-content'
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.
*/
export const MaxLengthWarning: React.FC = () => {
const [showMaxLengthWarningModal, setShowMaxLengthWarningModal] = useState(false)
const [modalVisibility, showModal, closeModal] = useBooleanState()
const maxLengthWarningAlreadyShown = useRef(false)
const maxDocumentLength = useApplicationState((state) => state.config.maxDocumentLength)
const hideWarning = useCallback(() => setShowMaxLengthWarningModal(false), [])
const markdownContent = useNoteMarkdownContent()
useEffect(() => {
if (markdownContent.length > maxDocumentLength && !maxLengthWarningAlreadyShown.current) {
setShowMaxLengthWarningModal(true)
showModal()
maxLengthWarningAlreadyShown.current = true
}
if (markdownContent.length <= maxDocumentLength) {
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
*/
@ -12,30 +12,27 @@ import type {
} 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 { useBooleanState } from '../../../hooks/common/use-boolean-state'
export const CommunicatorImageLightbox: React.FC = () => {
const [lightboxDetails, setLightboxDetails] = useState<ImageDetails | undefined>(undefined)
const [show, setShow] = useState<boolean>(false)
const [modalVisibility, showModal, closeModal] = useBooleanState()
useEditorReceiveHandler(
CommunicationMessageType.IMAGE_CLICKED,
useCallback(
(values: ImageClickedMessage) => {
setLightboxDetails?.(values.details)
setShow(true)
showModal()
},
[setLightboxDetails]
[showModal]
)
)
const hideLightbox = useCallback(() => {
setShow(false)
}, [])
return (
<ImageLightboxModal
show={show}
onHide={hideLightbox}
show={modalVisibility}
onHide={closeModal}
src={lightboxDetails?.src}
alt={lightboxDetails?.alt}
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
*/
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 { SidebarButton } from '../sidebar-button/sidebar-button'
import type { SpecificSidebarEntryProps } from '../types'
@ -14,7 +14,7 @@ import { cypressId } from '../../../../utils/cypress-attribute'
import { showErrorNotification } from '../../../../redux/ui-notifications/methods'
import { deleteNote } from '../../../../api/notes'
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.
@ -24,15 +24,11 @@ import { ShowIf } from '../../../common/show-if/show-if'
*/
export const DeleteNoteSidebarEntry: React.FC<PropsWithChildren<SpecificSidebarEntryProps>> = ({ hide, className }) => {
useTranslation()
const [showDialog, setShowDialog] = useState(false)
const noteId = useApplicationState((state) => state.noteDetails.id)
const openDialog = useCallback(() => setShowDialog(true), [])
const closeDialog = useCallback(() => setShowDialog(false), [])
const [modalVisibility, showModal, closeModal] = useBooleanState()
const deleteNoteAndCloseDialog = useCallback(() => {
deleteNote(noteId)
.catch(showErrorNotification('landing.history.error.deleteNote.text'))
.finally(() => setShowDialog(false))
}, [noteId])
deleteNote(noteId).catch(showErrorNotification('landing.history.error.deleteNote.text')).finally(closeModal)
}, [closeModal, noteId])
return (
<Fragment>
@ -41,12 +37,10 @@ export const DeleteNoteSidebarEntry: React.FC<PropsWithChildren<SpecificSidebarE
icon={'trash'}
className={className}
hide={hide}
onClick={openDialog}>
onClick={showModal}>
<Trans i18nKey={'landing.history.menu.deleteNote'} />
</SidebarButton>
<ShowIf condition={showDialog}>
<DeleteNoteModal onHide={closeDialog} onConfirm={deleteNoteAndCloseDialog} show={showDialog} />
</ShowIf>
<DeleteNoteModal onHide={closeModal} onConfirm={deleteNoteAndCloseDialog} show={modalVisibility} />
</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
*/
import React, { Fragment, useState } from 'react'
import React, { Fragment } from 'react'
import { Trans, useTranslation } from 'react-i18next'
import { NoteInfoModal } from '../../document-bar/note-info/note-info-modal'
import { SidebarButton } from '../sidebar-button/sidebar-button'
import type { SpecificSidebarEntryProps } from '../types'
import { cypressId } from '../../../../utils/cypress-attribute'
import { useBooleanState } from '../../../../hooks/common/use-boolean-state'
export const NoteInfoSidebarEntry: React.FC<SpecificSidebarEntryProps> = ({ className, hide }) => {
const [showModal, setShowModal] = useState(false)
const [modalVisibility, showModal, closeModal] = useBooleanState()
useTranslation()
return (
@ -21,11 +22,11 @@ export const NoteInfoSidebarEntry: React.FC<SpecificSidebarEntryProps> = ({ clas
hide={hide}
className={className}
icon={'line-chart'}
onClick={() => setShowModal(true)}
onClick={showModal}
{...cypressId('sidebar-btn-document-info')}>
<Trans i18nKey={'editor.modal.documentInfo.title'} />
</SidebarButton>
<NoteInfoModal show={showModal} onHide={() => setShowModal(false)} />
<NoteInfoModal show={modalVisibility} onHide={closeModal} />
</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
*/
import React, { Fragment, useState } from 'react'
import React, { Fragment } from 'react'
import { Trans, useTranslation } from 'react-i18next'
import { PermissionModal } from '../../document-bar/permissions/permission-modal'
import { SidebarButton } from '../sidebar-button/sidebar-button'
import type { SpecificSidebarEntryProps } from '../types'
import { useBooleanState } from '../../../../hooks/common/use-boolean-state'
export const PermissionsSidebarEntry: React.FC<SpecificSidebarEntryProps> = ({ className, hide }) => {
const [showModal, setShowModal] = useState(false)
const [modalVisibility, showModal, closeModal] = useBooleanState()
useTranslation()
return (
<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'} />
</SidebarButton>
<PermissionModal show={showModal} onHide={() => setShowModal(false)} />
<PermissionModal show={modalVisibility} onHide={closeModal} />
</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
*/
import React, { Fragment, useCallback, useState } from 'react'
import React, { Fragment } from 'react'
import { Trans } from 'react-i18next'
import { RevisionModal } from '../../document-bar/revisions/revision-modal'
import { SidebarButton } from '../sidebar-button/sidebar-button'
import type { SpecificSidebarEntryProps } from '../types'
import { useBooleanState } from '../../../../hooks/common/use-boolean-state'
export const RevisionSidebarEntry: React.FC<SpecificSidebarEntryProps> = ({ className, hide }) => {
const [showModal, setShowModal] = useState(false)
const onHide = useCallback(() => {
setShowModal(false)
}, [])
const onShow = useCallback(() => {
setShowModal(true)
}, [])
const [modalVisibility, showModal, closeModal] = useBooleanState()
return (
<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'} />
</SidebarButton>
<RevisionModal show={showModal} onHide={onHide} />
<RevisionModal show={modalVisibility} onHide={closeModal} />
</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
*/
import React, { Fragment, useState } from 'react'
import React, { Fragment } from 'react'
import { Trans, useTranslation } from 'react-i18next'
import { ShareModal } from '../../document-bar/share/share-modal'
import { SidebarButton } from '../sidebar-button/sidebar-button'
import type { SpecificSidebarEntryProps } from '../types'
import { useBooleanState } from '../../../../hooks/common/use-boolean-state'
export const ShareSidebarEntry: React.FC<SpecificSidebarEntryProps> = ({ className, hide }) => {
const [showModal, setShowModal] = useState(false)
const [modalVisibility, showModal, closeModal] = useBooleanState()
useTranslation()
return (
<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'} />
</SidebarButton>
<ShareModal show={showModal} onHide={() => setShowModal(false)} />
<ShareModal show={modalVisibility} onHide={closeModal} />
</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
*/
import React, { Fragment, useCallback, useState } from 'react'
import React, { Fragment, useCallback } from 'react'
import { Dropdown } from 'react-bootstrap'
import { Trans, useTranslation } from 'react-i18next'
import { ForkAwesomeIcon } from '../../common/fork-awesome/fork-awesome-icon'
import type { IconName } from '../../common/fork-awesome/types'
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 { useBooleanState } from '../../../hooks/common/use-boolean-state'
export interface DropdownItemWithDeletionModalProps {
onConfirm: () => void
@ -47,24 +48,24 @@ export const DropdownItemWithDeletionModal: React.FC<
className
}) => {
useTranslation()
const [showDialog, setShowDialog] = useState(false)
const [modalVisibility, showModal, closeModal] = useBooleanState()
const handleConfirm = useCallback(() => {
setShowDialog(false)
closeModal()
onConfirm()
}, [onConfirm])
const onHide = useCallback(() => setShowDialog(false), [])
}, [closeModal, onConfirm])
return (
<Fragment>
<Dropdown.Item onClick={() => setShowDialog(true)} className={className}>
<Dropdown.Item onClick={showModal} className={className}>
<ForkAwesomeIcon icon={modalIcon} fixedWidth={true} className='mx-2' />
<Trans i18nKey={itemI18nKey} />
</Dropdown.Item>
<DeleteNoteModal
optionalNoteTitle={noteTitle}
onConfirm={handleConfirm}
show={showDialog}
onHide={onHide}
show={modalVisibility}
onHide={closeModal}
modalTitleI18nKey={modalTitleI18nKey}
modalButtonI18nKey={modalButtonI18nKey}
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
*/
import React, { Fragment, useCallback, useState } from 'react'
import React, { Fragment, useCallback } from 'react'
import { Button } from 'react-bootstrap'
import { Trans, useTranslation } from 'react-i18next'
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 { showErrorNotification } from '../../../redux/ui-notifications/methods'
import { cypressId } from '../../../utils/cypress-attribute'
import { useBooleanState } from '../../../hooks/common/use-boolean-state'
export const ClearHistoryButton: React.FC = () => {
const { t } = useTranslation()
const [show, setShow] = useState(false)
const handleShow = () => setShow(true)
const handleClose = () => setShow(false)
const [modalVisibility, showModal, closeModal] = useBooleanState()
const onConfirm = useCallback(() => {
deleteAllHistoryEntries().catch((error: Error) => {
showErrorNotification('landing.history.error.deleteEntry.text')(error)
safeRefreshHistoryState()
})
handleClose()
}, [])
closeModal()
}, [closeModal])
return (
<Fragment>
<Button
variant={'light'}
title={t('landing.history.toolbar.clear')}
onClick={handleShow}
onClick={showModal}
{...cypressId('history-clear-button')}>
<ForkAwesomeIcon icon={'trash'} />
</Button>
<DeletionModal
onConfirm={onConfirm}
deletionButtonI18nKey={'landing.history.toolbar.clear'}
show={show}
onHide={handleClose}
show={modalVisibility}
onHide={closeModal}
title={'landing.history.modal.clearHistory.title'}>
<h5>
<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
*/
import React, { Fragment, useCallback, useState } from 'react'
import React, { Fragment } from 'react'
import { Trans } from 'react-i18next'
import { VersionInfoModal } from './version-info-modal'
import { cypressId } from '../../../../utils/cypress-attribute'
import { useBooleanState } from '../../../../hooks/common/use-boolean-state'
export const VersionInfoLink: React.FC = () => {
const [show, setShow] = useState(false)
const closeModal = useCallback(() => setShow(false), [])
const showModal = useCallback(() => setShow(true), [])
const [modalVisibility, showModal, closeModal] = useBooleanState()
return (
<Fragment>
<a {...cypressId('show-version-modal')} href={'#'} className={'text-light'} onClick={showModal}>
<Trans i18nKey={'landing.versionInfo.versionInfo'} />
</a>
<VersionInfoModal onHide={closeModal} show={show} />
<VersionInfoModal onHide={closeModal} show={modalVisibility} />
</Fragment>
)
}

View file

@ -37,11 +37,7 @@ export const AccessTokenDeletionModal: React.FC<AccessTokenDeletionModalProps> =
)
})
.catch(showErrorNotification('profile.modal.deleteAccessToken.failed'))
.finally(() => {
if (onHide) {
onHide()
}
})
.finally(() => onHide?.())
}, [token, onHide])
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
*/
import React, { useCallback, useMemo, useState } from 'react'
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'
@ -13,6 +13,7 @@ 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
@ -28,16 +29,12 @@ export const AccessTokenListEntry: React.FC<AccessTokenListEntryProps & AccessTo
onUpdateList
}) => {
useTranslation()
const [showDeletionModal, setShowDeletionModal] = useState(false)
const onShowDeletionModal = useCallback(() => {
setShowDeletionModal(true)
}, [])
const [modalVisibility, showModal, closeModal] = useBooleanState()
const onHideDeletionModal = useCallback(() => {
setShowDeletionModal(false)
closeModal()
onUpdateList()
}, [onUpdateList])
}, [closeModal, onUpdateList])
const lastUsed = useMemo(() => {
if (!token.lastUsedAt) {
@ -66,12 +63,12 @@ export const AccessTokenListEntry: React.FC<AccessTokenListEntryProps & AccessTo
<IconButton
icon='trash-o'
variant='danger'
onClick={onShowDeletionModal}
onClick={showModal}
{...cypressId('access-token-delete-button')}
/>
</Col>
</Row>
<AccessTokenDeletionModal token={token} show={showDeletionModal} onHide={onHideDeletionModal} />
<AccessTokenDeletionModal token={token} show={modalVisibility} onHide={onHideDeletionModal} />
</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
*/
import React, { Fragment, useCallback, useState } from 'react'
import React, { Fragment } from 'react'
import { Button, Card } 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 { 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.
*/
export const ProfileAccountManagement: React.FC = () => {
useTranslation()
const [showDeleteModal, setShowDeleteModal] = useState(false)
const onShowDeletionModal = useCallback(() => {
setShowDeleteModal(true)
}, [])
const onHideDeletionModal = useCallback(() => {
setShowDeleteModal(false)
}, [])
const [modalVisibility, showModal, closeModal] = useBooleanState()
return (
<Fragment>
@ -37,13 +30,13 @@ export const ProfileAccountManagement: React.FC = () => {
<ForkAwesomeIcon icon='cloud-download' fixedWidth={true} className='mx-2' />
<Trans i18nKey='profile.exportUserData' />
</Button>
<Button variant='danger' block onClick={onShowDeletionModal}>
<Button variant='danger' block onClick={showModal}>
<ForkAwesomeIcon icon='trash' fixedWidth={true} className='mx-2' />
<Trans i18nKey='profile.deleteUser' />
</Button>
</Card.Body>
</Card>
<AccountDeletionModal show={showDeleteModal} onHide={onHideDeletionModal} />
<AccountDeletionModal show={modalVisibility} onHide={closeModal} />
</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]
}