diff --git a/src/index.ts b/src/index.ts index 69db821..f6d58e0 100644 --- a/src/index.ts +++ b/src/index.ts @@ -7,7 +7,7 @@ import { } from '@jupyterlab/application'; import { ICommandPalette, MainAreaWidget } from '@jupyterlab/apputils'; import { FileBrowserModel, IDefaultFileBrowser } from '@jupyterlab/filebrowser'; -import { ILauncher, LauncherModel } from '@jupyterlab/launcher'; +import { ILauncher } from '@jupyterlab/launcher'; import { ISettingRegistry } from '@jupyterlab/settingregistry'; import { ITranslator } from '@jupyterlab/translation'; import { addIcon, launcherIcon } from '@jupyterlab/ui-components'; @@ -15,7 +15,13 @@ import { find } from '@lumino/algorithm'; import { ReadonlyPartialJSONObject } from '@lumino/coreutils'; import { DockPanel, TabBar, Widget } from '@lumino/widgets'; import { NewLauncher as Launcher } from './launcher'; -import { CommandIDs, ILauncherDatabase, MAIN_PLUGIN_ID } from './types'; +import { NewModel as Model } from './model'; +import { + CommandIDs, + ILauncherDatabase, + INewLauncher, + MAIN_PLUGIN_ID +} from './types'; import { addCommands } from './commands'; import { sessionDialogsPlugin } from './dialogs'; import { databasePlugin } from './database'; @@ -54,10 +60,10 @@ function activate( labShell: ILabShell | null, palette: ICommandPalette | null, defaultBrowser: IDefaultFileBrowser | null -): ILauncher { +): INewLauncher { const { commands, shell } = app; const trans = translator.load('jupyterlab-new-launcher'); - const model = new LauncherModel(); + const model = new Model(); if ( navigator.userAgent.indexOf('AppleWebKit') !== -1 && diff --git a/src/launcher.tsx b/src/launcher.tsx index f334c8c..d51e851 100644 --- a/src/launcher.tsx +++ b/src/launcher.tsx @@ -11,12 +11,14 @@ import { consoleIcon } from '@jupyterlab/ui-components'; import * as React from 'react'; +import { NewModel } from './model'; import { IItem, IKernelItem, ILastUsedDatabase, IFavoritesDatabase, - ISettingsLayout + ISettingsLayout, + ISectionOptions } from './types'; import { fileIcon, starIcon } from './icons'; import { Item } from './item'; @@ -35,6 +37,7 @@ function LauncherBody(props: { settings: ISettingRegistry.ISettings; favouritesChanged: ISignal; lastUsedChanged: ISignal; + sections: ISectionOptions[]; }): React.ReactElement { const { trans, cwd, typeItems, otherItems, favouritesChanged } = props; const [query, updateQuery] = React.useState(''); @@ -104,79 +107,29 @@ function LauncherBody(props: { const startCollapsed = props.settings.composite .collapsedSections as ISettingsLayout['collapsedSections']; - return ( -
-
-
-

- {trans.__('Current folder:')} {cwd ? cwd : '/'} -

-
-
- {otherItems.map(item => ( - - ))} -
-
- {searchAll ? ( -
- { - updateQuery(query ?? ''); - }} - initialQuery={''} - useFuzzyFilter={false} - /> -
- ) : null} - - {typeItems + const builtinSections: ISectionOptions[] = [ + { + className: 'jp-Launcher-openByType', + title: trans.__('Create Empty'), + icon: fileIcon, + id: 'create-empty', + rank: 1, + render: () => + typeItems .filter( item => !query || item.label.toLowerCase().indexOf(query.toLowerCase()) !== -1 ) - .map(item => ( - - ))} - - {showStarred ? ( - - {starred.length > 0 ? ( - item.execute()} - favouritesChanged={props.favouritesChanged} - lastUsedChanged={props.lastUsedChanged} - /> - ) : ( - 'No starred items' - )} - - ) : null} - + .map(item => ) + }, + { + className: 'jp-Launcher-openByKernel jp-Launcher-launchNotebook', + title: trans.__('Launch New Notebook'), + icon: notebookIcon, + id: 'launch-notebook', + rank: 3, + render: () => ( - - + ) + }, + { + className: 'jp-Launcher-openByKernel jp-Launcher-launchConsole', + title: trans.__('Launch New Console'), + icon: consoleIcon, + id: 'launch-console', + rank: 5, + render: () => ( - + ) + } + ]; + if (showStarred) { + builtinSections.push({ + className: 'jp-Launcher-openByKernel', + title: trans.__('Starred'), + icon: starIcon, + id: 'starred', + rank: 2, + render: () => + starred.length > 0 ? ( + item.execute()} + favouritesChanged={props.favouritesChanged} + lastUsedChanged={props.lastUsedChanged} + /> + ) : ( + 'No starred items' + ) + }); + } + const allSections = [...builtinSections, ...props.sections]; + + return ( +
+
+
+

+ {trans.__('Current folder:')} {cwd ? cwd : '/'} +

+
+
+ {otherItems.map(item => ( + + ))} +
+
+ {searchAll ? ( +
+ { + updateQuery(query ?? ''); + }} + initialQuery={''} + useFuzzyFilter={false} + /> +
+ ) : null} + {allSections + .sort((a, b) => a.rank - b.rank) + .map(section => ( + + {section.render()} + + ))}
); } @@ -216,6 +239,7 @@ export namespace NewLauncher { lastUsedDatabase: ILastUsedDatabase; favoritesDatabase: IFavoritesDatabase; settings: ISettingRegistry.ISettings; + model: NewModel; } } @@ -229,9 +253,15 @@ export class NewLauncher extends Launcher { this._lastUsedDatabase = options.lastUsedDatabase; this._favoritesDatabase = options.favoritesDatabase; this._settings = options.settings; + this._newModel = options.model; + this._newModel.sectionAdded.connect(() => { + this.update(); + }); } private _lastUsedDatabase: ILastUsedDatabase; private _favoritesDatabase: IFavoritesDatabase; + private _newModel: NewModel; + trans: TranslationBundle; renderCommand = (item: ILauncher.IItemOptions): IItem => { @@ -334,6 +364,7 @@ export class NewLauncher extends Launcher { settings={this._settings} favouritesChanged={this._favoritesDatabase.changed} lastUsedChanged={this._lastUsedDatabase.changed} + sections={this._newModel.sections} /> ); } diff --git a/src/model.ts b/src/model.ts new file mode 100644 index 0000000..7dea174 --- /dev/null +++ b/src/model.ts @@ -0,0 +1,15 @@ +import { INewLauncher, ISectionOptions } from './types'; +import { ISignal, Signal } from '@lumino/signaling'; +import { LauncherModel } from '@jupyterlab/launcher'; + +export class NewModel extends LauncherModel implements INewLauncher { + sections: ISectionOptions[] = []; + addSection(options: ISectionOptions) { + this.sections.push(options); + this._sectionAdded.emit(); + } + get sectionAdded(): ISignal { + return this._sectionAdded; + } + private _sectionAdded = new Signal(this); +} diff --git a/src/types.ts b/src/types.ts index a8078c8..8591456 100644 --- a/src/types.ts +++ b/src/types.ts @@ -4,9 +4,23 @@ import type { ILauncher } from '@jupyterlab/launcher'; import type { VirtualElement } from '@lumino/virtualdom'; import type { ISignal } from '@lumino/signaling'; import { Token } from '@lumino/coreutils'; +import type { LabIcon } from '@jupyterlab/ui-components'; export const MAIN_PLUGIN_ID = 'jupyterlab-new-launcher:plugin'; +export interface INewLauncher extends ILauncher { + addSection(options: ISectionOptions): void; +} + +export interface ISectionOptions { + id: string; + title: string; + className: string; + icon: LabIcon; + render: () => React.ReactNode; + rank: number; +} + /** * The command IDs used by the launcher plugin. */