Skip to content

Commit 64dee2c

Browse files
committed
-
1 parent 100192d commit 64dee2c

File tree

10 files changed

+760
-63
lines changed

10 files changed

+760
-63
lines changed

packages/collaboration-extension/src/collaboration.ts

Lines changed: 211 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -23,12 +23,12 @@ import {
2323
JupyterFrontEnd,
2424
JupyterFrontEndPlugin
2525
} from '@jupyterlab/application';
26-
import { IToolbarWidgetRegistry } from '@jupyterlab/apputils';
26+
import { Dialog, IToolbarWidgetRegistry } from '@jupyterlab/apputils';
2727
import {
2828
EditorExtensionRegistry,
2929
IEditorExtensionRegistry
3030
} from '@jupyterlab/codemirror';
31-
import { WebSocketAwarenessProvider } from '@jupyter/docprovider';
31+
import { requestDocMerge, WebSocketAwarenessProvider } from '@jupyter/docprovider';
3232
import {
3333
SidePanel,
3434
usersIcon,
@@ -37,20 +37,21 @@ import {
3737
import { URLExt } from '@jupyterlab/coreutils';
3838
import { ServerConnection } from '@jupyterlab/services';
3939
import { IStateDB, StateDB } from '@jupyterlab/statedb';
40-
import { ITranslator, nullTranslator } from '@jupyterlab/translation';
40+
import { ITranslator, nullTranslator, TranslationBundle } from '@jupyterlab/translation';
4141

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

44-
import { IAwareness } from '@jupyter/ydoc';
44+
import { IAwareness, ISharedNotebook, ISuggestions, NotebookChange } from '@jupyter/ydoc';
4545

4646
import {
4747
CollaboratorsPanel,
48+
SuggestionsPanel,
4849
IGlobalAwareness,
4950
IUserMenu,
5051
remoteUserCursors,
5152
RendererUserMenu,
5253
UserInfoPanel,
53-
UserMenu
54+
UserMenu,
5455
} from '@jupyter/collaboration';
5556

5657
import * as Y from 'yjs';
@@ -147,11 +148,12 @@ export const rtcPanelPlugin: JupyterFrontEndPlugin<void> = {
147148
id: '@jupyter/collaboration-extension:rtcPanel',
148149
description: 'Add side panel to display all currently connected users.',
149150
autoStart: true,
150-
requires: [IGlobalAwareness],
151+
requires: [IGlobalAwareness, ISuggestions],
151152
optional: [ITranslator],
152153
activate: (
153154
app: JupyterFrontEnd,
154155
awareness: Awareness,
156+
suggestions: ISuggestions,
155157
translator: ITranslator | null
156158
): void => {
157159
const { user } = app.serviceManager;
@@ -183,6 +185,10 @@ export const rtcPanelPlugin: JupyterFrontEndPlugin<void> = {
183185
);
184186
collaboratorsPanel.title.label = trans.__('Online Collaborators');
185187
userPanel.addWidget(collaboratorsPanel);
188+
189+
const suggestionsPanel = new SuggestionsPanel(fileopener, suggestions);
190+
suggestionsPanel.title.label = trans.__('Suggestions');
191+
userPanel.addWidget(suggestionsPanel);
186192
}
187193
};
188194

@@ -215,54 +221,217 @@ export const editingMode: JupyterFrontEndPlugin<void> = {
215221
id: '@jupyter/collaboration-extension:editingMode',
216222
description: 'A plugin to add editing mode to the notebook page.',
217223
autoStart: true,
218-
requires: [ITranslator],
224+
optional: [ITranslator],
219225
activate: (
220226
app: JupyterFrontEnd,
227+
translator: ITranslator | null
221228
) => {
222-
app.docRegistry.addWidgetExtension('Notebook', new EditingModeExtension());
229+
app.docRegistry.addWidgetExtension('Notebook', new EditingModeExtension(translator));
223230
},
224231
};
225232

226233
export class EditingModeExtension implements DocumentRegistry.IWidgetExtension<NotebookPanel, INotebookModel> {
227-
createNew(panel: NotebookPanel, context: DocumentRegistry.IContext<INotebookModel>): IDisposable {
228-
const menubar = new MenuBar();
229-
const commands = new CommandRegistry();
230-
const menu = new Menu({ commands });
231-
menu.title.label = 'Editing';
232-
menu.title.icon = caretDownIcon;
233-
addMenuItem(commands, menu, 'editing', 'Editing', context);
234-
addMenuItem(commands, menu, 'suggesting', 'Suggesting', context);
235-
menubar.addMenu(menu);
236-
237-
panel.toolbar.insertItem(990, 'editingMode', menubar);
234+
private _trans: TranslationBundle;
235+
236+
constructor(translator: ITranslator | null) {
237+
this._trans = (translator ?? nullTranslator).load('jupyter_collaboration');
238+
}
239+
240+
createNew(
241+
panel: NotebookPanel,
242+
context: DocumentRegistry.IContext<INotebookModel>
243+
): IDisposable {
244+
const editingMenubar = new MenuBar();
245+
const suggestionMenubar = new MenuBar();
246+
const reviewMenubar = new MenuBar();
247+
248+
const editingCommands = new CommandRegistry();
249+
const suggestionCommands = new CommandRegistry();
250+
const reviewCommands = new CommandRegistry();
251+
252+
const editingMenu = new Menu({ commands: editingCommands });
253+
const suggestionMenu = new Menu({ commands: suggestionCommands });
254+
const reviewMenu = new Menu({ commands: reviewCommands });
255+
256+
var myForkId = ''; // curently allows only one suggestion per user
257+
258+
editingMenu.title.label = 'Editing';
259+
editingMenu.title.icon = caretDownIcon;
260+
261+
suggestionMenu.title.label = 'Root';
262+
suggestionMenu.title.icon = caretDownIcon;
263+
264+
reviewMenu.title.label = 'Review';
265+
reviewMenu.title.icon = caretDownIcon;
266+
267+
editingCommands.addCommand('editing', {
268+
label: 'Editing',
269+
execute: () => {
270+
editingMenu.title.label = 'Editing';
271+
suggestionMenu.title.label = 'Root';
272+
}
273+
});
274+
editingCommands.addCommand('suggesting', {
275+
label: 'Suggesting',
276+
execute: () => {
277+
editingMenu.title.label = 'Suggesting';
278+
reviewMenu.clearItems();
279+
if (myForkId === '') {
280+
myForkId = 'pending';
281+
const provider = context.model.sharedModel.provider;
282+
provider.fork().then(newForkId => {
283+
myForkId = newForkId;
284+
provider.connectFork(newForkId);
285+
suggestionMenu.title.label = newForkId;
286+
});
287+
}
288+
else {
289+
suggestionMenu.title.label = myForkId;
290+
context.model.sharedModel.provider.connectFork(myForkId);
291+
}
292+
}
293+
});
294+
295+
suggestionCommands.addCommand('root', {
296+
label: 'Root',
297+
execute: () => {
298+
// we cannot review the root document
299+
reviewMenu.clearItems();
300+
suggestionMenu.title.label = 'Root';
301+
editingMenu.title.label = 'Editing';
302+
context.model.sharedModel.provider.connectFork(context.model.sharedModel.rootRoomId);
303+
}
304+
});
305+
306+
reviewCommands.addCommand('merge', {
307+
label: 'Merge',
308+
execute: () => {
309+
console.log('currentRoomId', context.model.sharedModel.currentRoomId);
310+
console.log('rootRoomId', context.model.sharedModel.rootRoomId);
311+
requestDocMerge(context.model.sharedModel.currentRoomId, context.model.sharedModel.rootRoomId);
312+
}
313+
});
314+
reviewCommands.addCommand('discard', {
315+
label: 'Discard',
316+
execute: () => {
317+
}
318+
});
319+
320+
editingMenu.addItem({type: 'command', command: 'editing'});
321+
editingMenu.addItem({type: 'command', command: 'suggesting'});
322+
323+
suggestionMenu.addItem({type: 'command', command: 'root'});
324+
325+
const _onStateChanged = (sender: ISharedNotebook, changes: NotebookChange) => {
326+
if (changes.stateChange) {
327+
changes.stateChange.forEach(value => {
328+
const forkPrefix = 'fork_';
329+
if (value.name.startsWith(forkPrefix)) {
330+
const newForkId = value.name.slice(forkPrefix.length);
331+
suggestionCommands.addCommand(newForkId, {
332+
label: newForkId,
333+
execute: () => {
334+
if (myForkId === newForkId) {
335+
editingMenu.title.label = 'Suggesting';
336+
// our suggestion, cannot be reviewed
337+
reviewMenu.clearItems();
338+
}
339+
else {
340+
editingMenu.title.label = 'Editing';
341+
// not our suggestion, can be reviewed
342+
reviewMenu.clearItems();
343+
reviewMenu.addItem({type: 'command', command: 'merge'});
344+
reviewMenu.addItem({type: 'command', command: 'discard'});
345+
}
346+
suggestionMenu.title.label = newForkId;
347+
context.model.sharedModel.provider.connectFork(newForkId);
348+
const dialog = new Dialog({
349+
title: this._trans.__('Suggestion'),
350+
body: this._trans.__('Your are now viewing the suggestion.'),
351+
buttons: [Dialog.okButton({ label: 'OK' })],
352+
});
353+
dialog.launch().then(resp => { dialog.close(); });
354+
}
355+
});
356+
suggestionMenu.addItem({type: 'command', command: newForkId});
357+
if ((myForkId !== 'pending') && (myForkId !== newForkId)) {
358+
const dialog = new Dialog({
359+
title: this._trans.__('New suggestion'),
360+
body: this._trans.__('Open notebook for suggestion?'),
361+
buttons: [
362+
Dialog.okButton({ label: 'Open' }),
363+
Dialog.cancelButton({ label: 'Discard' }),
364+
],
365+
});
366+
dialog.launch().then(resp => {
367+
dialog.close();
368+
if (resp.button.label === 'Open') {
369+
context.model.sharedModel.provider.connectFork(newForkId);
370+
suggestionMenu.title.label = newForkId;
371+
editingMenu.title.label = 'Editing';
372+
reviewMenu.clearItems();
373+
reviewMenu.addItem({type: 'command', command: 'merge'});
374+
reviewMenu.addItem({type: 'command', command: 'discard'});
375+
}
376+
});
377+
}
378+
}
379+
});
380+
}
381+
};
382+
383+
context.model.sharedModel.changed.connect(_onStateChanged, this);
384+
385+
editingMenubar.addMenu(editingMenu);
386+
suggestionMenubar.addMenu(suggestionMenu);
387+
reviewMenubar.addMenu(reviewMenu);
388+
389+
panel.toolbar.insertItem(997, 'editingMode', editingMenubar);
390+
panel.toolbar.insertItem(998, 'suggestions', suggestionMenubar);
391+
panel.toolbar.insertItem(999, 'review', reviewMenubar);
238392
return new DisposableDelegate(() => {
239-
menubar.dispose();
393+
editingMenubar.dispose();
394+
suggestionMenubar.dispose();
395+
reviewMenubar.dispose();
240396
});
241397
}
242398
}
243399

244400
/**
245-
* Helper Function to add menu items.
246-
*/
247-
function addMenuItem(
248-
commands: CommandRegistry,
249-
menu: Menu,
250-
command: string,
251-
label: string,
252-
context: DocumentRegistry.IContext<INotebookModel>,
253-
): void {
254-
commands.addCommand(command, {
255-
label: label,
256-
execute: () => {
257-
menu.title.label = label;
258-
if (command == 'suggesting') {
259-
const provider = context.model.sharedModel.provider;
260-
provider.fork().then(forkId => {provider.connectFork(forkId);});
261-
}
401+
* A plugin to provide shared document suggestions.
402+
*/
403+
export const suggestions: JupyterFrontEndPlugin<ISuggestions> = {
404+
id: '@jupyter/collaboration-extension:rtcGlobalSuggestions',
405+
description: 'A plugin to provide shared document suggestions.',
406+
autoStart: true,
407+
provides: ISuggestions,
408+
activate: (app: JupyterFrontEnd): ISuggestions => {
409+
console.log('suggestions plugin activated');
410+
return new Suggestions();
411+
},
412+
};
413+
414+
export class Suggestions implements ISuggestions {
415+
private _forkIds: string[];
416+
private _callbacks: any[];
417+
418+
constructor() {
419+
this._forkIds = [];
420+
this._callbacks = [];
421+
}
422+
423+
addFork(forkId: string) {
424+
this._forkIds.push(forkId);
425+
for (const callback of this._callbacks) {
426+
callback(forkId);
262427
}
263-
});
264-
menu.addItem({
265-
type: 'command',
266-
command: command
267-
});
428+
}
429+
430+
addCallback(callback: any) {
431+
this._callbacks.push(callback);
432+
}
433+
434+
get forks(): string[] {
435+
return this._forkIds;
436+
}
268437
}

packages/collaboration-extension/src/filebrowser.ts

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ import { ITranslator, nullTranslator } from '@jupyterlab/translation';
2424

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

27-
import { YFile, YNotebook } from '@jupyter/ydoc';
27+
import { ISuggestions, YFile, YNotebook } from '@jupyter/ydoc';
2828

2929
import { ICollaborativeDrive, YDrive } from '@jupyter/docprovider';
3030

@@ -42,14 +42,15 @@ export const drive: JupyterFrontEndPlugin<ICollaborativeDrive> = {
4242
id: '@jupyter/collaboration-extension:drive',
4343
description: 'The default collaborative drive provider',
4444
provides: ICollaborativeDrive,
45-
requires: [ITranslator],
45+
requires: [ITranslator, ISuggestions],
4646
optional: [],
4747
activate: (
4848
app: JupyterFrontEnd,
49-
translator: ITranslator
49+
translator: ITranslator,
50+
suggestions: ISuggestions
5051
): ICollaborativeDrive => {
5152
const trans = translator.load('jupyter_collaboration');
52-
const drive = new YDrive(app.serviceManager.user, trans);
53+
const drive = new YDrive(app.serviceManager.user, trans, suggestions);
5354
app.serviceManager.contents.addDrive(drive);
5455
return drive;
5556
}

packages/collaboration-extension/src/index.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,8 @@ import {
2020
rtcGlobalAwarenessPlugin,
2121
rtcPanelPlugin,
2222
userEditorCursors,
23-
editingMode
23+
editingMode,
24+
suggestions
2425
} from './collaboration';
2526
import { sharedLink } from './sharedlink';
2627

@@ -39,7 +40,8 @@ const plugins: JupyterFrontEndPlugin<any>[] = [
3940
rtcPanelPlugin,
4041
sharedLink,
4142
userEditorCursors,
42-
editingMode
43+
editingMode,
44+
suggestions
4345
];
4446

4547
export default plugins;

packages/collaboration/src/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@
77

88
export * from './tokens';
99
export * from './collaboratorspanel';
10+
export * from './suggestionspanel';
11+
//export * from './suggestions';
1012
export * from './cursors';
1113
export * from './menu';
1214
export * from './sharedlink';

0 commit comments

Comments
 (0)