Skip to content

Commit 772d681

Browse files
committed
Add fork_room and merge_room handlers
1 parent 8bac302 commit 772d681

File tree

4 files changed

+114
-40
lines changed

4 files changed

+114
-40
lines changed

jupyter_collaboration/app.py

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
from pycrdt_websocket.ystore import BaseYStore
99
from traitlets import Bool, Float, Type
1010

11-
from .handlers import DocSessionHandler, YDocWebSocketHandler
11+
from .handlers import DocForkHandler, DocMergeHandler, DocSessionHandler, YDocWebSocketHandler
1212
from .loaders import FileLoaderMapping
1313
from .stores import SQLiteYStore
1414
from .utils import EVENTS_SCHEMA_PATH
@@ -108,6 +108,20 @@ def initialize_handlers(self):
108108
},
109109
),
110110
(r"/api/collaboration/session/(.*)", DocSessionHandler),
111+
(
112+
r"/api/collaboration/fork_room/(.*)",
113+
DocForkHandler,
114+
{
115+
"ywebsocket_server": self.ywebsocket_server,
116+
}
117+
),
118+
(
119+
r"/api/collaboration/merge_room",
120+
DocMergeHandler,
121+
{
122+
"ywebsocket_server": self.ywebsocket_server,
123+
}
124+
),
111125
]
112126
)
113127

jupyter_collaboration/handlers.py

Lines changed: 66 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,13 @@
66
import asyncio
77
import json
88
import time
9-
import uuid
9+
from uuid import uuid4
1010
from typing import Any
1111

1212
from jupyter_server.auth import authorized
1313
from jupyter_server.base.handlers import APIHandler, JupyterHandler
1414
from jupyter_ydoc import ydocs as YDOCS
15+
from pycrdt import Doc
1516
from pycrdt_websocket.websocket_server import YRoom
1617
from pycrdt_websocket.ystore import BaseYStore
1718
from pycrdt_websocket.yutils import YMessageType, write_var_uint
@@ -30,7 +31,7 @@
3031

3132
YFILE = YDOCS["file"]
3233

33-
SERVER_SESSION = str(uuid.uuid4())
34+
SERVER_SESSION = str(uuid4())
3435

3536

3637
class YDocWebSocketHandler(WebSocketHandler, JupyterHandler):
@@ -381,3 +382,66 @@ async def put(self, path):
381382
)
382383
self.set_status(201)
383384
return self.finish(data)
385+
386+
387+
class DocForkHandler(APIHandler):
388+
"""
389+
Jupyter Server's handler to fork a document.
390+
"""
391+
392+
auth_resource = "contents"
393+
394+
def initialize(
395+
self,
396+
ywebsocket_server: JupyterWebsocketServer,
397+
) -> None:
398+
self._websocket_server = ywebsocket_server
399+
400+
@web.authenticated
401+
@authorized
402+
async def put(self, room_id):
403+
"""
404+
Creates a fork of a root document and returns its ID.
405+
"""
406+
idx = uuid4().hex
407+
408+
root_room = await self._websocket_server.get_room(room_id)
409+
update = root_room.ydoc.get_update()
410+
fork_ydoc = Doc()
411+
fork_ydoc.apply_update(update)
412+
fork_room = YRoom(fork_ydoc)
413+
self._websocket_server.add_room(idx, fork_room)
414+
root_room.fork_ydocs.add(fork_ydoc)
415+
data = json.dumps({
416+
"sessionId": SERVER_SESSION,
417+
"roomId": idx,
418+
})
419+
self.set_status(201)
420+
return self.finish(data)
421+
422+
423+
class DocMergeHandler(APIHandler):
424+
"""
425+
Jupyter Server's handler to merge a document.
426+
"""
427+
428+
auth_resource = "contents"
429+
430+
def initialize(
431+
self,
432+
ywebsocket_server: JupyterWebsocketServer,
433+
) -> None:
434+
self._websocket_server = ywebsocket_server
435+
436+
@web.authenticated
437+
@authorized
438+
async def put(self):
439+
"""
440+
Merges back a fork into a root document.
441+
"""
442+
model = self.get_json_body()
443+
fork_room = await self._websocket_server.get_room(model["fork_roomid"])
444+
root_room = await self._websocket_server.get_room(model["root_roomid"])
445+
update = fork_room.ydoc.get_update()
446+
root_room.ydoc.apply_update(update)
447+
self.set_status(200)

packages/collaboration-extension/src/collaboration.ts

Lines changed: 33 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -247,6 +247,7 @@ export class EditingModeExtension implements DocumentRegistry.IWidgetExtension<N
247247
const suggestionMenu = new Menu({ commands: suggestionCommands });
248248
const reviewMenu = new Menu({ commands: reviewCommands });
249249

250+
const sharedModel = context.model.sharedModel;
250251
var myForkId = ''; // curently allows only one suggestion per user
251252

252253
editingMenu.title.label = 'Editing';
@@ -263,6 +264,7 @@ export class EditingModeExtension implements DocumentRegistry.IWidgetExtension<N
263264
execute: () => {
264265
editingMenu.title.label = 'Editing';
265266
suggestionMenu.title.label = 'Root';
267+
open_dialog('Editing', this._trans);
266268
}
267269
});
268270
editingCommands.addCommand('suggesting', {
@@ -272,17 +274,17 @@ export class EditingModeExtension implements DocumentRegistry.IWidgetExtension<N
272274
reviewMenu.clearItems();
273275
if (myForkId === '') {
274276
myForkId = 'pending';
275-
const provider = context.model.sharedModel.provider;
276-
provider.fork().then(newForkId => {
277+
sharedModel.provider.fork().then(newForkId => {
277278
myForkId = newForkId;
278-
provider.connectFork(newForkId);
279+
sharedModel.provider.connectFork(newForkId);
279280
suggestionMenu.title.label = newForkId;
280281
});
281282
}
282283
else {
283284
suggestionMenu.title.label = myForkId;
284-
context.model.sharedModel.provider.connectFork(myForkId);
285+
sharedModel.provider.connectFork(myForkId);
285286
}
287+
open_dialog('Suggesting', this._trans);
286288
}
287289
});
288290

@@ -293,14 +295,15 @@ export class EditingModeExtension implements DocumentRegistry.IWidgetExtension<N
293295
reviewMenu.clearItems();
294296
suggestionMenu.title.label = 'Root';
295297
editingMenu.title.label = 'Editing';
296-
context.model.sharedModel.provider.connectFork(context.model.sharedModel.rootRoomId);
298+
sharedModel.provider.connectFork(sharedModel.rootRoomId);
299+
open_dialog('Editing', this._trans);
297300
}
298301
});
299302

300303
reviewCommands.addCommand('merge', {
301304
label: 'Merge',
302305
execute: () => {
303-
requestDocMerge(context.model.sharedModel.currentRoomId, context.model.sharedModel.rootRoomId);
306+
requestDocMerge(sharedModel.currentRoomId, sharedModel.rootRoomId);
304307
}
305308
});
306309
reviewCommands.addCommand('discard', {
@@ -336,29 +339,24 @@ export class EditingModeExtension implements DocumentRegistry.IWidgetExtension<N
336339
reviewMenu.addItem({type: 'command', command: 'discard'});
337340
}
338341
suggestionMenu.title.label = newForkId;
339-
context.model.sharedModel.provider.connectFork(newForkId);
340-
const dialog = new Dialog({
341-
title: this._trans.__('Suggestion'),
342-
body: this._trans.__('Your are now viewing the suggestion.'),
343-
buttons: [Dialog.okButton({ label: 'OK' })],
344-
});
345-
dialog.launch().then(resp => { dialog.close(); });
342+
sharedModel.provider.connectFork(newForkId);
343+
open_dialog('Suggesting', this._trans);
346344
}
347345
});
348346
suggestionMenu.addItem({type: 'command', command: newForkId});
349347
if ((myForkId !== 'pending') && (myForkId !== newForkId)) {
350348
const dialog = new Dialog({
351349
title: this._trans.__('New suggestion'),
352-
body: this._trans.__('Open notebook for suggestion?'),
350+
body: this._trans.__('View suggestion?'),
353351
buttons: [
354-
Dialog.okButton({ label: 'Open' }),
352+
Dialog.okButton({ label: 'View' }),
355353
Dialog.cancelButton({ label: 'Discard' }),
356354
],
357355
});
358356
dialog.launch().then(resp => {
359357
dialog.close();
360-
if (resp.button.label === 'Open') {
361-
context.model.sharedModel.provider.connectFork(newForkId);
358+
if (resp.button.label === 'View') {
359+
sharedModel.provider.connectFork(newForkId);
362360
suggestionMenu.title.label = newForkId;
363361
editingMenu.title.label = 'Editing';
364362
reviewMenu.clearItems();
@@ -372,7 +370,7 @@ export class EditingModeExtension implements DocumentRegistry.IWidgetExtension<N
372370
}
373371
};
374372

375-
context.model.sharedModel.changed.connect(_onStateChanged, this);
373+
sharedModel.changed.connect(_onStateChanged, this);
376374

377375
editingMenubar.addMenu(editingMenu);
378376
suggestionMenubar.addMenu(suggestionMenu);
@@ -388,3 +386,20 @@ export class EditingModeExtension implements DocumentRegistry.IWidgetExtension<N
388386
});
389387
}
390388
}
389+
390+
391+
function open_dialog(title: string, trans: TranslationBundle) {
392+
var body: string;
393+
if (title === 'Editing') {
394+
body = 'You are now directly editing the document.'
395+
}
396+
else {
397+
body = 'Your edits now become suggestions to the document.'
398+
}
399+
const dialog = new Dialog({
400+
title: trans.__(title),
401+
body: trans.__(body),
402+
buttons: [Dialog.okButton({ label: 'OK' })],
403+
});
404+
dialog.launch().then(resp => { dialog.close(); });
405+
}

packages/collaboration/style/base.css

Lines changed: 0 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -9,22 +9,3 @@
99
.jp-shared-link-body {
1010
user-select: none;
1111
}
12-
13-
.jp-EditingMode {
14-
display: flex;
15-
flex-direction: column;
16-
align-items: center;
17-
justify-content: center;
18-
}
19-
20-
.jp-EditingMode .lm-MenuBar-itemIcon svg {
21-
vertical-align: sub;
22-
}
23-
24-
.jp-nb-editing-mode-button > .jp-ToolbarButtonComponent::part(content) {
25-
flex-direction: row-reverse;
26-
}
27-
28-
.jp-nb-editing-mode-button > .jp-ToolbarButtonComponent > svg {
29-
padding-left: 3px;
30-
}

0 commit comments

Comments
 (0)