diff --git a/jupyter_collaboration/app.py b/jupyter_collaboration/app.py index cb19a176..e36f513b 100644 --- a/jupyter_collaboration/app.py +++ b/jupyter_collaboration/app.py @@ -8,7 +8,7 @@ from pycrdt_websocket.ystore import BaseYStore from traitlets import Bool, Float, Type -from .handlers import DocForkHandler, DocMergeHandler, DocSessionHandler, YDocWebSocketHandler +from .handlers import DocForkHandler, DocDeleteHandler, DocMergeHandler, DocSessionHandler, YDocWebSocketHandler from .loaders import FileLoaderMapping from .stores import SQLiteYStore from .utils import AWARENESS_EVENTS_SCHEMA_PATH, EVENTS_SCHEMA_PATH @@ -123,6 +123,13 @@ def initialize_handlers(self): "ywebsocket_server": self.ywebsocket_server, } ), + ( + r"/api/collaboration/delete_room", + DocDeleteHandler, + { + "ywebsocket_server": self.ywebsocket_server, + } + ), ] ) diff --git a/jupyter_collaboration/handlers.py b/jupyter_collaboration/handlers.py index 3c0a205e..af12972a 100644 --- a/jupyter_collaboration/handlers.py +++ b/jupyter_collaboration/handlers.py @@ -471,13 +471,53 @@ async def put(self): 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 - update = fork_ydoc.get_update() - root_ydoc.apply_update(update) + 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) diff --git a/packages/collaboration-extension/src/collaboration.ts b/packages/collaboration-extension/src/collaboration.ts index 006ff5cb..b259f476 100644 --- a/packages/collaboration-extension/src/collaboration.ts +++ b/packages/collaboration-extension/src/collaboration.ts @@ -28,7 +28,7 @@ import { EditorExtensionRegistry, IEditorExtensionRegistry } from '@jupyterlab/codemirror'; -import { requestDocMerge, WebSocketAwarenessProvider } from '@jupyter/docprovider'; +import { requestDocDelete, requestDocMerge, WebSocketAwarenessProvider } from '@jupyter/docprovider'; import { SidePanel, usersIcon, @@ -310,6 +310,7 @@ export class EditingModeExtension implements DocumentRegistry.IWidgetExtension { + requestDocDelete(sharedModel.currentRoomId, sharedModel.rootRoomId); } }); @@ -322,7 +323,7 @@ export class EditingModeExtension implements DocumentRegistry.IWidgetExtension { const forkPrefix = 'fork_'; - if (value.name === 'merge') { + if (value.name === 'merge' || value.name === 'delete') { // FIXME: a client who is not connected to the fork should not see this update if (sharedModel.currentRoomId === value.newValue) { editingMenu.title.label = 'Editing'; @@ -331,7 +332,8 @@ export class EditingModeExtension implements DocumentRegistry.IWidgetExtension { + 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 }) + }; + + 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; +} diff --git a/packages/docprovider/src/yprovider.ts b/packages/docprovider/src/yprovider.ts index e2c6cf80..a3990adc 100644 --- a/packages/docprovider/src/yprovider.ts +++ b/packages/docprovider/src/yprovider.ts @@ -96,9 +96,25 @@ export class WebSocketProvider implements IDocumentProvider { return forkId; } - connect(roomId: string) { + 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, roomId, diff --git a/yarn.lock b/yarn.lock index a8ca2ec0..2797ea8d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2156,7 +2156,7 @@ __metadata: "@jupyter/ydoc@file:.yalc/@jupyter/ydoc::locator=%40jupyter%2Freal-time-collaboration%40workspace%3A.": version: 2.0.1 - resolution: "@jupyter/ydoc@file:.yalc/@jupyter/ydoc#.yalc/@jupyter/ydoc::hash=e50509&locator=%40jupyter%2Freal-time-collaboration%40workspace%3A." + resolution: "@jupyter/ydoc@file:.yalc/@jupyter/ydoc#.yalc/@jupyter/ydoc::hash=045bce&locator=%40jupyter%2Freal-time-collaboration%40workspace%3A." dependencies: "@jupyterlab/application": ^4.0.0 "@jupyterlab/nbformat": ^3.0.0 || ^4.0.0-alpha.21 || ^4.0.0 @@ -2165,7 +2165,7 @@ __metadata: "@lumino/signaling": ^1.10.0 || ^2.0.0 y-protocols: ^1.0.5 yjs: ^13.5.40 - checksum: c54e335aebc1f0b28241fe0031d5f47513a7a90621b5cca10e6aec3a965adea96390bd8e2c51191e448a9c276ca284ff563eb4870844233798e79a03560b1648 + checksum: aed2b93d1f9d447e7ad1be8699dc3a31968f4cfd4772a1ed1330308eb08ba8d14973df24bae4e49bd93f416f646b84edb44c567487403e7d0bdb665e6ca0e29f languageName: node linkType: hard