From f4161929b917e78bc9b21a232a0b5930d76531f1 Mon Sep 17 00:00:00 2001 From: David Brochart Date: Fri, 19 Apr 2024 11:05:48 +0200 Subject: [PATCH 1/2] Support suggestions --- .../src/collaboration.ts | 242 +++++++++++++++++- packages/collaboration-extension/src/index.ts | 6 +- packages/docprovider/src/requests.ts | 92 ++++++- packages/docprovider/src/ydrive.ts | 1 + packages/docprovider/src/yprovider.ts | 92 ++++++- .../jupyter_server_ydoc/app.py | 24 ++ .../jupyter_server_ydoc/handlers.py | 118 ++++++++- 7 files changed, 557 insertions(+), 18 deletions(-) diff --git a/packages/collaboration-extension/src/collaboration.ts b/packages/collaboration-extension/src/collaboration.ts index cb45671b..a79a15c8 100644 --- a/packages/collaboration-extension/src/collaboration.ts +++ b/packages/collaboration-extension/src/collaboration.ts @@ -5,28 +5,44 @@ * @module collaboration-extension */ +import { + DocumentRegistry +} from '@jupyterlab/docregistry'; + +import { + NotebookPanel, INotebookModel +} from '@jupyterlab/notebook'; + +import { + IDisposable, DisposableDelegate +} from '@lumino/disposable'; + +import { CommandRegistry } from '@lumino/commands'; + import { JupyterFrontEnd, JupyterFrontEndPlugin } from '@jupyterlab/application'; -import { IToolbarWidgetRegistry } from '@jupyterlab/apputils'; +import { Dialog, IToolbarWidgetRegistry } from '@jupyterlab/apputils'; import { EditorExtensionRegistry, IEditorExtensionRegistry } from '@jupyterlab/codemirror'; import { + requestDocDelete, + requestDocMerge, IGlobalAwareness, WebSocketAwarenessProvider } from '@jupyter/docprovider'; -import { SidePanel, usersIcon } from '@jupyterlab/ui-components'; +import { SidePanel, usersIcon, caretDownIcon } from '@jupyterlab/ui-components'; import { URLExt } from '@jupyterlab/coreutils'; import { ServerConnection } from '@jupyterlab/services'; import { IStateDB, StateDB } from '@jupyterlab/statedb'; -import { ITranslator, nullTranslator } from '@jupyterlab/translation'; +import { ITranslator, nullTranslator, TranslationBundle } from '@jupyterlab/translation'; import { Menu, MenuBar } from '@lumino/widgets'; -import { IAwareness } from '@jupyter/ydoc'; +import { IAwareness, ISharedNotebook, NotebookChange } from '@jupyter/ydoc'; import { CollaboratorsPanel, @@ -192,3 +208,221 @@ export const userEditorCursors: JupyterFrontEndPlugin = { }); } }; + +/** + * A plugin to add editing mode to the notebook page + */ +export const editingMode: JupyterFrontEndPlugin = { + id: '@jupyter/collaboration-extension:editingMode', + description: 'A plugin to add editing mode to the notebook page.', + autoStart: true, + optional: [ITranslator], + activate: ( + app: JupyterFrontEnd, + translator: ITranslator | null + ) => { + app.docRegistry.addWidgetExtension('Notebook', new EditingModeExtension(translator)); + }, +}; + +export class EditingModeExtension implements DocumentRegistry.IWidgetExtension { + private _trans: TranslationBundle; + + constructor(translator: ITranslator | null) { + this._trans = (translator ?? nullTranslator).load('jupyter_collaboration'); + } + + createNew( + panel: NotebookPanel, + context: DocumentRegistry.IContext + ): IDisposable { + const editingMenubar = new MenuBar(); + const suggestionMenubar = new MenuBar(); + const reviewMenubar = new MenuBar(); + + const editingCommands = new CommandRegistry(); + const suggestionCommands = new CommandRegistry(); + const reviewCommands = new CommandRegistry(); + + const editingMenu = new Menu({ commands: editingCommands }); + const suggestionMenu = new Menu({ commands: suggestionCommands }); + const reviewMenu = new Menu({ commands: reviewCommands }); + + const sharedModel = context.model.sharedModel; + const suggestions: {[key: string]: Menu.IItem} = {}; + var myForkId = ''; // curently allows only one suggestion per user + + editingMenu.title.label = 'Editing'; + editingMenu.title.icon = caretDownIcon; + + suggestionMenu.title.label = 'Root'; + suggestionMenu.title.icon = caretDownIcon; + + reviewMenu.title.label = 'Review'; + reviewMenu.title.icon = caretDownIcon; + + editingCommands.addCommand('editing', { + label: 'Editing', + execute: () => { + editingMenu.title.label = 'Editing'; + suggestionMenu.title.label = 'Root'; + open_dialog('Editing', this._trans); + } + }); + editingCommands.addCommand('suggesting', { + label: 'Suggesting', + execute: () => { + editingMenu.title.label = 'Suggesting'; + reviewMenu.clearItems(); + if (myForkId === '') { + myForkId = 'pending'; + sharedModel.provider.fork().then(newForkId => { + myForkId = newForkId; + sharedModel.provider.connect(newForkId); + suggestionMenu.title.label = newForkId; + }); + } + else { + suggestionMenu.title.label = myForkId; + sharedModel.provider.connect(myForkId); + } + open_dialog('Suggesting', this._trans); + } + }); + + suggestionCommands.addCommand('root', { + label: 'Root', + execute: () => { + // we cannot review the root document + reviewMenu.clearItems(); + suggestionMenu.title.label = 'Root'; + editingMenu.title.label = 'Editing'; + sharedModel.provider.connect(sharedModel.rootRoomId); + open_dialog('Editing', this._trans); + } + }); + + reviewCommands.addCommand('merge', { + label: 'Merge', + execute: () => { + requestDocMerge(sharedModel.currentRoomId, sharedModel.rootRoomId); + } + }); + reviewCommands.addCommand('discard', { + label: 'Discard', + execute: () => { + requestDocDelete(sharedModel.currentRoomId, sharedModel.rootRoomId); + } + }); + + editingMenu.addItem({type: 'command', command: 'editing'}); + editingMenu.addItem({type: 'command', command: 'suggesting'}); + + suggestionMenu.addItem({type: 'command', command: 'root'}); + + const _onStateChanged = (sender: ISharedNotebook, changes: NotebookChange) => { + if (changes.stateChange) { + changes.stateChange.forEach(value => { + const forkPrefix = 'fork_'; + if (value.name === 'merge' || value.name === 'delete') { + // we are on fork + if (sharedModel.currentRoomId === value.newValue) { + reviewMenu.clearItems(); + const merge = value.name === 'merge'; + sharedModel.provider.connect(sharedModel.rootRoomId, merge); + open_dialog('Editing', this._trans); + myForkId = ''; + } + } + else if (value.name.startsWith(forkPrefix)) { + // we are on root + const forkId = value.name.slice(forkPrefix.length); + if (value.newValue === 'new') { + suggestionCommands.addCommand(forkId, { + label: forkId, + execute: () => { + editingMenu.title.label = 'Suggesting'; + reviewMenu.clearItems(); + reviewMenu.addItem({type: 'command', command: 'merge'}); + reviewMenu.addItem({type: 'command', command: 'discard'}); + suggestionMenu.title.label = forkId; + sharedModel.provider.connect(forkId); + open_dialog('Suggesting', this._trans); + } + }); + const item = suggestionMenu.addItem({type: 'command', command: forkId}); + suggestions[forkId] = item; + if (myForkId !== forkId) { + if (myForkId !== 'pending') { + const dialog = new Dialog({ + title: this._trans.__('New suggestion'), + body: this._trans.__('View suggestion?'), + buttons: [ + Dialog.okButton({ label: 'View' }), + Dialog.cancelButton({ label: 'Discard' }), + ], + }); + dialog.launch().then(resp => { + dialog.close(); + if (resp.button.label === 'View') { + sharedModel.provider.connect(forkId); + suggestionMenu.title.label = forkId; + editingMenu.title.label = 'Suggesting'; + reviewMenu.clearItems(); + reviewMenu.addItem({type: 'command', command: 'merge'}); + reviewMenu.addItem({type: 'command', command: 'discard'}); + } + }); + } + else { + reviewMenu.clearItems(); + reviewMenu.addItem({type: 'command', command: 'merge'}); + reviewMenu.addItem({type: 'command', command: 'discard'}); + } + } + } + else if (value.newValue === undefined) { + editingMenu.title.label = 'Editing'; + suggestionMenu.title.label = 'Root'; + const item: Menu.IItem = suggestions[value.oldValue]; + delete suggestions[value.oldValue]; + suggestionMenu.removeItem(item); + } + } + }); + } + }; + + sharedModel.changed.connect(_onStateChanged, this); + + editingMenubar.addMenu(editingMenu); + suggestionMenubar.addMenu(suggestionMenu); + reviewMenubar.addMenu(reviewMenu); + + panel.toolbar.insertItem(997, 'editingMode', editingMenubar); + panel.toolbar.insertItem(998, 'suggestions', suggestionMenubar); + panel.toolbar.insertItem(999, 'review', reviewMenubar); + return new DisposableDelegate(() => { + editingMenubar.dispose(); + suggestionMenubar.dispose(); + reviewMenubar.dispose(); + }); + } +} + + +function open_dialog(title: string, trans: TranslationBundle) { + var body: string; + if (title === 'Editing') { + body = 'You are now directly editing the document.' + } + else { + body = 'Your edits now become suggestions to the document.' + } + const dialog = new Dialog({ + title: trans.__(title), + body: trans.__(body), + buttons: [Dialog.okButton({ label: 'OK' })], + }); + dialog.launch().then(resp => { dialog.close(); }); +} diff --git a/packages/collaboration-extension/src/index.ts b/packages/collaboration-extension/src/index.ts index 196b75c0..0a34643e 100644 --- a/packages/collaboration-extension/src/index.ts +++ b/packages/collaboration-extension/src/index.ts @@ -12,7 +12,8 @@ import { menuBarPlugin, rtcGlobalAwarenessPlugin, rtcPanelPlugin, - userEditorCursors + userEditorCursors, + editingMode } from './collaboration'; import { sharedLink } from './sharedlink'; @@ -25,7 +26,8 @@ const plugins: JupyterFrontEndPlugin[] = [ rtcGlobalAwarenessPlugin, rtcPanelPlugin, sharedLink, - userEditorCursors + userEditorCursors, + editingMode ]; export default plugins; diff --git a/packages/docprovider/src/requests.ts b/packages/docprovider/src/requests.ts index 51a6ece3..e55e5daf 100644 --- a/packages/docprovider/src/requests.ts +++ b/packages/docprovider/src/requests.ts @@ -13,6 +13,9 @@ import { ServerConnection, Contents } from '@jupyterlab/services'; const DOC_SESSION_URL = 'api/collaboration/session'; const DOC_FORK_URL = 'api/collaboration/undo_redo'; const TIMELINE_URL = 'api/collaboration/timeline'; +const DOC_FORK2_URL = 'api/collaboration/fork_room'; +const DOC_MERGE_URL = 'api/collaboration/merge_room'; +const DOC_DELETE_URL = 'api/collaboration/delete_room'; /** * Document session model @@ -107,14 +110,101 @@ export async function requestUndoRedo( ): Promise { const settings = ServerConnection.makeSettings(); let url = URLExt.join( + +export async function requestDocFork( + roomid: string, +): Promise { + const settings = ServerConnection.makeSettings(); + const url = URLExt.join( settings.baseUrl, - DOC_FORK_URL, + DOC_FORK2_URL, encodeURIComponent(roomid) ); url = url.concat(`?action=${action}&&steps=${steps}&&forkRoom=${forkRoom}`); const body = { method: 'PUT' }; +======= + const body = {method: 'PUT'}; + + let response: Response; + try { + response = await ServerConnection.makeRequest(url, body, settings); + } catch (error) { + throw new ServerConnection.NetworkError(error as Error); + } + + let data: any = await response.text(); + + if (data.length > 0) { + try { + data = JSON.parse(data); + } catch (error) { + console.log('Not a JSON response body.', response); + } + } + + if (!response.ok) { + throw new ServerConnection.ResponseError(response, data.message || data); + } + + return data; +} + + +export async function requestDocMerge( + forkRoomid: string, + rootRoomid: string +): Promise { + const settings = ServerConnection.makeSettings(); + const url = URLExt.join( + settings.baseUrl, + DOC_MERGE_URL + ); + const body = { + method: 'PUT', + body: JSON.stringify({ fork_roomid: forkRoomid, root_roomid: rootRoomid }) + }; + + let response: Response; + try { + response = await ServerConnection.makeRequest(url, body, settings); + } catch (error) { + throw new ServerConnection.NetworkError(error as Error); + } + + let data: any = await response.text(); + + if (data.length > 0) { + try { + data = JSON.parse(data); + } catch (error) { + console.log('Not a JSON response body.', response); + } + } + + if (!response.ok) { + throw new ServerConnection.ResponseError(response, data.message || data); + } + + return data; +} + + +export async function requestDocDelete( + forkRoomid: string, + rootRoomid: string, +): Promise { + const settings = ServerConnection.makeSettings(); + const url = URLExt.join( + settings.baseUrl, + DOC_DELETE_URL, + ); + const body = { + method: 'DELETE', + body: JSON.stringify({ fork_roomid: forkRoomid, root_roomid: rootRoomid }) + }; +>>>>>>> f64d251 (Support suggestions) let response: Response; try { diff --git a/packages/docprovider/src/ydrive.ts b/packages/docprovider/src/ydrive.ts index a501d9fa..f3042f49 100644 --- a/packages/docprovider/src/ydrive.ts +++ b/packages/docprovider/src/ydrive.ts @@ -215,6 +215,7 @@ export class YDrive extends Drive implements ICollaborativeDrive { }); }); + sharedModel.provider = provider; sharedModel.disposed.connect(() => { const provider = this._providers.get(key); if (provider) { diff --git a/packages/docprovider/src/yprovider.ts b/packages/docprovider/src/yprovider.ts index 341187dc..c091dd88 100644 --- a/packages/docprovider/src/yprovider.ts +++ b/packages/docprovider/src/yprovider.ts @@ -8,15 +8,14 @@ import { User } from '@jupyterlab/services'; import { TranslationBundle } from '@jupyterlab/translation'; import { PromiseDelegate } from '@lumino/coreutils'; -import { IDisposable } from '@lumino/disposable'; import { Signal } from '@lumino/signaling'; -import { DocumentChange, YDocument } from '@jupyter/ydoc'; +import { DocumentChange, IDocumentProvider, YDocument } from '@jupyter/ydoc'; import { Awareness } from 'y-protocols/awareness'; import { WebsocketProvider as YWebsocketProvider } from 'y-websocket'; -import { requestDocSession } from './requests'; +import { requestDocFork, requestDocSession } from './requests'; import { IForkProvider } from './ydrive'; /** @@ -44,6 +43,7 @@ export class WebSocketProvider implements IDocumentProvider, IForkProvider { */ constructor(options: WebSocketProvider.IOptions) { this._isDisposed = false; + this._sessionId = options.sessionId ?? ''; this._path = options.path; this._contentType = options.contentType; this._format = options.format; @@ -52,15 +52,14 @@ export class WebSocketProvider implements IDocumentProvider, IForkProvider { this._awareness = options.model.awareness; this._yWebsocketProvider = null; this._trans = options.translator; + this._user = options.user; - const user = options.user; - - user.ready + this._user.ready .then(() => { - this._onUserChanged(user); + this._onUserChanged(this._user); }) .catch(e => console.error(e)); - user.userChanged.connect(this._onUserChanged, this); + this._user.userChanged.connect(this._onUserChanged, this); this._connect().catch(e => console.warn(e)); } @@ -112,13 +111,79 @@ export class WebSocketProvider implements IDocumentProvider, IForkProvider { this._path ); + const response = await requestDocFork(`${session.format}:${session.type}:${session.fileId}`); + const forkId = response.roomId; + this._sharedModel.currentRoomId = forkId; + this._sharedModel.addFork(forkId); + + return forkId; + } + + async fork(): Promise { + const session = await requestDocSession( + this._format, + this._contentType, + this._path + ); + + const response = await requestDocFork(`${session.format}:${session.type}:${session.fileId}`); + const forkId = response.roomId; + this._sharedModel.currentRoomId = forkId; + this._sharedModel.addFork(forkId); + + return forkId; + } + + connect(roomId: string, merge?: boolean) { + this._sharedModel.currentRoomId = roomId; + this._yWebsocketProvider?.disconnect(); + if (roomId === this._sharedModel.rootRoomId) { + // connecting to the root + // don't bring our changes there if not merging + if (merge !== true) { + while (this._sharedModel.undoManager.canUndo()) { + this._sharedModel.undoManager.undo(); + } + } + this._sharedModel.undoManager.clear(); + } + else { + // connecting to a fork + // keep track of changes so that we can undo them when connecting back to root + this._sharedModel.undoManager.clear(); + } + this._yWebsocketProvider = new YWebsocketProvider( this._serverUrl, - `${session.format}:${session.type}:${session.fileId}`, + roomId, this._sharedModel.ydoc, { disableBc: true, - params: { sessionId: session.sessionId }, + params: { sessionId: this._sessionId }, + awareness: this._awareness + } + ); + } + + private async _connect(): Promise { + if (this._sharedModel.rootRoomId === '') { + const session = await requestDocSession( + this._format, + this._contentType, + this._path + ); + this._sharedModel.rootRoomId = `${session.format}:${session.type}:${session.fileId}`; + this._sharedModel.currentRoomId = this._sharedModel.rootRoomId; + this._sessionId = session.sessionId; + } + + this._yWebsocketProvider = new YWebsocketProvider( + this._serverUrl, + this._sharedModel.rootRoomId, + this._sharedModel.ydoc, + { + disableBc: true, + params: { sessionId: this._sessionId }, awareness: this._awareness } ); @@ -185,12 +250,14 @@ export class WebSocketProvider implements IDocumentProvider, IForkProvider { private _contentType: string; private _format: string; private _isDisposed: boolean; + private _sessionId: string; private _path: string; private _ready = new PromiseDelegate(); private _serverUrl: string; private _sharedModel: YDocument; private _yWebsocketProvider: YWebsocketProvider | null; private _trans: TranslationBundle; + private _user: User.IManager; } /** @@ -235,5 +302,10 @@ export namespace WebSocketProvider { * The jupyterlab translator */ translator: TranslationBundle; + + /** + * The document session ID, if the document is a fork + */ + sessionId?: string; } } diff --git a/projects/jupyter-server-ydoc/jupyter_server_ydoc/app.py b/projects/jupyter-server-ydoc/jupyter_server_ydoc/app.py index e286cc13..da56149b 100644 --- a/projects/jupyter-server-ydoc/jupyter_server_ydoc/app.py +++ b/projects/jupyter-server-ydoc/jupyter_server_ydoc/app.py @@ -14,6 +14,9 @@ from traitlets import Bool, Float, Type from .handlers import ( + DocForkHandler, + DocDeleteHandler, + DocMergeHandler, DocSessionHandler, TimelineHandler, UndoRedoHandler, @@ -150,6 +153,27 @@ def initialize_handlers(self): "ywebsocket_server": self.ywebsocket_server, }, ), + ( + r"/api/collaboration/fork_room/(.*)", + DocForkHandler, + { + "ywebsocket_server": self.ywebsocket_server, + } + ), + ( + r"/api/collaboration/merge_room", + DocMergeHandler, + { + "ywebsocket_server": self.ywebsocket_server, + } + ), + ( + r"/api/collaboration/delete_room", + DocDeleteHandler, + { + "ywebsocket_server": self.ywebsocket_server, + } + ), ] ) diff --git a/projects/jupyter-server-ydoc/jupyter_server_ydoc/handlers.py b/projects/jupyter-server-ydoc/jupyter_server_ydoc/handlers.py index f04004fe..cba0efc0 100644 --- a/projects/jupyter-server-ydoc/jupyter_server_ydoc/handlers.py +++ b/projects/jupyter-server-ydoc/jupyter_server_ydoc/handlers.py @@ -15,7 +15,7 @@ from jupyter_server.base.handlers import APIHandler, JupyterHandler from jupyter_server.utils import ensure_async from jupyter_ydoc import ydocs as YDOCS -from pycrdt import Doc, UndoManager, YMessageType, write_var_uint +from pycrdt import Doc, Map, UndoManager, YMessageType, write_var_uint from pycrdt_websocket.websocket_server import YRoom from pycrdt_websocket.ystore import BaseYStore from tornado import web @@ -596,3 +596,119 @@ async def _cleanup_undo_manager(self, room_id: str) -> None: if room_id in FORK_DOCUMENTS: del FORK_DOCUMENTS[room_id] self.log.info(f"Fork Document for {room_id} has been removed.") + + +class DocForkHandler(APIHandler): + """ + Jupyter Server's handler to fork a document. + """ + + auth_resource = "contents" + + def initialize( + self, + ywebsocket_server: JupyterWebsocketServer, + ) -> None: + self._websocket_server = ywebsocket_server + + @web.authenticated + @authorized + async def put(self, room_id): + """ + Creates a fork of a root document and returns its ID. + """ + idx = uuid4().hex + + root_room = await self._websocket_server.get_room(room_id) + update = root_room.ydoc.get_update() + fork_ydoc = Doc() + fork_ydoc.apply_update(update) + fork_room = YRoom(ydoc=fork_ydoc) + self._websocket_server.add_room(idx, fork_room) + root_room.fork_ydocs.add(fork_ydoc) + data = json.dumps({ + "sessionId": SERVER_SESSION, + "roomId": idx, + }) + self.set_status(201) + return self.finish(data) + + +class DocMergeHandler(APIHandler): + """ + Jupyter Server's handler to merge a document. + """ + + auth_resource = "contents" + + def initialize( + self, + ywebsocket_server: JupyterWebsocketServer, + ) -> None: + self._websocket_server = ywebsocket_server + + @web.authenticated + @authorized + async def put(self): + """ + Merges back a fork into a root document. + """ + model = self.get_json_body() + fork_roomid = model["fork_roomid"] + root_room = await self._websocket_server.get_room(model["root_roomid"]) + root_ydoc = root_room.ydoc + idx = f"fork_{fork_roomid}" + root_state = root_ydoc.get("state", type=Map) + if idx in root_state: + del root_state[idx] + else: + self.set_status(404) + raise RuntimeError(f"Could not find root document fork with ID: {fork_roomid}") + fork_room = await self._websocket_server.get_room(fork_roomid) + fork_ydoc = fork_room.ydoc + fork_update = fork_ydoc.get_update() + root_ydoc.apply_update(fork_update) + root_room.fork_ydocs.remove(fork_ydoc) + fork_state = fork_ydoc.get("state", type=Map) + fork_state["merge"] = fork_roomid + #self._websocket_server.delete_room(name=fork_roomid) + self.set_status(200) + + +class DocDeleteHandler(APIHandler): + """ + Jupyter Server's handler to delete a document. + """ + + auth_resource = "contents" + + def initialize( + self, + ywebsocket_server: JupyterWebsocketServer, + ) -> None: + self._websocket_server = ywebsocket_server + + @web.authenticated + @authorized + async def delete(self): + """ + Deletes a forked document. + """ + model = self.get_json_body() + fork_roomid = model["fork_roomid"] + root_room = await self._websocket_server.get_room(model["root_roomid"]) + root_ydoc = root_room.ydoc + idx = f"fork_{fork_roomid}" + root_state = root_ydoc.get("state", type=Map) + if idx in root_state: + del root_state[idx] + else: + self.set_status(404) + raise RuntimeError(f"Could not find root document fork with ID: {fork_roomid}") + fork_room = await self._websocket_server.get_room(fork_roomid) + fork_ydoc = fork_room.ydoc + root_room.fork_ydocs.remove(fork_ydoc) + fork_state = fork_ydoc.get("state", type=Map) + fork_state["delete"] = fork_roomid + #self._websocket_server.delete_room(name=fork_roomid) + self.set_status(200) From df67eeb1521d9cb27682fd68f5921a928de8c6d7 Mon Sep 17 00:00:00 2001 From: David Brochart Date: Fri, 30 Aug 2024 09:52:34 +0200 Subject: [PATCH 2/2] - --- package.json | 3 + packages/docprovider/src/requests.ts | 37 +++++++- packages/docprovider/src/yprovider.ts | 25 ----- .../jupyter_server_ydoc/handlers.py | 4 +- yarn.lock | 94 +++++++++++++++++-- 5 files changed, 123 insertions(+), 40 deletions(-) diff --git a/package.json b/package.json index b78c25d8..c3896567 100644 --- a/package.json +++ b/package.json @@ -65,5 +65,8 @@ "stylelint-prettier": "^3.0.0", "typedoc": "~0.23.28", "typescript": "~5.0.4" + }, + "resolutions": { + "@jupyter/ydoc": "file:.yalc/@jupyter/ydoc" } } diff --git a/packages/docprovider/src/requests.ts b/packages/docprovider/src/requests.ts index e55e5daf..37240993 100644 --- a/packages/docprovider/src/requests.ts +++ b/packages/docprovider/src/requests.ts @@ -110,6 +110,38 @@ export async function requestUndoRedo( ): Promise { const settings = ServerConnection.makeSettings(); let url = URLExt.join( + settings.baseUrl, + DOC_FORK_URL, + encodeURIComponent(roomid) + ); + + url = url.concat(`?action=${action}&&steps=${steps}&&forkRoom=${forkRoom}`); + + const body = { method: 'PUT' }; + + let response: Response; + try { + response = await ServerConnection.makeRequest(url, body, settings); + } catch (error) { + throw new ServerConnection.NetworkError(error as Error); + } + + let data: any = await response.text(); + + if (data.length > 0) { + try { + data = JSON.parse(data); + } catch (error) { + console.log('Not a JSON response body.', response); + } + } + + if (!response.ok) { + throw new ServerConnection.ResponseError(response, data.message || data); + } + + return data; +} export async function requestDocFork( roomid: string, @@ -121,10 +153,6 @@ export async function requestDocFork( encodeURIComponent(roomid) ); - url = url.concat(`?action=${action}&&steps=${steps}&&forkRoom=${forkRoom}`); - - const body = { method: 'PUT' }; -======= const body = {method: 'PUT'}; let response: Response; @@ -204,7 +232,6 @@ export async function requestDocDelete( method: 'DELETE', body: JSON.stringify({ fork_roomid: forkRoomid, root_roomid: rootRoomid }) }; ->>>>>>> f64d251 (Support suggestions) let response: Response; try { diff --git a/packages/docprovider/src/yprovider.ts b/packages/docprovider/src/yprovider.ts index c091dd88..6634177f 100644 --- a/packages/docprovider/src/yprovider.ts +++ b/packages/docprovider/src/yprovider.ts @@ -18,16 +18,6 @@ import { WebsocketProvider as YWebsocketProvider } from 'y-websocket'; import { requestDocFork, requestDocSession } from './requests'; import { IForkProvider } from './ydrive'; -/** - * An interface for a document provider. - */ -export interface IDocumentProvider extends IDisposable { - /** - * Returns a Promise that resolves when the document provider is ready. - */ - readonly ready: Promise; -} - /** * A class to provide Yjs synchronization over WebSocket. * @@ -104,21 +94,6 @@ export class WebSocketProvider implements IDocumentProvider, IForkProvider { this._connect(); } - private async _connect(): Promise { - const session = await requestDocSession( - this._format, - this._contentType, - this._path - ); - - const response = await requestDocFork(`${session.format}:${session.type}:${session.fileId}`); - const forkId = response.roomId; - this._sharedModel.currentRoomId = forkId; - this._sharedModel.addFork(forkId); - - return forkId; - } - async fork(): Promise { const session = await requestDocSession( this._format, diff --git a/projects/jupyter-server-ydoc/jupyter_server_ydoc/handlers.py b/projects/jupyter-server-ydoc/jupyter_server_ydoc/handlers.py index cba0efc0..f92ffb44 100644 --- a/projects/jupyter-server-ydoc/jupyter_server_ydoc/handlers.py +++ b/projects/jupyter-server-ydoc/jupyter_server_ydoc/handlers.py @@ -6,7 +6,6 @@ import asyncio import json import time -import uuid from logging import Logger from typing import Any from uuid import uuid4 @@ -36,8 +35,7 @@ YFILE = YDOCS["file"] - -SERVER_SESSION = str(uuid.uuid4()) +SERVER_SESSION = str(uuid4()) FORK_DOCUMENTS = {} diff --git a/yarn.lock b/yarn.lock index fcbee350..3cc1eded 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2224,13 +2224,6 @@ __metadata: "@jupyter/ydoc@npm:^2.0.0 || ^3.0.0-a3": version: 3.0.0-a4 resolution: "@jupyter/ydoc@npm:3.0.0-a4" - dependencies: - "@jupyterlab/nbformat": ^3.0.0 || ^4.0.0-alpha.21 || ^4.0.0 - "@lumino/coreutils": ^1.11.0 || ^2.0.0 - "@lumino/disposable": ^1.10.0 || ^2.0.0 - "@lumino/signaling": ^1.10.0 || ^2.0.0 - y-protocols: ^1.0.5 - yjs: ^13.5.40 checksum: ccd4d8b3c46346e14e4e20f093c0147349c403f1a61d624bc01a95fb805f41c8c5c4db54c5c43c59dc2ef7aeebb53a451a7fc75875c144578bf180c0d20c1878 languageName: node linkType: hard @@ -2246,6 +2239,37 @@ __metadata: y-protocols: ^1.0.5 yjs: ^13.5.40 checksum: f10268d4d990f454279e3908a172755ed5885fa81bb70c31bdf66923598b283d26491741bece137d1c348619861e9b7f8354296773fe5352b1915e69101a9fb0 +======= + checksum: 18ec446906af603ec9a9b78d891e6b2967f5cb934ffc724e0ad0f1640af82d7f06f64123ecf53fffb202b9cc2f1f10e909b75df06e6df313a84865635112df07 + languageName: node + linkType: hard + +"@jupyterlab/application@npm:^4.0.0": + version: 4.2.5 + resolution: "@jupyterlab/application@npm:4.2.5" + dependencies: + "@fortawesome/fontawesome-free": ^5.12.0 + "@jupyterlab/apputils": ^4.3.5 + "@jupyterlab/coreutils": ^6.2.5 + "@jupyterlab/docregistry": ^4.2.5 + "@jupyterlab/rendermime": ^4.2.5 + "@jupyterlab/rendermime-interfaces": ^3.10.5 + "@jupyterlab/services": ^7.2.5 + "@jupyterlab/statedb": ^4.2.5 + "@jupyterlab/translation": ^4.2.5 + "@jupyterlab/ui-components": ^4.2.5 + "@lumino/algorithm": ^2.0.1 + "@lumino/application": ^2.3.1 + "@lumino/commands": ^2.3.0 + "@lumino/coreutils": ^2.1.2 + "@lumino/disposable": ^2.1.2 + "@lumino/messaging": ^2.0.1 + "@lumino/polling": ^2.1.2 + "@lumino/properties": ^2.0.1 + "@lumino/signaling": ^2.1.2 + "@lumino/widgets": ^2.3.2 + checksum: c424ea191ef4da45eeae44e366e2b3cb23426cc72c0321226c83000c02b91fa7c4bc54978aa0b0e9416211cce9c17469204fc2b133cb2bec3d8896a0b2f75ce1 +>>>>>>> 4b5fdf7 (-) languageName: node linkType: hard @@ -2652,6 +2676,15 @@ __metadata: languageName: node linkType: hard +"@jupyterlab/nbformat@npm:^4.2.5": + version: 4.2.5 + resolution: "@jupyterlab/nbformat@npm:4.2.5" + dependencies: + "@lumino/coreutils": ^2.1.2 + checksum: b3ad2026969bfa59f8cfb7b1a991419f96f7e6dc8c4acf4ac166c210d7ab99631350c785e9b04350095488965d2824492c8adbff24a2e26db615457545426b3c + languageName: node + linkType: hard + "@jupyterlab/notebook@npm:^4.2.0": version: 4.2.4 resolution: "@jupyterlab/notebook@npm:4.2.4" @@ -2822,6 +2855,22 @@ __metadata: languageName: node linkType: hard +"@jupyterlab/statusbar@npm:^4.2.5": + version: 4.2.5 + resolution: "@jupyterlab/statusbar@npm:4.2.5" + dependencies: + "@jupyterlab/ui-components": ^4.2.5 + "@lumino/algorithm": ^2.0.1 + "@lumino/coreutils": ^2.1.2 + "@lumino/disposable": ^2.1.2 + "@lumino/messaging": ^2.0.1 + "@lumino/signaling": ^2.1.2 + "@lumino/widgets": ^2.3.2 + react: ^18.2.0 + checksum: fa429b88a5bcd6889b9ac32b5f2500cb10a968cc636ca8dede17972535cc47454cb7fc96518fc8def76935f826b66b071752d0fd26afdacba579f6f3785e97b2 + languageName: node + linkType: hard + "@jupyterlab/testing@npm:^4.0.0": version: 4.2.4 resolution: "@jupyterlab/testing@npm:4.2.4" @@ -2911,6 +2960,37 @@ __metadata: languageName: node linkType: hard +"@jupyterlab/ui-components@npm:^4.2.5": + version: 4.2.5 + resolution: "@jupyterlab/ui-components@npm:4.2.5" + dependencies: + "@jupyter/react-components": ^0.15.3 + "@jupyter/web-components": ^0.15.3 + "@jupyterlab/coreutils": ^6.2.5 + "@jupyterlab/observables": ^5.2.5 + "@jupyterlab/rendermime-interfaces": ^3.10.5 + "@jupyterlab/translation": ^4.2.5 + "@lumino/algorithm": ^2.0.1 + "@lumino/commands": ^2.3.0 + "@lumino/coreutils": ^2.1.2 + "@lumino/disposable": ^2.1.2 + "@lumino/messaging": ^2.0.1 + "@lumino/polling": ^2.1.2 + "@lumino/properties": ^2.0.1 + "@lumino/signaling": ^2.1.2 + "@lumino/virtualdom": ^2.0.1 + "@lumino/widgets": ^2.3.2 + "@rjsf/core": ^5.13.4 + "@rjsf/utils": ^5.13.4 + react: ^18.2.0 + react-dom: ^18.2.0 + typestyle: ^2.0.4 + peerDependencies: + react: ^18.2.0 + checksum: 9d2b887910a3b0d41645388c5ac6183d6fd2f3af4567de9b077b2492b1a9380f98c4598a4ae6d1c3186624ed4f956bedf8ba37adb5f772c96555761384a93e1e + languageName: node + linkType: hard + "@lerna/child-process@npm:6.6.2": version: 6.6.2 resolution: "@lerna/child-process@npm:6.6.2"