From 940957b35319fa0f52af1e1ec279a24ae5582e83 Mon Sep 17 00:00:00 2001 From: Nicolas Brichet Date: Mon, 15 Apr 2024 19:26:18 +0200 Subject: [PATCH] Add buttons to move the chat between sidepanel and main area --- .../schema/chat-panel.json | 15 +++ .../src/index.ts | 34 +++++- .../src/token.ts | 6 +- .../src/widget.tsx | 35 +++++-- .../style/base.css | 10 +- .../jupyterlab_collaborative_chat.spec.ts | 97 ++++++++++++++++-- .../moveToMain-linux.png | Bin 0 -> 238 bytes .../moveToSide-linux.png | Bin 0 -> 238 bytes 8 files changed, 178 insertions(+), 19 deletions(-) create mode 100644 packages/jupyterlab-collaborative-chat/schema/chat-panel.json create mode 100644 packages/jupyterlab-collaborative-chat/ui-tests/tests/jupyterlab_collaborative_chat.spec.ts-snapshots/moveToMain-linux.png create mode 100644 packages/jupyterlab-collaborative-chat/ui-tests/tests/jupyterlab_collaborative_chat.spec.ts-snapshots/moveToSide-linux.png diff --git a/packages/jupyterlab-collaborative-chat/schema/chat-panel.json b/packages/jupyterlab-collaborative-chat/schema/chat-panel.json new file mode 100644 index 0000000..9079e20 --- /dev/null +++ b/packages/jupyterlab-collaborative-chat/schema/chat-panel.json @@ -0,0 +1,15 @@ +{ + "title": "Jupyter collaborative chat", + "description": "Configuration for the chat commands", + "type": "object", + "jupyter.lab.toolbars": { + "Chat": [ + { + "name": "moveToSide", + "command": "collaborative-chat:moveToSide" + } + ] + }, + "properties": {}, + "additionalProperties": false +} diff --git a/packages/jupyterlab-collaborative-chat/src/index.ts b/packages/jupyterlab-collaborative-chat/src/index.ts index 774574b..39933cb 100644 --- a/packages/jupyterlab-collaborative-chat/src/index.ts +++ b/packages/jupyterlab-collaborative-chat/src/index.ts @@ -28,6 +28,7 @@ import { IRenderMimeRegistry } from '@jupyterlab/rendermime'; import { Contents } from '@jupyterlab/services'; import { ISettingRegistry } from '@jupyterlab/settingregistry'; import { ITranslator, nullTranslator } from '@jupyterlab/translation'; +import { launchIcon } from '@jupyterlab/ui-components'; import { Awareness } from 'y-protocols/awareness'; import { @@ -109,7 +110,6 @@ export const docFactories: JupyterFrontEndPlugin = { pluginIds.docFactories, translator ); - console.log('Create toolbarFactory', toolbarFactory); } // Wait for the application to be restored and @@ -326,6 +326,7 @@ const chatCommands: JupyterFrontEndPlugin = { } if (inSidePanel && chatPanel) { + app.shell.activateById(chatPanel.id); // The chat is opened in the chat panel. const model = await drive.get(filepath); @@ -442,6 +443,37 @@ const chatPanel: JupyterFrontEndPlugin = { } }); + /* + * Command to move a chat from the main area to the side panel. + * + */ + commands.addCommand(CommandIDs.moveToSide, { + label: 'Move the chat to the side panel', + caption: 'Move the chat to the side panel', + icon: launchIcon, + execute: async () => { + const widget = app.shell.currentWidget; + // Ensure widget is a CollaborativeChatWidget and is in main area + if ( + !widget || + !(widget instanceof CollaborativeChatWidget) || + !Array.from(app.shell.widgets('main')).includes(widget) + ) { + console.error( + `The command '${CommandIDs.moveToSide}' should be executed from the toolbar button only` + ); + return; + } + // Remove potential drive prefix + const filepath = widget.context.path.split(':').pop(); + commands.execute(CommandIDs.openChat, { + filepath, + inSidePanel: true + }); + widget.dispose(); + } + }); + return chatPanel; } }; diff --git a/packages/jupyterlab-collaborative-chat/src/token.ts b/packages/jupyterlab-collaborative-chat/src/token.ts index c958214..3f89d83 100644 --- a/packages/jupyterlab-collaborative-chat/src/token.ts +++ b/packages/jupyterlab-collaborative-chat/src/token.ts @@ -59,7 +59,11 @@ export const CommandIDs = { /** * Open a chat file. */ - openChat: 'collaborative-chat:open' + openChat: 'collaborative-chat:open', + /** + * Move a main widget to the side panel + */ + moveToSide: 'collaborative-chat:moveToSide' }; /** diff --git a/packages/jupyterlab-collaborative-chat/src/widget.tsx b/packages/jupyterlab-collaborative-chat/src/widget.tsx index 6e1fcbf..1b2fed5 100644 --- a/packages/jupyterlab-collaborative-chat/src/widget.tsx +++ b/packages/jupyterlab-collaborative-chat/src/widget.tsx @@ -14,19 +14,20 @@ import { closeIcon, CommandToolbarButton, HTMLSelect, + launchIcon, PanelWithToolbar, ReactWidget, SidePanel, ToolbarButton } from '@jupyterlab/ui-components'; import { CommandRegistry } from '@lumino/commands'; +import { Message } from '@lumino/messaging'; +import { ISignal, Signal } from '@lumino/signaling'; import { AccordionPanel, Panel } from '@lumino/widgets'; import React, { useState } from 'react'; import { CollaborativeChatModel } from './model'; -import { CommandIDs } from './token'; -import { ISignal, Signal } from '@lumino/signaling'; -import { Message } from '@lumino/messaging'; +import { CommandIDs, chatFileType } from './token'; const MAIN_PANEL_CLASS = 'jp-collab-chat_main-panel'; const SIDEPANEL_CLASS = 'jp-collab-chat-sidepanel'; @@ -53,6 +54,7 @@ export class CollaborativeChatWidget extends DocumentWidget< * Dispose of the resources held by the widget. */ dispose(): void { + this.context.dispose(); this.content.dispose(); super.dispose(); } @@ -135,16 +137,17 @@ export class ChatPanel extends SidePanel { rmRegistry: this._rmRegistry, themeManager: this._themeManager }); - this.addWidget(new ChatSection({ widget, name })); + this.addWidget(new ChatSection({ name, widget, commands: this._commands })); } updateChatNames = async (): Promise => { + const extension = chatFileType.extensions[0]; this._drive .get('.') .then(model => { const chatsName = (model.content as any[]) - .filter(f => f.type === 'file' && f.name.endsWith('.chat')) - .map(f => PathExt.basename(f.name, '.chat')); + .filter(f => f.type === 'file' && f.name.endsWith(extension)) + .map(f => PathExt.basename(f.name, extension)); this._chatNamesChanged.emit(chatsName); }) .catch(e => console.error('Error getting the chat files from drive', e)); @@ -174,7 +177,7 @@ export class ChatPanel extends SidePanel { ); if (index === -1) { this._commands.execute(CommandIDs.openChat, { - filepath: `${value}.chat`, + filepath: `${value}${chatFileType.extensions[0]}`, inSidePanel: true }); } else if (!this.widgets[index].isVisible) { @@ -237,14 +240,29 @@ class ChatSection extends PanelWithToolbar { this.title.caption = this._name; this.toolbar.addClass(TOOLBAR_CLASS); + const moveToMain = new ToolbarButton({ + icon: launchIcon, + iconLabel: 'Move the chat to the main area', + className: 'jp-mod-styled', + onClick: () => { + this.model.dispose(); + options.commands.execute(CommandIDs.openChat, { + filepath: `${this._name}${chatFileType.extensions[0]}` + }); + this.dispose(); + } + }); + const closeButton = new ToolbarButton({ icon: closeIcon, + iconLabel: 'Close the chat', className: 'jp-mod-styled', onClick: () => { this.model.dispose(); this.dispose(); } }); + this.toolbar.addItem('collaborativeChat-main', moveToMain); this.toolbar.addItem('collaborativeChat-close', closeButton); this.addWidget(options.widget); @@ -276,8 +294,9 @@ export namespace ChatSection { * Options to build a chat section. */ export interface IOptions extends Panel.IOptions { - widget: ChatWidget; + commands: CommandRegistry; name: string; + widget: ChatWidget; } } diff --git a/packages/jupyterlab-collaborative-chat/style/base.css b/packages/jupyterlab-collaborative-chat/style/base.css index fb906fe..a0ae1ce 100644 --- a/packages/jupyterlab-collaborative-chat/style/base.css +++ b/packages/jupyterlab-collaborative-chat/style/base.css @@ -4,9 +4,15 @@ */ /* - See the JupyterLab Developer Guide for useful CSS Patterns: + See the JupyterLab Developer Guide for useful CSS Patterns: - https://jupyterlab.readthedocs.io/en/stable/developer/css.html + https://jupyterlab.readthedocs.io/en/stable/developer/css.html */ @import url('~@jupyter/chat/style/index.css'); + +.jp-collab-chat_main-panel + .jp-ToolbarButtonComponent[data-command='collaborative-chat:moveToSide'] + svg { + transform: rotate(180deg); +} diff --git a/packages/jupyterlab-collaborative-chat/ui-tests/tests/jupyterlab_collaborative_chat.spec.ts b/packages/jupyterlab-collaborative-chat/ui-tests/tests/jupyterlab_collaborative_chat.spec.ts index 337ca77..5468cae 100644 --- a/packages/jupyterlab-collaborative-chat/ui-tests/tests/jupyterlab_collaborative_chat.spec.ts +++ b/packages/jupyterlab-collaborative-chat/ui-tests/tests/jupyterlab_collaborative_chat.spec.ts @@ -58,6 +58,22 @@ const openChat = async ( return (await page.activity.getPanelLocator(filename)) as Locator; }; +const openChatToSide = async ( + page: IJupyterLabPageFixture, + filename: string +): Promise => { + const panel = page.locator('.jp-SidePanel.jp-collab-chat-sidepanel'); + await page.evaluate(async filepath => { + const inSidePanel = true; + await window.jupyterapp.commands.execute('collaborative-chat:open', { + filepath, + inSidePanel + }); + }, filename); + await expect(panel).toBeVisible(); + return panel; +}; + const openSettings = async ( page: IJupyterLabPageFixture, globalSettings?: boolean @@ -70,7 +86,9 @@ const openSettings = async ( return (await page.activity.getPanelLocator('Settings')) as Locator; }; -const openPanel = async (page: IJupyterLabPageFixture): Promise => { +const openSidePanel = async ( + page: IJupyterLabPageFixture +): Promise => { const panel = page.locator('.jp-SidePanel.jp-collab-chat-sidepanel'); if (!(await panel?.isVisible())) { @@ -692,7 +710,7 @@ test.describe('#chatPanel', () => { }); test('chat panel should contain a toolbar', async ({ page }) => { - const panel = await openPanel(page); + const panel = await openSidePanel(page); const toolbar = panel.locator('.jp-SidePanel-toolbar'); await expect(toolbar).toHaveCount(1); @@ -703,7 +721,7 @@ test.describe('#chatPanel', () => { }); test('chat panel should not contain a chat at init', async ({ page }) => { - const panel = await openPanel(page); + const panel = await openSidePanel(page); const content = panel.locator('.jp-SidePanel-content'); await expect(content).toBeEmpty(); }); @@ -715,7 +733,7 @@ test.describe('#chatPanel', () => { let dialog: Locator; test.beforeEach(async ({ page }) => { - panel = await openPanel(page); + panel = await openSidePanel(page); const addButton = panel.locator( '.jp-SidePanel-toolbar .jp-Toolbar-item.jp-collab-chat-add' ); @@ -797,7 +815,7 @@ test.describe('#chatPanel', () => { // reload to update the chat list // FIX: add listener on file creation await page.reload(); - panel = await openPanel(page); + panel = await openSidePanel(page); select = panel.locator( '.jp-SidePanel-toolbar .jp-Toolbar-item.jp-collab-chat-open select' ); @@ -813,7 +831,7 @@ test.describe('#chatPanel', () => { // reload to update the chat list // FIX: add listener on file creation await page.reload(); - panel = await openPanel(page); + panel = await openSidePanel(page); select = panel.locator( '.jp-SidePanel-toolbar .jp-Toolbar-item.jp-collab-chat-open select' ); @@ -828,8 +846,73 @@ test.describe('#chatPanel', () => { chatTitle.locator('.lm-AccordionPanel-titleLabel') ).toHaveText(name); - await chatTitle.getByRole('button').click(); + await chatTitle.getByTitle('Close the chat').click(); await expect(chatTitle).toHaveCount(0); }); }); + + test.describe('#movingChat', () => { + const filename = 'my-chat.chat'; + + test.use({ mockSettings: { ...galata.DEFAULT_SETTINGS } }); + + test.beforeEach(async ({ page }) => { + // Create a chat file + await page.filebrowser.contents.uploadContent('{}', 'text', filename); + }); + + test.afterEach(async ({ page }) => { + if (await page.filebrowser.contents.fileExists(filename)) { + await page.filebrowser.contents.deleteFile(filename); + } + }); + + test('main widget toolbar should have a button', async ({ page }) => { + const chatPanel = await openChat(page, filename); + const button = chatPanel.getByTitle('Move the chat to the side panel'); + expect(button).toBeVisible(); + expect(await button.screenshot()).toMatchSnapshot('moveToSide.png'); + }); + + test('chat should move to the side panel', async ({ page }) => { + const chatPanel = await openChat(page, filename); + const button = chatPanel.getByTitle('Move the chat to the side panel'); + await button.click(); + await expect(chatPanel).not.toBeAttached(); + + const sidePanel = page.locator('.jp-SidePanel.jp-collab-chat-sidepanel'); + await expect(sidePanel).toBeVisible(); + const chatTitle = sidePanel.locator( + '.jp-SidePanel-content .jp-AccordionPanel-title' + ); + await expect(chatTitle).toHaveCount(1); + await expect( + chatTitle.locator('.lm-AccordionPanel-titleLabel') + ).toHaveText(filename.split('.')[0]); + }); + + test('side panel should contain a button to move the chat', async ({ + page + }) => { + const sidePanel = await openChatToSide(page, filename); + const chatTitle = sidePanel + .locator('.jp-SidePanel-content .jp-AccordionPanel-title') + .first(); + const button = chatTitle.getByTitle('Move the chat to the main area'); + expect(button).toBeVisible(); + expect(await button.screenshot()).toMatchSnapshot('moveToMain.png'); + }); + + test('chat should move to the main area', async ({ page }) => { + const sidePanel = await openChatToSide(page, filename); + const chatTitle = sidePanel + .locator('.jp-SidePanel-content .jp-AccordionPanel-title') + .first(); + const button = chatTitle.getByTitle('Move the chat to the main area'); + await button.click(); + expect(chatTitle).not.toBeAttached(); + + await expect(page.activity.getTabLocator(filename)).toBeVisible(); + }); + }); }); diff --git a/packages/jupyterlab-collaborative-chat/ui-tests/tests/jupyterlab_collaborative_chat.spec.ts-snapshots/moveToMain-linux.png b/packages/jupyterlab-collaborative-chat/ui-tests/tests/jupyterlab_collaborative_chat.spec.ts-snapshots/moveToMain-linux.png new file mode 100644 index 0000000000000000000000000000000000000000..14cad7523476a1033a3450375de19962f28b8780 GIT binary patch literal 238 zcmVPx#s7XXYR5*?8R51?2APjV>{soj@!5Bo|kr|9eKCT3d13ww;?U%K|B-kA?3w=L{hPD5V^QEWj8e(FY(RPt(MR zSm~QGB_aqRfcGA>*46&yO!s|Px#s7XXYR5*?8)G-RfAQ%PUm(sf=dWu{n?#9VOWDA19DYp_74`Zhmll+B-xCHtp zdFVHYzfI0LqjDa#!9R*8RZi1{7~@X>V+>mBZJCJ-!$8$!sJ-{O)0?B~y4FX_q?F*C zyM_uOKI;$y=6Ocn_t(%8G{!(miLUETq2xq)@3AZk#&J9ienqXdP)ea~+f83y%{!d) oL)Yv-Y3ktF*Kj~<