Skip to content

Commit cf64ac0

Browse files
committed
add select input settings screen
1 parent 28e205f commit cf64ac0

File tree

8 files changed

+832
-1
lines changed

8 files changed

+832
-1
lines changed

build/esbuild/build.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -379,6 +379,11 @@ async function buildAll() {
379379
path.join(extensionFolder, 'src', 'webviews', 'webview-side', 'integrations', 'index.tsx'),
380380
path.join(extensionFolder, 'dist', 'webviews', 'webview-side', 'integrations', 'index.js'),
381381
{ target: 'web', watch: watchAll }
382+
),
383+
build(
384+
path.join(extensionFolder, 'src', 'webviews', 'webview-side', 'selectInputSettings', 'index.tsx'),
385+
path.join(extensionFolder, 'dist', 'webviews', 'webview-side', 'selectInputSettings', 'index.js'),
386+
{ target: 'web', watch: watchAll }
382387
)
383388
);
384389

src/notebooks/deepnote/deepnoteInputBlockCellStatusBarProvider.ts

Lines changed: 41 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,9 +18,11 @@ import {
1818
workspace
1919
} from 'vscode';
2020
import { IExtensionSyncActivationService } from '../../platform/activation/types';
21-
import { injectable } from 'inversify';
21+
import { inject, injectable } from 'inversify';
2222
import type { Pocket } from '../../platform/deepnote/pocket';
2323
import { formatInputBlockCellContent } from './inputBlockContentFormatter';
24+
import { SelectInputSettingsWebviewProvider } from './selectInputSettingsWebview';
25+
import { IExtensionContext } from '../../platform/common/types';
2426

2527
/**
2628
* Provides status bar items for Deepnote input block cells to display their block type.
@@ -32,9 +34,14 @@ export class DeepnoteInputBlockCellStatusBarItemProvider
3234
{
3335
private readonly disposables: Disposable[] = [];
3436
private readonly _onDidChangeCellStatusBarItems = new EventEmitter<void>();
37+
private readonly selectInputSettingsWebview: SelectInputSettingsWebviewProvider;
3538

3639
public readonly onDidChangeCellStatusBarItems = this._onDidChangeCellStatusBarItems.event;
3740

41+
constructor(@inject(IExtensionContext) extensionContext: IExtensionContext) {
42+
this.selectInputSettingsWebview = new SelectInputSettingsWebviewProvider(extensionContext);
43+
}
44+
3845
// List of supported Deepnote input block types
3946
private readonly INPUT_BLOCK_TYPES = [
4047
'input-text',
@@ -139,6 +146,16 @@ export class DeepnoteInputBlockCellStatusBarItemProvider
139146
})
140147
);
141148

149+
// Select input: configure settings
150+
this.disposables.push(
151+
commands.registerCommand('deepnote.selectInputSettings', async (cell?: NotebookCell) => {
152+
const activeCell = cell || this.getActiveCell();
153+
if (activeCell) {
154+
await this.selectInputSettings(activeCell);
155+
}
156+
})
157+
);
158+
142159
// Checkbox: toggle value
143160
this.disposables.push(
144161
commands.registerCommand('deepnote.checkboxToggle', async (cell?: NotebookCell) => {
@@ -318,6 +335,19 @@ export class DeepnoteInputBlockCellStatusBarItemProvider
318335
}
319336
});
320337

338+
// Settings button
339+
items.push({
340+
text: l10n.t('$(gear) Settings'),
341+
alignment: 1,
342+
priority: 79,
343+
tooltip: l10n.t('Configure select input settings\nClick to open'),
344+
command: {
345+
title: l10n.t('Settings'),
346+
command: 'deepnote.selectInputSettings',
347+
arguments: [cell]
348+
}
349+
});
350+
321351
// Show source variable if select type is from-variable
322352
if (selectType === 'from-variable' && sourceVariable) {
323353
items.push({
@@ -845,6 +875,16 @@ export class DeepnoteInputBlockCellStatusBarItemProvider
845875
await this.updateCellMetadata(cell, { deepnote_variable_value: filePath });
846876
}
847877

878+
/**
879+
* Handler for select input: configure settings
880+
*/
881+
private async selectInputSettings(cell: NotebookCell): Promise<void> {
882+
await this.selectInputSettingsWebview.show(cell);
883+
// The webview will handle saving the settings
884+
// Trigger a status bar refresh after the webview closes
885+
this._onDidChangeCellStatusBarItems.fire();
886+
}
887+
848888
/**
849889
* Handler for date input: choose date
850890
*/
Lines changed: 250 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,250 @@
1+
// Copyright (c) Microsoft Corporation.
2+
// Licensed under the MIT License.
3+
4+
import {
5+
Disposable,
6+
NotebookCell,
7+
NotebookEdit,
8+
NotebookRange,
9+
Uri,
10+
ViewColumn,
11+
WebviewPanel,
12+
window,
13+
workspace,
14+
WorkspaceEdit
15+
} from 'vscode';
16+
import { inject, injectable } from 'inversify';
17+
import { IExtensionContext } from '../../platform/common/types';
18+
19+
interface SelectInputSettings {
20+
allowMultipleValues: boolean;
21+
allowEmptyValue: boolean;
22+
selectType: 'from-options' | 'from-variable';
23+
options: string[];
24+
selectedVariable: string;
25+
}
26+
27+
/**
28+
* Manages the webview panel for select input settings
29+
*/
30+
@injectable()
31+
export class SelectInputSettingsWebviewProvider {
32+
private currentPanel: WebviewPanel | undefined;
33+
private readonly disposables: Disposable[] = [];
34+
private currentCell: NotebookCell | undefined;
35+
private resolvePromise: ((settings: SelectInputSettings | null) => void) | undefined;
36+
37+
constructor(@inject(IExtensionContext) private readonly extensionContext: IExtensionContext) {}
38+
39+
/**
40+
* Show the select input settings webview
41+
*/
42+
public async show(cell: NotebookCell): Promise<SelectInputSettings | null> {
43+
this.currentCell = cell;
44+
45+
const column = window.activeTextEditor ? window.activeTextEditor.viewColumn : ViewColumn.One;
46+
47+
// If we already have a panel, dispose it and create a new one
48+
if (this.currentPanel) {
49+
this.currentPanel.dispose();
50+
}
51+
52+
// Create a new panel
53+
this.currentPanel = window.createWebviewPanel(
54+
'deepnoteSelectInputSettings',
55+
'Select Input Settings',
56+
column || ViewColumn.One,
57+
{
58+
enableScripts: true,
59+
retainContextWhenHidden: true,
60+
localResourceRoots: [this.extensionContext.extensionUri]
61+
}
62+
);
63+
64+
// Set the webview's initial html content
65+
this.currentPanel.webview.html = this.getWebviewContent();
66+
67+
// Handle messages from the webview
68+
this.currentPanel.webview.onDidReceiveMessage(
69+
async (message) => {
70+
await this.handleMessage(message);
71+
},
72+
null,
73+
this.disposables
74+
);
75+
76+
// Reset when the current panel is closed
77+
this.currentPanel.onDidDispose(
78+
() => {
79+
this.currentPanel = undefined;
80+
this.currentCell = undefined;
81+
if (this.resolvePromise) {
82+
this.resolvePromise(null);
83+
this.resolvePromise = undefined;
84+
}
85+
this.disposables.forEach((d) => d.dispose());
86+
this.disposables.length = 0;
87+
},
88+
null,
89+
this.disposables
90+
);
91+
92+
// Send initial data
93+
await this.sendInitialData();
94+
95+
// Return a promise that resolves when the user saves or cancels
96+
return new Promise((resolve) => {
97+
this.resolvePromise = resolve;
98+
});
99+
}
100+
101+
private async sendInitialData(): Promise<void> {
102+
if (!this.currentPanel || !this.currentCell) {
103+
return;
104+
}
105+
106+
const metadata = this.currentCell.metadata as Record<string, unknown> | undefined;
107+
108+
const settings: SelectInputSettings = {
109+
allowMultipleValues: (metadata?.deepnote_allow_multiple_values as boolean) ?? false,
110+
allowEmptyValue: (metadata?.deepnote_allow_empty_values as boolean) ?? false,
111+
selectType: (metadata?.deepnote_variable_select_type as 'from-options' | 'from-variable') ?? 'from-options',
112+
options: (metadata?.deepnote_variable_custom_options as string[]) ?? [],
113+
selectedVariable: (metadata?.deepnote_variable_selected_variable as string) ?? ''
114+
};
115+
116+
await this.currentPanel.webview.postMessage({
117+
type: 'init',
118+
settings
119+
});
120+
}
121+
122+
private async handleMessage(message: { type: string; settings?: SelectInputSettings }): Promise<void> {
123+
switch (message.type) {
124+
case 'save':
125+
if (message.settings && this.currentCell) {
126+
await this.saveSettings(message.settings);
127+
if (this.resolvePromise) {
128+
this.resolvePromise(message.settings);
129+
this.resolvePromise = undefined;
130+
}
131+
this.currentPanel?.dispose();
132+
}
133+
break;
134+
135+
case 'cancel':
136+
if (this.resolvePromise) {
137+
this.resolvePromise(null);
138+
this.resolvePromise = undefined;
139+
}
140+
this.currentPanel?.dispose();
141+
break;
142+
}
143+
}
144+
145+
private async saveSettings(settings: SelectInputSettings): Promise<void> {
146+
if (!this.currentCell) {
147+
return;
148+
}
149+
150+
const edit = new WorkspaceEdit();
151+
const metadata = { ...(this.currentCell.metadata as Record<string, unknown>) };
152+
153+
metadata.deepnote_allow_multiple_values = settings.allowMultipleValues;
154+
metadata.deepnote_allow_empty_values = settings.allowEmptyValue;
155+
metadata.deepnote_variable_select_type = settings.selectType;
156+
metadata.deepnote_variable_custom_options = settings.options;
157+
metadata.deepnote_variable_selected_variable = settings.selectedVariable;
158+
159+
// Update the options field based on the select type
160+
if (settings.selectType === 'from-options') {
161+
metadata.deepnote_variable_options = settings.options;
162+
}
163+
164+
// Replace the cell with updated metadata
165+
const cellData = {
166+
kind: this.currentCell.kind,
167+
languageId: this.currentCell.document.languageId,
168+
value: this.currentCell.document.getText(),
169+
metadata
170+
};
171+
172+
edit.set(this.currentCell.notebook.uri, [
173+
NotebookEdit.replaceCells(new NotebookRange(this.currentCell.index, this.currentCell.index + 1), [cellData])
174+
]);
175+
176+
await workspace.applyEdit(edit);
177+
}
178+
179+
private getWebviewContent(): string {
180+
if (!this.currentPanel) {
181+
return '';
182+
}
183+
184+
const webview = this.currentPanel.webview;
185+
const nonce = this.getNonce();
186+
187+
// Get URIs for the React app
188+
const scriptUri = webview.asWebviewUri(
189+
Uri.joinPath(
190+
this.extensionContext.extensionUri,
191+
'dist',
192+
'webviews',
193+
'webview-side',
194+
'selectInputSettings',
195+
'index.js'
196+
)
197+
);
198+
const styleUri = webview.asWebviewUri(
199+
Uri.joinPath(
200+
this.extensionContext.extensionUri,
201+
'dist',
202+
'webviews',
203+
'webview-side',
204+
'selectInputSettings',
205+
'selectInputSettings.css'
206+
)
207+
);
208+
const codiconUri = webview.asWebviewUri(
209+
Uri.joinPath(
210+
this.extensionContext.extensionUri,
211+
'dist',
212+
'webviews',
213+
'webview-side',
214+
'react-common',
215+
'codicon',
216+
'codicon.css'
217+
)
218+
);
219+
220+
return `<!DOCTYPE html>
221+
<html lang="en">
222+
<head>
223+
<meta charset="UTF-8">
224+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
225+
<meta http-equiv="Content-Security-Policy" content="default-src 'none'; style-src ${webview.cspSource}; script-src 'nonce-${nonce}'; font-src ${webview.cspSource};">
226+
<link rel="stylesheet" href="${codiconUri}">
227+
<link rel="stylesheet" href="${styleUri}">
228+
<title>Select Input Settings</title>
229+
</head>
230+
<body>
231+
<div id="root"></div>
232+
<script nonce="${nonce}" src="${scriptUri}"></script>
233+
</body>
234+
</html>`;
235+
}
236+
237+
private getNonce(): string {
238+
let text = '';
239+
const possible = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
240+
for (let i = 0; i < 32; i++) {
241+
text += possible.charAt(Math.floor(Math.random() * possible.length));
242+
}
243+
return text;
244+
}
245+
246+
public dispose(): void {
247+
this.currentPanel?.dispose();
248+
this.disposables.forEach((d) => d.dispose());
249+
}
250+
}

0 commit comments

Comments
 (0)