Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
85 changes: 71 additions & 14 deletions src/example-sidebar.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import { Dialog, ReactWidget, showDialog } from '@jupyterlab/apputils';

import { codeIcon, markdownIcon } from '@jupyterlab/ui-components';

import * as React from 'react';

import { Message } from '@lumino/messaging';
Expand All @@ -8,12 +10,14 @@ export namespace ExampleSidebar {
export interface IExampleRecord {
name: string;
path: string;
readmePath: string;
description: string;
}

export interface IOptions {
fetchExamples: () => Promise<ReadonlyArray<IExampleRecord>>;
onOpenExample: (examplePath: string) => Promise<void> | void;
onOpenReadme: (readmePath: string) => Promise<void> | void;
}
}

Expand All @@ -22,6 +26,7 @@ export class ExampleSidebar extends ReactWidget {
super();
this._fetchExamples = options.fetchExamples;
this._onOpenExample = options.onOpenExample;
this._onOpenReadme = options.onOpenReadme;
this.addClass('jp-PluginPlayground-sidebar');
this.addClass('jp-PluginPlayground-exampleSidebar');
}
Expand Down Expand Up @@ -97,17 +102,46 @@ export class ExampleSidebar extends ReactWidget {
<span className="jp-PluginPlayground-entryLabel jp-PluginPlayground-exampleName">
{example.name}
</span>
<button
className="jp-Button jp-mod-styled jp-mod-minimal jp-PluginPlayground-actionButton jp-PluginPlayground-exampleOpenButton"
type="button"
aria-label={`Open example ${example.name}`}
title="Open example file"
onClick={() => {
void this._openExample(example);
}}
>
Open
</button>
<div className="jp-PluginPlayground-actions jp-PluginPlayground-exampleActions">
<button
className="jp-Button jp-mod-styled jp-mod-minimal jp-PluginPlayground-actionButton jp-PluginPlayground-exampleOpenButton"
type="button"
aria-label={`Open source for ${example.name}`}
title="Open example source file"
onClick={() => {
void this._openExample(example);
}}
>
{React.createElement(codeIcon.react, {
tag: 'span',
elementSize: 'normal',
className:
'jp-PluginPlayground-actionIcon jp-PluginPlayground-exampleActionIcon'
})}
<span className="jp-PluginPlayground-actionLabel">
Code
</span>
</button>
<button
className="jp-Button jp-mod-styled jp-mod-minimal jp-PluginPlayground-actionButton jp-PluginPlayground-exampleReadmeButton"
type="button"
aria-label={`Open README for ${example.name}`}
title="Open example README"
onClick={() => {
void this._openReadme(example);
}}
>
{React.createElement(markdownIcon.react, {
tag: 'span',
elementSize: 'normal',
className:
'jp-PluginPlayground-actionIcon jp-PluginPlayground-exampleActionIcon'
})}
<span className="jp-PluginPlayground-actionLabel">
README
</span>
</button>
</div>
</div>
<p className="jp-PluginPlayground-description jp-PluginPlayground-exampleDescription">
{example.description}
Expand Down Expand Up @@ -145,15 +179,37 @@ export class ExampleSidebar extends ReactWidget {

private async _openExample(
example: ExampleSidebar.IExampleRecord
): Promise<void> {
await this._openPath(
example.path,
this._onOpenExample,
'example source file'
);
}

private async _openReadme(
example: ExampleSidebar.IExampleRecord
): Promise<void> {
await this._openPath(
example.readmePath,
this._onOpenReadme,
'example README'
);
}

private async _openPath(
path: string,
openPath: (path: string) => Promise<void> | void,
label: string
): Promise<void> {
try {
await this._onOpenExample(example.path);
await openPath(path);
} catch (error) {
const message =
error instanceof Error ? error.message : 'Unknown opening error';
await showDialog({
title: 'Failed to open extension example',
body: `Could not open "${example.path}". ${message}`,
title: 'Failed to open extension example file',
body: `Could not open ${label} "${path}". ${message}`,
buttons: [Dialog.okButton()]
});
}
Expand All @@ -165,6 +221,7 @@ export class ExampleSidebar extends ReactWidget {
private readonly _onOpenExample: (
examplePath: string
) => Promise<void> | void;
private readonly _onOpenReadme: (readmePath: string) => Promise<void> | void;
private _query = '';
private _examples: ReadonlyArray<ExampleSidebar.IExampleRecord> = [];
private _isLoading = false;
Expand Down
23 changes: 21 additions & 2 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -204,7 +204,8 @@ class PluginPlayground {

const exampleSidebar = new ExampleSidebar({
fetchExamples: this._discoverExtensionExamples.bind(this),
onOpenExample: this._openExtensionExample.bind(this)
onOpenExample: this._openExtensionExample.bind(this),
onOpenReadme: this._openExtensionExampleReadme.bind(this)
});
exampleSidebar.id = 'jp-plugin-example-sidebar';
exampleSidebar.title.label = 'Extension Examples';
Expand Down Expand Up @@ -476,8 +477,23 @@ class PluginPlayground {
}

private async _openExtensionExample(examplePath: string): Promise<void> {
await this._openExampleFile(examplePath);
}

private async _openExtensionExampleReadme(readmePath: string): Promise<void> {
if (this.app.commands.hasCommand('markdownviewer:open')) {
await this.app.commands.execute('markdownviewer:open', {
path: readmePath
});
return;
}

await this._openExampleFile(readmePath);
}

private async _openExampleFile(path: string): Promise<void> {
await this.app.commands.execute('docmanager:open', {
path: normalizeContentsPath(examplePath),
path: normalizeContentsPath(path),
factory: 'Editor'
});
}
Expand Down Expand Up @@ -509,6 +525,9 @@ class PluginPlayground {
discovered.push({
name: item.name,
path: entrypoint,
readmePath: normalizeContentsPath(
this._joinPath(exampleDirectory, 'README.md')
),
description
});
}
Expand Down
2 changes: 1 addition & 1 deletion src/token-sidebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,7 @@ export class TokenSidebar extends ReactWidget {
<code className="jp-PluginPlayground-entryLabel jp-PluginPlayground-tokenString">
{token.name}
</code>
<div className="jp-PluginPlayground-tokenActions">
<div className="jp-PluginPlayground-actions">
<button
className="jp-Button jp-mod-styled jp-mod-minimal jp-PluginPlayground-actionButton jp-PluginPlayground-importButton"
type="button"
Expand Down
34 changes: 33 additions & 1 deletion style/base.css
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,10 @@
padding: var(--jp-code-padding);
}

.jp-PluginPlayground-exampleSidebarInner {
container-type: inline-size;
}

.jp-PluginPlayground-count {
color: var(--jp-ui-font-color2);
font-size: var(--jp-ui-font-size0);
Expand Down Expand Up @@ -64,7 +68,7 @@
overflow-wrap: anywhere;
}

.jp-PluginPlayground-tokenActions {
.jp-PluginPlayground-actions {
display: inline-flex;
gap: 4px;
}
Expand Down Expand Up @@ -109,6 +113,20 @@
width: 14px;
}

.jp-PluginPlayground-actionLabel {
white-space: nowrap;
}

.jp-PluginPlayground-exampleActionIcon {
height: 16px;
width: 16px;
}

.jp-PluginPlayground-exampleActionIcon svg {
height: 16px;
width: 16px;
}

.jp-PluginPlayground-description {
color: var(--jp-ui-font-color2);
font-size: var(--jp-ui-font-size0);
Expand All @@ -123,3 +141,17 @@
.jp-PluginPlayground-exampleError {
color: var(--jp-error-color1);
}

@container (max-width: 360px) {
.jp-PluginPlayground-exampleActions {
gap: 2px;
}

.jp-PluginPlayground-exampleActions .jp-PluginPlayground-actionButton {
padding: 2px 4px;
}

.jp-PluginPlayground-exampleActions .jp-PluginPlayground-actionLabel {
display: none;
}
}
19 changes: 19 additions & 0 deletions ui-tests/tests/plugin-playground.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,7 @@ test('opens a dummy extension example from the sidebar', async ({ page }) => {
const integrationExampleName = 'integration-example';
const integrationExampleRoot = `extension-examples/${integrationExampleName}`;
const expectedPath = `${integrationExampleRoot}/src/index.ts`;
const expectedReadmePath = `${integrationExampleRoot}/README.md`;

await page.contents.uploadContent(
JSON.stringify(
Expand All @@ -125,6 +126,11 @@ test('opens a dummy extension example from the sidebar', async ({ page }) => {
'text',
expectedPath
);
await page.contents.uploadContent(
'# Integration Example\n\nThis README explains the example.\n',
'text',
expectedReadmePath
);

await page.goto();
const section = await openSidebarPanel(page, EXAMPLE_SECTION_ID);
Expand All @@ -147,6 +153,19 @@ test('opens a dummy extension example from the sidebar', async ({ page }) => {
const path = current?.context?.path;
return path === pathToOpen;
}, expectedPath);

const readmeButton = exampleItems
.first()
.locator('.jp-PluginPlayground-exampleReadmeButton');
await expect(readmeButton).toBeVisible();
await readmeButton.click();

await page.waitForFunction((pathToOpen: string) => {
const current = window.jupyterapp.shell
.currentWidget as FileEditorWidget | null;
const path = current?.context?.path;
return path === pathToOpen;
}, expectedReadmePath);
});

test('loads current editor file as a plugin extension', async ({
Expand Down
Loading