diff --git a/CHANGELOG.md b/CHANGELOG.md index a7653652e..7c5901a51 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -44,6 +44,7 @@ - All images can be clicked to show them in full screen. - Code blocks have a 'Copy code to clipboard' button. - Code blocks with 'vega-lite' as language are rendered as [vega-lite diagrams](https://vega.github.io/vega-lite/examples/). +- Markdown files can be imported into an existing note directly from the editor. ### Changed diff --git a/cypress/fixtures/import.md b/cypress/fixtures/import.md new file mode 100644 index 000000000..2f720bb41 --- /dev/null +++ b/cypress/fixtures/import.md @@ -0,0 +1,2 @@ +# Some short import test file +:) diff --git a/cypress/integration/import.spec.ts b/cypress/integration/import.spec.ts new file mode 100644 index 000000000..98328172c --- /dev/null +++ b/cypress/integration/import.spec.ts @@ -0,0 +1,42 @@ +describe('Import markdown file', () => { + 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('import on blank note', () => { + cy.get('button#editor-menu-import') + .click() + cy.get('.import-md-file') + .click() + cy.get('div[aria-labelledby="editor-menu-import"] > input[type=file]') + .attachFile('import.md') + cy.get('.CodeMirror-code > div:nth-of-type(1) > .CodeMirror-line > span > span') + .should('have.text', '# Some short import test file') + cy.get('.CodeMirror-code > div:nth-of-type(2) > .CodeMirror-line > span > span') + .should('have.text', ':)') + }) + + it('import on note with content', () => { + cy.get('.CodeMirror textarea') + .type('test\nabc', { force: true }) + cy.get('button#editor-menu-import') + .click() + cy.get('.import-md-file') + .click() + cy.get('div[aria-labelledby="editor-menu-import"] > input[type=file]') + .attachFile('import.md') + cy.get('.CodeMirror-code > div:nth-of-type(1) > .CodeMirror-line > span > span') + .should('have.text', 'test') + cy.get('.CodeMirror-code > div:nth-of-type(2) > .CodeMirror-line > span > span') + .should('have.text', 'abc') + cy.get('.CodeMirror-code > div:nth-of-type(3) > .CodeMirror-line > span > span') + .should('have.text', '# Some short import test file') + cy.get('.CodeMirror-code > div:nth-of-type(4) > .CodeMirror-line > span > span') + .should('have.text', ':)') + }) +}) diff --git a/cypress/support/index.ts b/cypress/support/index.ts index a6f148d4b..bdf6065ee 100644 --- a/cypress/support/index.ts +++ b/cypress/support/index.ts @@ -14,6 +14,7 @@ // *********************************************************** import 'cypress-commands' +import 'cypress-file-upload' import './checkLinks' import './config' import './login' diff --git a/package.json b/package.json index 13b839e1b..13de0eebd 100644 --- a/package.json +++ b/package.json @@ -159,6 +159,7 @@ "cross-env": "7.0.2", "cypress": "5.3.0", "cypress-commands": "1.1.0", + "cypress-file-upload": "^4.1.1", "eslint-plugin-chai-friendly": "0.6.0", "eslint-plugin-cypress": "2.11.2", "http-server": "0.12.3", diff --git a/public/locales/en.json b/public/locales/en.json index 30799384a..832e8b7d7 100644 --- a/public/locales/en.json +++ b/public/locales/en.json @@ -301,7 +301,8 @@ "pdf": "PDF export is unavailable." }, "import": { - "clipboard": "Clipboard" + "clipboard": "Clipboard", + "file": "Markdown file" }, "modal": { "snippetImport": { diff --git a/src/components/editor/document-bar/document-bar.tsx b/src/components/editor/document-bar/document-bar.tsx index 0f379572a..4ed941137 100644 --- a/src/components/editor/document-bar/document-bar.tsx +++ b/src/components/editor/document-bar/document-bar.tsx @@ -6,16 +6,15 @@ 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 } from './menus/import-menu' +import { ImportMenu, ImportProps } from './menus/import-menu' import { PermissionButton } from './permissions/permission-button' import { RevisionButton } from './revisions/revision-button' export interface DocumentBarProps { title: string - noteContent: string } -export const DocumentBar: React.FC = ({ title, noteContent }) => { +export const DocumentBar: React.FC = ({ title, noteContent, updateNoteContent }) => { useTranslation() return ( @@ -28,7 +27,7 @@ export const DocumentBar: React.FC = ({ title, noteContent })
- + diff --git a/src/components/editor/document-bar/import/import-file.tsx b/src/components/editor/document-bar/import/import-file.tsx new file mode 100644 index 000000000..b34926ecd --- /dev/null +++ b/src/components/editor/document-bar/import/import-file.tsx @@ -0,0 +1,45 @@ +import React, { Fragment, useCallback, useRef } from 'react' +import { Dropdown } from 'react-bootstrap' +import { Trans } from 'react-i18next' +import { ForkAwesomeIcon } from '../../../common/fork-awesome/fork-awesome-icon' +import { ImportProps } from '../menus/import-menu' + +export const ImportFile: React.FC = ({ noteContent, updateNoteContent }) => { + const fileInputReference = useRef(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 (noteContent.length === 0) { + updateNoteContent(newContent) + } else { + updateNoteContent(noteContent + '\n' + newContent) + } + }) + fileReader.addEventListener('loadend', () => { + fileInput.value = '' + }) + fileReader.readAsText(file) + }) + fileInput.click() + }, [fileInputReference, noteContent, updateNoteContent]) + + return ( + + + + + + + + ) +} diff --git a/src/components/editor/document-bar/menus/import-menu.tsx b/src/components/editor/document-bar/menus/import-menu.tsx index 1df500a2d..d43b972fe 100644 --- a/src/components/editor/document-bar/menus/import-menu.tsx +++ b/src/components/editor/document-bar/menus/import-menu.tsx @@ -2,8 +2,14 @@ import React from 'react' import { Dropdown } from 'react-bootstrap' import { Trans } from 'react-i18next' import { ForkAwesomeIcon } from '../../../common/fork-awesome/fork-awesome-icon' +import { ImportFile } from '../import/import-file' -export const ImportMenu: React.FC = () => { +export interface ImportProps { + noteContent: string + updateNoteContent: (content: string) => void +} + +export const ImportMenu: React.FC = ({ updateNoteContent, noteContent }) => { return ( @@ -27,6 +33,7 @@ export const ImportMenu: React.FC = () => { + ) diff --git a/src/components/editor/editor.tsx b/src/components/editor/editor.tsx index e856144aa..c7d9fd543 100644 --- a/src/components/editor/editor.tsx +++ b/src/components/editor/editor.tsx @@ -115,7 +115,7 @@ export const Editor: React.FC = () => {
- + setMarkdownContent(newContent)}/>