Improvement/move document content into redux (#691)

This commit is contained in:
mrdrogdrog 2020-10-28 22:15:00 +01:00 committed by GitHub
parent 0750695e2f
commit 1690a7bdcf
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
15 changed files with 101 additions and 51 deletions

View file

@ -6,7 +6,7 @@ import { ConnectionIndicator } from './connection-indicator/connection-indicator
import { DocumentInfoButton } from './document-info/document-info-button'
import { EditorMenu } from './menus/editor-menu'
import { ExportMenu } from './menus/export-menu'
import { ImportMenu, ImportProps } from './menus/import-menu'
import { ImportMenu } from './menus/import-menu'
import { PermissionButton } from './permissions/permission-button'
import { RevisionButton } from './revisions/revision-button'
@ -14,7 +14,7 @@ export interface DocumentBarProps {
title: string
}
export const DocumentBar: React.FC<DocumentBarProps & ImportProps> = ({ title, noteContent, updateNoteContent }) => {
export const DocumentBar: React.FC<DocumentBarProps> = ({ title }) => {
useTranslation()
return (
@ -22,13 +22,13 @@ export const DocumentBar: React.FC<DocumentBarProps & ImportProps> = ({ title, n
<div className="navbar-nav">
<ShareLinkButton/>
<DocumentInfoButton/>
<RevisionButton noteContent={noteContent}/>
<RevisionButton/>
<PinToHistoryButton/>
<PermissionButton/>
</div>
<div className="ml-auto navbar-nav">
<ImportMenu updateNoteContent={updateNoteContent} noteContent={noteContent}/>
<ExportMenu title={title} noteContent={noteContent}/>
<ImportMenu/>
<ExportMenu title={title}/>
<EditorMenu noteTitle={title}/>
<ConnectionIndicator/>
</div>

View file

@ -1,10 +1,14 @@
import React, { Fragment, useCallback, useRef } from 'react'
import { Dropdown } from 'react-bootstrap'
import { Trans } from 'react-i18next'
import { useSelector } from 'react-redux'
import { ApplicationState } from '../../../../redux'
import { setDocumentContent } from '../../../../redux/document-content/methods'
import { ForkAwesomeIcon } from '../../../common/fork-awesome/fork-awesome-icon'
import { ImportProps } from '../menus/import-menu'
export const ImportFile: React.FC<ImportProps> = ({ noteContent, updateNoteContent }) => {
export const ImportFile: React.FC = () => {
const markdownContent = useSelector((state: ApplicationState) => state.documentContent.content)
const fileInputReference = useRef<HTMLInputElement>(null)
const doImport = useCallback(() => {
const fileInput = fileInputReference.current
@ -19,10 +23,10 @@ export const ImportFile: React.FC<ImportProps> = ({ noteContent, updateNoteConte
const fileReader = new FileReader()
fileReader.addEventListener('load', () => {
const newContent = fileReader.result as string
if (noteContent.length === 0) {
updateNoteContent(newContent)
if (markdownContent.length === 0) {
setDocumentContent(newContent)
} else {
updateNoteContent(noteContent + '\n' + newContent)
setDocumentContent(markdownContent + '\n' + newContent)
}
})
fileReader.addEventListener('loadend', () => {
@ -31,7 +35,7 @@ export const ImportFile: React.FC<ImportProps> = ({ noteContent, updateNoteConte
fileReader.readAsText(file)
})
fileInput.click()
}, [fileInputReference, noteContent, updateNoteContent])
}, [markdownContent])
return (
<Fragment>

View file

@ -7,10 +7,9 @@ import { MarkdownExportDropdownItem } from './export/markdown'
export interface ExportMenuProps {
title: string
noteContent: string
}
export const ExportMenu: React.FC<ExportMenuProps> = ({ title, noteContent }) => {
export const ExportMenu: React.FC<ExportMenuProps> = ({ title }) => {
useTranslation()
return (
<Dropdown className='small mx-1' alignRight={true}>
@ -40,10 +39,7 @@ export const ExportMenu: React.FC<ExportMenuProps> = ({ title, noteContent }) =>
<Dropdown.Header>
<Trans i18nKey='editor.documentBar.download'/>
</Dropdown.Header>
<MarkdownExportDropdownItem
title={title}
noteContent={noteContent}
/>
<MarkdownExportDropdownItem title={title} />
<Dropdown.Item className='small'>
<ForkAwesomeIcon icon='file-code-o' className={'mx-2'}/>
HTML

View file

@ -1,16 +1,19 @@
import React from 'react'
import { Dropdown } from 'react-bootstrap'
import { useSelector } from 'react-redux'
import { ApplicationState } from '../../../../../redux'
import { download } from '../../../../common/download/download'
import { ForkAwesomeIcon } from '../../../../common/fork-awesome/fork-awesome-icon'
export interface MarkdownExportDropdownItemProps {
title: string
noteContent: string
}
export const MarkdownExportDropdownItem: React.FC<MarkdownExportDropdownItemProps> = ({ title, noteContent }) => {
export const MarkdownExportDropdownItem: React.FC<MarkdownExportDropdownItemProps> = ({ title }) => {
const markdownContent = useSelector((state: ApplicationState) => state.documentContent.content)
return (
<Dropdown.Item className='small' onClick={() => download(noteContent, `${title}.md`, 'text/markdown')}>
<Dropdown.Item className='small' onClick={() => download(markdownContent, `${title}.md`, 'text/markdown')}>
<ForkAwesomeIcon icon='file-text' className={'mx-2'}/>
Markdown
</Dropdown.Item>

View file

@ -4,12 +4,7 @@ import { Trans } from 'react-i18next'
import { ForkAwesomeIcon } from '../../../common/fork-awesome/fork-awesome-icon'
import { ImportFile } from '../import/import-file'
export interface ImportProps {
noteContent: string
updateNoteContent: (content: string) => void
}
export const ImportMenu: React.FC<ImportProps> = ({ updateNoteContent, noteContent }) => {
export const ImportMenu: React.FC = () => {
return (
<Dropdown className='small mx-1' alignRight={true}>
<Dropdown.Toggle variant='light' size='sm' id='editor-menu-import' className=''>
@ -33,7 +28,7 @@ export const ImportMenu: React.FC<ImportProps> = ({ updateNoteContent, noteConte
<ForkAwesomeIcon icon='clipboard' className={'mx-2'}/>
<Trans i18nKey='editor.import.clipboard'/>
</Dropdown.Item>
<ImportFile updateNoteContent={updateNoteContent} noteContent={noteContent}/>
<ImportFile/>
</Dropdown.Menu>
</Dropdown>
)

View file

@ -2,17 +2,13 @@ import React, { Fragment, useState } from 'react'
import { TranslatedIconButton } from '../../../common/icon-button/translated-icon-button'
import { RevisionModal } from './revision-modal'
export interface RevisionButtonProps {
noteContent: string
}
export const RevisionButton: React.FC<RevisionButtonProps> = ({ noteContent }) => {
export const RevisionButton: React.FC = () => {
const [show, setShow] = useState(false)
return (
<Fragment>
<TranslatedIconButton size={'sm'} className={'mx-1'} icon={'history'} variant={'light'} i18nKey={'editor.documentBar.revision'} onClick={() => setShow(true)}/>
<RevisionModal show={show} onHide={() => setShow(false)} titleI18nKey={'editor.modal.revision.title'} icon={'history'} noteContent={noteContent}/>
<RevisionModal show={show} onHide={() => setShow(false)} titleI18nKey={'editor.modal.revision.title'} icon={'history'}/>
</Fragment>
)
}

View file

@ -10,12 +10,11 @@ import { UserResponse } from '../../../../api/users/types'
import { ApplicationState } from '../../../../redux'
import { CommonModal, CommonModalProps } from '../../../common/modals/common-modal'
import { ShowIf } from '../../../common/show-if/show-if'
import { RevisionButtonProps } from './revision-button'
import { RevisionModalListEntry } from './revision-modal-list-entry'
import './revision-modal.scss'
import { downloadRevision, getUserDataForRevision } from './utils'
export const RevisionModal: React.FC<CommonModalProps & RevisionButtonProps> = ({ show, onHide, icon, titleI18nKey, noteContent }) => {
export const RevisionModal: React.FC<CommonModalProps> = ({ show, onHide, icon, titleI18nKey }) => {
useTranslation()
const [revisions, setRevisions] = useState<RevisionListEntry[]>([])
const [selectedRevisionTimestamp, setSelectedRevisionTimestamp] = useState<number | null>(null)
@ -47,6 +46,8 @@ export const RevisionModal: React.FC<CommonModalProps & RevisionButtonProps> = (
}).catch(() => setError(true))
}, [selectedRevisionTimestamp, id])
const markdownContent = useSelector((state: ApplicationState) => state.documentContent.content)
return (
<CommonModal show={show} onHide={onHide} titleI18nKey={titleI18nKey} icon={icon} closeButton={true} size={'xl'} additionalClasses='revision-modal'>
<Modal.Body>
@ -75,7 +76,7 @@ export const RevisionModal: React.FC<CommonModalProps & RevisionButtonProps> = (
<ShowIf condition={!error && !!selectedRevision}>
<ReactDiffViewer
oldValue={selectedRevision?.content}
newValue={noteContent}
newValue={markdownContent}
splitView={false}
compareMethod={DiffMethod.WORDS}
useDarkTheme={darkModeEnabled}

View file

@ -1,7 +1,9 @@
import React, { RefObject, useState } from 'react'
import { Dropdown } from 'react-bootstrap'
import { useSelector } from 'react-redux'
import useResizeObserver from 'use-resize-observer'
import { TocAst } from '../../../external-types/markdown-it-toc-done-right/interface'
import { ApplicationState } from '../../../redux'
import { ForkAwesomeIcon } from '../../common/fork-awesome/fork-awesome-icon'
import { ShowIf } from '../../common/show-if/show-if'
import { LineMarkerPosition } from '../../markdown-renderer/types'
@ -10,7 +12,6 @@ import { TableOfContents } from '../table-of-contents/table-of-contents'
import { YAMLMetaData } from '../yaml-metadata/yaml-metadata'
export interface DocumentRenderPaneProps {
content: string
extraClasses?: string
onFirstHeadingChange: (firstHeading: string | undefined) => void
onLineMarkerPositionChanged?: (lineMarkerPosition: LineMarkerPosition[]) => void
@ -23,7 +24,6 @@ export interface DocumentRenderPaneProps {
}
export const DocumentRenderPane: React.FC<DocumentRenderPaneProps> = ({
content,
extraClasses,
onFirstHeadingChange,
onLineMarkerPositionChanged,
@ -37,6 +37,7 @@ export const DocumentRenderPane: React.FC<DocumentRenderPaneProps> = ({
const [tocAst, setTocAst] = useState<TocAst>()
const { width } = useResizeObserver(rendererReference ? { ref: rendererReference } : undefined)
const realWidth = width || 0
const markdownContent = useSelector((state: ApplicationState) => state.documentContent.content)
return (
<div className={`bg-light flex-fill pb-5 flex-row d-flex w-100 h-100 ${extraClasses ?? ''}`}
@ -45,7 +46,7 @@ export const DocumentRenderPane: React.FC<DocumentRenderPaneProps> = ({
<div className={'bg-light flex-fill'}>
<FullMarkdownRenderer
className={'flex-fill mb-3'}
content={content}
content={markdownContent}
onFirstHeadingChange={onFirstHeadingChange}
onLineMarkerPositionChanged={onLineMarkerPositionChanged}
onMetaDataChange={onMetadataChange}

View file

@ -1,4 +1,6 @@
import React, { useMemo, useRef, useState } from 'react'
import { useSelector } from 'react-redux'
import { ApplicationState } from '../../../redux'
import { LineMarkerPosition } from '../../markdown-renderer/types'
import { useScrollToLineMark } from '../scroll/hooks/use-scroll-to-line-mark'
import { useUserScroll } from '../scroll/hooks/use-user-scroll'
@ -6,7 +8,6 @@ import { ScrollProps } from '../scroll/scroll-props'
import { DocumentRenderPane, DocumentRenderPaneProps } from './document-render-pane'
export const ScrollingDocumentRenderPane: React.FC<DocumentRenderPaneProps & ScrollProps> = ({
content,
scrollState,
wide,
onFirstHeadingChange,
@ -15,16 +16,16 @@ export const ScrollingDocumentRenderPane: React.FC<DocumentRenderPaneProps & Scr
onScroll,
onTaskCheckedChange
}) => {
const markdownContent = useSelector((state: ApplicationState) => state.documentContent.content)
const renderer = useRef<HTMLDivElement>(null)
const [lineMarks, setLineMarks] = useState<LineMarkerPosition[]>()
const contentLineCount = useMemo(() => content.split('\n').length, [content])
const contentLineCount = useMemo(() => markdownContent.split('\n').length, [markdownContent])
useScrollToLineMark(scrollState, lineMarks, contentLineCount, renderer)
const userScroll = useUserScroll(lineMarks, renderer, onScroll)
return (
<DocumentRenderPane
content={content}
extraClasses={'overflow-y-scroll'}
rendererReference={renderer}
wide={wide}

View file

@ -5,6 +5,7 @@ import useMedia from 'use-media'
import { useApplyDarkMode } from '../../hooks/common/use-apply-dark-mode'
import { useDocumentTitle } from '../../hooks/common/use-document-title'
import { ApplicationState } from '../../redux'
import { setDocumentContent } from '../../redux/document-content/methods'
import { setEditorMode } from '../../redux/editor/methods'
import { extractNoteTitle } from '../common/document-title/note-title-extractor'
import { MotdBanner } from '../common/motd-banner/motd-banner'
@ -34,7 +35,7 @@ const TASK_REGEX = /(\s*[-*] )(\[[ xX]])( .*)/
export const Editor: React.FC = () => {
const { t } = useTranslation()
const untitledNote = t('editor.untitledNote')
const [markdownContent, setMarkdownContent] = useState(editorTestContent)
const markdownContent = useSelector((state: ApplicationState) => state.documentContent.content)
const isWide = useMedia({ minWidth: 576 })
const [documentTitle, setDocumentTitle] = useState(untitledNote)
const noteMetadata = useRef<YAMLMetaData>()
@ -49,6 +50,10 @@ export const Editor: React.FC = () => {
rendererScrollState: { firstLineInView: 1, scrolledPercentage: 0 }
}))
useEffect(() => {
setDocumentContent(editorTestContent)
}, [])
const updateDocumentTitle = useCallback(() => {
const noteTitle = extractNoteTitle(untitledNote, noteMetadata.current, firstHeading.current)
setDocumentTitle(noteTitle)
@ -71,9 +76,9 @@ export const Editor: React.FC = () => {
const before = results[1]
const after = results[3]
lines[lineInMarkdown] = `${before}[${checked ? 'x' : ' '}]${after}`
setMarkdownContent(lines.join('\n'))
setDocumentContent(lines.join('\n'))
}
}, [markdownContent, setMarkdownContent])
}, [markdownContent])
useViewModeShortcuts()
@ -105,12 +110,12 @@ export const Editor: React.FC = () => {
<MotdBanner/>
<div className={'d-flex flex-column vh-100'}>
<AppBar mode={AppBarMode.EDITOR}/>
<DocumentBar title={documentTitle} noteContent={markdownContent} updateNoteContent={(newContent) => setMarkdownContent(newContent)}/>
<DocumentBar title={documentTitle}/>
<Splitter
showLeft={editorMode === EditorMode.EDITOR || editorMode === EditorMode.BOTH}
left={
<EditorPane
onContentChange={content => setMarkdownContent(content)}
onContentChange={setDocumentContent}
content={markdownContent}
scrollState={scrollState.editorScrollState}
onScroll={onEditorScroll}
@ -120,7 +125,6 @@ export const Editor: React.FC = () => {
showRight={editorMode === EditorMode.PREVIEW || (editorMode === EditorMode.BOTH)}
right={
<ScrollingDocumentRenderPane
content={markdownContent}
onFirstHeadingChange={onFirstHeadingChange}
onMakeScrollSource={() => {
scrollSource.current = ScrollSource.RENDERER

View file

@ -5,6 +5,7 @@ import { useParams } from 'react-router'
import { getNote, Note } from '../../api/notes'
import { useApplyDarkMode } from '../../hooks/common/use-apply-dark-mode'
import { useDocumentTitle } from '../../hooks/common/use-document-title'
import { setDocumentContent } from '../../redux/document-content/methods'
import { extractNoteTitle } from '../common/document-title/note-title-extractor'
import { MotdBanner } from '../common/motd-banner/motd-banner'
import { ShowIf } from '../common/show-if/show-if'
@ -42,7 +43,10 @@ export const PadViewOnly: React.FC = () => {
useEffect(() => {
getNote(id)
.then(note => setNoteData(note))
.then(note => {
setNoteData(note)
setDocumentContent(note.content)
})
.catch(() => setError(true))
.finally(() => setLoading(false))
}, [id])
@ -80,7 +84,6 @@ export const PadViewOnly: React.FC = () => {
viewCount={noteData?.viewcount ?? 0}
/>
<DocumentRenderPane
content={noteData?.content ?? ''}
onFirstHeadingChange={onFirstHeadingChange}
onMetadataChange={onMetadataChange}
onTaskCheckedChange={() => false}

View file

@ -0,0 +1,10 @@
import { store } from '..'
import { DocumentContentActionType, SetDocumentContentAction } from './types'
export const setDocumentContent = (content: string): void => {
const action: SetDocumentContentAction = {
type: DocumentContentActionType.SET_DOCUMENT_CONTENT,
content: content
}
store.dispatch(action)
}

View file

@ -0,0 +1,15 @@
import { Reducer } from 'redux'
import { DocumentContent, DocumentContentAction, DocumentContentActionType, SetDocumentContentAction } from './types'
export const initialState: DocumentContent = {
content: ''
}
export const DocumentContentReducer: Reducer<DocumentContent, DocumentContentAction> = (state: DocumentContent = initialState, action: DocumentContentAction) => {
switch (action.type) {
case DocumentContentActionType.SET_DOCUMENT_CONTENT:
return { content: (action as SetDocumentContentAction).content }
default:
return state
}
}

View file

@ -0,0 +1,17 @@
import { Action } from 'redux'
export enum DocumentContentActionType {
SET_DOCUMENT_CONTENT = 'document-content/set',
}
export interface DocumentContent {
content: string
}
export interface DocumentContentAction extends Action<DocumentContentActionType> {
type: DocumentContentActionType
}
export interface SetDocumentContentAction extends DocumentContentAction {
content: string
}

View file

@ -7,6 +7,8 @@ import { BannerState } from './banner/types'
import { ConfigReducer } from './config/reducers'
import { DarkModeConfigReducer } from './dark-mode/reducers'
import { DarkModeConfig } from './dark-mode/types'
import { DocumentContentReducer } from './document-content/reducers'
import { DocumentContent } from './document-content/types'
import { EditorConfigReducer } from './editor/reducers'
import { EditorConfig } from './editor/types'
import { UserReducer } from './user/reducers'
@ -19,6 +21,7 @@ export interface ApplicationState {
apiUrl: ApiUrlObject;
editorConfig: EditorConfig;
darkMode: DarkModeConfig;
documentContent: DocumentContent;
}
export const allReducers: Reducer<ApplicationState> = combineReducers<ApplicationState>({
@ -27,7 +30,8 @@ export const allReducers: Reducer<ApplicationState> = combineReducers<Applicatio
banner: BannerReducer,
apiUrl: ApiUrlReducer,
editorConfig: EditorConfigReducer,
darkMode: DarkModeConfigReducer
darkMode: DarkModeConfigReducer,
documentContent: DocumentContentReducer
})
export const store = createStore(allReducers)