mirror of
https://github.com/hedgedoc/hedgedoc.git
synced 2024-11-26 11:43:59 -05:00
improve: Move notifications from redux into context
Signed-off-by: Tilman Vatteroth <git@tilmanvatteroth.de>
This commit is contained in:
parent
b797f07aa5
commit
03d87f59f8
38 changed files with 362 additions and 376 deletions
|
@ -11,7 +11,7 @@ import { ForkAwesomeIcon } from '../../../common/fork-awesome/fork-awesome-icon'
|
||||||
import { removeGroupPermission, setGroupPermission } from '../../../../api/permissions'
|
import { removeGroupPermission, setGroupPermission } from '../../../../api/permissions'
|
||||||
import { useApplicationState } from '../../../../hooks/common/use-application-state'
|
import { useApplicationState } from '../../../../hooks/common/use-application-state'
|
||||||
import { setNotePermissionsFromServer } from '../../../../redux/note-details/methods'
|
import { setNotePermissionsFromServer } from '../../../../redux/note-details/methods'
|
||||||
import { showErrorNotification } from '../../../../redux/ui-notifications/methods'
|
import { useUiNotifications } from '../../../notifications/ui-notification-boundary'
|
||||||
|
|
||||||
export interface PermissionEntrySpecialGroupProps {
|
export interface PermissionEntrySpecialGroupProps {
|
||||||
level: AccessLevel
|
level: AccessLevel
|
||||||
|
@ -27,6 +27,7 @@ export interface PermissionEntrySpecialGroupProps {
|
||||||
export const PermissionEntrySpecialGroup: React.FC<PermissionEntrySpecialGroupProps> = ({ level, type }) => {
|
export const PermissionEntrySpecialGroup: React.FC<PermissionEntrySpecialGroupProps> = ({ level, type }) => {
|
||||||
const noteId = useApplicationState((state) => state.noteDetails.primaryAddress)
|
const noteId = useApplicationState((state) => state.noteDetails.primaryAddress)
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
|
const { showErrorNotification } = useUiNotifications()
|
||||||
|
|
||||||
const onSetEntryReadOnly = useCallback(() => {
|
const onSetEntryReadOnly = useCallback(() => {
|
||||||
setGroupPermission(noteId, type, false)
|
setGroupPermission(noteId, type, false)
|
||||||
|
@ -34,7 +35,7 @@ export const PermissionEntrySpecialGroup: React.FC<PermissionEntrySpecialGroupPr
|
||||||
setNotePermissionsFromServer(updatedPermissions)
|
setNotePermissionsFromServer(updatedPermissions)
|
||||||
})
|
})
|
||||||
.catch(showErrorNotification('editor.modal.permissions.error'))
|
.catch(showErrorNotification('editor.modal.permissions.error'))
|
||||||
}, [noteId, type])
|
}, [noteId, showErrorNotification, type])
|
||||||
|
|
||||||
const onSetEntryWriteable = useCallback(() => {
|
const onSetEntryWriteable = useCallback(() => {
|
||||||
setGroupPermission(noteId, type, true)
|
setGroupPermission(noteId, type, true)
|
||||||
|
@ -42,7 +43,7 @@ export const PermissionEntrySpecialGroup: React.FC<PermissionEntrySpecialGroupPr
|
||||||
setNotePermissionsFromServer(updatedPermissions)
|
setNotePermissionsFromServer(updatedPermissions)
|
||||||
})
|
})
|
||||||
.catch(showErrorNotification('editor.modal.permissions.error'))
|
.catch(showErrorNotification('editor.modal.permissions.error'))
|
||||||
}, [noteId, type])
|
}, [noteId, showErrorNotification, type])
|
||||||
|
|
||||||
const onSetEntryDenied = useCallback(() => {
|
const onSetEntryDenied = useCallback(() => {
|
||||||
removeGroupPermission(noteId, type)
|
removeGroupPermission(noteId, type)
|
||||||
|
@ -50,7 +51,7 @@ export const PermissionEntrySpecialGroup: React.FC<PermissionEntrySpecialGroupPr
|
||||||
setNotePermissionsFromServer(updatedPermissions)
|
setNotePermissionsFromServer(updatedPermissions)
|
||||||
})
|
})
|
||||||
.catch(showErrorNotification('editor.modal.permissions.error'))
|
.catch(showErrorNotification('editor.modal.permissions.error'))
|
||||||
}, [noteId, type])
|
}, [noteId, showErrorNotification, type])
|
||||||
|
|
||||||
const name = useMemo(() => {
|
const name = useMemo(() => {
|
||||||
switch (type) {
|
switch (type) {
|
||||||
|
|
|
@ -14,7 +14,7 @@ import { ShowIf } from '../../../common/show-if/show-if'
|
||||||
import { removeUserPermission, setUserPermission } from '../../../../api/permissions'
|
import { removeUserPermission, setUserPermission } from '../../../../api/permissions'
|
||||||
import { useApplicationState } from '../../../../hooks/common/use-application-state'
|
import { useApplicationState } from '../../../../hooks/common/use-application-state'
|
||||||
import { setNotePermissionsFromServer } from '../../../../redux/note-details/methods'
|
import { setNotePermissionsFromServer } from '../../../../redux/note-details/methods'
|
||||||
import { showErrorNotification } from '../../../../redux/ui-notifications/methods'
|
import { useUiNotifications } from '../../../notifications/ui-notification-boundary'
|
||||||
|
|
||||||
export interface PermissionEntryUserProps {
|
export interface PermissionEntryUserProps {
|
||||||
entry: NoteUserPermissionEntry
|
entry: NoteUserPermissionEntry
|
||||||
|
@ -27,6 +27,7 @@ export interface PermissionEntryUserProps {
|
||||||
*/
|
*/
|
||||||
export const PermissionEntryUser: React.FC<PermissionEntryUserProps> = ({ entry }) => {
|
export const PermissionEntryUser: React.FC<PermissionEntryUserProps> = ({ entry }) => {
|
||||||
const noteId = useApplicationState((state) => state.noteDetails.primaryAddress)
|
const noteId = useApplicationState((state) => state.noteDetails.primaryAddress)
|
||||||
|
const { showErrorNotification } = useUiNotifications()
|
||||||
|
|
||||||
const onRemoveEntry = useCallback(() => {
|
const onRemoveEntry = useCallback(() => {
|
||||||
removeUserPermission(noteId, entry.username)
|
removeUserPermission(noteId, entry.username)
|
||||||
|
@ -34,7 +35,7 @@ export const PermissionEntryUser: React.FC<PermissionEntryUserProps> = ({ entry
|
||||||
setNotePermissionsFromServer(updatedPermissions)
|
setNotePermissionsFromServer(updatedPermissions)
|
||||||
})
|
})
|
||||||
.catch(showErrorNotification('editor.modal.permissions.error'))
|
.catch(showErrorNotification('editor.modal.permissions.error'))
|
||||||
}, [noteId, entry.username])
|
}, [noteId, entry.username, showErrorNotification])
|
||||||
|
|
||||||
const onSetEntryReadOnly = useCallback(() => {
|
const onSetEntryReadOnly = useCallback(() => {
|
||||||
setUserPermission(noteId, entry.username, false)
|
setUserPermission(noteId, entry.username, false)
|
||||||
|
@ -42,7 +43,7 @@ export const PermissionEntryUser: React.FC<PermissionEntryUserProps> = ({ entry
|
||||||
setNotePermissionsFromServer(updatedPermissions)
|
setNotePermissionsFromServer(updatedPermissions)
|
||||||
})
|
})
|
||||||
.catch(showErrorNotification('editor.modal.permissions.error'))
|
.catch(showErrorNotification('editor.modal.permissions.error'))
|
||||||
}, [noteId, entry.username])
|
}, [noteId, entry.username, showErrorNotification])
|
||||||
|
|
||||||
const onSetEntryWriteable = useCallback(() => {
|
const onSetEntryWriteable = useCallback(() => {
|
||||||
setUserPermission(noteId, entry.username, true)
|
setUserPermission(noteId, entry.username, true)
|
||||||
|
@ -50,7 +51,7 @@ export const PermissionEntryUser: React.FC<PermissionEntryUserProps> = ({ entry
|
||||||
setNotePermissionsFromServer(updatedPermissions)
|
setNotePermissionsFromServer(updatedPermissions)
|
||||||
})
|
})
|
||||||
.catch(showErrorNotification('editor.modal.permissions.error'))
|
.catch(showErrorNotification('editor.modal.permissions.error'))
|
||||||
}, [noteId, entry.username])
|
}, [noteId, entry.username, showErrorNotification])
|
||||||
|
|
||||||
const { value, loading, error } = useAsync(async () => {
|
const { value, loading, error } = useAsync(async () => {
|
||||||
return await getUser(entry.username)
|
return await getUser(entry.username)
|
||||||
|
|
|
@ -9,8 +9,8 @@ import { PermissionOwnerChange } from './permission-owner-change'
|
||||||
import { PermissionOwnerInfo } from './permission-owner-info'
|
import { PermissionOwnerInfo } from './permission-owner-info'
|
||||||
import { setNoteOwner } from '../../../../api/permissions'
|
import { setNoteOwner } from '../../../../api/permissions'
|
||||||
import { useApplicationState } from '../../../../hooks/common/use-application-state'
|
import { useApplicationState } from '../../../../hooks/common/use-application-state'
|
||||||
import { showErrorNotification } from '../../../../redux/ui-notifications/methods'
|
|
||||||
import { setNotePermissionsFromServer } from '../../../../redux/note-details/methods'
|
import { setNotePermissionsFromServer } from '../../../../redux/note-details/methods'
|
||||||
|
import { useUiNotifications } from '../../../notifications/ui-notification-boundary'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Section in the permissions modal for managing the owner of a note.
|
* Section in the permissions modal for managing the owner of a note.
|
||||||
|
@ -18,6 +18,7 @@ import { setNotePermissionsFromServer } from '../../../../redux/note-details/met
|
||||||
export const PermissionSectionOwner: React.FC = () => {
|
export const PermissionSectionOwner: React.FC = () => {
|
||||||
const noteId = useApplicationState((state) => state.noteDetails.primaryAddress)
|
const noteId = useApplicationState((state) => state.noteDetails.primaryAddress)
|
||||||
const [changeOwner, setChangeOwner] = useState(false)
|
const [changeOwner, setChangeOwner] = useState(false)
|
||||||
|
const { showErrorNotification } = useUiNotifications()
|
||||||
|
|
||||||
const onSetChangeOwner = useCallback(() => {
|
const onSetChangeOwner = useCallback(() => {
|
||||||
setChangeOwner(true)
|
setChangeOwner(true)
|
||||||
|
@ -34,7 +35,7 @@ export const PermissionSectionOwner: React.FC = () => {
|
||||||
setChangeOwner(false)
|
setChangeOwner(false)
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
[noteId]
|
[noteId, showErrorNotification]
|
||||||
)
|
)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
|
@ -10,7 +10,7 @@ import { PermissionEntryUser } from './permission-entry-user'
|
||||||
import { PermissionAddEntryField } from './permission-add-entry-field'
|
import { PermissionAddEntryField } from './permission-add-entry-field'
|
||||||
import { setUserPermission } from '../../../../api/permissions'
|
import { setUserPermission } from '../../../../api/permissions'
|
||||||
import { setNotePermissionsFromServer } from '../../../../redux/note-details/methods'
|
import { setNotePermissionsFromServer } from '../../../../redux/note-details/methods'
|
||||||
import { showErrorNotification } from '../../../../redux/ui-notifications/methods'
|
import { useUiNotifications } from '../../../notifications/ui-notification-boundary'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Section of the permission modal for managing user access to the note.
|
* Section of the permission modal for managing user access to the note.
|
||||||
|
@ -19,6 +19,7 @@ export const PermissionSectionUsers: React.FC = () => {
|
||||||
useTranslation()
|
useTranslation()
|
||||||
const userPermissions = useApplicationState((state) => state.noteDetails.permissions.sharedToUsers)
|
const userPermissions = useApplicationState((state) => state.noteDetails.permissions.sharedToUsers)
|
||||||
const noteId = useApplicationState((state) => state.noteDetails.primaryAddress)
|
const noteId = useApplicationState((state) => state.noteDetails.primaryAddress)
|
||||||
|
const { showErrorNotification } = useUiNotifications()
|
||||||
|
|
||||||
const userEntries = useMemo(() => {
|
const userEntries = useMemo(() => {
|
||||||
return userPermissions.map((entry) => <PermissionEntryUser key={entry.username} entry={entry} />)
|
return userPermissions.map((entry) => <PermissionEntryUser key={entry.username} entry={entry} />)
|
||||||
|
@ -32,7 +33,7 @@ export const PermissionSectionUsers: React.FC = () => {
|
||||||
})
|
})
|
||||||
.catch(showErrorNotification('editor.modal.permissions.error'))
|
.catch(showErrorNotification('editor.modal.permissions.error'))
|
||||||
},
|
},
|
||||||
[noteId]
|
[noteId, showErrorNotification]
|
||||||
)
|
)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
|
@ -13,10 +13,10 @@ import { UserAvatar } from '../../../common/user-avatar/user-avatar'
|
||||||
import styles from './revision-list-entry.module.scss'
|
import styles from './revision-list-entry.module.scss'
|
||||||
import type { RevisionMetadata } from '../../../../api/revisions/types'
|
import type { RevisionMetadata } from '../../../../api/revisions/types'
|
||||||
import { getUserDataForRevision } from './utils'
|
import { getUserDataForRevision } from './utils'
|
||||||
import { showErrorNotification } from '../../../../redux/ui-notifications/methods'
|
|
||||||
import { useAsync } from 'react-use'
|
import { useAsync } from 'react-use'
|
||||||
import { ShowIf } from '../../../common/show-if/show-if'
|
import { ShowIf } from '../../../common/show-if/show-if'
|
||||||
import { WaitSpinner } from '../../../common/wait-spinner/wait-spinner'
|
import { WaitSpinner } from '../../../common/wait-spinner/wait-spinner'
|
||||||
|
import { useUiNotifications } from '../../../notifications/ui-notification-boundary'
|
||||||
|
|
||||||
export interface RevisionListEntryProps {
|
export interface RevisionListEntryProps {
|
||||||
active: boolean
|
active: boolean
|
||||||
|
@ -33,6 +33,7 @@ export interface RevisionListEntryProps {
|
||||||
*/
|
*/
|
||||||
export const RevisionListEntry: React.FC<RevisionListEntryProps> = ({ active, onSelect, revision }) => {
|
export const RevisionListEntry: React.FC<RevisionListEntryProps> = ({ active, onSelect, revision }) => {
|
||||||
useTranslation()
|
useTranslation()
|
||||||
|
const { showErrorNotification } = useUiNotifications()
|
||||||
|
|
||||||
const revisionCreationTime = useMemo(() => {
|
const revisionCreationTime = useMemo(() => {
|
||||||
return DateTime.fromISO(revision.createdAt).toFormat('DDDD T')
|
return DateTime.fromISO(revision.createdAt).toFormat('DDDD T')
|
||||||
|
|
|
@ -10,7 +10,7 @@ import { downloadRevision } from './utils'
|
||||||
import type { ModalVisibilityProps } from '../../../common/modals/common-modal'
|
import type { ModalVisibilityProps } from '../../../common/modals/common-modal'
|
||||||
import { useApplicationState } from '../../../../hooks/common/use-application-state'
|
import { useApplicationState } from '../../../../hooks/common/use-application-state'
|
||||||
import { getRevision } from '../../../../api/revisions'
|
import { getRevision } from '../../../../api/revisions'
|
||||||
import { showErrorNotification } from '../../../../redux/ui-notifications/methods'
|
import { useUiNotifications } from '../../../notifications/ui-notification-boundary'
|
||||||
|
|
||||||
export interface RevisionModalFooterProps {
|
export interface RevisionModalFooterProps {
|
||||||
selectedRevisionId?: number
|
selectedRevisionId?: number
|
||||||
|
@ -29,6 +29,7 @@ export const RevisionModalFooter: React.FC<RevisionModalFooterProps & Pick<Modal
|
||||||
}) => {
|
}) => {
|
||||||
useTranslation()
|
useTranslation()
|
||||||
const noteIdentifier = useApplicationState((state) => state.noteDetails.primaryAddress)
|
const noteIdentifier = useApplicationState((state) => state.noteDetails.primaryAddress)
|
||||||
|
const { showErrorNotification } = useUiNotifications()
|
||||||
|
|
||||||
const onRevertToRevision = useCallback(() => {
|
const onRevertToRevision = useCallback(() => {
|
||||||
// TODO Websocket message handler missing
|
// TODO Websocket message handler missing
|
||||||
|
@ -45,7 +46,7 @@ export const RevisionModalFooter: React.FC<RevisionModalFooterProps & Pick<Modal
|
||||||
downloadRevision(noteIdentifier, revision)
|
downloadRevision(noteIdentifier, revision)
|
||||||
})
|
})
|
||||||
.catch(showErrorNotification(''))
|
.catch(showErrorNotification(''))
|
||||||
}, [noteIdentifier, selectedRevisionId])
|
}, [noteIdentifier, selectedRevisionId, showErrorNotification])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Modal.Footer>
|
<Modal.Footer>
|
||||||
|
|
|
@ -16,7 +16,6 @@ import { Sidebar } from './sidebar/sidebar'
|
||||||
import { Splitter } from './splitter/splitter'
|
import { Splitter } from './splitter/splitter'
|
||||||
import type { DualScrollState, ScrollState } from './synced-scroll/scroll-props'
|
import type { DualScrollState, ScrollState } from './synced-scroll/scroll-props'
|
||||||
import { useEditorModeFromUrl } from './hooks/use-editor-mode-from-url'
|
import { useEditorModeFromUrl } from './hooks/use-editor-mode-from-url'
|
||||||
import { UiNotifications } from '../notifications/ui-notifications'
|
|
||||||
import { useUpdateLocalHistoryEntry } from './hooks/use-update-local-history-entry'
|
import { useUpdateLocalHistoryEntry } from './hooks/use-update-local-history-entry'
|
||||||
import { useApplicationState } from '../../hooks/common/use-application-state'
|
import { useApplicationState } from '../../hooks/common/use-application-state'
|
||||||
import { EditorDocumentRenderer } from './editor-document-renderer/editor-document-renderer'
|
import { EditorDocumentRenderer } from './editor-document-renderer/editor-document-renderer'
|
||||||
|
@ -128,7 +127,6 @@ export const EditorPageContent: React.FC = () => {
|
||||||
return (
|
return (
|
||||||
<ChangeEditorContentContextProvider>
|
<ChangeEditorContentContextProvider>
|
||||||
<NoteAndAppTitleHead />
|
<NoteAndAppTitleHead />
|
||||||
<UiNotifications />
|
|
||||||
<MotdModal />
|
<MotdModal />
|
||||||
<div className={'d-flex flex-column vh-100'}>
|
<div className={'d-flex flex-column vh-100'}>
|
||||||
<AppBar mode={AppBarMode.EDITOR} />
|
<AppBar mode={AppBarMode.EDITOR} />
|
||||||
|
|
|
@ -15,7 +15,7 @@ import { replaceInContent } from '../tool-bar/formatters/replace-in-content'
|
||||||
import type { CursorSelection } from '../tool-bar/formatters/types/cursor-selection'
|
import type { CursorSelection } from '../tool-bar/formatters/types/cursor-selection'
|
||||||
import type { EditorView } from '@codemirror/view'
|
import type { EditorView } from '@codemirror/view'
|
||||||
import type { ContentFormatter } from '../../change-content-context/change-content-context'
|
import type { ContentFormatter } from '../../change-content-context/change-content-context'
|
||||||
import { showErrorNotification } from '../../../../redux/ui-notifications/methods'
|
import { useUiNotifications } from '../../../notifications/ui-notification-boundary'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param view the codemirror instance that is used to insert the Markdown code
|
* @param view the codemirror instance that is used to insert the Markdown code
|
||||||
|
@ -36,36 +36,41 @@ type handleUploadSignature = (
|
||||||
* Provides a callback that uploads a given file and inserts the correct Markdown code into the current editor.
|
* Provides a callback that uploads a given file and inserts the correct Markdown code into the current editor.
|
||||||
*/
|
*/
|
||||||
export const useHandleUpload = (): handleUploadSignature => {
|
export const useHandleUpload = (): handleUploadSignature => {
|
||||||
return useCallback((view, file, cursorSelection, description, additionalUrlText) => {
|
const { showErrorNotification } = useUiNotifications()
|
||||||
const changeContent = (callback: ContentFormatter) => changeEditorContent(view, callback)
|
|
||||||
if (!file || !supportedMimeTypes.includes(file.type) || !changeContent) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
const randomId = Math.random().toString(36).slice(7)
|
|
||||||
const uploadFileInfo = description
|
|
||||||
? t('editor.upload.uploadFile.withDescription', { fileName: file.name, description: description })
|
|
||||||
: t('editor.upload.uploadFile.withoutDescription', { fileName: file.name })
|
|
||||||
|
|
||||||
const uploadPlaceholder = `![${uploadFileInfo}](upload-${randomId}${additionalUrlText ?? ''})`
|
return useCallback(
|
||||||
const noteId = getGlobalState().noteDetails.id
|
(view, file, cursorSelection, description, additionalUrlText) => {
|
||||||
changeContent(({ currentSelection }) => {
|
const changeContent = (callback: ContentFormatter) => changeEditorContent(view, callback)
|
||||||
return replaceSelection(cursorSelection ?? currentSelection, uploadPlaceholder, false)
|
if (!file || !supportedMimeTypes.includes(file.type) || !changeContent) {
|
||||||
})
|
return
|
||||||
uploadFile(noteId, file)
|
}
|
||||||
.then(({ url }) => {
|
const randomId = Math.random().toString(36).slice(7)
|
||||||
const replacement = `![${description ?? file.name ?? ''}](${url}${additionalUrlText ?? ''})`
|
const uploadFileInfo = description
|
||||||
changeContent(({ markdownContent }) => [
|
? t('editor.upload.uploadFile.withDescription', { fileName: file.name, description: description })
|
||||||
replaceInContent(markdownContent, uploadPlaceholder, replacement),
|
: t('editor.upload.uploadFile.withoutDescription', { fileName: file.name })
|
||||||
undefined
|
|
||||||
])
|
const uploadPlaceholder = `![${uploadFileInfo}](upload-${randomId}${additionalUrlText ?? ''})`
|
||||||
|
const noteId = getGlobalState().noteDetails.id
|
||||||
|
changeContent(({ currentSelection }) => {
|
||||||
|
return replaceSelection(cursorSelection ?? currentSelection, uploadPlaceholder, false)
|
||||||
})
|
})
|
||||||
.catch((error: Error) => {
|
uploadFile(noteId, file)
|
||||||
showErrorNotification('editor.upload.failed', { fileName: file.name })(error)
|
.then(({ url }) => {
|
||||||
const replacement = `![upload of ${file.name} failed]()`
|
const replacement = `![${description ?? file.name ?? ''}](${url}${additionalUrlText ?? ''})`
|
||||||
changeContent(({ markdownContent }) => [
|
changeContent(({ markdownContent }) => [
|
||||||
replaceInContent(markdownContent, uploadPlaceholder, replacement),
|
replaceInContent(markdownContent, uploadPlaceholder, replacement),
|
||||||
undefined
|
undefined
|
||||||
])
|
])
|
||||||
})
|
})
|
||||||
}, [])
|
.catch((error: Error) => {
|
||||||
|
showErrorNotification('editor.upload.failed', { fileName: file.name })(error)
|
||||||
|
const replacement = `![upload of ${file.name} failed]()`
|
||||||
|
changeContent(({ markdownContent }) => [
|
||||||
|
replaceInContent(markdownContent, uploadPlaceholder, replacement),
|
||||||
|
undefined
|
||||||
|
])
|
||||||
|
})
|
||||||
|
},
|
||||||
|
[showErrorNotification]
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -11,12 +11,12 @@ import { SidebarButton } from '../sidebar-button/sidebar-button'
|
||||||
import type { SpecificSidebarEntryProps } from '../types'
|
import type { SpecificSidebarEntryProps } from '../types'
|
||||||
import { useApplicationState } from '../../../../hooks/common/use-application-state'
|
import { useApplicationState } from '../../../../hooks/common/use-application-state'
|
||||||
import { cypressId } from '../../../../utils/cypress-attribute'
|
import { cypressId } from '../../../../utils/cypress-attribute'
|
||||||
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 { useBooleanState } from '../../../../hooks/common/use-boolean-state'
|
import { useBooleanState } from '../../../../hooks/common/use-boolean-state'
|
||||||
import { useRouter } from 'next/router'
|
import { useRouter } from 'next/router'
|
||||||
import { Logger } from '../../../../utils/logger'
|
import { Logger } from '../../../../utils/logger'
|
||||||
|
import { useUiNotifications } from '../../../notifications/ui-notification-boundary'
|
||||||
|
|
||||||
const logger = new Logger('note-deletion')
|
const logger = new Logger('note-deletion')
|
||||||
|
|
||||||
|
@ -31,6 +31,8 @@ export const DeleteNoteSidebarEntry: React.FC<PropsWithChildren<SpecificSidebarE
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const noteId = useApplicationState((state) => state.noteDetails.id)
|
const noteId = useApplicationState((state) => state.noteDetails.id)
|
||||||
const [modalVisibility, showModal, closeModal] = useBooleanState()
|
const [modalVisibility, showModal, closeModal] = useBooleanState()
|
||||||
|
const { showErrorNotification } = useUiNotifications()
|
||||||
|
|
||||||
const deleteNoteAndCloseDialog = useCallback(() => {
|
const deleteNoteAndCloseDialog = useCallback(() => {
|
||||||
deleteNote(noteId)
|
deleteNote(noteId)
|
||||||
.then(() => {
|
.then(() => {
|
||||||
|
@ -38,7 +40,7 @@ export const DeleteNoteSidebarEntry: React.FC<PropsWithChildren<SpecificSidebarE
|
||||||
})
|
})
|
||||||
.catch(showErrorNotification('landing.history.error.deleteNote.text'))
|
.catch(showErrorNotification('landing.history.error.deleteNote.text'))
|
||||||
.finally(closeModal)
|
.finally(closeModal)
|
||||||
}, [closeModal, noteId, router])
|
}, [closeModal, noteId, router, showErrorNotification])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Fragment>
|
<Fragment>
|
||||||
|
|
|
@ -9,9 +9,9 @@ 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'
|
||||||
import { toggleHistoryEntryPinning } from '../../../../redux/history/methods'
|
import { toggleHistoryEntryPinning } from '../../../../redux/history/methods'
|
||||||
import { showErrorNotification } from '../../../../redux/ui-notifications/methods'
|
|
||||||
import { useApplicationState } from '../../../../hooks/common/use-application-state'
|
import { useApplicationState } from '../../../../hooks/common/use-application-state'
|
||||||
import styles from './pin-note-sidebar-entry.module.css'
|
import styles from './pin-note-sidebar-entry.module.css'
|
||||||
|
import { useUiNotifications } from '../../../notifications/ui-notification-boundary'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sidebar entry button that toggles the pinned status of the current note in the history.
|
* Sidebar entry button that toggles the pinned status of the current note in the history.
|
||||||
|
@ -23,6 +23,7 @@ export const PinNoteSidebarEntry: React.FC<SpecificSidebarEntryProps> = ({ class
|
||||||
useTranslation()
|
useTranslation()
|
||||||
const id = useApplicationState((state) => state.noteDetails.id)
|
const id = useApplicationState((state) => state.noteDetails.id)
|
||||||
const history = useApplicationState((state) => state.history)
|
const history = useApplicationState((state) => state.history)
|
||||||
|
const { showErrorNotification } = useUiNotifications()
|
||||||
|
|
||||||
const isPinned = useMemo(() => {
|
const isPinned = useMemo(() => {
|
||||||
const entry = history.find((entry) => entry.identifier === id)
|
const entry = history.find((entry) => entry.identifier === id)
|
||||||
|
@ -34,7 +35,7 @@ export const PinNoteSidebarEntry: React.FC<SpecificSidebarEntryProps> = ({ class
|
||||||
|
|
||||||
const onPinClicked = useCallback(() => {
|
const onPinClicked = useCallback(() => {
|
||||||
toggleHistoryEntryPinning(id).catch(showErrorNotification('landing.history.error.updateEntry.text'))
|
toggleHistoryEntryPinning(id).catch(showErrorNotification('landing.history.error.updateEntry.text'))
|
||||||
}, [id])
|
}, [id, showErrorNotification])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SidebarButton
|
<SidebarButton
|
||||||
|
|
|
@ -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
|
||||||
*/
|
*/
|
||||||
|
@ -13,11 +13,11 @@ import { HistoryTable } from '../history-table/history-table'
|
||||||
import { ViewStateEnum } from '../history-toolbar/history-toolbar'
|
import { ViewStateEnum } from '../history-toolbar/history-toolbar'
|
||||||
import { removeHistoryEntry, toggleHistoryEntryPinning } from '../../../redux/history/methods'
|
import { removeHistoryEntry, toggleHistoryEntryPinning } from '../../../redux/history/methods'
|
||||||
import { deleteNote } from '../../../api/notes'
|
import { deleteNote } from '../../../api/notes'
|
||||||
import { showErrorNotification } from '../../../redux/ui-notifications/methods'
|
|
||||||
import { useApplicationState } from '../../../hooks/common/use-application-state'
|
import { useApplicationState } from '../../../hooks/common/use-application-state'
|
||||||
import { sortAndFilterEntries } from '../utils'
|
import { sortAndFilterEntries } from '../utils'
|
||||||
import { useHistoryToolbarState } from '../history-toolbar/toolbar-context/use-history-toolbar-state'
|
import { useHistoryToolbarState } from '../history-toolbar/toolbar-context/use-history-toolbar-state'
|
||||||
import type { HistoryEntryWithOrigin } from '../../../api/history/types'
|
import type { HistoryEntryWithOrigin } from '../../../api/history/types'
|
||||||
|
import { useUiNotifications } from '../../notifications/ui-notification-boundary'
|
||||||
|
|
||||||
type OnEntryClick = (entryId: string) => void
|
type OnEntryClick = (entryId: string) => void
|
||||||
|
|
||||||
|
@ -46,27 +46,36 @@ export const HistoryContent: React.FC = () => {
|
||||||
const [lastPageIndex, setLastPageIndex] = useState(0)
|
const [lastPageIndex, setLastPageIndex] = useState(0)
|
||||||
|
|
||||||
const allEntries = useApplicationState((state) => state.history)
|
const allEntries = useApplicationState((state) => state.history)
|
||||||
|
|
||||||
const [historyToolbarState] = useHistoryToolbarState()
|
const [historyToolbarState] = useHistoryToolbarState()
|
||||||
|
const { showErrorNotification } = useUiNotifications()
|
||||||
|
|
||||||
const entriesToShow = useMemo<HistoryEntryWithOrigin[]>(
|
const entriesToShow = useMemo<HistoryEntryWithOrigin[]>(
|
||||||
() => sortAndFilterEntries(allEntries, historyToolbarState),
|
() => sortAndFilterEntries(allEntries, historyToolbarState),
|
||||||
[allEntries, historyToolbarState]
|
[allEntries, historyToolbarState]
|
||||||
)
|
)
|
||||||
|
|
||||||
const onPinClick = useCallback((noteId: string) => {
|
const onPinClick = useCallback(
|
||||||
toggleHistoryEntryPinning(noteId).catch(showErrorNotification('landing.history.error.updateEntry.text'))
|
(noteId: string) => {
|
||||||
}, [])
|
toggleHistoryEntryPinning(noteId).catch(showErrorNotification('landing.history.error.updateEntry.text'))
|
||||||
|
},
|
||||||
|
[showErrorNotification]
|
||||||
|
)
|
||||||
|
|
||||||
const onDeleteClick = useCallback((noteId: string) => {
|
const onDeleteClick = useCallback(
|
||||||
deleteNote(noteId)
|
(noteId: string) => {
|
||||||
.then(() => removeHistoryEntry(noteId))
|
deleteNote(noteId)
|
||||||
.catch(showErrorNotification('landing.history.error.deleteNote.text'))
|
.then(() => removeHistoryEntry(noteId))
|
||||||
}, [])
|
.catch(showErrorNotification('landing.history.error.deleteNote.text'))
|
||||||
|
},
|
||||||
|
[showErrorNotification]
|
||||||
|
)
|
||||||
|
|
||||||
const onRemoveClick = useCallback((noteId: string) => {
|
const onRemoveClick = useCallback(
|
||||||
removeHistoryEntry(noteId).catch(showErrorNotification('landing.history.error.deleteEntry.text'))
|
(noteId: string) => {
|
||||||
}, [])
|
removeHistoryEntry(noteId).catch(showErrorNotification('landing.history.error.deleteEntry.text'))
|
||||||
|
},
|
||||||
|
[showErrorNotification]
|
||||||
|
)
|
||||||
|
|
||||||
const historyContent = useMemo(() => {
|
const historyContent = useMemo(() => {
|
||||||
switch (historyToolbarState.viewState) {
|
switch (historyToolbarState.viewState) {
|
||||||
|
|
|
@ -9,10 +9,11 @@ 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'
|
||||||
import { DeletionModal } from '../../common/modals/deletion-modal'
|
import { DeletionModal } from '../../common/modals/deletion-modal'
|
||||||
import { deleteAllHistoryEntries, safeRefreshHistoryState } from '../../../redux/history/methods'
|
import { deleteAllHistoryEntries } from '../../../redux/history/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'
|
import { useBooleanState } from '../../../hooks/common/use-boolean-state'
|
||||||
|
import { useUiNotifications } from '../../notifications/ui-notification-boundary'
|
||||||
|
import { useSafeRefreshHistoryStateCallback } from './hooks/use-safe-refresh-history-state'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Renders a button to clear the complete history of the user.
|
* Renders a button to clear the complete history of the user.
|
||||||
|
@ -21,6 +22,8 @@ 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 [modalVisibility, showModal, closeModal] = useBooleanState()
|
const [modalVisibility, showModal, closeModal] = useBooleanState()
|
||||||
|
const { showErrorNotification } = useUiNotifications()
|
||||||
|
const safeRefreshHistoryState = useSafeRefreshHistoryStateCallback()
|
||||||
|
|
||||||
const onConfirm = useCallback(() => {
|
const onConfirm = useCallback(() => {
|
||||||
deleteAllHistoryEntries().catch((error: Error) => {
|
deleteAllHistoryEntries().catch((error: Error) => {
|
||||||
|
@ -28,7 +31,7 @@ export const ClearHistoryButton: React.FC = () => {
|
||||||
safeRefreshHistoryState()
|
safeRefreshHistoryState()
|
||||||
})
|
})
|
||||||
closeModal()
|
closeModal()
|
||||||
}, [closeModal])
|
}, [closeModal, safeRefreshHistoryState, showErrorNotification])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Fragment>
|
<Fragment>
|
||||||
|
|
|
@ -1,14 +1,14 @@
|
||||||
/*
|
/*
|
||||||
* 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 } from 'react'
|
import React from 'react'
|
||||||
import { Button } from 'react-bootstrap'
|
import { Button } from 'react-bootstrap'
|
||||||
import { ForkAwesomeIcon } from '../../common/fork-awesome/fork-awesome-icon'
|
import { ForkAwesomeIcon } from '../../common/fork-awesome/fork-awesome-icon'
|
||||||
import { safeRefreshHistoryState } from '../../../redux/history/methods'
|
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
|
import { useSafeRefreshHistoryStateCallback } from './hooks/use-safe-refresh-history-state'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Fetches the current history from the server.
|
* Fetches the current history from the server.
|
||||||
|
@ -16,9 +16,7 @@ import { useTranslation } from 'react-i18next'
|
||||||
export const HistoryRefreshButton: React.FC = () => {
|
export const HistoryRefreshButton: React.FC = () => {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
|
|
||||||
const refreshHistory = useCallback(() => {
|
const refreshHistory = useSafeRefreshHistoryStateCallback()
|
||||||
safeRefreshHistoryState()
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Button variant={'light'} title={t('landing.history.toolbar.refresh')} onClick={refreshHistory}>
|
<Button variant={'light'} title={t('landing.history.toolbar.refresh')} onClick={refreshHistory}>
|
||||||
|
|
|
@ -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,8 +12,7 @@ import { ShowIf } from '../../common/show-if/show-if'
|
||||||
import { ClearHistoryButton } from './clear-history-button'
|
import { ClearHistoryButton } from './clear-history-button'
|
||||||
import { ExportHistoryButton } from './export-history-button'
|
import { ExportHistoryButton } from './export-history-button'
|
||||||
import { ImportHistoryButton } from './import-history-button'
|
import { ImportHistoryButton } from './import-history-button'
|
||||||
import { importHistoryEntries, safeRefreshHistoryState, setHistoryEntries } from '../../../redux/history/methods'
|
import { importHistoryEntries, setHistoryEntries } from '../../../redux/history/methods'
|
||||||
import { showErrorNotification } from '../../../redux/ui-notifications/methods'
|
|
||||||
import { useApplicationState } from '../../../hooks/common/use-application-state'
|
import { useApplicationState } from '../../../hooks/common/use-application-state'
|
||||||
import { KeywordSearchInput } from './keyword-search-input'
|
import { KeywordSearchInput } from './keyword-search-input'
|
||||||
import { TagSelectionInput } from './tag-selection-input'
|
import { TagSelectionInput } from './tag-selection-input'
|
||||||
|
@ -23,6 +22,8 @@ import { SortByLastVisitedButton } from './sort-by-last-visited-button'
|
||||||
import { HistoryViewModeToggleButton } from './history-view-mode-toggle-button'
|
import { HistoryViewModeToggleButton } from './history-view-mode-toggle-button'
|
||||||
import { useSyncToolbarStateToUrlEffect } from './toolbar-context/use-sync-toolbar-state-to-url-effect'
|
import { useSyncToolbarStateToUrlEffect } from './toolbar-context/use-sync-toolbar-state-to-url-effect'
|
||||||
import { HistoryEntryOrigin } from '../../../api/history/types'
|
import { HistoryEntryOrigin } from '../../../api/history/types'
|
||||||
|
import { useUiNotifications } from '../../notifications/ui-notification-boundary'
|
||||||
|
import { useSafeRefreshHistoryStateCallback } from './hooks/use-safe-refresh-history-state'
|
||||||
|
|
||||||
export enum ViewStateEnum {
|
export enum ViewStateEnum {
|
||||||
CARD,
|
CARD,
|
||||||
|
@ -36,7 +37,8 @@ export const HistoryToolbar: React.FC = () => {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const historyEntries = useApplicationState((state) => state.history)
|
const historyEntries = useApplicationState((state) => state.history)
|
||||||
const userExists = useApplicationState((state) => !!state.user)
|
const userExists = useApplicationState((state) => !!state.user)
|
||||||
|
const { showErrorNotification } = useUiNotifications()
|
||||||
|
const safeRefreshHistoryState = useSafeRefreshHistoryStateCallback()
|
||||||
useSyncToolbarStateToUrlEffect()
|
useSyncToolbarStateToUrlEffect()
|
||||||
|
|
||||||
const onUploadAllToRemote = useCallback(() => {
|
const onUploadAllToRemote = useCallback(() => {
|
||||||
|
@ -57,7 +59,7 @@ export const HistoryToolbar: React.FC = () => {
|
||||||
setHistoryEntries(historyEntries)
|
setHistoryEntries(historyEntries)
|
||||||
safeRefreshHistoryState()
|
safeRefreshHistoryState()
|
||||||
})
|
})
|
||||||
}, [userExists, historyEntries])
|
}, [userExists, historyEntries, showErrorNotification, safeRefreshHistoryState])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Form inline={true}>
|
<Form inline={true}>
|
||||||
|
|
|
@ -0,0 +1,19 @@
|
||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file)
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { useUiNotifications } from '../../../notifications/ui-notification-boundary'
|
||||||
|
import { useCallback } from 'react'
|
||||||
|
import { refreshHistoryState } from '../../../../redux/history/methods'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tries to refresh the history from the backend and shows notification if that request fails.
|
||||||
|
*/
|
||||||
|
export const useSafeRefreshHistoryStateCallback = () => {
|
||||||
|
const { showErrorNotification } = useUiNotifications()
|
||||||
|
return useCallback(() => {
|
||||||
|
refreshHistoryState().catch(showErrorNotification('landing.history.error.getHistory.text'))
|
||||||
|
}, [showErrorNotification])
|
||||||
|
}
|
|
@ -9,17 +9,13 @@ 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 type { HistoryExportJson, V1HistoryEntry } from '../../../redux/history/types'
|
import type { HistoryExportJson, V1HistoryEntry } from '../../../redux/history/types'
|
||||||
import {
|
import { convertV1History, importHistoryEntries, mergeHistoryEntries } from '../../../redux/history/methods'
|
||||||
convertV1History,
|
|
||||||
importHistoryEntries,
|
|
||||||
mergeHistoryEntries,
|
|
||||||
safeRefreshHistoryState
|
|
||||||
} from '../../../redux/history/methods'
|
|
||||||
import { dispatchUiNotification, showErrorNotification } from '../../../redux/ui-notifications/methods'
|
|
||||||
import { useApplicationState } from '../../../hooks/common/use-application-state'
|
import { useApplicationState } from '../../../hooks/common/use-application-state'
|
||||||
import { cypressId } from '../../../utils/cypress-attribute'
|
import { cypressId } from '../../../utils/cypress-attribute'
|
||||||
import type { HistoryEntryWithOrigin } from '../../../api/history/types'
|
import type { HistoryEntryWithOrigin } from '../../../api/history/types'
|
||||||
import { HistoryEntryOrigin } from '../../../api/history/types'
|
import { HistoryEntryOrigin } from '../../../api/history/types'
|
||||||
|
import { useUiNotifications } from '../../notifications/ui-notification-boundary'
|
||||||
|
import { useSafeRefreshHistoryStateCallback } from './hooks/use-safe-refresh-history-state'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Button that lets the user select a history JSON file and uploads imports that into the history.
|
* Button that lets the user select a history JSON file and uploads imports that into the history.
|
||||||
|
@ -30,6 +26,8 @@ export const ImportHistoryButton: React.FC = () => {
|
||||||
const historyState = useApplicationState((state) => state.history)
|
const historyState = useApplicationState((state) => state.history)
|
||||||
const uploadInput = useRef<HTMLInputElement>(null)
|
const uploadInput = useRef<HTMLInputElement>(null)
|
||||||
const [fileName, setFilename] = useState('')
|
const [fileName, setFilename] = useState('')
|
||||||
|
const { showErrorNotification, dispatchUiNotification } = useUiNotifications()
|
||||||
|
const safeRefreshHistoryState = useSafeRefreshHistoryStateCallback()
|
||||||
|
|
||||||
const onImportHistory = useCallback(
|
const onImportHistory = useCallback(
|
||||||
(entries: HistoryEntryWithOrigin[]): void => {
|
(entries: HistoryEntryWithOrigin[]): void => {
|
||||||
|
@ -39,7 +37,7 @@ export const ImportHistoryButton: React.FC = () => {
|
||||||
safeRefreshHistoryState()
|
safeRefreshHistoryState()
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
[historyState, userExists]
|
[historyState, safeRefreshHistoryState, showErrorNotification, userExists]
|
||||||
)
|
)
|
||||||
|
|
||||||
const resetInputField = useCallback(() => {
|
const resetInputField = useCallback(() => {
|
||||||
|
|
|
@ -10,7 +10,6 @@ import { Container } from 'react-bootstrap'
|
||||||
import { MotdModal } from '../common/motd-modal/motd-modal'
|
import { MotdModal } from '../common/motd-modal/motd-modal'
|
||||||
import { Footer } from './footer/footer'
|
import { Footer } from './footer/footer'
|
||||||
import { HeaderBar } from './navigation/header-bar/header-bar'
|
import { HeaderBar } from './navigation/header-bar/header-bar'
|
||||||
import { UiNotifications } from '../notifications/ui-notifications'
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Renders the layout for both intro and history page.
|
* Renders the layout for both intro and history page.
|
||||||
|
@ -20,7 +19,6 @@ import { UiNotifications } from '../notifications/ui-notifications'
|
||||||
export const LandingLayout: React.FC<PropsWithChildren<unknown>> = ({ children }) => {
|
export const LandingLayout: React.FC<PropsWithChildren<unknown>> = ({ children }) => {
|
||||||
return (
|
return (
|
||||||
<Fragment>
|
<Fragment>
|
||||||
<UiNotifications />
|
|
||||||
<MotdModal />
|
<MotdModal />
|
||||||
<Container className='text-light d-flex flex-column mvh-100'>
|
<Container className='text-light d-flex flex-column mvh-100'>
|
||||||
<HeaderBar />
|
<HeaderBar />
|
||||||
|
|
|
@ -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
|
||||||
*/
|
*/
|
||||||
|
@ -11,18 +11,19 @@ import { ForkAwesomeIcon } from '../../common/fork-awesome/fork-awesome-icon'
|
||||||
import { Trans, useTranslation } from 'react-i18next'
|
import { Trans, useTranslation } from 'react-i18next'
|
||||||
import { Dropdown } from 'react-bootstrap'
|
import { Dropdown } from 'react-bootstrap'
|
||||||
import { doLogout } from '../../../api/auth'
|
import { doLogout } from '../../../api/auth'
|
||||||
import { showErrorNotification } from '../../../redux/ui-notifications/methods'
|
import { useUiNotifications } from '../../notifications/ui-notification-boundary'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Renders a sign-out button as a dropdown item for the user-dropdown.
|
* Renders a sign-out button as a dropdown item for the user-dropdown.
|
||||||
*/
|
*/
|
||||||
export const SignOutDropdownButton: React.FC = () => {
|
export const SignOutDropdownButton: React.FC = () => {
|
||||||
useTranslation()
|
useTranslation()
|
||||||
|
const { showErrorNotification } = useUiNotifications()
|
||||||
|
|
||||||
const onSignOut = useCallback(() => {
|
const onSignOut = useCallback(() => {
|
||||||
clearUser()
|
clearUser()
|
||||||
doLogout().catch(showErrorNotification('login.logoutFailed'))
|
doLogout().catch(showErrorNotification('login.logoutFailed'))
|
||||||
}, [])
|
}, [showErrorNotification])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dropdown.Item dir='auto' onClick={onSignOut} {...cypressId('user-dropdown-sign-out-button')}>
|
<Dropdown.Item dir='auto' onClick={onSignOut} {...cypressId('user-dropdown-sign-out-button')}>
|
||||||
|
|
29
src/components/notifications/types.ts
Normal file
29
src/components/notifications/types.ts
Normal file
|
@ -0,0 +1,29 @@
|
||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file)
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
|
||||||
|
import type { TOptions } from 'i18next'
|
||||||
|
import type { IconName } from '../common/fork-awesome/types'
|
||||||
|
|
||||||
|
export interface UiNotificationButton {
|
||||||
|
label: string
|
||||||
|
onClick: () => void
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface DispatchOptions {
|
||||||
|
titleI18nOptions: TOptions | string
|
||||||
|
contentI18nOptions: TOptions | string
|
||||||
|
durationInSecond: number
|
||||||
|
icon?: IconName
|
||||||
|
buttons: UiNotificationButton[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface UiNotification extends DispatchOptions {
|
||||||
|
titleI18nKey: string
|
||||||
|
contentI18nKey: string
|
||||||
|
createdAtTimestamp: number
|
||||||
|
dismissed: boolean
|
||||||
|
uuid: string
|
||||||
|
}
|
114
src/components/notifications/ui-notification-boundary.tsx
Normal file
114
src/components/notifications/ui-notification-boundary.tsx
Normal file
|
@ -0,0 +1,114 @@
|
||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file)
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
|
||||||
|
import type { PropsWithChildren } from 'react'
|
||||||
|
import React, { createContext, useCallback, useContext, useMemo, useState } from 'react'
|
||||||
|
import { UiNotifications } from './ui-notifications'
|
||||||
|
import type { DispatchOptions, UiNotification } from './types'
|
||||||
|
import { DateTime } from 'luxon'
|
||||||
|
import type { TOptions } from 'i18next'
|
||||||
|
import { t } from 'i18next'
|
||||||
|
import { Logger } from '../../utils/logger'
|
||||||
|
import { v4 as uuid } from 'uuid'
|
||||||
|
|
||||||
|
const log = new Logger('Notifications')
|
||||||
|
|
||||||
|
interface UiNotificationContext {
|
||||||
|
dispatchUiNotification: (
|
||||||
|
titleI18nKey: string,
|
||||||
|
contentI18nKey: string,
|
||||||
|
dispatchOptions: Partial<DispatchOptions>
|
||||||
|
) => void
|
||||||
|
|
||||||
|
showErrorNotification: (messageI18nKey: string, messageI18nOptions?: TOptions | string) => (error: Error) => void
|
||||||
|
|
||||||
|
dismissNotification: (notificationUuid: string) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Provides utility functions to manipulate the notifications in the current context.
|
||||||
|
*/
|
||||||
|
export const useUiNotifications: () => UiNotificationContext = () => {
|
||||||
|
const communicatorFromContext = useContext(uiNotificationContext)
|
||||||
|
if (!communicatorFromContext) {
|
||||||
|
throw new Error('No ui notifications')
|
||||||
|
}
|
||||||
|
return communicatorFromContext
|
||||||
|
}
|
||||||
|
|
||||||
|
export const DEFAULT_DURATION_IN_SECONDS = 10
|
||||||
|
const uiNotificationContext = createContext<UiNotificationContext | undefined>(undefined)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Provides a UI-notification context for the given children.
|
||||||
|
*
|
||||||
|
* @param children The children that receive the context
|
||||||
|
*/
|
||||||
|
export const UiNotificationBoundary: React.FC<PropsWithChildren> = ({ children }) => {
|
||||||
|
const [uiNotifications, setUiNotifications] = useState<UiNotification[]>([])
|
||||||
|
|
||||||
|
const dispatchUiNotification = useCallback(
|
||||||
|
(
|
||||||
|
titleI18nKey: string,
|
||||||
|
contentI18nKey: string,
|
||||||
|
{ icon, durationInSecond, buttons, contentI18nOptions, titleI18nOptions }: Partial<DispatchOptions>
|
||||||
|
) => {
|
||||||
|
setUiNotifications((oldState) => [
|
||||||
|
...oldState,
|
||||||
|
{
|
||||||
|
titleI18nKey,
|
||||||
|
contentI18nKey,
|
||||||
|
createdAtTimestamp: DateTime.now().toSeconds(),
|
||||||
|
dismissed: false,
|
||||||
|
titleI18nOptions: titleI18nOptions ?? {},
|
||||||
|
contentI18nOptions: contentI18nOptions ?? {},
|
||||||
|
durationInSecond: durationInSecond ?? DEFAULT_DURATION_IN_SECONDS,
|
||||||
|
buttons: buttons ?? [],
|
||||||
|
icon: icon,
|
||||||
|
uuid: uuid()
|
||||||
|
}
|
||||||
|
])
|
||||||
|
},
|
||||||
|
[]
|
||||||
|
)
|
||||||
|
|
||||||
|
const showErrorNotification = useCallback(
|
||||||
|
(messageI18nKey: string, messageI18nOptions?: TOptions | string) =>
|
||||||
|
(error: Error): void => {
|
||||||
|
log.error(t(messageI18nKey, messageI18nOptions), error)
|
||||||
|
void dispatchUiNotification('common.errorOccurred', messageI18nKey, {
|
||||||
|
contentI18nOptions: messageI18nOptions,
|
||||||
|
icon: 'exclamation-triangle'
|
||||||
|
})
|
||||||
|
},
|
||||||
|
[dispatchUiNotification]
|
||||||
|
)
|
||||||
|
|
||||||
|
const dismissNotification = useCallback((notificationUuid: string): void => {
|
||||||
|
setUiNotifications((old) => {
|
||||||
|
const found = old.find((notification) => notification.uuid === notificationUuid)
|
||||||
|
if (found === undefined) {
|
||||||
|
return old
|
||||||
|
}
|
||||||
|
return old.filter((value) => value.uuid !== notificationUuid).concat({ ...found, dismissed: true })
|
||||||
|
})
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
const context = useMemo(() => {
|
||||||
|
return {
|
||||||
|
dispatchUiNotification: dispatchUiNotification,
|
||||||
|
showErrorNotification: showErrorNotification,
|
||||||
|
dismissNotification: dismissNotification
|
||||||
|
}
|
||||||
|
}, [dismissNotification, dispatchUiNotification, showErrorNotification])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<uiNotificationContext.Provider value={context}>
|
||||||
|
<UiNotifications notifications={uiNotifications} />
|
||||||
|
{children}
|
||||||
|
</uiNotificationContext.Provider>
|
||||||
|
)
|
||||||
|
}
|
|
@ -6,7 +6,6 @@
|
||||||
|
|
||||||
import React, { Fragment, useCallback, useEffect, useMemo, useState } from 'react'
|
import React, { Fragment, useCallback, useEffect, useMemo, useState } from 'react'
|
||||||
import { Button, ProgressBar, Toast } from 'react-bootstrap'
|
import { Button, ProgressBar, Toast } from 'react-bootstrap'
|
||||||
import type { UiNotification } from '../../redux/ui-notifications/types'
|
|
||||||
import { ForkAwesomeIcon } from '../common/fork-awesome/fork-awesome-icon'
|
import { ForkAwesomeIcon } from '../common/fork-awesome/fork-awesome-icon'
|
||||||
import { ShowIf } from '../common/show-if/show-if'
|
import { ShowIf } from '../common/show-if/show-if'
|
||||||
import type { IconName } from '../common/fork-awesome/types'
|
import type { IconName } from '../common/fork-awesome/types'
|
||||||
|
@ -14,58 +13,40 @@ import { Trans, useTranslation } from 'react-i18next'
|
||||||
import { Logger } from '../../utils/logger'
|
import { Logger } from '../../utils/logger'
|
||||||
import { cypressId } from '../../utils/cypress-attribute'
|
import { cypressId } from '../../utils/cypress-attribute'
|
||||||
import { useEffectOnce, useInterval } from 'react-use'
|
import { useEffectOnce, useInterval } from 'react-use'
|
||||||
import { dismissUiNotification } from '../../redux/ui-notifications/methods'
|
|
||||||
import styles from './notifications.module.scss'
|
import styles from './notifications.module.scss'
|
||||||
import { DateTime } from 'luxon'
|
import { DateTime } from 'luxon'
|
||||||
|
import type { UiNotification } from './types'
|
||||||
|
import { useUiNotifications } from './ui-notification-boundary'
|
||||||
|
|
||||||
const STEPS_PER_SECOND = 10
|
const STEPS_PER_SECOND = 10
|
||||||
const log = new Logger('UiNotificationToast')
|
const log = new Logger('UiNotificationToast')
|
||||||
|
|
||||||
export interface UiNotificationProps extends UiNotification {
|
export interface UiNotificationProps {
|
||||||
notificationId: number
|
notification: UiNotification
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Renders a single notification.
|
* Renders a single notification.
|
||||||
*
|
*
|
||||||
* @param titleI18nKey The i18n key for the title
|
* @param notification The notification to render
|
||||||
* @param contentI18nKey The i18n key for the content
|
|
||||||
* @param titleI18nOptions The i18n options for the title
|
|
||||||
* @param contentI18nOptions The i18n options for the content
|
|
||||||
* @param createdAtTimestamp The timestamp, when this notification was created.
|
|
||||||
* @param icon The optional icon to be used
|
|
||||||
* @param dismissed If the notification is already dismissed
|
|
||||||
* @param notificationId The notification id
|
|
||||||
* @param durationInSecond How long the notification should be shown
|
|
||||||
* @param buttons A list of {@link UiNotificationButton UiNotificationButtons} to be displayed
|
|
||||||
*/
|
*/
|
||||||
export const UiNotificationToast: React.FC<UiNotificationProps> = ({
|
export const UiNotificationToast: React.FC<UiNotificationProps> = ({ notification }) => {
|
||||||
titleI18nKey,
|
|
||||||
contentI18nKey,
|
|
||||||
titleI18nOptions,
|
|
||||||
contentI18nOptions,
|
|
||||||
createdAtTimestamp,
|
|
||||||
icon,
|
|
||||||
dismissed,
|
|
||||||
notificationId,
|
|
||||||
durationInSecond,
|
|
||||||
buttons
|
|
||||||
}) => {
|
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const [remainingSteps, setRemainingSteps] = useState<number>(() => durationInSecond * STEPS_PER_SECOND)
|
const [remainingSteps, setRemainingSteps] = useState<number>(() => notification.durationInSecond * STEPS_PER_SECOND)
|
||||||
|
const { dismissNotification } = useUiNotifications()
|
||||||
|
|
||||||
const dismissNow = useCallback(() => {
|
const dismissNow = useCallback(() => {
|
||||||
log.debug(`Dismiss notification ${notificationId} immediately`)
|
log.debug(`Dismiss notification ${notification.uuid} immediately`)
|
||||||
setRemainingSteps(0)
|
setRemainingSteps(0)
|
||||||
}, [notificationId])
|
}, [notification.uuid])
|
||||||
|
|
||||||
useEffectOnce(() => {
|
useEffectOnce(() => {
|
||||||
log.debug(`Show notification ${notificationId}`)
|
log.debug(`Show notification ${notification.uuid}`)
|
||||||
})
|
})
|
||||||
|
|
||||||
const formatCreatedAtDate = useCallback(() => {
|
const formatCreatedAtDate = useCallback(() => {
|
||||||
return DateTime.fromSeconds(createdAtTimestamp).toRelative({ style: 'short' })
|
return DateTime.fromSeconds(notification.createdAtTimestamp).toRelative({ style: 'short' })
|
||||||
}, [createdAtTimestamp])
|
}, [notification])
|
||||||
|
|
||||||
const [formattedCreatedAtDate, setFormattedCreatedAtDate] = useState(() => formatCreatedAtDate())
|
const [formattedCreatedAtDate, setFormattedCreatedAtDate] = useState(() => formatCreatedAtDate())
|
||||||
|
|
||||||
|
@ -74,19 +55,19 @@ export const UiNotificationToast: React.FC<UiNotificationProps> = ({
|
||||||
setRemainingSteps((lastRemainingSteps) => lastRemainingSteps - 1)
|
setRemainingSteps((lastRemainingSteps) => lastRemainingSteps - 1)
|
||||||
setFormattedCreatedAtDate(formatCreatedAtDate())
|
setFormattedCreatedAtDate(formatCreatedAtDate())
|
||||||
},
|
},
|
||||||
!dismissed && remainingSteps > 0 ? 1000 / STEPS_PER_SECOND : null
|
!notification.dismissed && remainingSteps > 0 ? 1000 / STEPS_PER_SECOND : null
|
||||||
)
|
)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (remainingSteps <= 0 && !dismissed) {
|
if (remainingSteps <= 0 && !notification.dismissed) {
|
||||||
log.debug(`Dismiss notification ${notificationId}`)
|
log.debug(`Dismiss notification ${notification.uuid}`)
|
||||||
dismissUiNotification(notificationId)
|
dismissNotification(notification.uuid)
|
||||||
}
|
}
|
||||||
}, [dismissed, remainingSteps, notificationId])
|
}, [remainingSteps, notification.dismissed, notification.uuid, dismissNotification])
|
||||||
|
|
||||||
const buttonsDom = useMemo(
|
const buttonsDom = useMemo(
|
||||||
() =>
|
() =>
|
||||||
buttons?.map((button, buttonIndex) => {
|
notification.buttons?.map((button, buttonIndex) => {
|
||||||
const buttonClick = () => {
|
const buttonClick = () => {
|
||||||
button.onClick()
|
button.onClick()
|
||||||
dismissNow()
|
dismissNow()
|
||||||
|
@ -97,11 +78,11 @@ export const UiNotificationToast: React.FC<UiNotificationProps> = ({
|
||||||
</Button>
|
</Button>
|
||||||
)
|
)
|
||||||
}),
|
}),
|
||||||
[buttons, dismissNow]
|
[dismissNow, notification.buttons]
|
||||||
)
|
)
|
||||||
|
|
||||||
const contentDom = useMemo(() => {
|
const contentDom = useMemo(() => {
|
||||||
return t(contentI18nKey, contentI18nOptions)
|
return t(notification.contentI18nKey, notification.contentI18nOptions)
|
||||||
.split('\n')
|
.split('\n')
|
||||||
.map((value, lineNumber) => {
|
.map((value, lineNumber) => {
|
||||||
return (
|
return (
|
||||||
|
@ -111,16 +92,20 @@ export const UiNotificationToast: React.FC<UiNotificationProps> = ({
|
||||||
</Fragment>
|
</Fragment>
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
}, [contentI18nKey, contentI18nOptions, t])
|
}, [notification.contentI18nKey, notification.contentI18nOptions, t])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Toast className={styles.toast} show={!dismissed} onClose={dismissNow} {...cypressId('notification-toast')}>
|
<Toast
|
||||||
|
className={styles.toast}
|
||||||
|
show={!notification.dismissed}
|
||||||
|
onClose={dismissNow}
|
||||||
|
{...cypressId('notification-toast')}>
|
||||||
<Toast.Header>
|
<Toast.Header>
|
||||||
<strong className='mr-auto'>
|
<strong className='mr-auto'>
|
||||||
<ShowIf condition={!!icon}>
|
<ShowIf condition={!!notification.icon}>
|
||||||
<ForkAwesomeIcon icon={icon as IconName} fixedWidth={true} className={'mr-1'} />
|
<ForkAwesomeIcon icon={notification.icon as IconName} fixedWidth={true} className={'mr-1'} />
|
||||||
</ShowIf>
|
</ShowIf>
|
||||||
<Trans i18nKey={titleI18nKey} tOptions={titleI18nOptions} />
|
<Trans i18nKey={notification.titleI18nKey} tOptions={notification.titleI18nOptions} />
|
||||||
</strong>
|
</strong>
|
||||||
<small>{formattedCreatedAtDate}</small>
|
<small>{formattedCreatedAtDate}</small>
|
||||||
</Toast.Header>
|
</Toast.Header>
|
||||||
|
@ -128,7 +113,7 @@ export const UiNotificationToast: React.FC<UiNotificationProps> = ({
|
||||||
<ProgressBar
|
<ProgressBar
|
||||||
variant={'info'}
|
variant={'info'}
|
||||||
now={remainingSteps}
|
now={remainingSteps}
|
||||||
max={durationInSecond * STEPS_PER_SECOND}
|
max={notification.durationInSecond * STEPS_PER_SECOND}
|
||||||
min={STEPS_PER_SECOND}
|
min={STEPS_PER_SECOND}
|
||||||
className={styles.progress}
|
className={styles.progress}
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -7,18 +7,22 @@
|
||||||
import React, { useMemo } from 'react'
|
import React, { useMemo } from 'react'
|
||||||
import { UiNotificationToast } from './ui-notification-toast'
|
import { UiNotificationToast } from './ui-notification-toast'
|
||||||
import styles from './notifications.module.scss'
|
import styles from './notifications.module.scss'
|
||||||
import { useApplicationState } from '../../hooks/common/use-application-state'
|
import type { UiNotification } from './types'
|
||||||
|
|
||||||
|
export interface UiNotificationsProps {
|
||||||
|
notifications: UiNotification[]
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Renders {@link UiNotification notifications} in the top right corner.
|
* Renders {@link UiNotification notifications} in the top right corner sorted by creation time..
|
||||||
|
*
|
||||||
|
* @param notifications The notification to render
|
||||||
*/
|
*/
|
||||||
export const UiNotifications: React.FC = () => {
|
export const UiNotifications: React.FC<UiNotificationsProps> = ({ notifications }) => {
|
||||||
const notifications = useApplicationState((state) => state.uiNotifications)
|
|
||||||
|
|
||||||
const notificationElements = useMemo(() => {
|
const notificationElements = useMemo(() => {
|
||||||
return notifications.map((notification, notificationIndex) => (
|
return notifications
|
||||||
<UiNotificationToast key={notificationIndex} notificationId={notificationIndex} {...notification} />
|
.sort((a, b) => b.createdAtTimestamp - a.createdAtTimestamp)
|
||||||
))
|
.map((notification) => <UiNotificationToast key={notification.uuid} notification={notification} />)
|
||||||
}, [notifications])
|
}, [notifications])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
|
@ -8,8 +8,8 @@ import { DateTime } from 'luxon'
|
||||||
import type { FormEvent } from 'react'
|
import type { FormEvent } from 'react'
|
||||||
import { useCallback } from 'react'
|
import { useCallback } from 'react'
|
||||||
import { postNewAccessToken } from '../../../../../api/tokens'
|
import { postNewAccessToken } from '../../../../../api/tokens'
|
||||||
import { showErrorNotification } from '../../../../../redux/ui-notifications/methods'
|
|
||||||
import type { AccessTokenWithSecret } from '../../../../../api/tokens/types'
|
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.
|
* Callback for requesting a new access token from the API and returning the response token and secret.
|
||||||
|
@ -24,6 +24,8 @@ export const useOnCreateToken = (
|
||||||
expiryDate: string,
|
expiryDate: string,
|
||||||
setNewTokenWithSecret: (token: AccessTokenWithSecret) => void
|
setNewTokenWithSecret: (token: AccessTokenWithSecret) => void
|
||||||
): ((event: FormEvent) => void) => {
|
): ((event: FormEvent) => void) => {
|
||||||
|
const { showErrorNotification } = useUiNotifications()
|
||||||
|
|
||||||
return useCallback(
|
return useCallback(
|
||||||
(event: FormEvent) => {
|
(event: FormEvent) => {
|
||||||
event.preventDefault()
|
event.preventDefault()
|
||||||
|
@ -34,6 +36,6 @@ export const useOnCreateToken = (
|
||||||
})
|
})
|
||||||
.catch(showErrorNotification('profile.accessTokens.creationFailed'))
|
.catch(showErrorNotification('profile.accessTokens.creationFailed'))
|
||||||
},
|
},
|
||||||
[expiryDate, label, setNewTokenWithSecret]
|
[expiryDate, label, setNewTokenWithSecret, showErrorNotification]
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -12,7 +12,7 @@ import { Button, Modal } from 'react-bootstrap'
|
||||||
import { Trans, useTranslation } from 'react-i18next'
|
import { Trans, useTranslation } from 'react-i18next'
|
||||||
import type { AccessToken } from '../../../api/tokens/types'
|
import type { AccessToken } from '../../../api/tokens/types'
|
||||||
import { deleteAccessToken } from '../../../api/tokens'
|
import { deleteAccessToken } from '../../../api/tokens'
|
||||||
import { dispatchUiNotification, showErrorNotification } from '../../../redux/ui-notifications/methods'
|
import { useUiNotifications } from '../../notifications/ui-notification-boundary'
|
||||||
|
|
||||||
export interface AccessTokenDeletionModalProps extends ModalVisibilityProps {
|
export interface AccessTokenDeletionModalProps extends ModalVisibilityProps {
|
||||||
token: AccessToken
|
token: AccessToken
|
||||||
|
@ -27,6 +27,7 @@ export interface AccessTokenDeletionModalProps extends ModalVisibilityProps {
|
||||||
*/
|
*/
|
||||||
export const AccessTokenDeletionModal: React.FC<AccessTokenDeletionModalProps> = ({ show, token, onHide }) => {
|
export const AccessTokenDeletionModal: React.FC<AccessTokenDeletionModalProps> = ({ show, token, onHide }) => {
|
||||||
useTranslation()
|
useTranslation()
|
||||||
|
const { showErrorNotification, dispatchUiNotification } = useUiNotifications()
|
||||||
|
|
||||||
const onConfirmDelete = useCallback(() => {
|
const onConfirmDelete = useCallback(() => {
|
||||||
deleteAccessToken(token.keyId)
|
deleteAccessToken(token.keyId)
|
||||||
|
@ -43,7 +44,7 @@ export const AccessTokenDeletionModal: React.FC<AccessTokenDeletionModalProps> =
|
||||||
})
|
})
|
||||||
.catch(showErrorNotification('profile.modal.deleteAccessToken.failed'))
|
.catch(showErrorNotification('profile.modal.deleteAccessToken.failed'))
|
||||||
.finally(() => onHide?.())
|
.finally(() => onHide?.())
|
||||||
}, [token, onHide])
|
}, [token.keyId, token.label, showErrorNotification, dispatchUiNotification, onHide])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<CommonModal
|
<CommonModal
|
||||||
|
|
|
@ -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
|
||||||
*/
|
*/
|
||||||
|
@ -11,7 +11,7 @@ import type { AccessToken } from '../../../api/tokens/types'
|
||||||
import { ShowIf } from '../../common/show-if/show-if'
|
import { ShowIf } from '../../common/show-if/show-if'
|
||||||
import { AccessTokenListEntry } from './access-token-list-entry'
|
import { AccessTokenListEntry } from './access-token-list-entry'
|
||||||
import { AccessTokenCreationForm } from './access-token-creation-form/access-token-creation-form'
|
import { AccessTokenCreationForm } from './access-token-creation-form/access-token-creation-form'
|
||||||
import { showErrorNotification } from '../../../redux/ui-notifications/methods'
|
import { useUiNotifications } from '../../notifications/ui-notification-boundary'
|
||||||
|
|
||||||
export interface AccessTokenUpdateProps {
|
export interface AccessTokenUpdateProps {
|
||||||
onUpdateList: () => void
|
onUpdateList: () => void
|
||||||
|
@ -23,6 +23,7 @@ export interface AccessTokenUpdateProps {
|
||||||
export const ProfileAccessTokens: React.FC = () => {
|
export const ProfileAccessTokens: React.FC = () => {
|
||||||
useTranslation()
|
useTranslation()
|
||||||
const [accessTokens, setAccessTokens] = useState<AccessToken[]>([])
|
const [accessTokens, setAccessTokens] = useState<AccessToken[]>([])
|
||||||
|
const { showErrorNotification } = useUiNotifications()
|
||||||
|
|
||||||
const refreshAccessTokens = useCallback(() => {
|
const refreshAccessTokens = useCallback(() => {
|
||||||
getAccessTokenList()
|
getAccessTokenList()
|
||||||
|
@ -30,7 +31,7 @@ export const ProfileAccessTokens: React.FC = () => {
|
||||||
setAccessTokens(tokens)
|
setAccessTokens(tokens)
|
||||||
})
|
})
|
||||||
.catch(showErrorNotification('profile.accessTokens.loadingFailed'))
|
.catch(showErrorNotification('profile.accessTokens.loadingFailed'))
|
||||||
}, [])
|
}, [showErrorNotification])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
refreshAccessTokens()
|
refreshAccessTokens()
|
||||||
|
|
|
@ -12,7 +12,7 @@ import { Button, Modal } from 'react-bootstrap'
|
||||||
import { CountdownButton } from '../../common/countdown-button/countdown-button'
|
import { CountdownButton } from '../../common/countdown-button/countdown-button'
|
||||||
import { deleteUser } from '../../../api/me'
|
import { deleteUser } from '../../../api/me'
|
||||||
import { clearUser } from '../../../redux/user/methods'
|
import { clearUser } from '../../../redux/user/methods'
|
||||||
import { dispatchUiNotification, showErrorNotification } from '../../../redux/ui-notifications/methods'
|
import { useUiNotifications } from '../../notifications/ui-notification-boundary'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Confirmation modal for deleting your account.
|
* Confirmation modal for deleting your account.
|
||||||
|
@ -22,6 +22,7 @@ import { dispatchUiNotification, showErrorNotification } from '../../../redux/ui
|
||||||
*/
|
*/
|
||||||
export const AccountDeletionModal: React.FC<ModalVisibilityProps> = ({ show, onHide }) => {
|
export const AccountDeletionModal: React.FC<ModalVisibilityProps> = ({ show, onHide }) => {
|
||||||
useTranslation()
|
useTranslation()
|
||||||
|
const { showErrorNotification, dispatchUiNotification } = useUiNotifications()
|
||||||
|
|
||||||
const deleteUserAccount = useCallback(() => {
|
const deleteUserAccount = useCallback(() => {
|
||||||
deleteUser()
|
deleteUser()
|
||||||
|
@ -39,7 +40,7 @@ export const AccountDeletionModal: React.FC<ModalVisibilityProps> = ({ show, onH
|
||||||
onHide()
|
onHide()
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}, [onHide])
|
}, [dispatchUiNotification, onHide, showErrorNotification])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<CommonModal show={show} title={'profile.modal.deleteUser.message'} onHide={onHide} showCloseButton={true}>
|
<CommonModal show={show} title={'profile.modal.deleteUser.message'} onHide={onHide} showCloseButton={true}>
|
||||||
|
|
|
@ -9,17 +9,18 @@ import React, { useCallback, useMemo, useRef, useState } from 'react'
|
||||||
import { Button, Card, Form } from 'react-bootstrap'
|
import { Button, Card, Form } from 'react-bootstrap'
|
||||||
import { Trans, useTranslation } from 'react-i18next'
|
import { Trans, useTranslation } from 'react-i18next'
|
||||||
import { doLocalPasswordChange } from '../../../api/auth/local'
|
import { doLocalPasswordChange } from '../../../api/auth/local'
|
||||||
import { dispatchUiNotification, showErrorNotification } from '../../../redux/ui-notifications/methods'
|
|
||||||
import { useOnInputChange } from '../../../hooks/common/use-on-input-change'
|
import { useOnInputChange } from '../../../hooks/common/use-on-input-change'
|
||||||
import { NewPasswordField } from '../../common/fields/new-password-field'
|
import { NewPasswordField } from '../../common/fields/new-password-field'
|
||||||
import { PasswordAgainField } from '../../common/fields/password-again-field'
|
import { PasswordAgainField } from '../../common/fields/password-again-field'
|
||||||
import { CurrentPasswordField } from '../../common/fields/current-password-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.
|
* Profile page section for changing the password when using internal login.
|
||||||
*/
|
*/
|
||||||
export const ProfileChangePassword: React.FC = () => {
|
export const ProfileChangePassword: React.FC = () => {
|
||||||
useTranslation()
|
useTranslation()
|
||||||
|
const { showErrorNotification, dispatchUiNotification } = useUiNotifications()
|
||||||
const [oldPassword, setOldPassword] = useState('')
|
const [oldPassword, setOldPassword] = useState('')
|
||||||
const [newPassword, setNewPassword] = useState('')
|
const [newPassword, setNewPassword] = useState('')
|
||||||
const [newPasswordAgain, setNewPasswordAgain] = useState('')
|
const [newPasswordAgain, setNewPasswordAgain] = useState('')
|
||||||
|
@ -34,11 +35,11 @@ export const ProfileChangePassword: React.FC = () => {
|
||||||
(event: FormEvent) => {
|
(event: FormEvent) => {
|
||||||
event.preventDefault()
|
event.preventDefault()
|
||||||
doLocalPasswordChange(oldPassword, newPassword)
|
doLocalPasswordChange(oldPassword, newPassword)
|
||||||
.then(() => {
|
.then(() =>
|
||||||
return dispatchUiNotification('profile.changePassword.successTitle', 'profile.changePassword.successText', {
|
dispatchUiNotification('profile.changePassword.successTitle', 'profile.changePassword.successText', {
|
||||||
icon: 'check'
|
icon: 'check'
|
||||||
})
|
})
|
||||||
})
|
)
|
||||||
.catch(showErrorNotification('profile.changePassword.failed'))
|
.catch(showErrorNotification('profile.changePassword.failed'))
|
||||||
.finally(() => {
|
.finally(() => {
|
||||||
if (formRef.current) {
|
if (formRef.current) {
|
||||||
|
@ -49,7 +50,7 @@ export const ProfileChangePassword: React.FC = () => {
|
||||||
setNewPasswordAgain('')
|
setNewPasswordAgain('')
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
[oldPassword, newPassword]
|
[oldPassword, newPassword, showErrorNotification, dispatchUiNotification]
|
||||||
)
|
)
|
||||||
|
|
||||||
const ready = useMemo(() => {
|
const ready = useMemo(() => {
|
||||||
|
|
|
@ -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
|
||||||
*/
|
*/
|
||||||
|
@ -11,9 +11,9 @@ import { Trans, useTranslation } from 'react-i18next'
|
||||||
import { updateDisplayName } from '../../../api/me'
|
import { updateDisplayName } from '../../../api/me'
|
||||||
import { fetchAndSetUser } from '../../login-page/auth/utils'
|
import { fetchAndSetUser } from '../../login-page/auth/utils'
|
||||||
import { useApplicationState } from '../../../hooks/common/use-application-state'
|
import { useApplicationState } from '../../../hooks/common/use-application-state'
|
||||||
import { showErrorNotification } from '../../../redux/ui-notifications/methods'
|
|
||||||
import { DisplayNameField } from '../../common/fields/display-name-field'
|
import { DisplayNameField } from '../../common/fields/display-name-field'
|
||||||
import { useOnInputChange } from '../../../hooks/common/use-on-input-change'
|
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.
|
* Profile page section for changing the current display name.
|
||||||
|
@ -22,6 +22,7 @@ export const ProfileDisplayName: React.FC = () => {
|
||||||
useTranslation()
|
useTranslation()
|
||||||
const userName = useApplicationState((state) => state.user?.displayName)
|
const userName = useApplicationState((state) => state.user?.displayName)
|
||||||
const [displayName, setDisplayName] = useState(userName ?? '')
|
const [displayName, setDisplayName] = useState(userName ?? '')
|
||||||
|
const { showErrorNotification } = useUiNotifications()
|
||||||
|
|
||||||
const onChangeDisplayName = useOnInputChange(setDisplayName)
|
const onChangeDisplayName = useOnInputChange(setDisplayName)
|
||||||
const onSubmitNameChange = useCallback(
|
const onSubmitNameChange = useCallback(
|
||||||
|
@ -31,7 +32,7 @@ export const ProfileDisplayName: React.FC = () => {
|
||||||
.then(fetchAndSetUser)
|
.then(fetchAndSetUser)
|
||||||
.catch(showErrorNotification('profile.changeDisplayNameFailed'))
|
.catch(showErrorNotification('profile.changeDisplayNameFailed'))
|
||||||
},
|
},
|
||||||
[displayName]
|
[displayName, showErrorNotification]
|
||||||
)
|
)
|
||||||
|
|
||||||
const formSubmittable = useMemo(() => {
|
const formSubmittable = useMemo(() => {
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
/*
|
/*
|
||||||
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 { AppProps } from 'next/app'
|
import type { AppProps } from 'next/app'
|
||||||
import { ErrorBoundary } from '../components/error-boundary/error-boundary'
|
import { ErrorBoundary } from '../components/error-boundary/error-boundary'
|
||||||
|
@ -11,6 +11,7 @@ import '../../global-styles/index.scss'
|
||||||
import type { NextPage } from 'next'
|
import type { NextPage } from 'next'
|
||||||
import { BaseHead } from '../components/layout/base-head'
|
import { BaseHead } from '../components/layout/base-head'
|
||||||
import { StoreProvider } from '../redux/store-provider'
|
import { StoreProvider } from '../redux/store-provider'
|
||||||
|
import { UiNotificationBoundary } from '../components/notifications/ui-notification-boundary'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The actual hedgedoc next js app.
|
* The actual hedgedoc next js app.
|
||||||
|
@ -22,7 +23,9 @@ const HedgeDocApp: NextPage<AppProps> = ({ Component, pageProps }: AppProps) =>
|
||||||
<BaseHead />
|
<BaseHead />
|
||||||
<ApplicationLoader>
|
<ApplicationLoader>
|
||||||
<ErrorBoundary>
|
<ErrorBoundary>
|
||||||
<Component {...pageProps} />
|
<UiNotificationBoundary>
|
||||||
|
<Component {...pageProps} />
|
||||||
|
</UiNotificationBoundary>
|
||||||
</ErrorBoundary>
|
</ErrorBoundary>
|
||||||
</ApplicationLoader>
|
</ApplicationLoader>
|
||||||
</StoreProvider>
|
</StoreProvider>
|
||||||
|
|
|
@ -1,17 +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, { useEffect } from 'react'
|
import React, { useEffect } from 'react'
|
||||||
import type { NextPage } from 'next'
|
import type { NextPage } from 'next'
|
||||||
import { Trans, useTranslation } from 'react-i18next'
|
import { Trans, useTranslation } from 'react-i18next'
|
||||||
import { HistoryToolbar } from '../components/history-page/history-toolbar/history-toolbar'
|
import { HistoryToolbar } from '../components/history-page/history-toolbar/history-toolbar'
|
||||||
import { safeRefreshHistoryState } from '../redux/history/methods'
|
|
||||||
import { Row } from 'react-bootstrap'
|
import { Row } from 'react-bootstrap'
|
||||||
import { HistoryContent } from '../components/history-page/history-content/history-content'
|
import { HistoryContent } from '../components/history-page/history-content/history-content'
|
||||||
import { LandingLayout } from '../components/landing-layout/landing-layout'
|
import { LandingLayout } from '../components/landing-layout/landing-layout'
|
||||||
import { HistoryToolbarStateContextProvider } from '../components/history-page/history-toolbar/toolbar-context/history-toolbar-state-context-provider'
|
import { HistoryToolbarStateContextProvider } from '../components/history-page/history-toolbar/toolbar-context/history-toolbar-state-context-provider'
|
||||||
|
import { useSafeRefreshHistoryStateCallback } from '../components/history-page/history-toolbar/hooks/use-safe-refresh-history-state'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The page that shows the local and remote note history.
|
* The page that shows the local and remote note history.
|
||||||
|
@ -19,9 +19,10 @@ import { HistoryToolbarStateContextProvider } from '../components/history-page/h
|
||||||
const HistoryPage: NextPage = () => {
|
const HistoryPage: NextPage = () => {
|
||||||
useTranslation()
|
useTranslation()
|
||||||
|
|
||||||
|
const safeRefreshHistoryStateCallback = useSafeRefreshHistoryStateCallback()
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
safeRefreshHistoryState()
|
safeRefreshHistoryStateCallback()
|
||||||
}, [])
|
}, [safeRefreshHistoryStateCallback])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<LandingLayout>
|
<LandingLayout>
|
||||||
|
|
|
@ -23,7 +23,7 @@ import { LandingLayout } from '../components/landing-layout/landing-layout'
|
||||||
import { useRouter } from 'next/router'
|
import { useRouter } from 'next/router'
|
||||||
import type { NextPage } from 'next'
|
import type { NextPage } from 'next'
|
||||||
import { Redirect } from '../components/common/redirect'
|
import { Redirect } from '../components/common/redirect'
|
||||||
import { dispatchUiNotification } from '../redux/ui-notifications/methods'
|
import { useUiNotifications } from '../components/notifications/ui-notification-boundary'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Renders the registration page with fields for username, display name, password, password retype and information about terms and conditions.
|
* Renders the registration page with fields for username, display name, password, password retype and information about terms and conditions.
|
||||||
|
@ -40,6 +40,8 @@ export const RegisterPage: NextPage = () => {
|
||||||
const [passwordAgain, setPasswordAgain] = useState('')
|
const [passwordAgain, setPasswordAgain] = useState('')
|
||||||
const [error, setError] = useState<RegisterErrorType>()
|
const [error, setError] = useState<RegisterErrorType>()
|
||||||
|
|
||||||
|
const { dispatchUiNotification } = useUiNotifications()
|
||||||
|
|
||||||
const doRegisterSubmit = useCallback(
|
const doRegisterSubmit = useCallback(
|
||||||
(event: FormEvent) => {
|
(event: FormEvent) => {
|
||||||
doLocalRegister(username, displayName, password)
|
doLocalRegister(username, displayName, password)
|
||||||
|
@ -55,7 +57,7 @@ export const RegisterPage: NextPage = () => {
|
||||||
})
|
})
|
||||||
event.preventDefault()
|
event.preventDefault()
|
||||||
},
|
},
|
||||||
[username, displayName, password, router]
|
[username, displayName, password, dispatchUiNotification, router]
|
||||||
)
|
)
|
||||||
|
|
||||||
const ready = useMemo(() => {
|
const ready = useMemo(() => {
|
||||||
|
|
|
@ -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
|
||||||
*/
|
*/
|
||||||
|
@ -9,7 +9,6 @@ import { useApplyDarkMode } from '../../hooks/common/use-apply-dark-mode'
|
||||||
import { MotdModal } from '../../components/common/motd-modal/motd-modal'
|
import { MotdModal } from '../../components/common/motd-modal/motd-modal'
|
||||||
import { AppBar, AppBarMode } from '../../components/editor-page/app-bar/app-bar'
|
import { AppBar, AppBarMode } from '../../components/editor-page/app-bar/app-bar'
|
||||||
import { EditorToRendererCommunicatorContextProvider } from '../../components/editor-page/render-context/editor-to-renderer-communicator-context-provider'
|
import { EditorToRendererCommunicatorContextProvider } from '../../components/editor-page/render-context/editor-to-renderer-communicator-context-provider'
|
||||||
import { UiNotifications } from '../../components/notifications/ui-notifications'
|
|
||||||
import { DocumentReadOnlyPageContent } from '../../components/document-read-only-page/document-read-only-page-content'
|
import { DocumentReadOnlyPageContent } from '../../components/document-read-only-page/document-read-only-page-content'
|
||||||
import { NoteAndAppTitleHead } from '../../components/layout/note-and-app-title-head'
|
import { NoteAndAppTitleHead } from '../../components/layout/note-and-app-title-head'
|
||||||
import { NoteLoadingBoundary } from '../../components/common/note-loading-boundary/note-loading-boundary'
|
import { NoteLoadingBoundary } from '../../components/common/note-loading-boundary/note-loading-boundary'
|
||||||
|
@ -23,7 +22,6 @@ export const DocumentReadOnlyPage: React.FC = () => {
|
||||||
<EditorToRendererCommunicatorContextProvider>
|
<EditorToRendererCommunicatorContextProvider>
|
||||||
<NoteLoadingBoundary>
|
<NoteLoadingBoundary>
|
||||||
<NoteAndAppTitleHead />
|
<NoteAndAppTitleHead />
|
||||||
<UiNotifications />
|
|
||||||
<MotdModal />
|
<MotdModal />
|
||||||
<div className={'d-flex flex-column mvh-100 bg-light'}>
|
<div className={'d-flex flex-column mvh-100 bg-light'}>
|
||||||
<AppBar mode={AppBarMode.BASIC} />
|
<AppBar mode={AppBarMode.BASIC} />
|
||||||
|
|
2
src/redux/application-state.d.ts
vendored
2
src/redux/application-state.d.ts
vendored
|
@ -10,7 +10,6 @@ import type { OptionalMotdState } from './motd/types'
|
||||||
import type { EditorConfig } from './editor/types'
|
import type { EditorConfig } from './editor/types'
|
||||||
import type { DarkModeConfig } from './dark-mode/types'
|
import type { DarkModeConfig } from './dark-mode/types'
|
||||||
import type { NoteDetails } from './note-details/types/note-details'
|
import type { NoteDetails } from './note-details/types/note-details'
|
||||||
import type { UiNotificationState } from './ui-notifications/types'
|
|
||||||
import type { RendererStatus } from './renderer-status/types'
|
import type { RendererStatus } from './renderer-status/types'
|
||||||
import type { HistoryEntryWithOrigin } from '../api/history/types'
|
import type { HistoryEntryWithOrigin } from '../api/history/types'
|
||||||
import type { RealtimeState } from './realtime/types'
|
import type { RealtimeState } from './realtime/types'
|
||||||
|
@ -23,7 +22,6 @@ export interface ApplicationState {
|
||||||
editorConfig: EditorConfig
|
editorConfig: EditorConfig
|
||||||
darkMode: DarkModeConfig
|
darkMode: DarkModeConfig
|
||||||
noteDetails: NoteDetails
|
noteDetails: NoteDetails
|
||||||
uiNotifications: UiNotificationState
|
|
||||||
rendererStatus: RendererStatus
|
rendererStatus: RendererStatus
|
||||||
realtime: RealtimeState
|
realtime: RealtimeState
|
||||||
}
|
}
|
||||||
|
|
|
@ -20,7 +20,6 @@ import { addRemoteOriginToHistoryEntry, historyEntryToHistoryEntryPutDto } from
|
||||||
import { Logger } from '../../utils/logger'
|
import { Logger } from '../../utils/logger'
|
||||||
import type { HistoryEntry, HistoryEntryWithOrigin } from '../../api/history/types'
|
import type { HistoryEntry, HistoryEntryWithOrigin } from '../../api/history/types'
|
||||||
import { HistoryEntryOrigin } from '../../api/history/types'
|
import { HistoryEntryOrigin } from '../../api/history/types'
|
||||||
import { showErrorNotification } from '../ui-notifications/methods'
|
|
||||||
|
|
||||||
const log = new Logger('Redux > History')
|
const log = new Logger('Redux > History')
|
||||||
|
|
||||||
|
@ -177,13 +176,6 @@ export const refreshHistoryState = async (): Promise<void> => {
|
||||||
setHistoryEntries(allEntries)
|
setHistoryEntries(allEntries)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Refreshes the history state and shows an error in case of failure.
|
|
||||||
*/
|
|
||||||
export const safeRefreshHistoryState = (): void => {
|
|
||||||
refreshHistoryState().catch(showErrorNotification('landing.history.error.getHistory.text'))
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Stores the history entries marked as local from the redux to the user's local-storage.
|
* Stores the history entries marked as local from the redux to the user's local-storage.
|
||||||
*/
|
*/
|
||||||
|
|
|
@ -13,7 +13,6 @@ import { HistoryReducer } from './history/reducers'
|
||||||
import { EditorConfigReducer } from './editor/reducers'
|
import { EditorConfigReducer } from './editor/reducers'
|
||||||
import { DarkModeConfigReducer } from './dark-mode/reducers'
|
import { DarkModeConfigReducer } from './dark-mode/reducers'
|
||||||
import { NoteDetailsReducer } from './note-details/reducer'
|
import { NoteDetailsReducer } from './note-details/reducer'
|
||||||
import { UiNotificationReducer } from './ui-notifications/reducers'
|
|
||||||
import { RendererStatusReducer } from './renderer-status/reducers'
|
import { RendererStatusReducer } from './renderer-status/reducers'
|
||||||
import type { ApplicationState } from './application-state'
|
import type { ApplicationState } from './application-state'
|
||||||
import { RealtimeReducer } from './realtime/reducers'
|
import { RealtimeReducer } from './realtime/reducers'
|
||||||
|
@ -26,7 +25,6 @@ export const allReducers: Reducer<ApplicationState> = combineReducers<Applicatio
|
||||||
editorConfig: EditorConfigReducer,
|
editorConfig: EditorConfigReducer,
|
||||||
darkMode: DarkModeConfigReducer,
|
darkMode: DarkModeConfigReducer,
|
||||||
noteDetails: NoteDetailsReducer,
|
noteDetails: NoteDetailsReducer,
|
||||||
uiNotifications: UiNotificationReducer,
|
|
||||||
rendererStatus: RendererStatusReducer,
|
rendererStatus: RendererStatusReducer,
|
||||||
realtime: RealtimeReducer
|
realtime: RealtimeReducer
|
||||||
})
|
})
|
||||||
|
|
|
@ -1,83 +0,0 @@
|
||||||
/*
|
|
||||||
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
|
|
||||||
*
|
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
|
||||||
*/
|
|
||||||
|
|
||||||
import type { TOptions } from 'i18next'
|
|
||||||
import { t } from 'i18next'
|
|
||||||
import { store } from '../index'
|
|
||||||
import type { DismissUiNotificationAction, DispatchOptions, DispatchUiNotificationAction } from './types'
|
|
||||||
import { UiNotificationActionType } from './types'
|
|
||||||
import { DateTime } from 'luxon'
|
|
||||||
import { Logger } from '../../utils/logger'
|
|
||||||
|
|
||||||
const log = new Logger('Redux > Notifications')
|
|
||||||
|
|
||||||
export const DEFAULT_DURATION_IN_SECONDS = 10
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Dispatches a new UI Notification into the global application state.
|
|
||||||
*
|
|
||||||
* @param titleI18nKey I18n key used to show the localized title
|
|
||||||
* @param contentI18nKey I18n key used to show the localized content
|
|
||||||
* @param icon The icon in the upper left corner
|
|
||||||
* @param durationInSecond Show duration of the notification. If omitted then a {@link DEFAULT_DURATION_IN_SECONDS default value} will be used.
|
|
||||||
* @param buttons A array of actions that are shown in the notification
|
|
||||||
* @param contentI18nOptions Options to configure the translation of the title. (e.g. variables)
|
|
||||||
* @param titleI18nOptions Options to configure the translation of the content. (e.g. variables)
|
|
||||||
* @return a promise that resolves as soon as the notification id available.
|
|
||||||
*/
|
|
||||||
export const dispatchUiNotification = async (
|
|
||||||
titleI18nKey: string,
|
|
||||||
contentI18nKey: string,
|
|
||||||
{ icon, durationInSecond, buttons, contentI18nOptions, titleI18nOptions }: Partial<DispatchOptions>
|
|
||||||
): Promise<number> => {
|
|
||||||
return new Promise((resolve) => {
|
|
||||||
store.dispatch({
|
|
||||||
type: UiNotificationActionType.DISPATCH_NOTIFICATION,
|
|
||||||
notificationIdCallback: (notificationId: number) => {
|
|
||||||
resolve(notificationId)
|
|
||||||
},
|
|
||||||
notification: {
|
|
||||||
titleI18nKey,
|
|
||||||
contentI18nKey,
|
|
||||||
createdAtTimestamp: DateTime.now().toSeconds(),
|
|
||||||
dismissed: false,
|
|
||||||
titleI18nOptions: titleI18nOptions ?? {},
|
|
||||||
contentI18nOptions: contentI18nOptions ?? {},
|
|
||||||
durationInSecond: durationInSecond ?? DEFAULT_DURATION_IN_SECONDS,
|
|
||||||
buttons: buttons ?? [],
|
|
||||||
icon: icon
|
|
||||||
}
|
|
||||||
} as DispatchUiNotificationAction)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Dismisses a notification. It won't be removed from the global application state but hidden.
|
|
||||||
*
|
|
||||||
* @param notificationId The id of the notification to dismissed. Can be obtained from the returned promise of {@link dispatchUiNotification}
|
|
||||||
*/
|
|
||||||
export const dismissUiNotification = (notificationId: number): void => {
|
|
||||||
store.dispatch({
|
|
||||||
type: UiNotificationActionType.DISMISS_NOTIFICATION,
|
|
||||||
notificationId
|
|
||||||
} as DismissUiNotificationAction)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Dispatches an notification that is specialized for errors.
|
|
||||||
*
|
|
||||||
* @param messageI18nKey i18n key for the message
|
|
||||||
* @param messageI18nOptions i18n options for the message
|
|
||||||
*/
|
|
||||||
export const showErrorNotification =
|
|
||||||
(messageI18nKey: string, messageI18nOptions?: TOptions | string) =>
|
|
||||||
(error: Error): void => {
|
|
||||||
log.error(t(messageI18nKey, messageI18nOptions), error)
|
|
||||||
void dispatchUiNotification('common.errorOccurred', messageI18nKey, {
|
|
||||||
contentI18nOptions: messageI18nOptions,
|
|
||||||
icon: 'exclamation-triangle'
|
|
||||||
})
|
|
||||||
}
|
|
|
@ -1,53 +0,0 @@
|
||||||
/*
|
|
||||||
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
|
|
||||||
*
|
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
|
||||||
*/
|
|
||||||
|
|
||||||
import type { Reducer } from 'redux'
|
|
||||||
import type { UiNotification, UiNotificationActions, UiNotificationState } from './types'
|
|
||||||
import { UiNotificationActionType } from './types'
|
|
||||||
|
|
||||||
export const UiNotificationReducer: Reducer<UiNotificationState, UiNotificationActions> = (
|
|
||||||
state: UiNotificationState = [],
|
|
||||||
action: UiNotificationActions
|
|
||||||
) => {
|
|
||||||
switch (action.type) {
|
|
||||||
case UiNotificationActionType.DISPATCH_NOTIFICATION:
|
|
||||||
return addNewNotification(state, action.notification, action.notificationIdCallback)
|
|
||||||
case UiNotificationActionType.DISMISS_NOTIFICATION:
|
|
||||||
return dismissNotification(state, action.notificationId)
|
|
||||||
default:
|
|
||||||
return state
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Creates a new {@link UiNotificationState notification state} by appending the given {@link UiNotification}.
|
|
||||||
* @param state The current ui notification state
|
|
||||||
* @param notification The new notification
|
|
||||||
* @param notificationIdCallback This callback is executed with the id of the new notification
|
|
||||||
* @return The new {@link UiNotificationState notification state}
|
|
||||||
*/
|
|
||||||
const addNewNotification = (
|
|
||||||
state: UiNotificationState,
|
|
||||||
notification: UiNotification,
|
|
||||||
notificationIdCallback: (notificationId: number) => void
|
|
||||||
): UiNotificationState => {
|
|
||||||
const newState = [...state, notification]
|
|
||||||
notificationIdCallback(newState.length - 1)
|
|
||||||
return newState
|
|
||||||
}
|
|
||||||
|
|
||||||
const dismissNotification = (
|
|
||||||
notificationState: UiNotificationState,
|
|
||||||
notificationIndex: number
|
|
||||||
): UiNotificationState => {
|
|
||||||
const newArray = [...notificationState]
|
|
||||||
const oldNotification = newArray[notificationIndex]
|
|
||||||
newArray[notificationIndex] = {
|
|
||||||
...oldNotification,
|
|
||||||
dismissed: true
|
|
||||||
}
|
|
||||||
return newArray
|
|
||||||
}
|
|
|
@ -1,49 +0,0 @@
|
||||||
/*
|
|
||||||
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
|
|
||||||
*
|
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
|
||||||
*/
|
|
||||||
|
|
||||||
import type { Action } from 'redux'
|
|
||||||
import type { IconName } from '../../components/common/fork-awesome/types'
|
|
||||||
import type { TOptions } from 'i18next'
|
|
||||||
|
|
||||||
export enum UiNotificationActionType {
|
|
||||||
DISPATCH_NOTIFICATION = 'notification/dispatch',
|
|
||||||
DISMISS_NOTIFICATION = 'notification/dismiss'
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface UiNotificationButton {
|
|
||||||
label: string
|
|
||||||
onClick: () => void
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface DispatchOptions {
|
|
||||||
titleI18nOptions: TOptions | string
|
|
||||||
contentI18nOptions: TOptions | string
|
|
||||||
durationInSecond: number
|
|
||||||
icon?: IconName
|
|
||||||
buttons: UiNotificationButton[]
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface UiNotification extends DispatchOptions {
|
|
||||||
titleI18nKey: string
|
|
||||||
contentI18nKey: string
|
|
||||||
createdAtTimestamp: number
|
|
||||||
dismissed: boolean
|
|
||||||
}
|
|
||||||
|
|
||||||
export type UiNotificationActions = DispatchUiNotificationAction | DismissUiNotificationAction
|
|
||||||
|
|
||||||
export interface DispatchUiNotificationAction extends Action<UiNotificationActionType> {
|
|
||||||
type: UiNotificationActionType.DISPATCH_NOTIFICATION
|
|
||||||
notification: UiNotification
|
|
||||||
notificationIdCallback: (notificationId: number) => void
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface DismissUiNotificationAction extends Action<UiNotificationActionType> {
|
|
||||||
type: UiNotificationActionType.DISMISS_NOTIFICATION
|
|
||||||
notificationId: number
|
|
||||||
}
|
|
||||||
|
|
||||||
export type UiNotificationState = UiNotification[]
|
|
Loading…
Reference in a new issue