From 2950628729ef1841e6718363b4b72de3c3f3040a Mon Sep 17 00:00:00 2001 From: David Brochart Date: Fri, 23 Feb 2024 16:07:14 +0100 Subject: [PATCH 01/11] Support suggestions --- package.json | 4 + packages/collaboration-extension/package.json | 2 +- .../src/collaboration.ts | 78 +++++++++++++++- packages/collaboration-extension/src/index.ts | 6 +- packages/collaboration/package.json | 1 + packages/collaboration/style/base.css | 19 ++++ packages/docprovider/package.json | 6 +- packages/docprovider/src/index.ts | 1 + packages/docprovider/src/requests.ts | 40 +++++++++ packages/docprovider/src/ydrive.ts | 1 + packages/docprovider/src/yprovider.ts | 90 +++++++++++++++---- 11 files changed, 224 insertions(+), 24 deletions(-) diff --git a/package.json b/package.json index 1bad1fa0..b9fe7b34 100644 --- a/package.json +++ b/package.json @@ -65,5 +65,9 @@ "stylelint-prettier": "^3.0.0", "typedoc": "~0.23.28", "typescript": "~5.0.4" + }, + "resolutions": { + "@jupyter/ydoc": "file:.yalc/@jupyter/ydoc", + "@jupyterlab/services": "file:.yalc/@jupyterlab/services" } } diff --git a/packages/collaboration-extension/package.json b/packages/collaboration-extension/package.json index e8812ce1..76f3b77a 100644 --- a/packages/collaboration-extension/package.json +++ b/packages/collaboration-extension/package.json @@ -55,7 +55,7 @@ "dependencies": { "@jupyter/collaboration": "^2.0.11", "@jupyter/docprovider": "^2.0.11", - "@jupyter/ydoc": "^1.1.0-a0", + "@jupyter/ydoc": "file:.yalc/@jupyter/ydoc", "@jupyterlab/application": "^4.0.5", "@jupyterlab/apputils": "^4.0.5", "@jupyterlab/codemirror": "^4.0.5", diff --git a/packages/collaboration-extension/src/collaboration.ts b/packages/collaboration-extension/src/collaboration.ts index e2502bca..5a409e6b 100644 --- a/packages/collaboration-extension/src/collaboration.ts +++ b/packages/collaboration-extension/src/collaboration.ts @@ -5,6 +5,20 @@ * @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 @@ -15,7 +29,11 @@ import { IEditorExtensionRegistry } from '@jupyterlab/codemirror'; import { 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'; @@ -189,3 +207,61 @@ 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, + requires: [ITranslator], + activate: ( + app: JupyterFrontEnd, + ) => { + app.docRegistry.addWidgetExtension('Notebook', new EditingModeExtension()); + }, +}; + +export class EditingModeExtension implements DocumentRegistry.IWidgetExtension { + createNew(panel: NotebookPanel, context: DocumentRegistry.IContext): IDisposable { + const menubar = new MenuBar(); + const commands = new CommandRegistry(); + const menu = new Menu({ commands }); + menu.title.label = 'Editing'; + menu.title.icon = caretDownIcon; + addMenuItem(commands, menu, 'editing', 'Editing', context); + addMenuItem(commands, menu, 'suggesting', 'Suggesting', context); + menubar.addMenu(menu); + + panel.toolbar.insertItem(990, 'editingMode', menubar); + return new DisposableDelegate(() => { + menubar.dispose(); + }); + } +} + +/** + * Helper Function to add menu items. + */ +function addMenuItem( + commands: CommandRegistry, + menu: Menu, + command: string, + label: string, + context: DocumentRegistry.IContext, +): void { + commands.addCommand(command, { + label: label, + execute: () => { + menu.title.label = label; + if (command == 'suggesting') { + context.model.sharedModel.getProvider('root').fork(); + } + } + }); + menu.addItem({ + type: 'command', + command: command + }); +} diff --git a/packages/collaboration-extension/src/index.ts b/packages/collaboration-extension/src/index.ts index 513befa1..3e1be19b 100644 --- a/packages/collaboration-extension/src/index.ts +++ b/packages/collaboration-extension/src/index.ts @@ -19,7 +19,8 @@ import { menuBarPlugin, rtcGlobalAwarenessPlugin, rtcPanelPlugin, - userEditorCursors + userEditorCursors, + editingMode } from './collaboration'; import { sharedLink } from './sharedlink'; @@ -37,7 +38,8 @@ const plugins: JupyterFrontEndPlugin[] = [ rtcGlobalAwarenessPlugin, rtcPanelPlugin, sharedLink, - userEditorCursors + userEditorCursors, + editingMode ]; export default plugins; diff --git a/packages/collaboration/package.json b/packages/collaboration/package.json index 1f813402..8cebcff2 100644 --- a/packages/collaboration/package.json +++ b/packages/collaboration/package.json @@ -42,6 +42,7 @@ "@codemirror/state": "^6.2.0", "@codemirror/view": "^6.7.0", "@jupyter/docprovider": "^2.0.11", + "@jupyter/ydoc": "file:.yalc/@jupyter/ydoc", "@jupyterlab/apputils": "^4.0.5", "@jupyterlab/coreutils": "^6.0.5", "@jupyterlab/services": "^7.0.5", diff --git a/packages/collaboration/style/base.css b/packages/collaboration/style/base.css index 41d787b5..a79e836c 100644 --- a/packages/collaboration/style/base.css +++ b/packages/collaboration/style/base.css @@ -9,3 +9,22 @@ .jp-shared-link-body { user-select: none; } + +.jp-EditingMode { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; +} + +.jp-EditingMode .lm-MenuBar-itemIcon svg { + vertical-align: sub; +} + +.jp-nb-editing-mode-button > .jp-ToolbarButtonComponent::part(content) { + flex-direction: row-reverse; +} + +.jp-nb-editing-mode-button > .jp-ToolbarButtonComponent > svg { + padding-left: 3px; +} diff --git a/packages/docprovider/package.json b/packages/docprovider/package.json index fec4a135..47fa53d0 100644 --- a/packages/docprovider/package.json +++ b/packages/docprovider/package.json @@ -41,7 +41,7 @@ "watch": "tsc -b --watch" }, "dependencies": { - "@jupyter/ydoc": "^1.1.0-a0", + "@jupyter/ydoc": "file:.yalc/@jupyter/ydoc", "@jupyterlab/coreutils": "^6.0.5", "@jupyterlab/services": "^7.0.5", "@lumino/coreutils": "^2.1.0", @@ -51,6 +51,10 @@ "y-websocket": "^1.3.15", "yjs": "^13.5.40" }, + "resolutions": { + "@jupyterlab/services": "file:.yalc/@jupyterlab/services", + "@jupyter/ydoc": "file:.yalc/@jupyter/ydoc" + }, "devDependencies": { "@jupyterlab/testing": "^4.0.5", "@types/jest": "^29.2.0", diff --git a/packages/docprovider/src/index.ts b/packages/docprovider/src/index.ts index 62edebda..abfd4873 100644 --- a/packages/docprovider/src/index.ts +++ b/packages/docprovider/src/index.ts @@ -8,6 +8,7 @@ */ export * from './awareness'; +export * from './requests'; export * from './ydrive'; export * from './yprovider'; export * from './tokens'; diff --git a/packages/docprovider/src/requests.ts b/packages/docprovider/src/requests.ts index 54e7c364..5dab5697 100644 --- a/packages/docprovider/src/requests.ts +++ b/packages/docprovider/src/requests.ts @@ -11,6 +11,7 @@ import { ServerConnection, Contents } from '@jupyterlab/services'; * See https://github.com/jupyterlab/jupyter_collaboration */ const DOC_SESSION_URL = 'api/collaboration/session'; +const DOC_FORK_URL = 'api/collaboration/fork_room'; /** * Document session model @@ -73,3 +74,42 @@ export async function requestDocSession( return data; } + + +export async function requestDocFork( + roomid: string, +): Promise { + const settings = ServerConnection.makeSettings(); + const url = URLExt.join( + settings.baseUrl, + DOC_FORK_URL, + encodeURIComponent(roomid) + ); + const body = { + method: 'PUT', + body: JSON.stringify({ roomid }) + }; + + 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/ydrive.ts b/packages/docprovider/src/ydrive.ts index b8ec4d60..3dd4b5d2 100644 --- a/packages/docprovider/src/ydrive.ts +++ b/packages/docprovider/src/ydrive.ts @@ -147,6 +147,7 @@ export class YDrive extends Drive implements ICollaborativeDrive { const key = `${options.format}:${options.contentType}:${options.path}`; this._providers.set(key, provider); + sharedModel.setProvider('root', 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 4061ebeb..f17d06e4 100644 --- a/packages/docprovider/src/yprovider.ts +++ b/packages/docprovider/src/yprovider.ts @@ -8,25 +8,15 @@ 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'; -/** - * 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. @@ -42,6 +32,8 @@ export class WebSocketProvider implements IDocumentProvider { */ constructor(options: WebSocketProvider.IOptions) { this._isDisposed = false; + this._isFork = options.isFork || false; + this._sessionId = options.sessionId || ''; this._path = options.path; this._contentType = options.contentType; this._format = options.format; @@ -50,15 +42,14 @@ export class WebSocketProvider implements IDocumentProvider { 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)); } @@ -91,16 +82,21 @@ export class WebSocketProvider implements IDocumentProvider { Signal.clearData(this); } - private async _connect(): Promise { + async fork(): Promise { + this._yWebsocketProvider?.disconnect(); + 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._yWebsocketProvider = new YWebsocketProvider( this._serverUrl, - `${session.format}:${session.type}:${session.fileId}`, + forkId, this._sharedModel.ydoc, { disableBc: true, @@ -109,6 +105,49 @@ export class WebSocketProvider implements IDocumentProvider { } ); + this._sharedModel.addFork(forkId); + } + + connectFork(forkId: string, sharedModel: YDocument): IDocumentProvider { + return new WebSocketProvider({ + isFork: true, + sessionId: this._sessionId, + url: this._serverUrl, + path: forkId, + format: '', + contentType: '', + model: sharedModel, + user: this._user, + translator: this._trans + }); + } + + private async _connect(): Promise { + var roomId: string; + if (this._isFork) { + roomId = this._path; + } + else { + const session = await requestDocSession( + this._format, + this._contentType, + this._path + ); + roomId = `${session.format}:${session.type}:${session.fileId}`; + this._sessionId = session.sessionId; + } + + this._yWebsocketProvider = new YWebsocketProvider( + this._serverUrl, + roomId, + this._sharedModel.ydoc, + { + disableBc: true, + params: { sessionId: this._sessionId! }, + awareness: this._awareness + } + ); + this._yWebsocketProvider.on('sync', this._onSync); this._yWebsocketProvider.on('connection-close', this._onConnectionClosed); } @@ -142,12 +181,15 @@ export class WebSocketProvider implements IDocumentProvider { private _contentType: string; private _format: string; private _isDisposed: boolean; + private _isFork: 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; } /** @@ -192,5 +234,15 @@ export namespace WebSocketProvider { * The jupyterlab translator */ translator: TranslationBundle; + + /** + * The document session ID, if the document is a fork + */ + sessionId?: string; + + /** + * Whether the document is a fork of a root document + */ + isFork?: boolean; } } From 1023e545fe4d5f145e68eecd443168f3a7a3dc87 Mon Sep 17 00:00:00 2001 From: David Brochart Date: Tue, 27 Feb 2024 10:03:41 +0100 Subject: [PATCH 02/11] Remove document provider _isFork attribute --- packages/docprovider/src/yprovider.ts | 31 +++--- yarn.lock | 130 ++++++++++++++++++++------ 2 files changed, 115 insertions(+), 46 deletions(-) diff --git a/packages/docprovider/src/yprovider.ts b/packages/docprovider/src/yprovider.ts index f17d06e4..65fa0459 100644 --- a/packages/docprovider/src/yprovider.ts +++ b/packages/docprovider/src/yprovider.ts @@ -32,8 +32,7 @@ export class WebSocketProvider implements IDocumentProvider { */ constructor(options: WebSocketProvider.IOptions) { this._isDisposed = false; - this._isFork = options.isFork || false; - this._sessionId = options.sessionId || ''; + this._sessionId = options.sessionId ?? ''; this._path = options.path; this._contentType = options.contentType; this._format = options.format; @@ -83,8 +82,6 @@ export class WebSocketProvider implements IDocumentProvider { } async fork(): Promise { - this._yWebsocketProvider?.disconnect(); - const session = await requestDocSession( this._format, this._contentType, @@ -94,6 +91,13 @@ export class WebSocketProvider implements IDocumentProvider { const response = await requestDocFork(`${session.format}:${session.type}:${session.fileId}`); const forkId = response.roomId; + this._sharedModel.forkId = forkId; + + // the fork has to be advertised before the ydoc is connected to the forked room + // so that it targets the root room + this._sharedModel.addFork(forkId); + + this._yWebsocketProvider?.disconnect(); this._yWebsocketProvider = new YWebsocketProvider( this._serverUrl, forkId, @@ -104,13 +108,12 @@ export class WebSocketProvider implements IDocumentProvider { awareness: this._awareness } ); - - this._sharedModel.addFork(forkId); } connectFork(forkId: string, sharedModel: YDocument): IDocumentProvider { + sharedModel.forkId = forkId; + return new WebSocketProvider({ - isFork: true, sessionId: this._sessionId, url: this._serverUrl, path: forkId, @@ -124,10 +127,7 @@ export class WebSocketProvider implements IDocumentProvider { private async _connect(): Promise { var roomId: string; - if (this._isFork) { - roomId = this._path; - } - else { + if (this._sharedModel.forkId === 'root') { const session = await requestDocSession( this._format, this._contentType, @@ -136,6 +136,9 @@ export class WebSocketProvider implements IDocumentProvider { roomId = `${session.format}:${session.type}:${session.fileId}`; this._sessionId = session.sessionId; } + else { + roomId = this._path; + } this._yWebsocketProvider = new YWebsocketProvider( this._serverUrl, @@ -181,7 +184,6 @@ export class WebSocketProvider implements IDocumentProvider { private _contentType: string; private _format: string; private _isDisposed: boolean; - private _isFork: boolean; private _sessionId: string; private _path: string; private _ready = new PromiseDelegate(); @@ -239,10 +241,5 @@ export namespace WebSocketProvider { * The document session ID, if the document is a fork */ sessionId?: string; - - /** - * Whether the document is a fork of a root document - */ - isFork?: boolean; } } diff --git a/yarn.lock b/yarn.lock index f52db1a8..bac3a559 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2035,7 +2035,7 @@ __metadata: dependencies: "@jupyter/collaboration": ^2.0.11 "@jupyter/docprovider": ^2.0.11 - "@jupyter/ydoc": ^1.1.0-a0 + "@jupyter/ydoc": "file:.yalc/@jupyter/ydoc" "@jupyterlab/application": ^4.0.5 "@jupyterlab/apputils": ^4.0.5 "@jupyterlab/builder": ^4.0.5 @@ -2070,6 +2070,7 @@ __metadata: "@codemirror/state": ^6.2.0 "@codemirror/view": ^6.7.0 "@jupyter/docprovider": ^2.0.11 + "@jupyter/ydoc": "file:.yalc/@jupyter/ydoc" "@jupyterlab/apputils": ^4.0.5 "@jupyterlab/coreutils": ^6.0.5 "@jupyterlab/services": ^7.0.5 @@ -2090,7 +2091,7 @@ __metadata: version: 0.0.0-use.local resolution: "@jupyter/docprovider@workspace:packages/docprovider" dependencies: - "@jupyter/ydoc": ^1.1.0-a0 + "@jupyter/ydoc": "file:.yalc/@jupyter/ydoc" "@jupyterlab/coreutils": ^6.0.5 "@jupyterlab/services": ^7.0.5 "@jupyterlab/testing": ^4.0.5 @@ -2130,23 +2131,9 @@ __metadata: languageName: unknown linkType: soft -"@jupyter/ydoc@npm:^1.0.2": - version: 1.0.2 - resolution: "@jupyter/ydoc@npm:1.0.2" - 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: 739f9630940466b3cfcd7b742dd06479f81772ca13f863d057af0bbb5e318829506969066ab72977e7c721644982b5c8f88cf44e1ae81955ed1c27e87632d1f2 - languageName: node - linkType: hard - -"@jupyter/ydoc@npm:^1.1.0-a0": - version: 1.1.0-a0 - resolution: "@jupyter/ydoc@npm:1.1.0-a0" +"@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=c66726&locator=%40jupyter%2Freal-time-collaboration%40workspace%3A." dependencies: "@jupyterlab/nbformat": ^3.0.0 || ^4.0.0-alpha.21 || ^4.0.0 "@lumino/coreutils": ^1.11.0 || ^2.0.0 @@ -2154,7 +2141,7 @@ __metadata: "@lumino/signaling": ^1.10.0 || ^2.0.0 y-protocols: ^1.0.5 yjs: ^13.5.40 - checksum: 0099bacb2884a460867658e7f5a944e4d6eca7c4d3d68a6b31102be77d8fc9b6ba162af55c20f81c70f7fef4e9d9329eee4da32063c0caa0c6e3f10a488f95b5 + checksum: f5cac20b6b88fb9ced006923ad7164bb53fcc7565b66bc4bd454bcf72595ed3f0689c479166caabff57bd5a5f73534ce3af08623123a30870e39382c74374d61 languageName: node linkType: hard @@ -2385,6 +2372,20 @@ __metadata: languageName: node linkType: hard +"@jupyterlab/coreutils@npm:^6.1.2": + version: 6.1.2 + resolution: "@jupyterlab/coreutils@npm:6.1.2" + dependencies: + "@lumino/coreutils": ^2.1.2 + "@lumino/disposable": ^2.1.2 + "@lumino/signaling": ^2.1.2 + minimist: ~1.2.0 + path-browserify: ^1.0.0 + url-parse: ~1.5.4 + checksum: 6dcc812e0ebae28f902ece4acd58aee8103033b23a3bac0935d4d9d8c9c10f8797b422f4e8b0be53fac4781811fb9b82874ce499cd69a6d198986e0cdb4a97ff + languageName: node + linkType: hard + "@jupyterlab/docmanager@npm:^4.0.5": version: 4.0.5 resolution: "@jupyterlab/docmanager@npm:4.0.5" @@ -2562,6 +2563,15 @@ __metadata: languageName: node linkType: hard +"@jupyterlab/nbformat@npm:^4.1.2": + version: 4.1.2 + resolution: "@jupyterlab/nbformat@npm:4.1.2" + dependencies: + "@lumino/coreutils": ^2.1.2 + checksum: 6c55337b667dcc5a6282f93972a30d227ba7c3f576fc4b60069408dd114dff1bc9f743bb6f984da088dfda25b7c4f25f13a472cd5c05b24af2e32b6b17172c6b + languageName: node + linkType: hard + "@jupyterlab/notebook@npm:^4.0.5": version: 4.0.5 resolution: "@jupyterlab/notebook@npm:4.0.5" @@ -2663,22 +2673,22 @@ __metadata: languageName: node linkType: hard -"@jupyterlab/services@npm:^7.0.5": - version: 7.0.5 - resolution: "@jupyterlab/services@npm:7.0.5" +"@jupyterlab/services@file:.yalc/@jupyterlab/services::locator=%40jupyter%2Freal-time-collaboration%40workspace%3A.": + version: 7.1.2 + resolution: "@jupyterlab/services@file:.yalc/@jupyterlab/services#.yalc/@jupyterlab/services::hash=cec64c&locator=%40jupyter%2Freal-time-collaboration%40workspace%3A." dependencies: - "@jupyter/ydoc": ^1.0.2 - "@jupyterlab/coreutils": ^6.0.5 - "@jupyterlab/nbformat": ^4.0.5 - "@jupyterlab/settingregistry": ^4.0.5 - "@jupyterlab/statedb": ^4.0.5 + "@jupyter/ydoc": ^1.1.1 + "@jupyterlab/coreutils": ^6.1.2 + "@jupyterlab/nbformat": ^4.1.2 + "@jupyterlab/settingregistry": ^4.1.2 + "@jupyterlab/statedb": ^4.1.2 "@lumino/coreutils": ^2.1.2 "@lumino/disposable": ^2.1.2 "@lumino/polling": ^2.1.2 "@lumino/properties": ^2.0.1 "@lumino/signaling": ^2.1.2 ws: ^8.11.0 - checksum: cf4176dbb73c08e777b5e6ca26cba6ad7a142fc76ae6b46ef17ac7d8c8021f62d66e95e2ee0dbce5c33a0b2380750d440783d0398d787b8e8028920e04dd1d0b + checksum: 466c391f75583f033d62676e7e4d3d3db3062913f2b8a58a6323711cdf1d0f2e3061ed12d9965c220a5bb7c0b1d5bbac5cef9ec692198ad41b0d6311d77edf6c languageName: node linkType: hard @@ -2701,6 +2711,25 @@ __metadata: languageName: node linkType: hard +"@jupyterlab/settingregistry@npm:^4.1.2": + version: 4.1.2 + resolution: "@jupyterlab/settingregistry@npm:4.1.2" + dependencies: + "@jupyterlab/nbformat": ^4.1.2 + "@jupyterlab/statedb": ^4.1.2 + "@lumino/commands": ^2.2.0 + "@lumino/coreutils": ^2.1.2 + "@lumino/disposable": ^2.1.2 + "@lumino/signaling": ^2.1.2 + "@rjsf/utils": ^5.13.4 + ajv: ^8.12.0 + json5: ^2.2.3 + peerDependencies: + react: ">=16" + checksum: a0a1f9d3747caa3ac6e523df64b4023f9d3bc1c1c9a2982cdf8113a5ba3f191e10cd8a897e9bff111b9faa834b48c0666a6b03ce3749c9f9e5ffb43b9331c207 + languageName: node + linkType: hard + "@jupyterlab/statedb@npm:^4.0.5": version: 4.0.5 resolution: "@jupyterlab/statedb@npm:4.0.5" @@ -2714,6 +2743,19 @@ __metadata: languageName: node linkType: hard +"@jupyterlab/statedb@npm:^4.1.2": + version: 4.1.2 + resolution: "@jupyterlab/statedb@npm:4.1.2" + dependencies: + "@lumino/commands": ^2.2.0 + "@lumino/coreutils": ^2.1.2 + "@lumino/disposable": ^2.1.2 + "@lumino/properties": ^2.0.1 + "@lumino/signaling": ^2.1.2 + checksum: 30314f4f93441aac6d62068c264c94c0e694829c64ce0dc59e867ef4d188d396edc9c6868dd92ca514f6e7b15dc2568ff3f2de078a20283f60cc5ae70723bacc + languageName: node + linkType: hard + "@jupyterlab/statusbar@npm:^4.0.5": version: 4.0.5 resolution: "@jupyterlab/statusbar@npm:4.0.5" @@ -3110,6 +3152,21 @@ __metadata: languageName: node linkType: hard +"@lumino/commands@npm:^2.2.0": + version: 2.2.0 + resolution: "@lumino/commands@npm:2.2.0" + dependencies: + "@lumino/algorithm": ^2.0.1 + "@lumino/coreutils": ^2.1.2 + "@lumino/disposable": ^2.1.2 + "@lumino/domutils": ^2.0.1 + "@lumino/keyboard": ^2.0.1 + "@lumino/signaling": ^2.1.2 + "@lumino/virtualdom": ^2.0.1 + checksum: 093e9715491e5cef24bc80665d64841417b400f2fa595f9b60832a3b6340c405c94a6aa276911944a2c46d79a6229f3cc087b73f50852bba25ece805abd0fae9 + languageName: node + linkType: hard + "@lumino/coreutils@npm:^1.11.0 || ^2.0.0, @lumino/coreutils@npm:^1.11.0 || ^2.1.2, @lumino/coreutils@npm:^2.1.0, @lumino/coreutils@npm:^2.1.1, @lumino/coreutils@npm:^2.1.2": version: 2.1.2 resolution: "@lumino/coreutils@npm:2.1.2" @@ -3801,6 +3858,21 @@ __metadata: languageName: node linkType: hard +"@rjsf/utils@npm:^5.13.4": + version: 5.17.1 + resolution: "@rjsf/utils@npm:5.17.1" + dependencies: + json-schema-merge-allof: ^0.8.1 + jsonpointer: ^5.0.1 + lodash: ^4.17.21 + lodash-es: ^4.17.21 + react-is: ^18.2.0 + peerDependencies: + react: ^16.14.0 || >=17 + checksum: 83010de66b06f1046b023a0b7d0bf30b5f47b152893c3b12f1f42faa89e7c7d18b2f04fe2e9035e5f63454317f09e6d5753fc014d43b933c8023b71fc50c3acf + languageName: node + linkType: hard + "@sigstore/protobuf-specs@npm:^0.1.0": version: 0.1.0 resolution: "@sigstore/protobuf-specs@npm:0.1.0" From ea8f940d4ace60b1fc9795c1fb23294b868ac298 Mon Sep 17 00:00:00 2001 From: David Brochart Date: Wed, 28 Feb 2024 15:19:20 +0100 Subject: [PATCH 03/11] Support only one provider per document for now --- .../src/collaboration.ts | 3 +- packages/docprovider/src/ydrive.ts | 2 +- packages/docprovider/src/yprovider.ts | 40 +++++-------------- yarn.lock | 4 +- 4 files changed, 16 insertions(+), 33 deletions(-) diff --git a/packages/collaboration-extension/src/collaboration.ts b/packages/collaboration-extension/src/collaboration.ts index 5a409e6b..21efdc24 100644 --- a/packages/collaboration-extension/src/collaboration.ts +++ b/packages/collaboration-extension/src/collaboration.ts @@ -256,7 +256,8 @@ function addMenuItem( execute: () => { menu.title.label = label; if (command == 'suggesting') { - context.model.sharedModel.getProvider('root').fork(); + const provider = context.model.sharedModel.provider; + provider.fork().then(forkId => {provider.connectFork(forkId);}); } } }); diff --git a/packages/docprovider/src/ydrive.ts b/packages/docprovider/src/ydrive.ts index 3dd4b5d2..aab5b43c 100644 --- a/packages/docprovider/src/ydrive.ts +++ b/packages/docprovider/src/ydrive.ts @@ -147,7 +147,7 @@ export class YDrive extends Drive implements ICollaborativeDrive { const key = `${options.format}:${options.contentType}:${options.path}`; this._providers.set(key, provider); - sharedModel.setProvider('root', provider); + 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 65fa0459..22070979 100644 --- a/packages/docprovider/src/yprovider.ts +++ b/packages/docprovider/src/yprovider.ts @@ -81,7 +81,7 @@ export class WebSocketProvider implements IDocumentProvider { Signal.clearData(this); } - async fork(): Promise { + async fork(): Promise { const session = await requestDocSession( this._format, this._contentType, @@ -90,13 +90,14 @@ export class WebSocketProvider implements IDocumentProvider { const response = await requestDocFork(`${session.format}:${session.type}:${session.fileId}`); const forkId = response.roomId; - - this._sharedModel.forkId = forkId; - - // the fork has to be advertised before the ydoc is connected to the forked room - // so that it targets the root room + this._sharedModel.roomId = forkId; this._sharedModel.addFork(forkId); + return forkId; + } + + connectFork(forkId: string) { + this._sharedModel.roomId = forkId; this._yWebsocketProvider?.disconnect(); this._yWebsocketProvider = new YWebsocketProvider( this._serverUrl, @@ -104,45 +105,26 @@ export class WebSocketProvider implements IDocumentProvider { this._sharedModel.ydoc, { disableBc: true, - params: { sessionId: session.sessionId }, + params: { sessionId: this._sessionId }, awareness: this._awareness } ); } - connectFork(forkId: string, sharedModel: YDocument): IDocumentProvider { - sharedModel.forkId = forkId; - - return new WebSocketProvider({ - sessionId: this._sessionId, - url: this._serverUrl, - path: forkId, - format: '', - contentType: '', - model: sharedModel, - user: this._user, - translator: this._trans - }); - } - private async _connect(): Promise { - var roomId: string; - if (this._sharedModel.forkId === 'root') { + if (this._sharedModel.roomId === '') { const session = await requestDocSession( this._format, this._contentType, this._path ); - roomId = `${session.format}:${session.type}:${session.fileId}`; + this._sharedModel.roomId = `${session.format}:${session.type}:${session.fileId}`; this._sessionId = session.sessionId; } - else { - roomId = this._path; - } this._yWebsocketProvider = new YWebsocketProvider( this._serverUrl, - roomId, + this._sharedModel.roomId, this._sharedModel.ydoc, { disableBc: true, diff --git a/yarn.lock b/yarn.lock index bac3a559..216d215e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2133,7 +2133,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=c66726&locator=%40jupyter%2Freal-time-collaboration%40workspace%3A." + resolution: "@jupyter/ydoc@file:.yalc/@jupyter/ydoc#.yalc/@jupyter/ydoc::hash=d13236&locator=%40jupyter%2Freal-time-collaboration%40workspace%3A." dependencies: "@jupyterlab/nbformat": ^3.0.0 || ^4.0.0-alpha.21 || ^4.0.0 "@lumino/coreutils": ^1.11.0 || ^2.0.0 @@ -2141,7 +2141,7 @@ __metadata: "@lumino/signaling": ^1.10.0 || ^2.0.0 y-protocols: ^1.0.5 yjs: ^13.5.40 - checksum: f5cac20b6b88fb9ced006923ad7164bb53fcc7565b66bc4bd454bcf72595ed3f0689c479166caabff57bd5a5f73534ce3af08623123a30870e39382c74374d61 + checksum: 4b906adf8d1eb9461463456f39be7225281d8f9f59213af58dad3d1383f2436760f7a6a60b6d87795e95662c6ff4323c41069644cf6833b2ad2d1310aa9a46fd languageName: node linkType: hard From 1a4f266a61d06b731f31383d052d19d8144cddb6 Mon Sep 17 00:00:00 2001 From: David Brochart Date: Thu, 7 Mar 2024 18:10:32 +0100 Subject: [PATCH 04/11] - --- .../src/collaboration.ts | 253 ++++++++++-- .../src/filebrowser.ts | 9 +- packages/collaboration-extension/src/index.ts | 6 +- packages/collaboration/src/index.ts | 2 + .../collaboration/src/suggestionspanel.tsx | 92 +++++ packages/collaboration/src/tokens.ts | 7 + packages/docprovider/src/requests.ts | 39 +- packages/docprovider/src/ydrive.ts | 21 +- packages/docprovider/src/yprovider.ts | 11 +- yarn.lock | 383 +++++++++++++++++- 10 files changed, 760 insertions(+), 63 deletions(-) create mode 100644 packages/collaboration/src/suggestionspanel.tsx diff --git a/packages/collaboration-extension/src/collaboration.ts b/packages/collaboration-extension/src/collaboration.ts index 21efdc24..9ee439e0 100644 --- a/packages/collaboration-extension/src/collaboration.ts +++ b/packages/collaboration-extension/src/collaboration.ts @@ -23,12 +23,12 @@ import { JupyterFrontEnd, JupyterFrontEndPlugin } from '@jupyterlab/application'; -import { IToolbarWidgetRegistry } from '@jupyterlab/apputils'; +import { Dialog, IToolbarWidgetRegistry } from '@jupyterlab/apputils'; import { EditorExtensionRegistry, IEditorExtensionRegistry } from '@jupyterlab/codemirror'; -import { WebSocketAwarenessProvider } from '@jupyter/docprovider'; +import { requestDocMerge, WebSocketAwarenessProvider } from '@jupyter/docprovider'; import { SidePanel, usersIcon, @@ -37,20 +37,21 @@ import { 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, ISuggestions, NotebookChange } from '@jupyter/ydoc'; import { CollaboratorsPanel, + SuggestionsPanel, IGlobalAwareness, IUserMenu, remoteUserCursors, RendererUserMenu, UserInfoPanel, - UserMenu + UserMenu, } from '@jupyter/collaboration'; import * as Y from 'yjs'; @@ -147,11 +148,12 @@ export const rtcPanelPlugin: JupyterFrontEndPlugin = { id: '@jupyter/collaboration-extension:rtcPanel', description: 'Add side panel to display all currently connected users.', autoStart: true, - requires: [IGlobalAwareness], + requires: [IGlobalAwareness, ISuggestions], optional: [ITranslator], activate: ( app: JupyterFrontEnd, awareness: Awareness, + suggestions: ISuggestions, translator: ITranslator | null ): void => { const { user } = app.serviceManager; @@ -183,6 +185,10 @@ export const rtcPanelPlugin: JupyterFrontEndPlugin = { ); collaboratorsPanel.title.label = trans.__('Online Collaborators'); userPanel.addWidget(collaboratorsPanel); + + const suggestionsPanel = new SuggestionsPanel(fileopener, suggestions); + suggestionsPanel.title.label = trans.__('Suggestions'); + userPanel.addWidget(suggestionsPanel); } }; @@ -215,54 +221,217 @@ export const editingMode: JupyterFrontEndPlugin = { id: '@jupyter/collaboration-extension:editingMode', description: 'A plugin to add editing mode to the notebook page.', autoStart: true, - requires: [ITranslator], + optional: [ITranslator], activate: ( app: JupyterFrontEnd, + translator: ITranslator | null ) => { - app.docRegistry.addWidgetExtension('Notebook', new EditingModeExtension()); + app.docRegistry.addWidgetExtension('Notebook', new EditingModeExtension(translator)); }, }; export class EditingModeExtension implements DocumentRegistry.IWidgetExtension { - createNew(panel: NotebookPanel, context: DocumentRegistry.IContext): IDisposable { - const menubar = new MenuBar(); - const commands = new CommandRegistry(); - const menu = new Menu({ commands }); - menu.title.label = 'Editing'; - menu.title.icon = caretDownIcon; - addMenuItem(commands, menu, 'editing', 'Editing', context); - addMenuItem(commands, menu, 'suggesting', 'Suggesting', context); - menubar.addMenu(menu); - - panel.toolbar.insertItem(990, 'editingMode', menubar); + 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 }); + + 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'; + } + }); + editingCommands.addCommand('suggesting', { + label: 'Suggesting', + execute: () => { + editingMenu.title.label = 'Suggesting'; + reviewMenu.clearItems(); + if (myForkId === '') { + myForkId = 'pending'; + const provider = context.model.sharedModel.provider; + provider.fork().then(newForkId => { + myForkId = newForkId; + provider.connectFork(newForkId); + suggestionMenu.title.label = newForkId; + }); + } + else { + suggestionMenu.title.label = myForkId; + context.model.sharedModel.provider.connectFork(myForkId); + } + } + }); + + suggestionCommands.addCommand('root', { + label: 'Root', + execute: () => { + // we cannot review the root document + reviewMenu.clearItems(); + suggestionMenu.title.label = 'Root'; + editingMenu.title.label = 'Editing'; + context.model.sharedModel.provider.connectFork(context.model.sharedModel.rootRoomId); + } + }); + + reviewCommands.addCommand('merge', { + label: 'Merge', + execute: () => { + console.log('currentRoomId', context.model.sharedModel.currentRoomId); + console.log('rootRoomId', context.model.sharedModel.rootRoomId); + requestDocMerge(context.model.sharedModel.currentRoomId, context.model.sharedModel.rootRoomId); + } + }); + reviewCommands.addCommand('discard', { + label: 'Discard', + execute: () => { + } + }); + + 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.startsWith(forkPrefix)) { + const newForkId = value.name.slice(forkPrefix.length); + suggestionCommands.addCommand(newForkId, { + label: newForkId, + execute: () => { + if (myForkId === newForkId) { + editingMenu.title.label = 'Suggesting'; + // our suggestion, cannot be reviewed + reviewMenu.clearItems(); + } + else { + editingMenu.title.label = 'Editing'; + // not our suggestion, can be reviewed + reviewMenu.clearItems(); + reviewMenu.addItem({type: 'command', command: 'merge'}); + reviewMenu.addItem({type: 'command', command: 'discard'}); + } + suggestionMenu.title.label = newForkId; + context.model.sharedModel.provider.connectFork(newForkId); + const dialog = new Dialog({ + title: this._trans.__('Suggestion'), + body: this._trans.__('Your are now viewing the suggestion.'), + buttons: [Dialog.okButton({ label: 'OK' })], + }); + dialog.launch().then(resp => { dialog.close(); }); + } + }); + suggestionMenu.addItem({type: 'command', command: newForkId}); + if ((myForkId !== 'pending') && (myForkId !== newForkId)) { + const dialog = new Dialog({ + title: this._trans.__('New suggestion'), + body: this._trans.__('Open notebook for suggestion?'), + buttons: [ + Dialog.okButton({ label: 'Open' }), + Dialog.cancelButton({ label: 'Discard' }), + ], + }); + dialog.launch().then(resp => { + dialog.close(); + if (resp.button.label === 'Open') { + context.model.sharedModel.provider.connectFork(newForkId); + suggestionMenu.title.label = newForkId; + editingMenu.title.label = 'Editing'; + reviewMenu.clearItems(); + reviewMenu.addItem({type: 'command', command: 'merge'}); + reviewMenu.addItem({type: 'command', command: 'discard'}); + } + }); + } + } + }); + } + }; + + context.model.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(() => { - menubar.dispose(); + editingMenubar.dispose(); + suggestionMenubar.dispose(); + reviewMenubar.dispose(); }); } } /** - * Helper Function to add menu items. - */ -function addMenuItem( - commands: CommandRegistry, - menu: Menu, - command: string, - label: string, - context: DocumentRegistry.IContext, -): void { - commands.addCommand(command, { - label: label, - execute: () => { - menu.title.label = label; - if (command == 'suggesting') { - const provider = context.model.sharedModel.provider; - provider.fork().then(forkId => {provider.connectFork(forkId);}); - } + * A plugin to provide shared document suggestions. +*/ +export const suggestions: JupyterFrontEndPlugin = { + id: '@jupyter/collaboration-extension:rtcGlobalSuggestions', + description: 'A plugin to provide shared document suggestions.', + autoStart: true, + provides: ISuggestions, + activate: (app: JupyterFrontEnd): ISuggestions => { + console.log('suggestions plugin activated'); + return new Suggestions(); + }, +}; + +export class Suggestions implements ISuggestions { + private _forkIds: string[]; + private _callbacks: any[]; + + constructor() { + this._forkIds = []; + this._callbacks = []; + } + + addFork(forkId: string) { + this._forkIds.push(forkId); + for (const callback of this._callbacks) { + callback(forkId); } - }); - menu.addItem({ - type: 'command', - command: command - }); + } + + addCallback(callback: any) { + this._callbacks.push(callback); + } + + get forks(): string[] { + return this._forkIds; + } } diff --git a/packages/collaboration-extension/src/filebrowser.ts b/packages/collaboration-extension/src/filebrowser.ts index 75348215..a3ecb15f 100644 --- a/packages/collaboration-extension/src/filebrowser.ts +++ b/packages/collaboration-extension/src/filebrowser.ts @@ -24,7 +24,7 @@ import { ITranslator, nullTranslator } from '@jupyterlab/translation'; import { CommandRegistry } from '@lumino/commands'; -import { YFile, YNotebook } from '@jupyter/ydoc'; +import { ISuggestions, YFile, YNotebook } from '@jupyter/ydoc'; import { ICollaborativeDrive, YDrive } from '@jupyter/docprovider'; @@ -42,14 +42,15 @@ export const drive: JupyterFrontEndPlugin = { id: '@jupyter/collaboration-extension:drive', description: 'The default collaborative drive provider', provides: ICollaborativeDrive, - requires: [ITranslator], + requires: [ITranslator, ISuggestions], optional: [], activate: ( app: JupyterFrontEnd, - translator: ITranslator + translator: ITranslator, + suggestions: ISuggestions ): ICollaborativeDrive => { const trans = translator.load('jupyter_collaboration'); - const drive = new YDrive(app.serviceManager.user, trans); + const drive = new YDrive(app.serviceManager.user, trans, suggestions); app.serviceManager.contents.addDrive(drive); return drive; } diff --git a/packages/collaboration-extension/src/index.ts b/packages/collaboration-extension/src/index.ts index 3e1be19b..e268aaf9 100644 --- a/packages/collaboration-extension/src/index.ts +++ b/packages/collaboration-extension/src/index.ts @@ -20,7 +20,8 @@ import { rtcGlobalAwarenessPlugin, rtcPanelPlugin, userEditorCursors, - editingMode + editingMode, + suggestions } from './collaboration'; import { sharedLink } from './sharedlink'; @@ -39,7 +40,8 @@ const plugins: JupyterFrontEndPlugin[] = [ rtcPanelPlugin, sharedLink, userEditorCursors, - editingMode + editingMode, + suggestions ]; export default plugins; diff --git a/packages/collaboration/src/index.ts b/packages/collaboration/src/index.ts index 05e4fff7..ef38378c 100644 --- a/packages/collaboration/src/index.ts +++ b/packages/collaboration/src/index.ts @@ -7,6 +7,8 @@ export * from './tokens'; export * from './collaboratorspanel'; +export * from './suggestionspanel'; +//export * from './suggestions'; export * from './cursors'; export * from './menu'; export * from './sharedlink'; diff --git a/packages/collaboration/src/suggestionspanel.tsx b/packages/collaboration/src/suggestionspanel.tsx new file mode 100644 index 00000000..ed12289a --- /dev/null +++ b/packages/collaboration/src/suggestionspanel.tsx @@ -0,0 +1,92 @@ +// Copyright (c) Jupyter Development Team. +// Distributed under the terms of the Modified BSD License. + +import * as React from 'react'; + +import { Panel } from '@lumino/widgets'; + +import { ReactWidget } from '@jupyterlab/apputils'; + +//import { PathExt } from '@jupyterlab/coreutils'; + +import { ISuggestions } from '@jupyter/ydoc'; + +//import { ICollaboratorAwareness } from './tokens'; + +export class SuggestionsPanel extends Panel { + private _body: SuggestionsBody; + + constructor(fileopener: (path: string) => void, suggestions: ISuggestions) { + super({}); + + this._body = new SuggestionsBody(fileopener, suggestions); + this.addWidget(this._body); + this.update(); + } +} + +/** + * The suggestions list. + */ +export class SuggestionsBody extends ReactWidget { + private _suggestions: ISuggestions; + //private _fileopener: (path: string) => void; + + constructor(fileopener: (path: string) => void, suggestions: ISuggestions) { + super(); + //this._fileopener = fileopener; + suggestions.addCallback((forkId: string) => { this.update(); }); + this._suggestions = suggestions; + } + + render(): React.ReactElement[] { + return this._suggestions.forks.map((value, i) => { + // let canOpenCurrent = false; + // let current = ''; + // let separator = ''; + // let currentFileLocation = ''; + + // if (value.current) { + // canOpenCurrent = true; + // const path = value.current.split(':'); + // currentFileLocation = `${path[1]}:${path[2]}`; + + // current = PathExt.basename(path[2]); + // current = + // current.length > 25 ? current.slice(0, 12).concat('…') : current; + // separator = '•'; + // } + + // const onClick = () => { + // if (canOpenCurrent) { + // this._fileopener(currentFileLocation); + // } + // }; + + // const displayName = `${value.user.display_name} ${separator} ${current}`; + + // return ( + //
+ //
+ // {value.user.initials} + //
+ // {displayName} + //
+ // ); + return ( +
+
+ {value} +
+
+ ); + }); + } +} diff --git a/packages/collaboration/src/tokens.ts b/packages/collaboration/src/tokens.ts index 5f717390..42cbbec3 100644 --- a/packages/collaboration/src/tokens.ts +++ b/packages/collaboration/src/tokens.ts @@ -24,6 +24,13 @@ export const IGlobalAwareness = new Token( '@jupyter/collaboration:IGlobalAwareness' ); +///** +// * The global suggestions token. +// */ +//export const IGlobalSuggestions = new Token( +// '@jupyter/collaboration:IGlobalSuggestions' +//); +// /** * An interface describing the user menu. */ diff --git a/packages/docprovider/src/requests.ts b/packages/docprovider/src/requests.ts index 5dab5697..f8a2d947 100644 --- a/packages/docprovider/src/requests.ts +++ b/packages/docprovider/src/requests.ts @@ -12,6 +12,7 @@ import { ServerConnection, Contents } from '@jupyterlab/services'; */ const DOC_SESSION_URL = 'api/collaboration/session'; const DOC_FORK_URL = 'api/collaboration/fork_room'; +const DOC_MERGE_URL = 'api/collaboration/merge_room'; /** * Document session model @@ -85,9 +86,45 @@ export async function requestDocFork( DOC_FORK_URL, encodeURIComponent(roomid) ); + 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({ roomid }) + body: JSON.stringify({ fork_roomid: forkRoomid, root_roomid: rootRoomid }) }; let response: Response; diff --git a/packages/docprovider/src/ydrive.ts b/packages/docprovider/src/ydrive.ts index aab5b43c..c31f05d8 100644 --- a/packages/docprovider/src/ydrive.ts +++ b/packages/docprovider/src/ydrive.ts @@ -5,7 +5,7 @@ import { PageConfig, URLExt } from '@jupyterlab/coreutils'; import { TranslationBundle } from '@jupyterlab/translation'; import { Contents, Drive, User } from '@jupyterlab/services'; -import { DocumentChange, ISharedDocument, YDocument } from '@jupyter/ydoc'; +import { DocumentChange, ISharedDocument, YDocument, ISuggestions } from '@jupyter/ydoc'; import { WebSocketProvider } from './yprovider'; import { @@ -14,6 +14,8 @@ import { SharedDocumentFactory } from './tokens'; +import * as Y from 'yjs'; + const DISABLE_RTC = PageConfig.getOption('disableRTC') === 'true' ? true : false; @@ -32,11 +34,12 @@ export class YDrive extends Drive implements ICollaborativeDrive { * * @param user - The user manager to add the identity to the awareness of documents. */ - constructor(user: User.IManager, translator: TranslationBundle) { + constructor(user: User.IManager, translator: TranslationBundle, suggestions: ISuggestions) { super({ name: 'RTC' }); this._user = user; this._trans = translator; this._providers = new Map(); + this._suggestions = suggestions; this.sharedModelFactory = new SharedModelFactory(this._onCreate); } @@ -126,6 +129,18 @@ export class YDrive extends Drive implements ICollaborativeDrive { return super.save(localPath, options); } + private _handleForks = (event: Y.YMapEvent) => { + const forkPrefix = 'fork_'; + event.changes.keys.forEach((change, key) => { + if (change.action === 'add') { + if (key.startsWith(forkPrefix)) { + const forkId = key.slice(forkPrefix.length); + this._suggestions.addFork(forkId); + } + } + }); + }; + private _onCreate = ( options: Contents.ISharedFactoryOptions, sharedModel: YDocument @@ -147,6 +162,7 @@ export class YDrive extends Drive implements ICollaborativeDrive { const key = `${options.format}:${options.contentType}:${options.path}`; this._providers.set(key, provider); + sharedModel.ystate.observe(this._handleForks); sharedModel.provider = provider; sharedModel.disposed.connect(() => { const provider = this._providers.get(key); @@ -167,6 +183,7 @@ export class YDrive extends Drive implements ICollaborativeDrive { private _user: User.IManager; private _trans: TranslationBundle; private _providers: Map; + private _suggestions: ISuggestions; } /** diff --git a/packages/docprovider/src/yprovider.ts b/packages/docprovider/src/yprovider.ts index 22070979..72d031bb 100644 --- a/packages/docprovider/src/yprovider.ts +++ b/packages/docprovider/src/yprovider.ts @@ -90,14 +90,14 @@ export class WebSocketProvider implements IDocumentProvider { const response = await requestDocFork(`${session.format}:${session.type}:${session.fileId}`); const forkId = response.roomId; - this._sharedModel.roomId = forkId; + this._sharedModel.currentRoomId = forkId; this._sharedModel.addFork(forkId); return forkId; } connectFork(forkId: string) { - this._sharedModel.roomId = forkId; + this._sharedModel.currentRoomId = forkId; this._yWebsocketProvider?.disconnect(); this._yWebsocketProvider = new YWebsocketProvider( this._serverUrl, @@ -112,19 +112,20 @@ export class WebSocketProvider implements IDocumentProvider { } private async _connect(): Promise { - if (this._sharedModel.roomId === '') { + if (this._sharedModel.rootRoomId === '') { const session = await requestDocSession( this._format, this._contentType, this._path ); - this._sharedModel.roomId = `${session.format}:${session.type}:${session.fileId}`; + 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.roomId, + this._sharedModel.rootRoomId, this._sharedModel.ydoc, { disableBc: true, diff --git a/yarn.lock b/yarn.lock index 216d215e..b6af0e61 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2108,6 +2108,17 @@ __metadata: languageName: unknown linkType: soft +"@jupyter/react-components@npm:^0.15.2": + version: 0.15.2 + resolution: "@jupyter/react-components@npm:0.15.2" + dependencies: + "@jupyter/web-components": ^0.15.2 + "@microsoft/fast-react-wrapper": ^0.3.22 + react: ">=17.0.0 <19.0.0" + checksum: d6d339ff9c2fed1fd5afda612be500d73c4a83eee5470d50e94020dadd1e389a3bf745c7240b0a48edbc6d3fdacec93367b7b5e40588f2df588419caada705be + languageName: node + linkType: hard + "@jupyter/real-time-collaboration@workspace:.": version: 0.0.0-use.local resolution: "@jupyter/real-time-collaboration@workspace:." @@ -2131,17 +2142,58 @@ __metadata: languageName: unknown linkType: soft +"@jupyter/web-components@npm:^0.15.2": + version: 0.15.2 + resolution: "@jupyter/web-components@npm:0.15.2" + dependencies: + "@microsoft/fast-colors": ^5.3.1 + "@microsoft/fast-element": ^1.12.0 + "@microsoft/fast-foundation": ^2.49.4 + "@microsoft/fast-web-utilities": ^5.4.1 + checksum: f272ef91de08e28f9414a26dbd2388e1a8985c90f4ab00231978cee49bd5212f812411397a9038d298c8c0c4b41eb28cc86f1127bc7ace309bda8df60c4a87c8 + languageName: node + linkType: hard + "@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=d13236&locator=%40jupyter%2Freal-time-collaboration%40workspace%3A." + resolution: "@jupyter/ydoc@file:.yalc/@jupyter/ydoc#.yalc/@jupyter/ydoc::hash=7b1261&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 "@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: 4b906adf8d1eb9461463456f39be7225281d8f9f59213af58dad3d1383f2436760f7a6a60b6d87795e95662c6ff4323c41069644cf6833b2ad2d1310aa9a46fd + checksum: c1f8d5c6854f5854b2754200be67f117586736c08deb5edc6dc7ce419d734d8797e0d009554479ab340ac02cf94f6bd60cce0186f2d1e2df8097a6b9c607b14c + languageName: node + linkType: hard + +"@jupyterlab/application@npm:^4.0.0": + version: 4.1.2 + resolution: "@jupyterlab/application@npm:4.1.2" + dependencies: + "@fortawesome/fontawesome-free": ^5.12.0 + "@jupyterlab/apputils": ^4.2.2 + "@jupyterlab/coreutils": ^6.1.2 + "@jupyterlab/docregistry": ^4.1.2 + "@jupyterlab/rendermime": ^4.1.2 + "@jupyterlab/rendermime-interfaces": ^3.9.2 + "@jupyterlab/services": ^7.1.2 + "@jupyterlab/statedb": ^4.1.2 + "@jupyterlab/translation": ^4.1.2 + "@jupyterlab/ui-components": ^4.1.2 + "@lumino/algorithm": ^2.0.1 + "@lumino/application": ^2.3.0 + "@lumino/commands": ^2.2.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.1 + checksum: 10be4cdfd08dfd69786a3cd9753d48246e507e6a1cf15572bc4938be1d53c5c3fe291153ff02f2ffc941f77ab3c648649a6e6e2db6bbbcfcaa1bcc17a525b90b languageName: node linkType: hard @@ -2202,6 +2254,35 @@ __metadata: languageName: node linkType: hard +"@jupyterlab/apputils@npm:^4.2.2": + version: 4.2.2 + resolution: "@jupyterlab/apputils@npm:4.2.2" + dependencies: + "@jupyterlab/coreutils": ^6.1.2 + "@jupyterlab/observables": ^5.1.2 + "@jupyterlab/rendermime-interfaces": ^3.9.2 + "@jupyterlab/services": ^7.1.2 + "@jupyterlab/settingregistry": ^4.1.2 + "@jupyterlab/statedb": ^4.1.2 + "@jupyterlab/statusbar": ^4.1.2 + "@jupyterlab/translation": ^4.1.2 + "@jupyterlab/ui-components": ^4.1.2 + "@lumino/algorithm": ^2.0.1 + "@lumino/commands": ^2.2.0 + "@lumino/coreutils": ^2.1.2 + "@lumino/disposable": ^2.1.2 + "@lumino/domutils": ^2.0.1 + "@lumino/messaging": ^2.0.1 + "@lumino/signaling": ^2.1.2 + "@lumino/virtualdom": ^2.0.1 + "@lumino/widgets": ^2.3.1 + "@types/react": ^18.0.26 + react: ^18.2.0 + sanitize-html: ~2.7.3 + checksum: 6d0811f30ba353d9ce67d515fdfff802f99a628b7403b4b7aa44291d634bd228c0073ddab5ed6d160eb7bdc214b23e540039c1c5fd1f76ba9635b4ca3cca1d30 + languageName: node + linkType: hard + "@jupyterlab/attachments@npm:^4.0.5": version: 4.0.5 resolution: "@jupyterlab/attachments@npm:4.0.5" @@ -2316,6 +2397,30 @@ __metadata: languageName: node linkType: hard +"@jupyterlab/codeeditor@npm:^4.1.2": + version: 4.1.2 + resolution: "@jupyterlab/codeeditor@npm:4.1.2" + dependencies: + "@codemirror/state": ^6.2.0 + "@jupyter/ydoc": ^1.1.1 + "@jupyterlab/apputils": ^4.2.2 + "@jupyterlab/coreutils": ^6.1.2 + "@jupyterlab/nbformat": ^4.1.2 + "@jupyterlab/observables": ^5.1.2 + "@jupyterlab/statusbar": ^4.1.2 + "@jupyterlab/translation": ^4.1.2 + "@jupyterlab/ui-components": ^4.1.2 + "@lumino/coreutils": ^2.1.2 + "@lumino/disposable": ^2.1.2 + "@lumino/dragdrop": ^2.1.4 + "@lumino/messaging": ^2.0.1 + "@lumino/signaling": ^2.1.2 + "@lumino/widgets": ^2.3.1 + react: ^18.2.0 + checksum: b09618bc80a6d62c11fe53ccfe51cae1bdeb7298fab00c71a1119efa8102b86b6aa828d75b00ede80821b7073e3d6c3bb48c93805572eef4563308e4d3e7da1e + languageName: node + linkType: hard + "@jupyterlab/codemirror@npm:^4.0.5": version: 4.0.5 resolution: "@jupyterlab/codemirror@npm:4.0.5" @@ -2434,6 +2539,32 @@ __metadata: languageName: node linkType: hard +"@jupyterlab/docregistry@npm:^4.1.2": + version: 4.1.2 + resolution: "@jupyterlab/docregistry@npm:4.1.2" + dependencies: + "@jupyter/ydoc": ^1.1.1 + "@jupyterlab/apputils": ^4.2.2 + "@jupyterlab/codeeditor": ^4.1.2 + "@jupyterlab/coreutils": ^6.1.2 + "@jupyterlab/observables": ^5.1.2 + "@jupyterlab/rendermime": ^4.1.2 + "@jupyterlab/rendermime-interfaces": ^3.9.2 + "@jupyterlab/services": ^7.1.2 + "@jupyterlab/translation": ^4.1.2 + "@jupyterlab/ui-components": ^4.1.2 + "@lumino/algorithm": ^2.0.1 + "@lumino/coreutils": ^2.1.2 + "@lumino/disposable": ^2.1.2 + "@lumino/messaging": ^2.0.1 + "@lumino/properties": ^2.0.1 + "@lumino/signaling": ^2.1.2 + "@lumino/widgets": ^2.3.1 + react: ^18.2.0 + checksum: 6dcef927286f66636d8f7f0a128d7c5084f60f1fc9969d33080f3b905b08c63036bb99602d484b52530f6176242b224ed65444fd7cfc2df7d44f0b0dd039ac40 + languageName: node + linkType: hard + "@jupyterlab/documentsearch@npm:^4.0.5": version: 4.0.5 resolution: "@jupyterlab/documentsearch@npm:4.0.5" @@ -2621,6 +2752,19 @@ __metadata: languageName: node linkType: hard +"@jupyterlab/observables@npm:^5.1.2": + version: 5.1.2 + resolution: "@jupyterlab/observables@npm:5.1.2" + dependencies: + "@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 + checksum: 5bf7ec19c02d7d1923f4bf4c048f182957332a4e1f5481af980f976d518fd1590034cd529d7a980c228586b1650a796361a18b38b00bf6465ac0967ba6cdc8c0 + languageName: node + linkType: hard + "@jupyterlab/outputarea@npm:^4.0.5": version: 4.0.5 resolution: "@jupyterlab/outputarea@npm:4.0.5" @@ -2653,6 +2797,16 @@ __metadata: languageName: node linkType: hard +"@jupyterlab/rendermime-interfaces@npm:^3.9.2": + version: 3.9.2 + resolution: "@jupyterlab/rendermime-interfaces@npm:3.9.2" + dependencies: + "@lumino/coreutils": ^1.11.0 || ^2.1.2 + "@lumino/widgets": ^1.37.2 || ^2.3.1 + checksum: 65d6d4fe8c241b9f1267058db43a8fca01ee9fb6a67a267826accfdd0a9e71f2143fcad778b5c6d8b5bf825440ee9b040088253866e8e1a840b7276fba266b88 + languageName: node + linkType: hard + "@jupyterlab/rendermime@npm:^4.0.5": version: 4.0.5 resolution: "@jupyterlab/rendermime@npm:4.0.5" @@ -2673,11 +2827,31 @@ __metadata: languageName: node linkType: hard +"@jupyterlab/rendermime@npm:^4.1.2": + version: 4.1.2 + resolution: "@jupyterlab/rendermime@npm:4.1.2" + dependencies: + "@jupyterlab/apputils": ^4.2.2 + "@jupyterlab/coreutils": ^6.1.2 + "@jupyterlab/nbformat": ^4.1.2 + "@jupyterlab/observables": ^5.1.2 + "@jupyterlab/rendermime-interfaces": ^3.9.2 + "@jupyterlab/services": ^7.1.2 + "@jupyterlab/translation": ^4.1.2 + "@lumino/coreutils": ^2.1.2 + "@lumino/messaging": ^2.0.1 + "@lumino/signaling": ^2.1.2 + "@lumino/widgets": ^2.3.1 + lodash.escape: ^4.0.1 + checksum: bcd161b0d78d2fe1051eaf10bafb70ffacc44ae946ee331acbc6112ecf100995e07204fe00b9f5abb5d60b4fd5b6899eaad7a44a921af42c2c4f39abecee7ab7 + languageName: node + linkType: hard + "@jupyterlab/services@file:.yalc/@jupyterlab/services::locator=%40jupyter%2Freal-time-collaboration%40workspace%3A.": version: 7.1.2 - resolution: "@jupyterlab/services@file:.yalc/@jupyterlab/services#.yalc/@jupyterlab/services::hash=cec64c&locator=%40jupyter%2Freal-time-collaboration%40workspace%3A." + resolution: "@jupyterlab/services@file:.yalc/@jupyterlab/services#.yalc/@jupyterlab/services::hash=8aee87&locator=%40jupyter%2Freal-time-collaboration%40workspace%3A." dependencies: - "@jupyter/ydoc": ^1.1.1 + "@jupyter/ydoc": ^2.0.0 "@jupyterlab/coreutils": ^6.1.2 "@jupyterlab/nbformat": ^4.1.2 "@jupyterlab/settingregistry": ^4.1.2 @@ -2688,7 +2862,7 @@ __metadata: "@lumino/properties": ^2.0.1 "@lumino/signaling": ^2.1.2 ws: ^8.11.0 - checksum: 466c391f75583f033d62676e7e4d3d3db3062913f2b8a58a6323711cdf1d0f2e3061ed12d9965c220a5bb7c0b1d5bbac5cef9ec692198ad41b0d6311d77edf6c + checksum: 0d47267288649c414f30da20db1edf3aa2b348b8f9f6dceb8daee5a74b9102a75320682817c4f97736c748108849b947cc3c5ea05580668f6489252ada832a7b languageName: node linkType: hard @@ -2772,6 +2946,22 @@ __metadata: languageName: node linkType: hard +"@jupyterlab/statusbar@npm:^4.1.2": + version: 4.1.2 + resolution: "@jupyterlab/statusbar@npm:4.1.2" + dependencies: + "@jupyterlab/ui-components": ^4.1.2 + "@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.1 + react: ^18.2.0 + checksum: 4d3e23149cbb6ded2741af5c0cd60f26c37ab36f4bae1c43e847e16559b2a779de85c0474ccd81f0f3decd2d4e6019a202681989a06a095762ad85105f6c1458 + languageName: node + linkType: hard + "@jupyterlab/testing@npm:^4.0.5": version: 4.0.5 resolution: "@jupyterlab/testing@npm:4.0.5" @@ -2831,6 +3021,19 @@ __metadata: languageName: node linkType: hard +"@jupyterlab/translation@npm:^4.1.2": + version: 4.1.2 + resolution: "@jupyterlab/translation@npm:4.1.2" + dependencies: + "@jupyterlab/coreutils": ^6.1.2 + "@jupyterlab/rendermime-interfaces": ^3.9.2 + "@jupyterlab/services": ^7.1.2 + "@jupyterlab/statedb": ^4.1.2 + "@lumino/coreutils": ^2.1.2 + checksum: e8261be05ff642434b8c1b439305e464f6c38eea2d1cfbdb38d1ac4922d6df88f157dd1593674c0a3ed90082763bd313610187b1a5007027aa275ed8ed5301e1 + languageName: node + linkType: hard + "@jupyterlab/ui-components@npm:^4.0.5": version: 4.0.5 resolution: "@jupyterlab/ui-components@npm:4.0.5" @@ -2860,6 +3063,37 @@ __metadata: languageName: node linkType: hard +"@jupyterlab/ui-components@npm:^4.1.2": + version: 4.1.2 + resolution: "@jupyterlab/ui-components@npm:4.1.2" + dependencies: + "@jupyter/react-components": ^0.15.2 + "@jupyter/web-components": ^0.15.2 + "@jupyterlab/coreutils": ^6.1.2 + "@jupyterlab/observables": ^5.1.2 + "@jupyterlab/rendermime-interfaces": ^3.9.2 + "@jupyterlab/translation": ^4.1.2 + "@lumino/algorithm": ^2.0.1 + "@lumino/commands": ^2.2.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.1 + "@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: d4c0141802dc62bc9eb7f79b01c83b795798d131ff224653a0f8c63881e45e28c8de565303db2be34ba09ba42f5503c5979897ae5b46e8fe923e0fedc21cc8eb + languageName: node + linkType: hard + "@lerna/child-process@npm:6.6.1": version: 6.6.1 resolution: "@lerna/child-process@npm:6.6.1" @@ -3128,6 +3362,17 @@ __metadata: languageName: node linkType: hard +"@lumino/application@npm:^2.3.0": + version: 2.3.0 + resolution: "@lumino/application@npm:2.3.0" + dependencies: + "@lumino/commands": ^2.2.0 + "@lumino/coreutils": ^2.1.2 + "@lumino/widgets": ^2.3.1 + checksum: 9d1eb5bc972ed158bf219604a53bbac1262059bc5b0123d3e041974486b9cbb8288abeeec916f3b62f62d7c32e716cccf8b73e4832ae927e4f9dd4e4b0cd37ed + languageName: node + linkType: hard + "@lumino/collections@npm:^2.0.1": version: 2.0.1 resolution: "@lumino/collections@npm:2.0.1" @@ -3200,6 +3445,16 @@ __metadata: languageName: node linkType: hard +"@lumino/dragdrop@npm:^2.1.4": + version: 2.1.4 + resolution: "@lumino/dragdrop@npm:2.1.4" + dependencies: + "@lumino/coreutils": ^2.1.2 + "@lumino/disposable": ^2.1.2 + checksum: 43d82484b13b38b612e7dfb424a840ed6a38d0db778af10655c4ba235c67b5b12db1683929b35a36ab2845f77466066dfd1ee25c1c273e8e175677eba9dc560d + languageName: node + linkType: hard + "@lumino/keyboard@npm:^2.0.1": version: 2.0.1 resolution: "@lumino/keyboard@npm:2.0.1" @@ -3273,6 +3528,72 @@ __metadata: languageName: node linkType: hard +"@lumino/widgets@npm:^1.37.2 || ^2.3.1, @lumino/widgets@npm:^2.3.1": + version: 2.3.1 + resolution: "@lumino/widgets@npm:2.3.1" + dependencies: + "@lumino/algorithm": ^2.0.1 + "@lumino/commands": ^2.2.0 + "@lumino/coreutils": ^2.1.2 + "@lumino/disposable": ^2.1.2 + "@lumino/domutils": ^2.0.1 + "@lumino/dragdrop": ^2.1.4 + "@lumino/keyboard": ^2.0.1 + "@lumino/messaging": ^2.0.1 + "@lumino/properties": ^2.0.1 + "@lumino/signaling": ^2.1.2 + "@lumino/virtualdom": ^2.0.1 + checksum: ba7b8f8839c1cd2a41dbda13281094eb6981a270cccf4f25a0cf83686dcc526a2d8044a20204317630bb7dd4a04d65361408c7623a921549c781afca84b91c67 + languageName: node + linkType: hard + +"@microsoft/fast-colors@npm:^5.3.1": + version: 5.3.1 + resolution: "@microsoft/fast-colors@npm:5.3.1" + checksum: ff87f402faadb4b5aeee3d27762566c11807f927cd4012b8bbc7f073ca68de0e2197f95330ff5dfd7038f4b4f0e2f51b11feb64c5d570f5c598d37850a5daf60 + languageName: node + linkType: hard + +"@microsoft/fast-element@npm:^1.12.0": + version: 1.12.0 + resolution: "@microsoft/fast-element@npm:1.12.0" + checksum: bbff4e9c83106d1d74f3eeedc87bf84832429e78fee59c6a4ae8164ee4f42667503f586896bea72341b4d2c76c244a3cb0d4fd0d5d3732755f00357714dd609e + languageName: node + linkType: hard + +"@microsoft/fast-foundation@npm:^2.49.4, @microsoft/fast-foundation@npm:^2.49.5": + version: 2.49.5 + resolution: "@microsoft/fast-foundation@npm:2.49.5" + dependencies: + "@microsoft/fast-element": ^1.12.0 + "@microsoft/fast-web-utilities": ^5.4.1 + tabbable: ^5.2.0 + tslib: ^1.13.0 + checksum: 8a4729e8193ee93f780dc88fac26561b42f2636e3f0a8e89bb1dfe256f50a01a21ed1d8e4d31ce40678807dc833e25f31ba735cb5d3c247b65219aeb2560c82c + languageName: node + linkType: hard + +"@microsoft/fast-react-wrapper@npm:^0.3.22": + version: 0.3.23 + resolution: "@microsoft/fast-react-wrapper@npm:0.3.23" + dependencies: + "@microsoft/fast-element": ^1.12.0 + "@microsoft/fast-foundation": ^2.49.5 + peerDependencies: + react: ">=16.9.0" + checksum: 45885e1868916d2aa9059e99c341c97da434331d9340a57128d4218081df68b5e1107031c608db9a550d6d1c3b010d516ed4f8dc5a8a2470058da6750dcd204a + languageName: node + linkType: hard + +"@microsoft/fast-web-utilities@npm:^5.4.1": + version: 5.4.1 + resolution: "@microsoft/fast-web-utilities@npm:5.4.1" + dependencies: + exenv-es6: ^1.1.1 + checksum: 303e87847f962944f474e3716c3eb305668243916ca9e0719e26bb9a32346144bc958d915c103776b3e552cea0f0f6233f839fad66adfdf96a8436b947288ca7 + languageName: node + linkType: hard + "@nodelib/fs.scandir@npm:2.1.5": version: 2.1.5 resolution: "@nodelib/fs.scandir@npm:2.1.5" @@ -3843,6 +4164,22 @@ __metadata: languageName: node linkType: hard +"@rjsf/core@npm:^5.13.4": + version: 5.17.1 + resolution: "@rjsf/core@npm:5.17.1" + dependencies: + lodash: ^4.17.21 + lodash-es: ^4.17.21 + markdown-to-jsx: ^7.4.1 + nanoid: ^3.3.7 + prop-types: ^15.8.1 + peerDependencies: + "@rjsf/utils": ^5.16.x + react: ^16.14.0 || >=17 + checksum: 2dead2886a4db152d259d3e85281c1fa5975eeac5f05c2840201ccc583ef1cf9d48c922cd404d515133e140eae7a8fca4aa63ccde0bcfe63d0b3fbe3cd621aed + languageName: node + linkType: hard + "@rjsf/utils@npm:^5.1.0": version: 5.6.2 resolution: "@rjsf/utils@npm:5.6.2" @@ -6799,6 +7136,13 @@ __metadata: languageName: node linkType: hard +"exenv-es6@npm:^1.1.1": + version: 1.1.1 + resolution: "exenv-es6@npm:1.1.1" + checksum: 7f2aa12025e6f06c48dc286f380cf3183bb19c6017b36d91695034a3e5124a7235c4f8ff24ca2eb88ae801322f0f99605cedfcfd996a5fcbba7669320e2a448e + languageName: node + linkType: hard + "exit@npm:^0.1.2": version: 0.1.2 resolution: "exit@npm:0.1.2" @@ -9647,6 +9991,15 @@ __metadata: languageName: node linkType: hard +"markdown-to-jsx@npm:^7.4.1": + version: 7.4.1 + resolution: "markdown-to-jsx@npm:7.4.1" + peerDependencies: + react: ">= 0.14.0" + checksum: 2888cb2389cb810ab35454a59d0623474a60a78e28f281ae0081f87053f6c59b033232a2cd269cc383a5edcaa1eab8ca4b3cf639fe4e1aa3fb418648d14bcc7d + languageName: node + linkType: hard + "marked@npm:^4.2.12": version: 4.3.0 resolution: "marked@npm:4.3.0" @@ -10018,6 +10371,15 @@ __metadata: languageName: node linkType: hard +"nanoid@npm:^3.3.7": + version: 3.3.7 + resolution: "nanoid@npm:3.3.7" + bin: + nanoid: bin/nanoid.cjs + checksum: d36c427e530713e4ac6567d488b489a36582ef89da1d6d4e3b87eded11eb10d7042a877958c6f104929809b2ab0bafa17652b076cdf84324aa75b30b722204f2 + languageName: node + linkType: hard + "napi-macros@npm:~2.0.0": version: 2.0.0 resolution: "napi-macros@npm:2.0.0" @@ -11444,7 +11806,7 @@ __metadata: languageName: node linkType: hard -"react@npm:^18.2.0": +"react@npm:>=17.0.0 <19.0.0, react@npm:^18.2.0": version: 18.2.0 resolution: "react@npm:18.2.0" dependencies: @@ -12646,6 +13008,13 @@ __metadata: languageName: node linkType: hard +"tabbable@npm:^5.2.0": + version: 5.3.3 + resolution: "tabbable@npm:5.3.3" + checksum: 1aa56e1bb617cc10616c407f4e756f0607f3e2d30f9803664d70b85db037ca27e75918ed1c71443f3dc902e21dc9f991ce4b52d63a538c9b69b3218d3babcd70 + languageName: node + linkType: hard + "table@npm:^6.8.1": version: 6.8.1 resolution: "table@npm:6.8.1" @@ -12943,7 +13312,7 @@ __metadata: languageName: node linkType: hard -"tslib@npm:^1.8.1": +"tslib@npm:^1.13.0, tslib@npm:^1.8.1": version: 1.14.1 resolution: "tslib@npm:1.14.1" checksum: dbe628ef87f66691d5d2959b3e41b9ca0045c3ee3c7c7b906cc1e328b39f199bb1ad9e671c39025bd56122ac57dfbf7385a94843b1cc07c60a4db74795829acd From 7c8ea7c75d9bc01c15987202e8626c0396918fbe Mon Sep 17 00:00:00 2001 From: David Brochart Date: Thu, 7 Mar 2024 18:23:19 +0100 Subject: [PATCH 05/11] - --- .../src/collaboration.ts | 53 +---------- .../src/filebrowser.ts | 9 +- packages/collaboration-extension/src/index.ts | 6 +- packages/collaboration/src/index.ts | 2 - .../collaboration/src/suggestionspanel.tsx | 92 ------------------- packages/collaboration/src/tokens.ts | 7 -- packages/docprovider/src/ydrive.ts | 21 +---- yarn.lock | 4 +- 8 files changed, 13 insertions(+), 181 deletions(-) delete mode 100644 packages/collaboration/src/suggestionspanel.tsx diff --git a/packages/collaboration-extension/src/collaboration.ts b/packages/collaboration-extension/src/collaboration.ts index 9ee439e0..ce750985 100644 --- a/packages/collaboration-extension/src/collaboration.ts +++ b/packages/collaboration-extension/src/collaboration.ts @@ -41,17 +41,16 @@ import { ITranslator, nullTranslator, TranslationBundle } from '@jupyterlab/tran import { Menu, MenuBar } from '@lumino/widgets'; -import { IAwareness, ISharedNotebook, ISuggestions, NotebookChange } from '@jupyter/ydoc'; +import { IAwareness, ISharedNotebook, NotebookChange } from '@jupyter/ydoc'; import { CollaboratorsPanel, - SuggestionsPanel, IGlobalAwareness, IUserMenu, remoteUserCursors, RendererUserMenu, UserInfoPanel, - UserMenu, + UserMenu } from '@jupyter/collaboration'; import * as Y from 'yjs'; @@ -148,12 +147,11 @@ export const rtcPanelPlugin: JupyterFrontEndPlugin = { id: '@jupyter/collaboration-extension:rtcPanel', description: 'Add side panel to display all currently connected users.', autoStart: true, - requires: [IGlobalAwareness, ISuggestions], + requires: [IGlobalAwareness], optional: [ITranslator], activate: ( app: JupyterFrontEnd, awareness: Awareness, - suggestions: ISuggestions, translator: ITranslator | null ): void => { const { user } = app.serviceManager; @@ -185,10 +183,6 @@ export const rtcPanelPlugin: JupyterFrontEndPlugin = { ); collaboratorsPanel.title.label = trans.__('Online Collaborators'); userPanel.addWidget(collaboratorsPanel); - - const suggestionsPanel = new SuggestionsPanel(fileopener, suggestions); - suggestionsPanel.title.label = trans.__('Suggestions'); - userPanel.addWidget(suggestionsPanel); } }; @@ -306,8 +300,6 @@ export class EditingModeExtension implements DocumentRegistry.IWidgetExtension { - console.log('currentRoomId', context.model.sharedModel.currentRoomId); - console.log('rootRoomId', context.model.sharedModel.rootRoomId); requestDocMerge(context.model.sharedModel.currentRoomId, context.model.sharedModel.rootRoomId); } }); @@ -396,42 +388,3 @@ export class EditingModeExtension implements DocumentRegistry.IWidgetExtension = { - id: '@jupyter/collaboration-extension:rtcGlobalSuggestions', - description: 'A plugin to provide shared document suggestions.', - autoStart: true, - provides: ISuggestions, - activate: (app: JupyterFrontEnd): ISuggestions => { - console.log('suggestions plugin activated'); - return new Suggestions(); - }, -}; - -export class Suggestions implements ISuggestions { - private _forkIds: string[]; - private _callbacks: any[]; - - constructor() { - this._forkIds = []; - this._callbacks = []; - } - - addFork(forkId: string) { - this._forkIds.push(forkId); - for (const callback of this._callbacks) { - callback(forkId); - } - } - - addCallback(callback: any) { - this._callbacks.push(callback); - } - - get forks(): string[] { - return this._forkIds; - } -} diff --git a/packages/collaboration-extension/src/filebrowser.ts b/packages/collaboration-extension/src/filebrowser.ts index a3ecb15f..75348215 100644 --- a/packages/collaboration-extension/src/filebrowser.ts +++ b/packages/collaboration-extension/src/filebrowser.ts @@ -24,7 +24,7 @@ import { ITranslator, nullTranslator } from '@jupyterlab/translation'; import { CommandRegistry } from '@lumino/commands'; -import { ISuggestions, YFile, YNotebook } from '@jupyter/ydoc'; +import { YFile, YNotebook } from '@jupyter/ydoc'; import { ICollaborativeDrive, YDrive } from '@jupyter/docprovider'; @@ -42,15 +42,14 @@ export const drive: JupyterFrontEndPlugin = { id: '@jupyter/collaboration-extension:drive', description: 'The default collaborative drive provider', provides: ICollaborativeDrive, - requires: [ITranslator, ISuggestions], + requires: [ITranslator], optional: [], activate: ( app: JupyterFrontEnd, - translator: ITranslator, - suggestions: ISuggestions + translator: ITranslator ): ICollaborativeDrive => { const trans = translator.load('jupyter_collaboration'); - const drive = new YDrive(app.serviceManager.user, trans, suggestions); + const drive = new YDrive(app.serviceManager.user, trans); app.serviceManager.contents.addDrive(drive); return drive; } diff --git a/packages/collaboration-extension/src/index.ts b/packages/collaboration-extension/src/index.ts index e268aaf9..3e1be19b 100644 --- a/packages/collaboration-extension/src/index.ts +++ b/packages/collaboration-extension/src/index.ts @@ -20,8 +20,7 @@ import { rtcGlobalAwarenessPlugin, rtcPanelPlugin, userEditorCursors, - editingMode, - suggestions + editingMode } from './collaboration'; import { sharedLink } from './sharedlink'; @@ -40,8 +39,7 @@ const plugins: JupyterFrontEndPlugin[] = [ rtcPanelPlugin, sharedLink, userEditorCursors, - editingMode, - suggestions + editingMode ]; export default plugins; diff --git a/packages/collaboration/src/index.ts b/packages/collaboration/src/index.ts index ef38378c..05e4fff7 100644 --- a/packages/collaboration/src/index.ts +++ b/packages/collaboration/src/index.ts @@ -7,8 +7,6 @@ export * from './tokens'; export * from './collaboratorspanel'; -export * from './suggestionspanel'; -//export * from './suggestions'; export * from './cursors'; export * from './menu'; export * from './sharedlink'; diff --git a/packages/collaboration/src/suggestionspanel.tsx b/packages/collaboration/src/suggestionspanel.tsx deleted file mode 100644 index ed12289a..00000000 --- a/packages/collaboration/src/suggestionspanel.tsx +++ /dev/null @@ -1,92 +0,0 @@ -// Copyright (c) Jupyter Development Team. -// Distributed under the terms of the Modified BSD License. - -import * as React from 'react'; - -import { Panel } from '@lumino/widgets'; - -import { ReactWidget } from '@jupyterlab/apputils'; - -//import { PathExt } from '@jupyterlab/coreutils'; - -import { ISuggestions } from '@jupyter/ydoc'; - -//import { ICollaboratorAwareness } from './tokens'; - -export class SuggestionsPanel extends Panel { - private _body: SuggestionsBody; - - constructor(fileopener: (path: string) => void, suggestions: ISuggestions) { - super({}); - - this._body = new SuggestionsBody(fileopener, suggestions); - this.addWidget(this._body); - this.update(); - } -} - -/** - * The suggestions list. - */ -export class SuggestionsBody extends ReactWidget { - private _suggestions: ISuggestions; - //private _fileopener: (path: string) => void; - - constructor(fileopener: (path: string) => void, suggestions: ISuggestions) { - super(); - //this._fileopener = fileopener; - suggestions.addCallback((forkId: string) => { this.update(); }); - this._suggestions = suggestions; - } - - render(): React.ReactElement[] { - return this._suggestions.forks.map((value, i) => { - // let canOpenCurrent = false; - // let current = ''; - // let separator = ''; - // let currentFileLocation = ''; - - // if (value.current) { - // canOpenCurrent = true; - // const path = value.current.split(':'); - // currentFileLocation = `${path[1]}:${path[2]}`; - - // current = PathExt.basename(path[2]); - // current = - // current.length > 25 ? current.slice(0, 12).concat('…') : current; - // separator = '•'; - // } - - // const onClick = () => { - // if (canOpenCurrent) { - // this._fileopener(currentFileLocation); - // } - // }; - - // const displayName = `${value.user.display_name} ${separator} ${current}`; - - // return ( - //
- //
- // {value.user.initials} - //
- // {displayName} - //
- // ); - return ( -
-
- {value} -
-
- ); - }); - } -} diff --git a/packages/collaboration/src/tokens.ts b/packages/collaboration/src/tokens.ts index 42cbbec3..5f717390 100644 --- a/packages/collaboration/src/tokens.ts +++ b/packages/collaboration/src/tokens.ts @@ -24,13 +24,6 @@ export const IGlobalAwareness = new Token( '@jupyter/collaboration:IGlobalAwareness' ); -///** -// * The global suggestions token. -// */ -//export const IGlobalSuggestions = new Token( -// '@jupyter/collaboration:IGlobalSuggestions' -//); -// /** * An interface describing the user menu. */ diff --git a/packages/docprovider/src/ydrive.ts b/packages/docprovider/src/ydrive.ts index c31f05d8..aab5b43c 100644 --- a/packages/docprovider/src/ydrive.ts +++ b/packages/docprovider/src/ydrive.ts @@ -5,7 +5,7 @@ import { PageConfig, URLExt } from '@jupyterlab/coreutils'; import { TranslationBundle } from '@jupyterlab/translation'; import { Contents, Drive, User } from '@jupyterlab/services'; -import { DocumentChange, ISharedDocument, YDocument, ISuggestions } from '@jupyter/ydoc'; +import { DocumentChange, ISharedDocument, YDocument } from '@jupyter/ydoc'; import { WebSocketProvider } from './yprovider'; import { @@ -14,8 +14,6 @@ import { SharedDocumentFactory } from './tokens'; -import * as Y from 'yjs'; - const DISABLE_RTC = PageConfig.getOption('disableRTC') === 'true' ? true : false; @@ -34,12 +32,11 @@ export class YDrive extends Drive implements ICollaborativeDrive { * * @param user - The user manager to add the identity to the awareness of documents. */ - constructor(user: User.IManager, translator: TranslationBundle, suggestions: ISuggestions) { + constructor(user: User.IManager, translator: TranslationBundle) { super({ name: 'RTC' }); this._user = user; this._trans = translator; this._providers = new Map(); - this._suggestions = suggestions; this.sharedModelFactory = new SharedModelFactory(this._onCreate); } @@ -129,18 +126,6 @@ export class YDrive extends Drive implements ICollaborativeDrive { return super.save(localPath, options); } - private _handleForks = (event: Y.YMapEvent) => { - const forkPrefix = 'fork_'; - event.changes.keys.forEach((change, key) => { - if (change.action === 'add') { - if (key.startsWith(forkPrefix)) { - const forkId = key.slice(forkPrefix.length); - this._suggestions.addFork(forkId); - } - } - }); - }; - private _onCreate = ( options: Contents.ISharedFactoryOptions, sharedModel: YDocument @@ -162,7 +147,6 @@ export class YDrive extends Drive implements ICollaborativeDrive { const key = `${options.format}:${options.contentType}:${options.path}`; this._providers.set(key, provider); - sharedModel.ystate.observe(this._handleForks); sharedModel.provider = provider; sharedModel.disposed.connect(() => { const provider = this._providers.get(key); @@ -183,7 +167,6 @@ export class YDrive extends Drive implements ICollaborativeDrive { private _user: User.IManager; private _trans: TranslationBundle; private _providers: Map; - private _suggestions: ISuggestions; } /** diff --git a/yarn.lock b/yarn.lock index b6af0e61..e2103696 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=7b1261&locator=%40jupyter%2Freal-time-collaboration%40workspace%3A." + resolution: "@jupyter/ydoc@file:.yalc/@jupyter/ydoc#.yalc/@jupyter/ydoc::hash=63b59a&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: c1f8d5c6854f5854b2754200be67f117586736c08deb5edc6dc7ce419d734d8797e0d009554479ab340ac02cf94f6bd60cce0186f2d1e2df8097a6b9c607b14c + checksum: 9e034badb962ea9d26e8ed279feb7fb85130d11920841e8cdeb930586c23c1b27071a32bf78211043c6dcc1f303736e3abe11f2bfc33cfbc024a2f2cf8e7bd2f languageName: node linkType: hard From 3354e1f4d872cb9d2d01018e63831059ccc8aaba Mon Sep 17 00:00:00 2001 From: David Brochart Date: Mon, 11 Mar 2024 11:29:33 +0100 Subject: [PATCH 06/11] Add fork_room and merge_room handlers --- jupyter_collaboration/app.py | 16 ++++- jupyter_collaboration/handlers.py | 68 ++++++++++++++++++- .../src/collaboration.ts | 51 +++++++++----- packages/collaboration/style/base.css | 19 ------ 4 files changed, 114 insertions(+), 40 deletions(-) diff --git a/jupyter_collaboration/app.py b/jupyter_collaboration/app.py index 2c35982e..cb19a176 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 DocSessionHandler, YDocWebSocketHandler +from .handlers import DocForkHandler, DocMergeHandler, DocSessionHandler, YDocWebSocketHandler from .loaders import FileLoaderMapping from .stores import SQLiteYStore from .utils import AWARENESS_EVENTS_SCHEMA_PATH, EVENTS_SCHEMA_PATH @@ -109,6 +109,20 @@ def initialize_handlers(self): }, ), (r"/api/collaboration/session/(.*)", DocSessionHandler), + ( + r"/api/collaboration/fork_room/(.*)", + DocForkHandler, + { + "ywebsocket_server": self.ywebsocket_server, + } + ), + ( + r"/api/collaboration/merge_room", + DocMergeHandler, + { + "ywebsocket_server": self.ywebsocket_server, + } + ), ] ) diff --git a/jupyter_collaboration/handlers.py b/jupyter_collaboration/handlers.py index 81b64b3d..3c5ddc80 100644 --- a/jupyter_collaboration/handlers.py +++ b/jupyter_collaboration/handlers.py @@ -6,12 +6,13 @@ import asyncio import json import time -import uuid +from uuid import uuid4 from typing import Any from jupyter_server.auth import authorized from jupyter_server.base.handlers import APIHandler, JupyterHandler from jupyter_ydoc import ydocs as YDOCS +from pycrdt import Doc from pycrdt_websocket.websocket_server import YRoom from pycrdt_websocket.ystore import BaseYStore from pycrdt_websocket.yutils import YMessageType, write_var_uint @@ -31,7 +32,7 @@ YFILE = YDOCS["file"] -SERVER_SESSION = str(uuid.uuid4()) +SERVER_SESSION = str(uuid4()) class YDocWebSocketHandler(WebSocketHandler, JupyterHandler): @@ -404,3 +405,66 @@ async def put(self, path): ) self.set_status(201) return self.finish(data) + + +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(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_room = await self._websocket_server.get_room(model["fork_roomid"]) + root_room = await self._websocket_server.get_room(model["root_roomid"]) + update = fork_room.ydoc.get_update() + root_room.ydoc.apply_update(update) + self.set_status(200) diff --git a/packages/collaboration-extension/src/collaboration.ts b/packages/collaboration-extension/src/collaboration.ts index ce750985..e863b2e5 100644 --- a/packages/collaboration-extension/src/collaboration.ts +++ b/packages/collaboration-extension/src/collaboration.ts @@ -247,6 +247,7 @@ export class EditingModeExtension implements DocumentRegistry.IWidgetExtension { editingMenu.title.label = 'Editing'; suggestionMenu.title.label = 'Root'; + open_dialog('Editing', this._trans); } }); editingCommands.addCommand('suggesting', { @@ -272,17 +274,17 @@ export class EditingModeExtension implements DocumentRegistry.IWidgetExtension { + sharedModel.provider.fork().then(newForkId => { myForkId = newForkId; - provider.connectFork(newForkId); + sharedModel.provider.connectFork(newForkId); suggestionMenu.title.label = newForkId; }); } else { suggestionMenu.title.label = myForkId; - context.model.sharedModel.provider.connectFork(myForkId); + sharedModel.provider.connectFork(myForkId); } + open_dialog('Suggesting', this._trans); } }); @@ -293,14 +295,15 @@ export class EditingModeExtension implements DocumentRegistry.IWidgetExtension { - requestDocMerge(context.model.sharedModel.currentRoomId, context.model.sharedModel.rootRoomId); + requestDocMerge(sharedModel.currentRoomId, sharedModel.rootRoomId); } }); reviewCommands.addCommand('discard', { @@ -336,29 +339,24 @@ export class EditingModeExtension implements DocumentRegistry.IWidgetExtension { dialog.close(); }); + sharedModel.provider.connectFork(newForkId); + open_dialog('Suggesting', this._trans); } }); suggestionMenu.addItem({type: 'command', command: newForkId}); if ((myForkId !== 'pending') && (myForkId !== newForkId)) { const dialog = new Dialog({ title: this._trans.__('New suggestion'), - body: this._trans.__('Open notebook for suggestion?'), + body: this._trans.__('View suggestion?'), buttons: [ - Dialog.okButton({ label: 'Open' }), + Dialog.okButton({ label: 'View' }), Dialog.cancelButton({ label: 'Discard' }), ], }); dialog.launch().then(resp => { dialog.close(); - if (resp.button.label === 'Open') { - context.model.sharedModel.provider.connectFork(newForkId); + if (resp.button.label === 'View') { + sharedModel.provider.connectFork(newForkId); suggestionMenu.title.label = newForkId; editingMenu.title.label = 'Editing'; reviewMenu.clearItems(); @@ -372,7 +370,7 @@ export class EditingModeExtension implements DocumentRegistry.IWidgetExtension { dialog.close(); }); +} diff --git a/packages/collaboration/style/base.css b/packages/collaboration/style/base.css index a79e836c..41d787b5 100644 --- a/packages/collaboration/style/base.css +++ b/packages/collaboration/style/base.css @@ -9,22 +9,3 @@ .jp-shared-link-body { user-select: none; } - -.jp-EditingMode { - display: flex; - flex-direction: column; - align-items: center; - justify-content: center; -} - -.jp-EditingMode .lm-MenuBar-itemIcon svg { - vertical-align: sub; -} - -.jp-nb-editing-mode-button > .jp-ToolbarButtonComponent::part(content) { - flex-direction: row-reverse; -} - -.jp-nb-editing-mode-button > .jp-ToolbarButtonComponent > svg { - padding-left: 3px; -} From adafa0a9489938126738f21fed2ba1122bba8d3b Mon Sep 17 00:00:00 2001 From: David Brochart Date: Tue, 12 Mar 2024 14:54:15 +0100 Subject: [PATCH 07/11] Finalize merge --- jupyter_collaboration/handlers.py | 25 +++- .../src/collaboration.ts | 111 +++++++++++------- 2 files changed, 89 insertions(+), 47 deletions(-) diff --git a/jupyter_collaboration/handlers.py b/jupyter_collaboration/handlers.py index 3c5ddc80..3c0a205e 100644 --- a/jupyter_collaboration/handlers.py +++ b/jupyter_collaboration/handlers.py @@ -12,8 +12,8 @@ from jupyter_server.auth import authorized from jupyter_server.base.handlers import APIHandler, JupyterHandler from jupyter_ydoc import ydocs as YDOCS -from pycrdt import Doc -from pycrdt_websocket.websocket_server import YRoom +from pycrdt import Doc, Map +from pycrdt_websocket.yroom import YRoom from pycrdt_websocket.ystore import BaseYStore from pycrdt_websocket.yutils import YMessageType, write_var_uint from tornado import web @@ -432,7 +432,7 @@ async def put(self, room_id): update = root_room.ydoc.get_update() fork_ydoc = Doc() fork_ydoc.apply_update(update) - fork_room = YRoom(fork_ydoc) + fork_room = YRoom(ydoc=fork_ydoc) self._websocket_server.add_room(idx, fork_room) root_room.fork_ydocs.add(fork_ydoc) data = json.dumps({ @@ -463,8 +463,21 @@ async def put(self): Merges back a fork into a root document. """ model = self.get_json_body() - fork_room = await self._websocket_server.get_room(model["fork_roomid"]) + fork_roomid = model["fork_roomid"] root_room = await self._websocket_server.get_room(model["root_roomid"]) - update = fork_room.ydoc.get_update() - root_room.ydoc.apply_update(update) + 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: + 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) + 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) diff --git a/packages/collaboration-extension/src/collaboration.ts b/packages/collaboration-extension/src/collaboration.ts index e863b2e5..342bcbca 100644 --- a/packages/collaboration-extension/src/collaboration.ts +++ b/packages/collaboration-extension/src/collaboration.ts @@ -248,6 +248,7 @@ export class EditingModeExtension implements DocumentRegistry.IWidgetExtension { const forkPrefix = 'fork_'; - if (value.name.startsWith(forkPrefix)) { - const newForkId = value.name.slice(forkPrefix.length); - suggestionCommands.addCommand(newForkId, { - label: newForkId, - execute: () => { - if (myForkId === newForkId) { - editingMenu.title.label = 'Suggesting'; - // our suggestion, cannot be reviewed - reviewMenu.clearItems(); - } - else { - editingMenu.title.label = 'Editing'; - // not our suggestion, can be reviewed - reviewMenu.clearItems(); - reviewMenu.addItem({type: 'command', command: 'merge'}); - reviewMenu.addItem({type: 'command', command: 'discard'}); - } - suggestionMenu.title.label = newForkId; - sharedModel.provider.connectFork(newForkId); - open_dialog('Suggesting', this._trans); - } - }); - suggestionMenu.addItem({type: 'command', command: newForkId}); - if ((myForkId !== 'pending') && (myForkId !== newForkId)) { - 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.connectFork(newForkId); - suggestionMenu.title.label = newForkId; - editingMenu.title.label = 'Editing'; - reviewMenu.clearItems(); - reviewMenu.addItem({type: 'command', command: 'merge'}); - reviewMenu.addItem({type: 'command', command: 'discard'}); + if (value.name === 'merge') { + // FIXME: a client who is not connected to the fork should not see this update + if (sharedModel.currentRoomId === value.newValue) { + editingMenu.title.label = 'Editing'; + suggestionMenu.title.label = 'Root'; + const item: Menu.IItem = suggestions[value.newValue]; + delete suggestions[value.newValue]; + suggestionMenu.removeItem(item); + reviewMenu.clearItems(); + sharedModel.provider.connectFork(sharedModel.rootRoomId); + open_dialog('Editing', this._trans); + } + } + else if (value.name.startsWith(forkPrefix)) { + const forkId = value.name.slice(forkPrefix.length); + if (value.newValue === 'new') { + suggestionCommands.addCommand(forkId, { + label: forkId, + execute: () => { + if (myForkId === forkId) { + editingMenu.title.label = 'Suggesting'; + // our suggestion, cannot be reviewed + reviewMenu.clearItems(); + } + else { + editingMenu.title.label = 'Editing'; + // not our suggestion, can be reviewed + reviewMenu.clearItems(); + reviewMenu.addItem({type: 'command', command: 'merge'}); + reviewMenu.addItem({type: 'command', command: 'discard'}); + } + suggestionMenu.title.label = forkId; + sharedModel.provider.connectFork(forkId); + open_dialog('Suggesting', this._trans); } }); + const item = suggestionMenu.addItem({type: 'command', command: forkId}); + suggestions[forkId] = item; + if ((myForkId !== 'pending') && (myForkId !== forkId)) { + 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.connectFork(forkId); + suggestionMenu.title.label = forkId; + editingMenu.title.label = 'Editing'; + reviewMenu.clearItems(); + reviewMenu.addItem({type: 'command', command: 'merge'}); + reviewMenu.addItem({type: 'command', command: 'discard'}); + } + }); + } + } + else if (value.newValue === undefined) { + if (sharedModel.currentRoomId === forkId) { + editingMenu.title.label = 'Editing'; + suggestionMenu.title.label = 'Root'; + const item: Menu.IItem = suggestions[value.newValue]; + delete suggestions[value.newValue]; + suggestionMenu.removeItem(item); + reviewMenu.clearItems(); + sharedModel.provider.connectFork(sharedModel.rootRoomId); + open_dialog('Editing', this._trans); + } } } }); From 58b854cd4dc2d7c6997b5a7fa501a6ef59a20001 Mon Sep 17 00:00:00 2001 From: David Brochart Date: Mon, 18 Mar 2024 10:58:07 +0100 Subject: [PATCH 08/11] Rename connectFork to connect --- .../collaboration-extension/src/collaboration.ts | 14 +++++++------- packages/docprovider/src/yprovider.ts | 6 +++--- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/packages/collaboration-extension/src/collaboration.ts b/packages/collaboration-extension/src/collaboration.ts index 342bcbca..006ff5cb 100644 --- a/packages/collaboration-extension/src/collaboration.ts +++ b/packages/collaboration-extension/src/collaboration.ts @@ -277,13 +277,13 @@ export class EditingModeExtension implements DocumentRegistry.IWidgetExtension { myForkId = newForkId; - sharedModel.provider.connectFork(newForkId); + sharedModel.provider.connect(newForkId); suggestionMenu.title.label = newForkId; }); } else { suggestionMenu.title.label = myForkId; - sharedModel.provider.connectFork(myForkId); + sharedModel.provider.connect(myForkId); } open_dialog('Suggesting', this._trans); } @@ -296,7 +296,7 @@ export class EditingModeExtension implements DocumentRegistry.IWidgetExtension { dialog.close(); if (resp.button.label === 'View') { - sharedModel.provider.connectFork(forkId); + sharedModel.provider.connect(forkId); suggestionMenu.title.label = forkId; editingMenu.title.label = 'Editing'; reviewMenu.clearItems(); @@ -390,7 +390,7 @@ export class EditingModeExtension implements DocumentRegistry.IWidgetExtension Date: Thu, 28 Mar 2024 11:21:47 +0100 Subject: [PATCH 09/11] - --- package.json | 3 +- yarn.lock | 77 ++++++++++++++++++++++++++++++++++++++++++++-------- 2 files changed, 67 insertions(+), 13 deletions(-) diff --git a/package.json b/package.json index b9fe7b34..6b8906de 100644 --- a/package.json +++ b/package.json @@ -67,7 +67,6 @@ "typescript": "~5.0.4" }, "resolutions": { - "@jupyter/ydoc": "file:.yalc/@jupyter/ydoc", - "@jupyterlab/services": "file:.yalc/@jupyterlab/services" + "@jupyter/ydoc": "file:.yalc/@jupyter/ydoc" } } diff --git a/yarn.lock b/yarn.lock index e2103696..a8ca2ec0 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=63b59a&locator=%40jupyter%2Freal-time-collaboration%40workspace%3A." + resolution: "@jupyter/ydoc@file:.yalc/@jupyter/ydoc#.yalc/@jupyter/ydoc::hash=e50509&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: 9e034badb962ea9d26e8ed279feb7fb85130d11920841e8cdeb930586c23c1b27071a32bf78211043c6dcc1f303736e3abe11f2bfc33cfbc024a2f2cf8e7bd2f + checksum: c54e335aebc1f0b28241fe0031d5f47513a7a90621b5cca10e6aec3a965adea96390bd8e2c51191e448a9c276ca284ff563eb4870844233798e79a03560b1648 languageName: node linkType: hard @@ -2491,6 +2491,20 @@ __metadata: languageName: node linkType: hard +"@jupyterlab/coreutils@npm:^6.1.5": + version: 6.1.5 + resolution: "@jupyterlab/coreutils@npm:6.1.5" + dependencies: + "@lumino/coreutils": ^2.1.2 + "@lumino/disposable": ^2.1.2 + "@lumino/signaling": ^2.1.2 + minimist: ~1.2.0 + path-browserify: ^1.0.0 + url-parse: ~1.5.4 + checksum: b91c5a374f3c97d62e2442bb5f12cb79c6e440b5f6aa4d4ed6e492e8ca38836f7068106bb7029834a4e5de1947a9c44c342d23bedf9a4611aafca33629aed049 + languageName: node + linkType: hard + "@jupyterlab/docmanager@npm:^4.0.5": version: 4.0.5 resolution: "@jupyterlab/docmanager@npm:4.0.5" @@ -2703,6 +2717,15 @@ __metadata: languageName: node linkType: hard +"@jupyterlab/nbformat@npm:^4.1.5": + version: 4.1.5 + resolution: "@jupyterlab/nbformat@npm:4.1.5" + dependencies: + "@lumino/coreutils": ^2.1.2 + checksum: d417d7eade40d389fea8593358b6455158cf3e67fa40c0c4c05c865852520acc466102109723c9cb16eecf95952617d79f7fe6be9da6ca3f601749bdecdfda97 + languageName: node + linkType: hard + "@jupyterlab/notebook@npm:^4.0.5": version: 4.0.5 resolution: "@jupyterlab/notebook@npm:4.0.5" @@ -2847,22 +2870,22 @@ __metadata: languageName: node linkType: hard -"@jupyterlab/services@file:.yalc/@jupyterlab/services::locator=%40jupyter%2Freal-time-collaboration%40workspace%3A.": - version: 7.1.2 - resolution: "@jupyterlab/services@file:.yalc/@jupyterlab/services#.yalc/@jupyterlab/services::hash=8aee87&locator=%40jupyter%2Freal-time-collaboration%40workspace%3A." +"@jupyterlab/services@npm:^7.0.5, @jupyterlab/services@npm:^7.1.2": + version: 7.1.5 + resolution: "@jupyterlab/services@npm:7.1.5" dependencies: - "@jupyter/ydoc": ^2.0.0 - "@jupyterlab/coreutils": ^6.1.2 - "@jupyterlab/nbformat": ^4.1.2 - "@jupyterlab/settingregistry": ^4.1.2 - "@jupyterlab/statedb": ^4.1.2 + "@jupyter/ydoc": ^1.1.1 + "@jupyterlab/coreutils": ^6.1.5 + "@jupyterlab/nbformat": ^4.1.5 + "@jupyterlab/settingregistry": ^4.1.5 + "@jupyterlab/statedb": ^4.1.5 "@lumino/coreutils": ^2.1.2 "@lumino/disposable": ^2.1.2 "@lumino/polling": ^2.1.2 "@lumino/properties": ^2.0.1 "@lumino/signaling": ^2.1.2 ws: ^8.11.0 - checksum: 0d47267288649c414f30da20db1edf3aa2b348b8f9f6dceb8daee5a74b9102a75320682817c4f97736c748108849b947cc3c5ea05580668f6489252ada832a7b + checksum: f4b20ee62e5c3c7e0fa5942d3deb95329beb5a9ea6295403eefc0d5a723665379a09c58b21bc6a9fed7a69990570e5cfb66bc314e037a452b678fc4ec237dc55 languageName: node linkType: hard @@ -2904,6 +2927,25 @@ __metadata: languageName: node linkType: hard +"@jupyterlab/settingregistry@npm:^4.1.5": + version: 4.1.5 + resolution: "@jupyterlab/settingregistry@npm:4.1.5" + dependencies: + "@jupyterlab/nbformat": ^4.1.5 + "@jupyterlab/statedb": ^4.1.5 + "@lumino/commands": ^2.2.0 + "@lumino/coreutils": ^2.1.2 + "@lumino/disposable": ^2.1.2 + "@lumino/signaling": ^2.1.2 + "@rjsf/utils": ^5.13.4 + ajv: ^8.12.0 + json5: ^2.2.3 + peerDependencies: + react: ">=16" + checksum: 576d49cbbb4a18ba5f55230938b67c6dbc6819dfafb75ece2d9d030913e69768ddcb2616de4f7dbd3bcd8aa35e292aee90fe98b91e7dccdaae2610c64ec07f94 + languageName: node + linkType: hard + "@jupyterlab/statedb@npm:^4.0.5": version: 4.0.5 resolution: "@jupyterlab/statedb@npm:4.0.5" @@ -2930,6 +2972,19 @@ __metadata: languageName: node linkType: hard +"@jupyterlab/statedb@npm:^4.1.5": + version: 4.1.5 + resolution: "@jupyterlab/statedb@npm:4.1.5" + dependencies: + "@lumino/commands": ^2.2.0 + "@lumino/coreutils": ^2.1.2 + "@lumino/disposable": ^2.1.2 + "@lumino/properties": ^2.0.1 + "@lumino/signaling": ^2.1.2 + checksum: e7f3ea9a5ebb04a602d93d1ddc9175a5b24a0f3814e99410ec3dba2dd3a86572ea61917d8a65e1b4b8c4ed25c8eaa814646a817a3b5d39b8a74a7b6cbb0071c1 + languageName: node + linkType: hard + "@jupyterlab/statusbar@npm:^4.0.5": version: 4.0.5 resolution: "@jupyterlab/statusbar@npm:4.0.5" From a63043aca1b2c4554c218ff42bd19374f93c5ae3 Mon Sep 17 00:00:00 2001 From: David Brochart Date: Tue, 2 Apr 2024 17:20:26 +0200 Subject: [PATCH 10/11] Allow undoing changes when connecting, add handler for deleting fork --- jupyter_collaboration/app.py | 9 +++- jupyter_collaboration/handlers.py | 44 ++++++++++++++++++- .../src/collaboration.ts | 8 ++-- packages/docprovider/src/requests.ts | 40 +++++++++++++++++ packages/docprovider/src/yprovider.ts | 18 +++++++- yarn.lock | 4 +- 6 files changed, 114 insertions(+), 9 deletions(-) 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 From 35f57bef22d3a5ea3ec99743c2a86e76a1f77006 Mon Sep 17 00:00:00 2001 From: David Brochart Date: Fri, 5 Apr 2024 16:35:09 +0200 Subject: [PATCH 11/11] - --- .../src/collaboration.ts | 87 +++++++++---------- 1 file changed, 39 insertions(+), 48 deletions(-) diff --git a/packages/collaboration-extension/src/collaboration.ts b/packages/collaboration-extension/src/collaboration.ts index b259f476..783684f1 100644 --- a/packages/collaboration-extension/src/collaboration.ts +++ b/packages/collaboration-extension/src/collaboration.ts @@ -324,37 +324,26 @@ export class EditingModeExtension implements DocumentRegistry.IWidgetExtension { const forkPrefix = 'fork_'; if (value.name === 'merge' || value.name === 'delete') { - // FIXME: a client who is not connected to the fork should not see this update + // we are on fork if (sharedModel.currentRoomId === value.newValue) { - editingMenu.title.label = 'Editing'; - suggestionMenu.title.label = 'Root'; - const item: Menu.IItem = suggestions[value.newValue]; - delete suggestions[value.newValue]; - suggestionMenu.removeItem(item); 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: () => { - if (myForkId === forkId) { - editingMenu.title.label = 'Suggesting'; - // our suggestion, cannot be reviewed - reviewMenu.clearItems(); - } - else { - editingMenu.title.label = 'Editing'; - // not our suggestion, can be reviewed - reviewMenu.clearItems(); - reviewMenu.addItem({type: 'command', command: 'merge'}); - reviewMenu.addItem({type: 'command', command: 'discard'}); - } + 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); @@ -362,39 +351,41 @@ export class EditingModeExtension implements DocumentRegistry.IWidgetExtension { - dialog.close(); - if (resp.button.label === 'View') { - sharedModel.provider.connect(forkId); - suggestionMenu.title.label = forkId; - editingMenu.title.label = 'Editing'; - reviewMenu.clearItems(); - reviewMenu.addItem({type: 'command', command: 'merge'}); - reviewMenu.addItem({type: 'command', command: 'discard'}); - } - }); + 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) { - if (sharedModel.currentRoomId === forkId) { - editingMenu.title.label = 'Editing'; - suggestionMenu.title.label = 'Root'; - const item: Menu.IItem = suggestions[value.newValue]; - delete suggestions[value.newValue]; - suggestionMenu.removeItem(item); - reviewMenu.clearItems(); - sharedModel.provider.connect(sharedModel.rootRoomId); - open_dialog('Editing', this._trans); - } + editingMenu.title.label = 'Editing'; + suggestionMenu.title.label = 'Root'; + const item: Menu.IItem = suggestions[value.oldValue]; + delete suggestions[value.oldValue]; + suggestionMenu.removeItem(item); } } });