Skip to content

Commit

Permalink
Support suggestions
Browse files Browse the repository at this point in the history
  • Loading branch information
davidbrochart committed Apr 19, 2024
1 parent adcde32 commit fd6ff70
Show file tree
Hide file tree
Showing 8 changed files with 574 additions and 32 deletions.
246 changes: 241 additions & 5 deletions packages/collaboration-extension/src/collaboration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,25 +5,43 @@
* @module collaboration-extension
*/

import {
DocumentRegistry
} from '@jupyterlab/docregistry';

import {
NotebookPanel, INotebookModel
} from '@jupyterlab/notebook';

import {
IDisposable, DisposableDelegate
} from '@lumino/disposable';

import { CommandRegistry } from '@lumino/commands';

import {
JupyterFrontEnd,
JupyterFrontEndPlugin
} from '@jupyterlab/application';
import { IToolbarWidgetRegistry } from '@jupyterlab/apputils';
import { Dialog, IToolbarWidgetRegistry } from '@jupyterlab/apputils';
import {
EditorExtensionRegistry,
IEditorExtensionRegistry
} from '@jupyterlab/codemirror';
import { WebSocketAwarenessProvider } from '@jupyter/docprovider';
import { SidePanel, usersIcon } from '@jupyterlab/ui-components';
import { requestDocDelete, requestDocMerge, WebSocketAwarenessProvider } from '@jupyter/docprovider';
import {
SidePanel,
usersIcon,
caretDownIcon
} from '@jupyterlab/ui-components';
import { URLExt } from '@jupyterlab/coreutils';
import { ServerConnection } from '@jupyterlab/services';
import { IStateDB, StateDB } from '@jupyterlab/statedb';
import { ITranslator, nullTranslator } from '@jupyterlab/translation';
import { ITranslator, nullTranslator, TranslationBundle } from '@jupyterlab/translation';

import { Menu, MenuBar } from '@lumino/widgets';

import { IAwareness } from '@jupyter/ydoc';
import { IAwareness, ISharedNotebook, NotebookChange } from '@jupyter/ydoc';

import {
CollaboratorsPanel,
Expand Down Expand Up @@ -189,3 +207,221 @@ export const userEditorCursors: JupyterFrontEndPlugin<void> = {
});
}
};

/**
* A plugin to add editing mode to the notebook page
*/
export const editingMode: JupyterFrontEndPlugin<void> = {
id: '@jupyter/collaboration-extension:editingMode',
description: 'A plugin to add editing mode to the notebook page.',
autoStart: true,
optional: [ITranslator],
activate: (
app: JupyterFrontEnd,
translator: ITranslator | null
) => {
app.docRegistry.addWidgetExtension('Notebook', new EditingModeExtension(translator));
},
};

export class EditingModeExtension implements DocumentRegistry.IWidgetExtension<NotebookPanel, INotebookModel> {
private _trans: TranslationBundle;

constructor(translator: ITranslator | null) {
this._trans = (translator ?? nullTranslator).load('jupyter_collaboration');
}

createNew(
panel: NotebookPanel,
context: DocumentRegistry.IContext<INotebookModel>
): IDisposable {
const editingMenubar = new MenuBar();
const suggestionMenubar = new MenuBar();
const reviewMenubar = new MenuBar();

const editingCommands = new CommandRegistry();
const suggestionCommands = new CommandRegistry();
const reviewCommands = new CommandRegistry();

const editingMenu = new Menu({ commands: editingCommands });
const suggestionMenu = new Menu({ commands: suggestionCommands });
const reviewMenu = new Menu({ commands: reviewCommands });

const sharedModel = context.model.sharedModel;
const suggestions: {[key: string]: Menu.IItem} = {};
var myForkId = ''; // curently allows only one suggestion per user

editingMenu.title.label = 'Editing';
editingMenu.title.icon = caretDownIcon;

suggestionMenu.title.label = 'Root';
suggestionMenu.title.icon = caretDownIcon;

reviewMenu.title.label = 'Review';
reviewMenu.title.icon = caretDownIcon;

editingCommands.addCommand('editing', {
label: 'Editing',
execute: () => {
editingMenu.title.label = 'Editing';
suggestionMenu.title.label = 'Root';
open_dialog('Editing', this._trans);
}
});
editingCommands.addCommand('suggesting', {
label: 'Suggesting',
execute: () => {
editingMenu.title.label = 'Suggesting';
reviewMenu.clearItems();
if (myForkId === '') {
myForkId = 'pending';
sharedModel.provider.fork().then(newForkId => {
myForkId = newForkId;
sharedModel.provider.connect(newForkId);
suggestionMenu.title.label = newForkId;
});
}
else {
suggestionMenu.title.label = myForkId;
sharedModel.provider.connect(myForkId);
}
open_dialog('Suggesting', this._trans);
}
});

suggestionCommands.addCommand('root', {
label: 'Root',
execute: () => {
// we cannot review the root document
reviewMenu.clearItems();
suggestionMenu.title.label = 'Root';
editingMenu.title.label = 'Editing';
sharedModel.provider.connect(sharedModel.rootRoomId);
open_dialog('Editing', this._trans);
}
});

reviewCommands.addCommand('merge', {
label: 'Merge',
execute: () => {
requestDocMerge(sharedModel.currentRoomId, sharedModel.rootRoomId);
}
});
reviewCommands.addCommand('discard', {
label: 'Discard',
execute: () => {
requestDocDelete(sharedModel.currentRoomId, sharedModel.rootRoomId);
}
});

editingMenu.addItem({type: 'command', command: 'editing'});
editingMenu.addItem({type: 'command', command: 'suggesting'});

suggestionMenu.addItem({type: 'command', command: 'root'});

const _onStateChanged = (sender: ISharedNotebook, changes: NotebookChange) => {
if (changes.stateChange) {
changes.stateChange.forEach(value => {
const forkPrefix = 'fork_';
if (value.name === 'merge' || value.name === 'delete') {
// we are on fork
if (sharedModel.currentRoomId === value.newValue) {
reviewMenu.clearItems();
const merge = value.name === 'merge';
sharedModel.provider.connect(sharedModel.rootRoomId, merge);
open_dialog('Editing', this._trans);
myForkId = '';
}
}
else if (value.name.startsWith(forkPrefix)) {
// we are on root
const forkId = value.name.slice(forkPrefix.length);
if (value.newValue === 'new') {
suggestionCommands.addCommand(forkId, {
label: forkId,
execute: () => {
editingMenu.title.label = 'Suggesting';
reviewMenu.clearItems();
reviewMenu.addItem({type: 'command', command: 'merge'});
reviewMenu.addItem({type: 'command', command: 'discard'});
suggestionMenu.title.label = forkId;
sharedModel.provider.connect(forkId);
open_dialog('Suggesting', this._trans);
}
});
const item = suggestionMenu.addItem({type: 'command', command: forkId});
suggestions[forkId] = item;
if (myForkId !== forkId) {
if (myForkId !== 'pending') {
const dialog = new Dialog({
title: this._trans.__('New suggestion'),
body: this._trans.__('View suggestion?'),
buttons: [
Dialog.okButton({ label: 'View' }),
Dialog.cancelButton({ label: 'Discard' }),
],
});
dialog.launch().then(resp => {
dialog.close();
if (resp.button.label === 'View') {
sharedModel.provider.connect(forkId);
suggestionMenu.title.label = forkId;
editingMenu.title.label = 'Suggesting';
reviewMenu.clearItems();
reviewMenu.addItem({type: 'command', command: 'merge'});
reviewMenu.addItem({type: 'command', command: 'discard'});
}
});
}
else {
reviewMenu.clearItems();
reviewMenu.addItem({type: 'command', command: 'merge'});
reviewMenu.addItem({type: 'command', command: 'discard'});
}
}
}
else if (value.newValue === undefined) {
editingMenu.title.label = 'Editing';
suggestionMenu.title.label = 'Root';
const item: Menu.IItem = suggestions[value.oldValue];
delete suggestions[value.oldValue];
suggestionMenu.removeItem(item);
}
}
});
}
};

sharedModel.changed.connect(_onStateChanged, this);

editingMenubar.addMenu(editingMenu);
suggestionMenubar.addMenu(suggestionMenu);
reviewMenubar.addMenu(reviewMenu);

panel.toolbar.insertItem(997, 'editingMode', editingMenubar);
panel.toolbar.insertItem(998, 'suggestions', suggestionMenubar);
panel.toolbar.insertItem(999, 'review', reviewMenubar);
return new DisposableDelegate(() => {
editingMenubar.dispose();
suggestionMenubar.dispose();
reviewMenubar.dispose();
});
}
}


function open_dialog(title: string, trans: TranslationBundle) {
var body: string;
if (title === 'Editing') {
body = 'You are now directly editing the document.'
}
else {
body = 'Your edits now become suggestions to the document.'
}
const dialog = new Dialog({
title: trans.__(title),
body: trans.__(body),
buttons: [Dialog.okButton({ label: 'OK' })],
});
dialog.launch().then(resp => { dialog.close(); });
}
6 changes: 4 additions & 2 deletions packages/collaboration-extension/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,8 @@ import {
menuBarPlugin,
rtcGlobalAwarenessPlugin,
rtcPanelPlugin,
userEditorCursors
userEditorCursors,
editingMode
} from './collaboration';
import { sharedLink } from './sharedlink';

Expand All @@ -25,7 +26,8 @@ const plugins: JupyterFrontEndPlugin<any>[] = [
rtcGlobalAwarenessPlugin,
rtcPanelPlugin,
sharedLink,
userEditorCursors
userEditorCursors,
editingMode
];

export default plugins;
1 change: 1 addition & 0 deletions packages/docprovider/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
*/

export * from './awareness';
export * from './requests';
export * from './ydrive';
export * from './yprovider';
export * from './tokens';
Loading

0 comments on commit fd6ff70

Please sign in to comment.