mirror of
https://github.com/hedgedoc/hedgedoc.git
synced 2025-05-15 07:34:42 -04:00
added upload functionality (#758)
Co-authored-by: Tilman Vatteroth <tilman.vatteroth@tu-dortmund.de> Signed-off-by: Tilman Vatteroth <tilman.vatteroth@tu-dortmund.de> Signed-off-by: Philip Molares <philip.molares@udo.edu>
This commit is contained in:
parent
8ce344512c
commit
0c0841639a
20 changed files with 401 additions and 73 deletions
BIN
cypress/fixtures/acme.png
Normal file
BIN
cypress/fixtures/acme.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 6.2 KiB |
3
cypress/fixtures/acme.png.license
Normal file
3
cypress/fixtures/acme.png.license
Normal file
|
@ -0,0 +1,3 @@
|
|||
SPDX-FileCopyrightText: 2020 The HedgeDoc developers (see AUTHORS file)
|
||||
|
||||
SPDX-License-Identifier: CC0-1.0
|
|
@ -38,7 +38,7 @@ describe('Intro', () => {
|
|||
})
|
||||
})
|
||||
|
||||
it('Versioncan be opened and closed', () => {
|
||||
it('Version can be opened and closed', () => {
|
||||
cy.get('#versionModal')
|
||||
.should('not.exist')
|
||||
cy.get('#version')
|
||||
|
|
98
cypress/integration/upload.spec.ts
Normal file
98
cypress/integration/upload.spec.ts
Normal file
|
@ -0,0 +1,98 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: 2020 The HedgeDoc developers (see AUTHORS file)
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
const imageUrl = 'http://example.com/non-existing.png'
|
||||
|
||||
describe('Upload', () => {
|
||||
beforeEach(() => {
|
||||
cy.visit('/n/test')
|
||||
cy.get('.btn.active.btn-outline-secondary > i.fa-columns')
|
||||
.should('exist')
|
||||
cy.get('.CodeMirror textarea')
|
||||
.type('{ctrl}a', { force: true })
|
||||
.type('{backspace}')
|
||||
})
|
||||
|
||||
it('check that text drag\'n\'drop still works', () => {
|
||||
const dataTransfer = new DataTransfer()
|
||||
cy.get('.CodeMirror textarea')
|
||||
.type('line 1\nline 2\nline3')
|
||||
cy.get('.CodeMirror-activeline > .CodeMirror-line > span')
|
||||
.dblclick()
|
||||
cy.get('.CodeMirror-line > span > .cm-matchhighlight')
|
||||
.trigger('dragstart', { dataTransfer })
|
||||
cy.get('.CodeMirror-code > div:nth-of-type(1) > .CodeMirror-line > span span')
|
||||
.trigger('drop', { dataTransfer })
|
||||
cy.get('.CodeMirror-code > div:nth-of-type(1) > .CodeMirror-line > span span')
|
||||
.should('have.text', 'linline3e 1')
|
||||
})
|
||||
|
||||
describe('upload works', () => {
|
||||
beforeEach(() => {
|
||||
cy.intercept({
|
||||
method: 'POST',
|
||||
url: '/api/v2/media/upload'
|
||||
}, {
|
||||
statusCode: 201,
|
||||
body: {
|
||||
link: imageUrl
|
||||
}
|
||||
})
|
||||
cy.fixture('acme.png').then(image => {
|
||||
this.image = image
|
||||
})
|
||||
})
|
||||
it('via button', () => {
|
||||
cy.get('.fa-upload')
|
||||
.click()
|
||||
cy.get('div.btn-group > input[type=file]')
|
||||
.attachFile({ filePath: 'acme.png', mimeType: 'image/png' })
|
||||
cy.get('.CodeMirror-activeline > .CodeMirror-line > span')
|
||||
.should('have.text', ``)
|
||||
})
|
||||
|
||||
it('via paste', () => {
|
||||
const pasteEvent = {
|
||||
clipboardData: {
|
||||
files: [Cypress.Blob.base64StringToBlob(this.image, 'image/png')]
|
||||
}
|
||||
}
|
||||
cy.get('.CodeMirror-scroll').trigger('paste', pasteEvent)
|
||||
cy.get('.CodeMirror-activeline > .CodeMirror-line > span')
|
||||
.should('have.text', ``)
|
||||
})
|
||||
|
||||
it('via drag and drop', () => {
|
||||
const dropEvent = {
|
||||
dataTransfer: {
|
||||
files: [Cypress.Blob.base64StringToBlob(this.image, 'image/png')],
|
||||
effectAllowed: 'uninitialized'
|
||||
}
|
||||
}
|
||||
cy.get('.CodeMirror-scroll').trigger('dragenter', dropEvent)
|
||||
cy.get('.CodeMirror-scroll').trigger('drop', dropEvent)
|
||||
cy.get('.CodeMirror-activeline > .CodeMirror-line > span')
|
||||
.should('have.text', ``)
|
||||
})
|
||||
})
|
||||
|
||||
it('upload fails', () => {
|
||||
cy.get('.CodeMirror textarea')
|
||||
.type('not empty')
|
||||
cy.intercept({
|
||||
method: 'POST',
|
||||
url: '/api/v2/media/upload'
|
||||
}, {
|
||||
statusCode: 400
|
||||
})
|
||||
cy.get('.fa-upload')
|
||||
.click()
|
||||
cy.get('input[type=file]')
|
||||
.attachFile({ filePath: 'acme.png', mimeType: 'image/png' })
|
||||
cy.get('.CodeMirror-activeline > .CodeMirror-line > span')
|
||||
.should('have.text', 'not empty')
|
||||
})
|
||||
})
|
|
@ -60,6 +60,9 @@
|
|||
}
|
||||
},
|
||||
"editor": {
|
||||
"upload": {
|
||||
"uploadFile": "Datei wird hochgeladen...{{fileName}}"
|
||||
},
|
||||
"help": {
|
||||
"contacts": {
|
||||
"title": "Kontakte",
|
||||
|
|
|
@ -178,6 +178,10 @@
|
|||
}
|
||||
},
|
||||
"editor": {
|
||||
"upload": {
|
||||
"uploadFile": "Uploading file...{{fileName}}",
|
||||
"dropImage": "Drop Image to insert"
|
||||
},
|
||||
"untitledNote": "Untitled",
|
||||
"placeholder": "← Start by entering a title here\n===\nVisit the features page if you don't know what to do.\nHappy hacking :)",
|
||||
"invalidYaml": "The yaml-header is invalid. See <0></0> for more information.",
|
||||
|
|
|
@ -18,3 +18,21 @@ export const getProxiedUrl = async (imageUrl: string): Promise<ImageProxyRespons
|
|||
expectResponseCode(response)
|
||||
return await response.json() as Promise<ImageProxyResponse>
|
||||
}
|
||||
|
||||
export interface UploadedMedia {
|
||||
link: string
|
||||
}
|
||||
|
||||
export const uploadFile = async (noteId: string, contentType: string, media: Blob): Promise<UploadedMedia> => {
|
||||
const response = await fetch(getApiUrl() + '/media/upload', {
|
||||
...defaultFetchConfig,
|
||||
headers: {
|
||||
'Content-Type': contentType,
|
||||
'HedgeDoc-Note': noteId
|
||||
},
|
||||
method: 'POST',
|
||||
body: media
|
||||
})
|
||||
expectResponseCode(response, 201)
|
||||
return await response.json() as Promise<UploadedMedia>
|
||||
}
|
||||
|
|
|
@ -0,0 +1,59 @@
|
|||
/*
|
||||
SPDX-FileCopyrightText: 2020 The HedgeDoc developers (see AUTHORS file)
|
||||
|
||||
SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import React, { Fragment, useCallback, useRef } from 'react'
|
||||
import { Button, Dropdown } from 'react-bootstrap'
|
||||
import { Trans, useTranslation } from 'react-i18next'
|
||||
import { ForkAwesomeIcon } from '../fork-awesome/fork-awesome-icon'
|
||||
import { IconName } from '../fork-awesome/types'
|
||||
|
||||
export interface HiddenInputMenuEntryProps {
|
||||
type: 'dropdown' | 'button'
|
||||
acceptedFiles: string
|
||||
i18nKey: string
|
||||
icon: IconName
|
||||
onLoad: (file: File) => Promise<void>
|
||||
}
|
||||
|
||||
export const HiddenInputMenuEntry: React.FC<HiddenInputMenuEntryProps> = ({ type, acceptedFiles, i18nKey, icon, onLoad }) => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
const fileInputReference = useRef<HTMLInputElement>(null)
|
||||
const onClick = useCallback(() => {
|
||||
const fileInput = fileInputReference.current
|
||||
if (!fileInput) {
|
||||
return
|
||||
}
|
||||
fileInput.addEventListener('change', () => {
|
||||
if (!fileInput.files || fileInput.files.length < 1) {
|
||||
return
|
||||
}
|
||||
const file = fileInput.files[0]
|
||||
onLoad(file).then(() => {
|
||||
fileInput.value = ''
|
||||
}).catch((error) => {
|
||||
console.error(error)
|
||||
})
|
||||
})
|
||||
fileInput.click()
|
||||
}, [onLoad])
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
<input type='file' ref={fileInputReference} className='d-none' accept={acceptedFiles}/>
|
||||
{
|
||||
type === 'dropdown'
|
||||
? <Dropdown.Item className={'small import-md-file'} onClick={onClick}>
|
||||
<ForkAwesomeIcon icon={icon} className={'mx-2'}/>
|
||||
<Trans i18nKey={i18nKey}/>
|
||||
</Dropdown.Item>
|
||||
: <Button variant='light' onClick={onClick} title={t(i18nKey)}>
|
||||
<ForkAwesomeIcon icon={icon}/>
|
||||
</Button>
|
||||
}
|
||||
</Fragment>
|
||||
)
|
||||
}
|
|
@ -1,55 +0,0 @@
|
|||
/*
|
||||
SPDX-FileCopyrightText: 2020 The HedgeDoc developers (see AUTHORS file)
|
||||
|
||||
SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
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'
|
||||
|
||||
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
|
||||
if (!fileInput) {
|
||||
return
|
||||
}
|
||||
fileInput.addEventListener('change', () => {
|
||||
if (!fileInput.files || fileInput.files.length < 1) {
|
||||
return
|
||||
}
|
||||
const file = fileInput.files[0]
|
||||
const fileReader = new FileReader()
|
||||
fileReader.addEventListener('load', () => {
|
||||
const newContent = fileReader.result as string
|
||||
if (markdownContent.length === 0) {
|
||||
setDocumentContent(newContent)
|
||||
} else {
|
||||
setDocumentContent(markdownContent + '\n' + newContent)
|
||||
}
|
||||
})
|
||||
fileReader.addEventListener('loadend', () => {
|
||||
fileInput.value = ''
|
||||
})
|
||||
fileReader.readAsText(file)
|
||||
})
|
||||
fileInput.click()
|
||||
}, [markdownContent])
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
<input type='file' ref={fileInputReference} className='d-none' accept='.md, text/markdown, text/plain'/>
|
||||
<Dropdown.Item className='small import-md-file' onClick={doImport}>
|
||||
<ForkAwesomeIcon icon='file-text-o' className={'mx-2'}/>
|
||||
<Trans i18nKey='editor.import.file'/>
|
||||
</Dropdown.Item>
|
||||
</Fragment>
|
||||
)
|
||||
}
|
|
@ -4,13 +4,39 @@ SPDX-FileCopyrightText: 2020 The HedgeDoc developers (see AUTHORS file)
|
|||
SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import React from 'react'
|
||||
import React, { useCallback } 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 { ImportFile } from '../import/import-file'
|
||||
import { HiddenInputMenuEntry } from '../../../common/hidden-input-menu-entry/hidden-input-menu-entry'
|
||||
|
||||
export const ImportMenu: React.FC = () => {
|
||||
const markdownContent = useSelector((state: ApplicationState) => state.documentContent.content)
|
||||
|
||||
const onImportMarkdown = useCallback((file: File) => {
|
||||
return new Promise<void>((resolve, reject) => {
|
||||
const fileReader = new FileReader()
|
||||
fileReader.addEventListener('load', () => {
|
||||
const newContent = fileReader.result as string
|
||||
if (markdownContent.length === 0) {
|
||||
setDocumentContent(newContent)
|
||||
} else {
|
||||
setDocumentContent(markdownContent + '\n' + newContent)
|
||||
}
|
||||
})
|
||||
fileReader.addEventListener('loadend', () => {
|
||||
resolve()
|
||||
})
|
||||
fileReader.addEventListener('error', (error) => {
|
||||
reject(error)
|
||||
})
|
||||
fileReader.readAsText(file)
|
||||
})
|
||||
}, [markdownContent])
|
||||
|
||||
return (
|
||||
<Dropdown className='small mx-1' alignRight={true}>
|
||||
<Dropdown.Toggle variant='light' size='sm' id='editor-menu-import' className=''>
|
||||
|
@ -34,7 +60,13 @@ export const ImportMenu: React.FC = () => {
|
|||
<ForkAwesomeIcon icon='clipboard' className={'mx-2'}/>
|
||||
<Trans i18nKey='editor.import.clipboard'/>
|
||||
</Dropdown.Item>
|
||||
<ImportFile/>
|
||||
<HiddenInputMenuEntry
|
||||
type={'dropdown'}
|
||||
acceptedFiles={'.md, text/markdown, text/plain'}
|
||||
i18nKey={'editor.import.file'}
|
||||
icon={'file-text-o'}
|
||||
onLoad={onImportMarkdown}
|
||||
/>
|
||||
</Dropdown.Menu>
|
||||
</Dropdown>
|
||||
)
|
||||
|
|
|
@ -21,6 +21,10 @@
|
|||
height: 100%;
|
||||
}
|
||||
|
||||
.react-codemirror2.file-drag .CodeMirror-cursors {
|
||||
visibility: visible;
|
||||
}
|
||||
|
||||
.no-ligatures .CodeMirror {
|
||||
//These two properties must be set separately because otherwise node-scss breaks.
|
||||
.CodeMirror-line, .CodeMirror-line-like {
|
||||
|
|
|
@ -40,6 +40,7 @@ import './editor-pane.scss'
|
|||
import { defaultKeyMap } from './key-map'
|
||||
import { createStatusInfo, defaultState, StatusBar, StatusBarInfo } from './status-bar/status-bar'
|
||||
import { ToolBar } from './tool-bar/tool-bar'
|
||||
import { handleUpload } from './upload-handler'
|
||||
|
||||
export interface EditorPaneProps {
|
||||
onContentChange: (content: string) => void
|
||||
|
@ -61,6 +62,33 @@ const onChange = (editor: Editor) => {
|
|||
}
|
||||
}
|
||||
|
||||
interface PasteEvent {
|
||||
clipboardData: {
|
||||
files: FileList
|
||||
},
|
||||
preventDefault: () => void
|
||||
}
|
||||
|
||||
const onPaste = (pasteEditor: Editor, event: PasteEvent) => {
|
||||
if (event && event.clipboardData && event.clipboardData.files) {
|
||||
event.preventDefault()
|
||||
const files: FileList = event.clipboardData.files
|
||||
if (files && files.length >= 1) {
|
||||
handleUpload(files[0], pasteEditor)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
interface DropEvent {
|
||||
pageX: number,
|
||||
pageY: number,
|
||||
dataTransfer: {
|
||||
files: FileList
|
||||
effectAllowed: string
|
||||
} | null
|
||||
preventDefault: () => void
|
||||
}
|
||||
|
||||
export const EditorPane: React.FC<EditorPaneProps & ScrollProps> = ({ onContentChange, content, scrollState, onScroll, onMakeScrollSource }) => {
|
||||
const { t } = useTranslation()
|
||||
const maxLength = useSelector((state: ApplicationState) => state.config.maxDocumentLength)
|
||||
|
@ -127,6 +155,21 @@ export const EditorPane: React.FC<EditorPaneProps & ScrollProps> = ({ onContentC
|
|||
setStatusBarInfo(createStatusInfo(editorWithActivity, maxLength))
|
||||
}, [maxLength])
|
||||
|
||||
const onDrop = useCallback((dropEditor: Editor, event: DropEvent) => {
|
||||
if (event && dropEditor && event.pageX && event.pageY && event.dataTransfer &&
|
||||
event.dataTransfer.files && event.dataTransfer.files.length >= 1) {
|
||||
event.preventDefault()
|
||||
const top: number = event.pageY
|
||||
const left: number = event.pageX
|
||||
const newCursor = dropEditor.coordsChar({ top, left }, 'page')
|
||||
dropEditor.setCursor(newCursor)
|
||||
const files: FileList = event.dataTransfer.files
|
||||
handleUpload(files[0], dropEditor)
|
||||
}
|
||||
}, [])
|
||||
|
||||
const onMaxLengthHide = useCallback(() => setShowMaxLengthWarning(false), [])
|
||||
|
||||
const codeMirrorOptions: EditorConfiguration = useMemo<EditorConfiguration>(() => ({
|
||||
...editorPreferences,
|
||||
mode: 'gfm',
|
||||
|
@ -158,8 +201,8 @@ export const EditorPane: React.FC<EditorPaneProps & ScrollProps> = ({ onContentC
|
|||
}), [t, editorPreferences])
|
||||
|
||||
return (
|
||||
<div className={'d-flex flex-column h-100'} onMouseEnter={onMakeScrollSource}>
|
||||
<MaxLengthWarningModal show={showMaxLengthWarning} onHide={() => setShowMaxLengthWarning(false)} maxLength={maxLength}/>
|
||||
<div className={'d-flex flex-column h-100 position-relative'} onMouseEnter={onMakeScrollSource}>
|
||||
<MaxLengthWarningModal show={showMaxLengthWarning} onHide={onMaxLengthHide} maxLength={maxLength}/>
|
||||
<ToolBar
|
||||
editor={editor}
|
||||
/>
|
||||
|
@ -168,6 +211,8 @@ export const EditorPane: React.FC<EditorPaneProps & ScrollProps> = ({ onContentC
|
|||
value={content}
|
||||
options={codeMirrorOptions}
|
||||
onChange={onChange}
|
||||
onPaste={onPaste}
|
||||
onDrop={onDrop}
|
||||
onCursorActivity={onCursorActivity}
|
||||
editorDidMount={onEditorDidMount}
|
||||
onBeforeChange={onBeforeChange}
|
||||
|
|
|
@ -5,10 +5,12 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
*/
|
||||
|
||||
import { Editor } from 'codemirror'
|
||||
import React from 'react'
|
||||
import React, { useCallback } from 'react'
|
||||
import { Button, ButtonGroup, ButtonToolbar } from 'react-bootstrap'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { ForkAwesomeIcon } from '../../../common/fork-awesome/fork-awesome-icon'
|
||||
import { HiddenInputMenuEntry } from '../../../common/hidden-input-menu-entry/hidden-input-menu-entry'
|
||||
import { handleUpload } from '../upload-handler'
|
||||
import { EditorPreferences } from './editor-preferences/editor-preferences'
|
||||
import { EmojiPickerButton } from './emoji-picker/emoji-picker-button'
|
||||
import { TablePickerButton } from './table-picker/table-picker-button'
|
||||
|
@ -32,6 +34,7 @@ import {
|
|||
superscriptSelection,
|
||||
underlineSelection
|
||||
} from './utils/toolbarButtonUtils'
|
||||
import { supportedMimeTypesJoined } from './utils/upload-image-mimetypes'
|
||||
|
||||
export interface ToolBarProps {
|
||||
editor: Editor | undefined
|
||||
|
@ -40,9 +43,12 @@ export interface ToolBarProps {
|
|||
export const ToolBar: React.FC<ToolBarProps> = ({ editor }) => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
const notImplemented = () => {
|
||||
alert('This feature is not yet implemented')
|
||||
}
|
||||
const onUploadImage = useCallback((file: File) => {
|
||||
if (editor) {
|
||||
handleUpload(file, editor)
|
||||
}
|
||||
return Promise.resolve()
|
||||
}, [editor])
|
||||
|
||||
if (!editor) {
|
||||
return null
|
||||
|
@ -97,9 +103,13 @@ export const ToolBar: React.FC<ToolBarProps> = ({ editor }) => {
|
|||
<Button variant='light' onClick={() => addImage(editor)} title={t('editor.editorToolbar.image')}>
|
||||
<ForkAwesomeIcon icon="picture-o"/>
|
||||
</Button>
|
||||
<Button variant='light' onClick={notImplemented} title={t('editor.editorToolbar.uploadImage')}>
|
||||
<ForkAwesomeIcon icon="upload"/>
|
||||
</Button>
|
||||
<HiddenInputMenuEntry
|
||||
type={'button'}
|
||||
acceptedFiles={supportedMimeTypesJoined}
|
||||
i18nKey={'editor.editorToolbar.uploadImage'}
|
||||
icon={'upload'}
|
||||
onLoad={onUploadImage}
|
||||
/>
|
||||
</ButtonGroup>
|
||||
<ButtonGroup className={'mx-1 flex-wrap'}>
|
||||
<TablePickerButton editor={editor}/>
|
||||
|
|
|
@ -0,0 +1,23 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: 2020 The HedgeDoc developers (see AUTHORS file)
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
export const supportedMimeTypes: string[] = [
|
||||
'application/pdf',
|
||||
'image/apng',
|
||||
'image/bmp',
|
||||
'image/gif',
|
||||
'image/heif',
|
||||
'image/heic',
|
||||
'image/heif-sequence',
|
||||
'image/heic-sequence',
|
||||
'image/jpeg',
|
||||
'image/png',
|
||||
'image/svg+xml',
|
||||
'image/tiff',
|
||||
'image/webp'
|
||||
]
|
||||
|
||||
export const supportedMimeTypesJoined = supportedMimeTypes.join(', ')
|
48
src/components/editor/editor-pane/upload-handler.ts
Normal file
48
src/components/editor/editor-pane/upload-handler.ts
Normal file
|
@ -0,0 +1,48 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: 2020 The HedgeDoc developers (see AUTHORS file)
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import { Editor } from 'codemirror'
|
||||
import i18n from 'i18next'
|
||||
import { uploadFile } from '../../../api/media'
|
||||
import { store } from '../../../redux'
|
||||
import { supportedMimeTypes } from './tool-bar/utils/upload-image-mimetypes'
|
||||
|
||||
export const handleUpload = (file: File, editor: Editor): void => {
|
||||
if (!file) {
|
||||
return
|
||||
}
|
||||
const mimeType = file.type
|
||||
if (!supportedMimeTypes.includes(mimeType)) {
|
||||
// this mimetype is not supported
|
||||
return
|
||||
}
|
||||
const cursor = editor.getCursor()
|
||||
const uploadPlaceholder = `![${i18n.t('editor.upload.uploadFile', { fileName: file.name })}]()`
|
||||
const noteId = store.getState().documentContent.noteId
|
||||
editor.replaceRange(uploadPlaceholder, cursor, cursor, '+input')
|
||||
uploadFile(noteId, mimeType, file)
|
||||
.then(({ link }) => {
|
||||
editor.replaceRange(getCorrectSyntaxForLink(mimeType, link), cursor, {
|
||||
line: cursor.line,
|
||||
ch: cursor.ch + uploadPlaceholder.length
|
||||
}, '+input')
|
||||
})
|
||||
.catch(() => {
|
||||
editor.replaceRange('', cursor, {
|
||||
line: cursor.line,
|
||||
ch: cursor.ch + uploadPlaceholder.length
|
||||
}, '+input')
|
||||
})
|
||||
}
|
||||
|
||||
const getCorrectSyntaxForLink = (mimeType: string, link: string): string => {
|
||||
switch (mimeType) {
|
||||
case 'application/pdf':
|
||||
return `{%pdf ${link} %}`
|
||||
default:
|
||||
return ``
|
||||
}
|
||||
}
|
|
@ -7,11 +7,12 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
import React, { Fragment, useCallback, useEffect, useRef, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useSelector } from 'react-redux'
|
||||
import { useParams } from 'react-router'
|
||||
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 { setDocumentContent, setNoteId } 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'
|
||||
|
@ -40,6 +41,7 @@ const TASK_REGEX = /(\s*[-*] )(\[[ xX]])( .*)/
|
|||
|
||||
export const Editor: React.FC = () => {
|
||||
const { t } = useTranslation()
|
||||
const { id } = useParams<EditorPathParams>()
|
||||
const untitledNote = t('editor.untitledNote')
|
||||
const markdownContent = useSelector((state: ApplicationState) => state.documentContent.content)
|
||||
const isWide = useMedia({ minWidth: 576 })
|
||||
|
@ -90,6 +92,10 @@ export const Editor: React.FC = () => {
|
|||
|
||||
const isFirstDraw = useFirstDraw()
|
||||
|
||||
useEffect(() => {
|
||||
setNoteId(id)
|
||||
}, [id])
|
||||
|
||||
useEffect(() => {
|
||||
if (!isFirstDraw && !isWide && editorMode === EditorMode.BOTH) {
|
||||
setEditorMode(EditorMode.PREVIEW)
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
*/
|
||||
|
||||
import { store } from '..'
|
||||
import { DocumentContentActionType, SetDocumentContentAction } from './types'
|
||||
import { DocumentContentActionType, SetDocumentContentAction, SetNoteIdAction } from './types'
|
||||
|
||||
export const setDocumentContent = (content: string): void => {
|
||||
const action: SetDocumentContentAction = {
|
||||
|
@ -14,3 +14,11 @@ export const setDocumentContent = (content: string): void => {
|
|||
}
|
||||
store.dispatch(action)
|
||||
}
|
||||
|
||||
export const setNoteId = (noteId: string): void => {
|
||||
const action: SetNoteIdAction = {
|
||||
type: DocumentContentActionType.SET_NOTE_ID,
|
||||
noteId: noteId
|
||||
}
|
||||
store.dispatch(action)
|
||||
}
|
||||
|
|
|
@ -5,16 +5,31 @@
|
|||
*/
|
||||
|
||||
import { Reducer } from 'redux'
|
||||
import { DocumentContent, DocumentContentAction, DocumentContentActionType, SetDocumentContentAction } from './types'
|
||||
import {
|
||||
DocumentContent,
|
||||
DocumentContentAction,
|
||||
DocumentContentActionType,
|
||||
SetDocumentContentAction,
|
||||
SetNoteIdAction
|
||||
} from './types'
|
||||
|
||||
export const initialState: DocumentContent = {
|
||||
content: ''
|
||||
content: '',
|
||||
noteId: ''
|
||||
}
|
||||
|
||||
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 }
|
||||
return {
|
||||
...state,
|
||||
content: (action as SetDocumentContentAction).content
|
||||
}
|
||||
case DocumentContentActionType.SET_NOTE_ID:
|
||||
return {
|
||||
...state,
|
||||
noteId: (action as SetNoteIdAction).noteId
|
||||
}
|
||||
default:
|
||||
return state
|
||||
}
|
||||
|
|
|
@ -8,10 +8,12 @@ import { Action } from 'redux'
|
|||
|
||||
export enum DocumentContentActionType {
|
||||
SET_DOCUMENT_CONTENT = 'document-content/set',
|
||||
SET_NOTE_ID = 'document-content/noteid/set'
|
||||
}
|
||||
|
||||
export interface DocumentContent {
|
||||
content: string
|
||||
noteId: string
|
||||
}
|
||||
|
||||
export interface DocumentContentAction extends Action<DocumentContentActionType> {
|
||||
|
@ -21,3 +23,7 @@ export interface DocumentContentAction extends Action<DocumentContentActionType>
|
|||
export interface SetDocumentContentAction extends DocumentContentAction {
|
||||
content: string
|
||||
}
|
||||
|
||||
export interface SetNoteIdAction extends DocumentContentAction {
|
||||
noteId: string
|
||||
}
|
||||
|
|
|
@ -9649,6 +9649,7 @@ micromatch@^4.0.0, micromatch@^4.0.2:
|
|||
|
||||
"midi@https://github.com/paulrosen/MIDI.js.git#abcjs":
|
||||
version "0.4.2"
|
||||
uid e593ffef81a0350f99448e3ab8111957145ff6b2
|
||||
resolved "https://github.com/paulrosen/MIDI.js.git#e593ffef81a0350f99448e3ab8111957145ff6b2"
|
||||
|
||||
miller-rabin@^4.0.0:
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue