Skip to content

Commit

Permalink
Draft implementation of starring
Browse files Browse the repository at this point in the history
  • Loading branch information
krassowski committed Apr 17, 2024
1 parent 34bbc9b commit 27b7c7d
Show file tree
Hide file tree
Showing 9 changed files with 380 additions and 17 deletions.
23 changes: 23 additions & 0 deletions src/favorites.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { ILauncher } from '@jupyterlab/launcher';

export interface IFavoritesDatabase {
get(item: ILauncher.IItemOptions): boolean | null;
set(item: ILauncher.IItemOptions, isFavourite: boolean): void;
}

export class FavoritesDatabase implements IFavoritesDatabase {
constructor() {
// TODO: use settings registry, or state db, or server to persist this info
this._db = new Map();
}
get(item: ILauncher.IItemOptions) {
return this._db.get(this._itemKey(item)) ?? null;
}
set(item: ILauncher.IItemOptions, isFavourite: boolean) {
this._db.set(this._itemKey(item), isFavourite);
}
private _itemKey(item: ILauncher.IItemOptions): string {
return item.command + '_' + JSON.stringify(item.args);
}
private _db: Map<string, boolean>;
}
7 changes: 7 additions & 0 deletions src/icons.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { LabIcon } from '@jupyterlab/ui-components';
import starSvgstr from '../style/icons/md/star.svg';

export const starIcon = new LabIcon({
name: 'jupyterlab-new-launcher:star',
svgstr: starSvgstr
});
5 changes: 4 additions & 1 deletion src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import { ReadonlyPartialJSONObject } from '@lumino/coreutils';
import { DockPanel, TabBar, Widget } from '@lumino/widgets';
import { NewLauncher as Launcher } from './launcher';
import { LastUsedDatabase } from './last_used';
import { FavoritesDatabase } from './favorites';

/**
* The command IDs used by the launcher plugin.
Expand Down Expand Up @@ -74,6 +75,7 @@ function activate(
}

const lastUsedDatabase = new LastUsedDatabase();
const favoritesDatabase = new FavoritesDatabase();

commands.addCommand(CommandIDs.create, {
label: trans.__('New Launcher'),
Expand All @@ -94,7 +96,8 @@ function activate(
callback,
commands,
translator,
lastUsedDatabase
lastUsedDatabase,
favoritesDatabase
});

launcher.model = model;
Expand Down
2 changes: 1 addition & 1 deletion src/last_used.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ export interface ILastUsedDatabase {
recordAsUsedNow(item: ILauncher.IItemOptions): void;
}

export class LastUsedDatabase {
export class LastUsedDatabase implements ILastUsedDatabase {
constructor() {
// TODO: use settings registry, or state db, or server to persist this info
this._db = new Map();
Expand Down
85 changes: 77 additions & 8 deletions src/launcher.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ import {
} from '@jupyterlab/ui-components';
import * as React from 'react';
import { ILastUsedDatabase } from './last_used';
import { IFavoritesDatabase } from './favorites';
import { starIcon } from './icons';

function TypeCard(props: {
trans: TranslationBundle;
Expand Down Expand Up @@ -48,6 +50,8 @@ interface IItem extends ILauncher.IItemOptions {
iconClass: string;
execute: () => Promise<void>;
lastUsed: Date | null;
starred: boolean;
toggleStar: () => void;
}

interface IKernelItem extends IItem {
Expand Down Expand Up @@ -153,9 +157,17 @@ function LauncherBody(props: {
}}
sortKey={'kernel'}
onRowClick={event => {
const element = (event.target as HTMLElement).querySelector(
'.jp-TableKernelItem'
)!;
const target = event.target as HTMLElement;
const row = target.closest('tr');
if (!row) {
return;
}
const cell = target.closest('td');
const starButton = cell?.querySelector('.jp-starIconButton');
if (starButton) {
return (starButton as HTMLElement).click();
}
const element = row.querySelector('.jp-TableKernelItem')!;
(element as HTMLElement).click();
}}
columns={[
Expand Down Expand Up @@ -189,8 +201,12 @@ function LauncherBody(props: {
renderCell: (row: IKernelItem) => (
<span
className="jp-TableKernelItem"
onClick={row.execute}
onClick={event => {
row.execute();
event.stopPropagation();
}}
onKeyDown={event => {
// TODO memoize func defs for perf
if (event.key === 'Enter') {
row.execute();
}
Expand Down Expand Up @@ -227,6 +243,37 @@ function LauncherBody(props: {
}
return a.lastUsed > b.lastUsed ? 1 : -1;
}
},
{
id: 'star',
label: '',
renderCell: (row: IKernelItem) => {
const [, forceUpdate] = React.useReducer(x => x + 1, 0);

const starred = row.starred;
const title = starred
? trans.__('Click to add this kernel to favourites')
: trans.__('Click to remove the kernel from favourites');
return (
<button
className={
starred
? 'jp-starIconButton jp-mod-starred'
: 'jp-starIconButton'
}
title={title}
onClick={event => {
row.toggleStar();
forceUpdate();
event.stopPropagation();
}}
>
<starIcon.react className="jp-starIcon" />
</button>
);
},
sort: (a: IKernelItem, b: IKernelItem) =>
Number(a.starred) - Number(b.starred)
}
]}
/>
Expand All @@ -238,6 +285,7 @@ function LauncherBody(props: {
export namespace NewLauncher {
export interface IOptions extends ILauncher.IOptions {
lastUsedDatabase: ILastUsedDatabase;
favoritesDatabase: IFavoritesDatabase;
}
}

Expand All @@ -247,8 +295,10 @@ export class NewLauncher extends Launcher {
this.commands = options.commands;
this.trans = this.translator.load('jupyterlab-new-launcher');
this._lastUsedDatabase = options.lastUsedDatabase;
this._favoritesDatabase = options.favoritesDatabase;
}
private _lastUsedDatabase: ILastUsedDatabase;
private _favoritesDatabase: IFavoritesDatabase;
trans: TranslationBundle;

renderCommand = (item: ILauncher.IItemOptions): IItem => {
Expand All @@ -260,15 +310,34 @@ export class NewLauncher extends Launcher {
const execute = async () => {
await this.commands.execute(item.command, args);
this._lastUsedDatabase.recordAsUsedNow(item);
obj.lastUsed = this._lastUsedDatabase.get(item);
};
const lastUsed = this._lastUsedDatabase.get(item);
return { ...item, icon, iconClass, label, caption, execute, lastUsed };
const starred = this._favoritesDatabase.get(item) ?? false;
const toggleStar = () => {
const wasStarred = this._favoritesDatabase.get(item);
const newState = !wasStarred;
obj.starred = newState;
this._favoritesDatabase.set(item, newState);
};
const obj = {
...item,
icon,
iconClass,
label,
caption,
execute,
lastUsed,
starred,
toggleStar
};

return obj;
};

renderKernelCommand = (item: ILauncher.IItemOptions): IItem => {
return {
...this.renderCommand(item)
};
// note: do not use spread syntax here or object attributes will get frozen
return this.renderCommand(item);
};

/**
Expand Down
4 changes: 4 additions & 0 deletions src/svg.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
declare module '*.svg' {
const script: string;
export default script;
}
65 changes: 58 additions & 7 deletions style/base.css
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
--jp-animation-depth: 10px;
--jp-animation-time: 150ms;
--jp-title-height: 24px;

padding: var(--jp-animation-depth);
}

Expand Down Expand Up @@ -46,26 +47,76 @@
}

.jp-LauncherBody {
margin: 0 12px;
padding: 0 12px;
height: 100%;
overflow-y: auto;
}

.jp-LauncherBody-Title {
font-size: 20px;
}

.jp-Launcher-openByKernel {
--jp-icon-size: 16px;
}

.jp-Launcher-openByKernel .jp-Launcher-kernelIcon {
width: 16px;
height: 16px;
width: var(--jp-icon-size);
height: var(--jp-icon-size);
}

.jp-Launcher-openByKernel .jp-LauncherCard-noKernelIcon {
font-size: 16px;
font-size: var(--jp-icon-size);
}

.jp-Launcher-openByKernel .jp-LauncherCard-icon {
height: 16px;
height: var(--jp-icon-size);
}

.jp-Launcher-openByKernel th[data-id='icon'],
.jp-Launcher-openByKernel th[data-id='star'] {
width: var(--jp-icon-size);
}

.jp-Launcher-openByKernel .jp-starIcon {
width: var(--jp-icon-size);
height: var(--jp-icon-size);
}

.jp-starIconButton {
--jp-transition-transform: rotate(72deg);

border: 0;
margin: 0;
padding: 0 6px;
background: transparent;
cursor: pointer;
}

.jp-starIcon .jp-star-filled {
opacity: 0;
}

.jp-Launcher-openByKernel th[data-id='icon'] {
width: 16px;
.jp-starIcon .jp-star-filled,
.jp-starIcon .jp-star-border {
transition-property: opacity, transform;
transition-duration: 0.2s;
transition-timing-function: ease-out;
transform-origin: center;
}

.jp-starIconButton.jp-mod-starred .jp-star-filled {
opacity: 1;
transform: var(--jp-transition-transform);
}

.jp-starIconButton.jp-mod-starred .jp-star-border {
opacity: 0;
transform: var(--jp-transition-transform);
}

/* Styles for scenario where full row is clickable */

.jp-Launcher-openByKernel tr {
cursor: pointer;
}
Loading

0 comments on commit 27b7c7d

Please sign in to comment.