From 285e135a2bac13fbc063e5c566ae588034163643 Mon Sep 17 00:00:00 2001 From: Raathigesh Date: Mon, 23 Aug 2021 21:17:12 +1000 Subject: [PATCH] Multi docs --- src/extension/api/DocsManager.ts | 109 +++++++++++++++++++++++++ src/extension/api/index.ts | 37 +++++++-- src/ui/App.tsx | 59 ++++++++++++-- src/ui/CreateDoc.tsx | 136 +++++++++++++++++++++++++++++++ src/ui/Editor.tsx | 39 ++++----- src/ui/types.ts | 6 ++ 6 files changed, 354 insertions(+), 32 deletions(-) create mode 100644 src/extension/api/DocsManager.ts create mode 100644 src/ui/CreateDoc.tsx create mode 100644 src/ui/types.ts diff --git a/src/extension/api/DocsManager.ts b/src/extension/api/DocsManager.ts new file mode 100644 index 0000000..92e0b14 --- /dev/null +++ b/src/extension/api/DocsManager.ts @@ -0,0 +1,109 @@ +import * as vscode from 'vscode'; + +const StateKey = 'paper-documents'; + +interface Document { + content: string; + id: string; + type: 'doc' | 'mindmap'; + name: string; +} + +interface State { + activeDocument: null | string; + documents: Document[]; +} + +export class DocsManager { + context: vscode.ExtensionContext; + + constructor(context: vscode.ExtensionContext) { + this.context = context; + } + + getActiveDocument(): Document { + if (this.context.workspaceState.get(StateKey) === undefined) { + const newDocId = new Date().valueOf().toString(); + this.context.workspaceState.update(StateKey, { + activeDocument: newDocId, + documents: [ + { + content: + this.context.workspaceState.get('paper-content') || + '', + id: newDocId, + type: 'doc', + name: 'Untitled', + }, + ], + } as State); + } + + const state: State = this.context.workspaceState.get(StateKey) as any; + const activeDocument = state.documents.find( + item => item.id === state.activeDocument + ); + if (!activeDocument) { + throw new Error('Active document not found.'); + } + return activeDocument; + } + + setActiveDocument(id: string) { + const state: State = this.context.workspaceState.get(StateKey) as any; + this.context.workspaceState.update(StateKey, { + ...state, + activeDocument: id, + } as State); + } + + updateDocument(id: string, content: string) { + const state: State = this.context.workspaceState.get(StateKey) as any; + const updatedDocuments = state.documents.map(item => { + if (item.id === id) { + return { + ...item, + content, + }; + } + return item; + }); + + const nextState: State = { + ...state, + documents: updatedDocuments, + }; + this.context.workspaceState.update(StateKey, nextState); + } + + createDocument(name: string, content: string, type: 'doc' | 'mindmap') { + const state: State = this.context.workspaceState.get(StateKey) as any; + const nextState: State = { + ...state, + documents: [ + ...state.documents, + { + id: new Date().valueOf().toString(), + content, + type, + name, + }, + ], + }; + this.context.workspaceState.update(StateKey, nextState); + } + + deleteDocument(id: string) { + const state: State = this.context.workspaceState.get(StateKey) as any; + const nextState: State = { + ...state, + documents: state.documents.filter(item => item.id !== id), + }; + this.context.workspaceState.update(StateKey, nextState); + } + + getDocumentsList(): Document[] { + const state: State = this.context.workspaceState.get(StateKey) as any; + return state.documents; + } +} diff --git a/src/extension/api/index.ts b/src/extension/api/index.ts index ac254ef..67f285c 100644 --- a/src/extension/api/index.ts +++ b/src/extension/api/index.ts @@ -6,11 +6,14 @@ import * as dirTree from 'directory-tree'; import { initializeStaticRoutes } from './static-files'; import { isAbsolute, join } from 'path'; +import { DocsManager } from './DocsManager'; export async function startApiServer( port: number, context: vscode.ExtensionContext ) { + const docsManager = new DocsManager(context); + const app = express(); app.use(bodyParser()); app.use(cors()); @@ -35,22 +38,46 @@ export async function startApiServer( ) ); } - vscode.window.showTextDocument(vscode.Uri.file(fullPath), { selection, }); - res.send('OK'); }); + app.post('/create', (req, res) => { + const name = req.body.name; + const type = req.body.type; + + docsManager.createDocument(name, '', type); + const documents = docsManager.getDocumentsList(); + res.json(documents); + }); + app.get('/content', (req, res) => { - const content = context.workspaceState.get('paper-content'); - res.send(content); + const document = docsManager.getActiveDocument(); + res.json(document); }); app.post('/content', (req, res) => { const content = req.body.content; - context.workspaceState.update('paper-content', content); + const id = req.body.id; + + if (!id) { + res.sendStatus(500); + } else { + docsManager.updateDocument(id, content); + res.send('OK'); + } + }); + + app.get('/documents', (req, res) => { + const documents = docsManager.getDocumentsList(); + res.json(documents); + }); + + app.post('/changeActiveDocument', (req, res) => { + const id = req.body.id; + docsManager.setActiveDocument(id); res.send('OK'); }); diff --git a/src/ui/App.tsx b/src/ui/App.tsx index c1d23bf..705b24a 100644 --- a/src/ui/App.tsx +++ b/src/ui/App.tsx @@ -4,10 +4,12 @@ import { GlobalStyle, theme as defaultTheme, } from '@chakra-ui/react'; -import React, { Fragment, useContext, useEffect, useState } from 'react'; +import React, { useCallback, useEffect, useState } from 'react'; import { ThemeProvider } from '@devtools-ds/themes'; -import copy from 'copy-to-clipboard'; +import { Grid } from 'react-feather'; import Editor from './Editor'; +import { ClientDoc } from './types'; +import CreateDoc from './CreateDoc'; const theme = { ...defaultTheme, @@ -39,13 +41,60 @@ const theme = { const API_URL = `http://localhost:${(window as any).port || '4545'}`; function App() { + const [activeDoc, setActiveDoc] = useState(null); + + const updateContent = useCallback( + async (content: string) => { + if (activeDoc === null) { + return; + } + + fetch(`${API_URL}/content`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + content, + id: activeDoc.id, + }), + }); + }, + [activeDoc] + ); + + async function getContent() { + const document: ClientDoc = await ( + await fetch(`${API_URL}/content`) + ).json(); + setActiveDoc(document); + } + + useEffect(() => { + getContent(); + }, [setActiveDoc]); + return ( -
- -
+ + + + { + getContent(); + }} + /> + + + {activeDoc && ( + + )} +
); diff --git a/src/ui/CreateDoc.tsx b/src/ui/CreateDoc.tsx new file mode 100644 index 0000000..a482cdf --- /dev/null +++ b/src/ui/CreateDoc.tsx @@ -0,0 +1,136 @@ +import { + Button, + Flex, + Input, + Popover, + PopoverArrow, + PopoverBody, + PopoverCloseButton, + PopoverContent, + PopoverFooter, + PopoverHeader, + PopoverTrigger, + Portal, +} from '@chakra-ui/react'; +import React, { useEffect, useState } from 'react'; +import { PlusSquare, ArrowRight } from 'react-feather'; +import { ClientDoc } from './types'; + +const API_URL = `http://localhost:${(window as any).port || '4545'}`; + +interface Props { + onActiveDocumentChange: () => void; +} + +export default function CreateDoc({ onActiveDocumentChange }: Props) { + const [docs, setDocs] = useState([]); + const [docName, setDocName] = useState(''); + + const getDocs = async () => { + const response = await fetch(`${API_URL}/documents`); + const documents = await response.json(); + setDocs(documents); + }; + + const changeActiveDocument = async (id: string) => { + await fetch(`${API_URL}/changeActiveDocument`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + id, + }), + }); + + onActiveDocumentChange(); + }; + + useEffect(() => { + getDocs(); + }, []); + + const createDoc = async (name: string) => { + const response = await fetch(`${API_URL}/create`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + name, + type: 'doc', + }), + }); + const documents = await response.json(); + setDocs(documents); + }; + + return ( + + + + + + + + Docs + + + {docs.map(item => ( + { + changeActiveDocument(item.id); + }} + > + {item.name || item.id} + + ))} + + + + setDocName(e.target.value)} + /> + + + + + + + ); +} diff --git a/src/ui/Editor.tsx b/src/ui/Editor.tsx index 972b3c2..a2997dc 100644 --- a/src/ui/Editor.tsx +++ b/src/ui/Editor.tsx @@ -1,4 +1,4 @@ -import React, { useEffect, useState } from 'react'; +import React, { useCallback, useEffect, useState } from 'react'; import { useEditor, EditorContent, FloatingMenu } from '@tiptap/react'; import StarterKit from '@tiptap/starter-kit'; import symbolBookmark from './editor-views/symbol-bookmark'; @@ -11,19 +11,12 @@ import { File, List, MousePointer, Map } from 'react-feather'; const API_URL = `http://localhost:${(window as any).port || '4545'}`; -const Editor = () => { - const updateContent = async (content: string) => { - fetch(`${API_URL}/content`, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ - content, - }), - }); - }; +interface Props { + content: string; + onChange: (content: string) => void; +} +const Editor = ({ content, onChange }: Props) => { const editor = useEditor({ extensions: [ StarterKit, @@ -33,19 +26,21 @@ const Editor = () => { mindmap, ], content: '', - onUpdate: ({ editor }) => { - updateContent(editor.getHTML()); - }, }); useEffect(() => { - async function getContent() { - const content = await (await fetch(`${API_URL}/content`)).text(); - editor?.commands.setContent(content); - } + const handler = ({ editor }: any) => { + onChange(editor.getHTML()); + }; + editor?.on('update', handler); + return () => { + editor?.off('update', handler); + }; + }, [editor, onChange]); - getContent(); - }, [editor]); + useEffect(() => { + editor?.commands.setContent(content); + }, [content, editor]); return ( <> diff --git a/src/ui/types.ts b/src/ui/types.ts new file mode 100644 index 0000000..ed79cf4 --- /dev/null +++ b/src/ui/types.ts @@ -0,0 +1,6 @@ +export interface ClientDoc { + content: string; + id: string; + type: 'doc' | 'mindmap'; + name: string; +}