Skip to content

Commit 7277539

Browse files
authored
feat: big number block UX and settings (#164)
* big number UX * fix webview initialization * fix typing into big number comparison settings * fix reinitialization of big number webview * polish big number status bar * make it clear comparison value is a variable * polish comparison button title * lint
1 parent 90bfefd commit 7277539

File tree

14 files changed

+1314
-199
lines changed

14 files changed

+1314
-199
lines changed

build/esbuild/build.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -384,6 +384,11 @@ async function buildAll() {
384384
path.join(extensionFolder, 'src', 'webviews', 'webview-side', 'selectInputSettings', 'index.tsx'),
385385
path.join(extensionFolder, 'dist', 'webviews', 'webview-side', 'selectInputSettings', 'index.js'),
386386
{ target: 'web', watch: watchAll }
387+
),
388+
build(
389+
path.join(extensionFolder, 'src', 'webviews', 'webview-side', 'bigNumberComparisonSettings', 'index.tsx'),
390+
path.join(extensionFolder, 'dist', 'webviews', 'webview-side', 'bigNumberComparisonSettings', 'index.js'),
391+
{ target: 'web', watch: watchAll }
387392
)
388393
);
389394

src/messageTypes.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -470,6 +470,20 @@ export type LocalizedMessages = {
470470
saveButton: string;
471471
cancelButton: string;
472472
failedToSave: string;
473+
// Big number comparison settings strings
474+
bigNumberComparisonTitle: string;
475+
enableComparison: string;
476+
comparisonTypeLabel: string;
477+
percentageChange: string;
478+
absoluteValue: string;
479+
comparisonValueLabel: string;
480+
comparisonValuePlaceholder: string;
481+
comparisonTitleLabel: string;
482+
comparisonTitlePlaceholder: string;
483+
comparisonTitleHelp: string;
484+
comparisonValueHelp: string;
485+
comparisonFormatLabel: string;
486+
comparisonFormatHelp: string;
473487
};
474488
// Map all messages to specific payloads
475489
export class IInteractiveWindowMapping {
Lines changed: 314 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,314 @@
1+
import {
2+
CancellationToken,
3+
Disposable,
4+
NotebookCell,
5+
NotebookEdit,
6+
Uri,
7+
ViewColumn,
8+
WebviewPanel,
9+
window,
10+
workspace,
11+
WorkspaceEdit
12+
} from 'vscode';
13+
import { inject, injectable } from 'inversify';
14+
15+
import { IExtensionContext } from '../../platform/common/types';
16+
import { LocalizedMessages } from '../../messageTypes';
17+
import * as localize from '../../platform/common/utils/localize';
18+
import {
19+
BigNumberComparisonSettings,
20+
BigNumberComparisonWebviewMessage
21+
} from '../../platform/notebooks/deepnote/types';
22+
import { WrappedError } from '../../platform/errors/types';
23+
import { logger } from '../../platform/logging';
24+
25+
/**
26+
* Manages the webview panel for big number comparison settings
27+
*/
28+
@injectable()
29+
export class BigNumberComparisonSettingsWebviewProvider {
30+
private currentPanel: WebviewPanel | undefined;
31+
private currentPanelId: number = 0;
32+
private readonly disposables: Disposable[] = [];
33+
private currentCell: NotebookCell | undefined;
34+
private resolvePromise: ((settings: BigNumberComparisonSettings | null) => void) | undefined;
35+
36+
constructor(@inject(IExtensionContext) private readonly extensionContext: IExtensionContext) {}
37+
38+
/**
39+
* Show the big number comparison settings webview
40+
*/
41+
public async show(cell: NotebookCell, token?: CancellationToken): Promise<BigNumberComparisonSettings | null> {
42+
this.currentCell = cell;
43+
44+
const column = window.activeTextEditor ? window.activeTextEditor.viewColumn : ViewColumn.One;
45+
46+
// If we already have a panel, cancel any outstanding operation before disposing
47+
if (this.currentPanel) {
48+
// Cancel the previous operation by resolving with null
49+
if (this.resolvePromise) {
50+
this.resolvePromise(null);
51+
this.resolvePromise = undefined;
52+
}
53+
// Now dispose the old panel
54+
this.currentPanel.dispose();
55+
}
56+
57+
// Increment panel ID to track this specific panel instance
58+
this.currentPanelId++;
59+
const panelId = this.currentPanelId;
60+
61+
// Create a new panel
62+
this.currentPanel = window.createWebviewPanel(
63+
'deepnoteBigNumberComparisonSettings',
64+
localize.BigNumberComparison.title,
65+
column || ViewColumn.One,
66+
{
67+
enableScripts: true,
68+
retainContextWhenHidden: true,
69+
localResourceRoots: [this.extensionContext.extensionUri]
70+
}
71+
);
72+
73+
// Set the webview's initial html content
74+
this.currentPanel.webview.html = this.getWebviewContent();
75+
76+
// Handle messages from the webview
77+
this.currentPanel.webview.onDidReceiveMessage(
78+
async (message: BigNumberComparisonWebviewMessage) => {
79+
await this.handleMessage(message);
80+
},
81+
null,
82+
this.disposables
83+
);
84+
85+
// Handle cancellation token if provided
86+
let cancellationDisposable: Disposable | undefined;
87+
if (token) {
88+
cancellationDisposable = token.onCancellationRequested(() => {
89+
// Only handle cancellation if this is still the current panel
90+
if (this.currentPanelId === panelId) {
91+
if (this.resolvePromise) {
92+
this.resolvePromise(null);
93+
this.resolvePromise = undefined;
94+
}
95+
this.currentPanel?.dispose();
96+
}
97+
});
98+
}
99+
100+
// Reset when the current panel is closed
101+
this.currentPanel.onDidDispose(
102+
() => {
103+
// Only handle disposal if this is still the current panel
104+
if (this.currentPanelId === panelId) {
105+
this.currentPanel = undefined;
106+
this.currentCell = undefined;
107+
if (this.resolvePromise) {
108+
this.resolvePromise(null);
109+
this.resolvePromise = undefined;
110+
}
111+
// Clean up cancellation listener
112+
cancellationDisposable?.dispose();
113+
this.disposables.forEach((d) => d.dispose());
114+
this.disposables.length = 0;
115+
}
116+
},
117+
null,
118+
this.disposables
119+
);
120+
121+
// Send initial data after a small delay to ensure webview is ready
122+
// This is necessary because postMessage can fail if sent before the webview is fully loaded
123+
setTimeout(async () => {
124+
await this.sendLocStrings();
125+
await this.sendInitialData();
126+
}, 100);
127+
128+
// Return a promise that resolves when the user saves or cancels
129+
return new Promise((resolve) => {
130+
this.resolvePromise = resolve;
131+
});
132+
}
133+
134+
private async sendInitialData(): Promise<void> {
135+
if (!this.currentPanel || !this.currentCell) {
136+
return;
137+
}
138+
139+
const metadata = this.currentCell.metadata as Record<string, unknown> | undefined;
140+
141+
const settings: BigNumberComparisonSettings = {
142+
enabled: (metadata?.deepnote_big_number_comparison_enabled as boolean) ?? false,
143+
comparisonType:
144+
(metadata?.deepnote_big_number_comparison_type as 'percentage-change' | 'absolute-value' | '') ?? '',
145+
comparisonValue: (metadata?.deepnote_big_number_comparison_value as string) ?? '',
146+
comparisonTitle: (metadata?.deepnote_big_number_comparison_title as string) ?? '',
147+
comparisonFormat: (metadata?.deepnote_big_number_comparison_format as string) ?? ''
148+
};
149+
150+
await this.currentPanel.webview.postMessage({
151+
type: 'init',
152+
settings
153+
});
154+
}
155+
156+
private async sendLocStrings(): Promise<void> {
157+
if (!this.currentPanel) {
158+
return;
159+
}
160+
161+
const locStrings: Partial<LocalizedMessages> = {
162+
bigNumberComparisonTitle: localize.BigNumberComparison.title,
163+
enableComparison: localize.BigNumberComparison.enableComparison,
164+
comparisonTypeLabel: localize.BigNumberComparison.comparisonTypeLabel,
165+
percentageChange: localize.BigNumberComparison.percentageChange,
166+
absoluteValue: localize.BigNumberComparison.absoluteValue,
167+
comparisonValueLabel: localize.BigNumberComparison.comparisonValueLabel,
168+
comparisonValuePlaceholder: localize.BigNumberComparison.comparisonValuePlaceholder,
169+
comparisonTitleLabel: localize.BigNumberComparison.comparisonTitleLabel,
170+
comparisonTitlePlaceholder: localize.BigNumberComparison.comparisonTitlePlaceholder,
171+
comparisonTitleHelp: localize.BigNumberComparison.comparisonTitleHelp,
172+
comparisonFormatLabel: localize.BigNumberComparison.comparisonFormatLabel,
173+
comparisonFormatHelp: localize.BigNumberComparison.comparisonFormatHelp,
174+
saveButton: localize.BigNumberComparison.saveButton,
175+
cancelButton: localize.BigNumberComparison.cancelButton
176+
};
177+
178+
await this.currentPanel.webview.postMessage({
179+
type: 'locInit',
180+
locStrings
181+
});
182+
}
183+
184+
private async handleMessage(message: BigNumberComparisonWebviewMessage): Promise<void> {
185+
switch (message.type) {
186+
case 'save':
187+
if (this.currentCell) {
188+
try {
189+
await this.saveSettings(message.settings);
190+
if (this.resolvePromise) {
191+
this.resolvePromise(message.settings);
192+
this.resolvePromise = undefined;
193+
}
194+
this.currentPanel?.dispose();
195+
} catch (error) {
196+
// Error is already shown to user in saveSettings
197+
logger.error('BigNumberComparisonSettingsWebview: Failed to save settings', error);
198+
}
199+
}
200+
break;
201+
202+
case 'cancel':
203+
if (this.resolvePromise) {
204+
this.resolvePromise(null);
205+
this.resolvePromise = undefined;
206+
}
207+
this.currentPanel?.dispose();
208+
break;
209+
210+
case 'init':
211+
case 'locInit':
212+
// These messages are sent from extension to webview, not handled here
213+
break;
214+
}
215+
}
216+
217+
private async saveSettings(settings: BigNumberComparisonSettings): Promise<void> {
218+
if (!this.currentCell) {
219+
return;
220+
}
221+
222+
const edit = new WorkspaceEdit();
223+
const metadata = { ...(this.currentCell.metadata as Record<string, unknown>) };
224+
225+
metadata.deepnote_big_number_comparison_enabled = settings.enabled;
226+
metadata.deepnote_big_number_comparison_type = settings.comparisonType;
227+
metadata.deepnote_big_number_comparison_value = settings.comparisonValue;
228+
metadata.deepnote_big_number_comparison_title = settings.comparisonTitle;
229+
metadata.deepnote_big_number_comparison_format = settings.comparisonFormat;
230+
231+
// Update cell metadata
232+
edit.set(this.currentCell.notebook.uri, [NotebookEdit.updateCellMetadata(this.currentCell.index, metadata)]);
233+
234+
try {
235+
const success = await workspace.applyEdit(edit);
236+
if (!success) {
237+
const errorMessage = localize.BigNumberComparison.failedToSave;
238+
logger.error(errorMessage);
239+
void window.showErrorMessage(errorMessage);
240+
throw new WrappedError(errorMessage, undefined);
241+
}
242+
} catch (error) {
243+
const errorMessage = localize.BigNumberComparison.failedToSave;
244+
const cause = error instanceof Error ? error : undefined;
245+
const causeMessage = cause?.message || String(error);
246+
logger.error(`${errorMessage}: ${causeMessage}`, error);
247+
void window.showErrorMessage(errorMessage);
248+
throw new WrappedError(errorMessage, cause);
249+
}
250+
}
251+
252+
private getWebviewContent(): string {
253+
if (!this.currentPanel) {
254+
return '';
255+
}
256+
257+
const webview = this.currentPanel.webview;
258+
const nonce = this.getNonce();
259+
260+
// Get URIs for the React app
261+
const scriptUri = webview.asWebviewUri(
262+
Uri.joinPath(
263+
this.extensionContext.extensionUri,
264+
'dist',
265+
'webviews',
266+
'webview-side',
267+
'bigNumberComparisonSettings',
268+
'index.js'
269+
)
270+
);
271+
const codiconUri = webview.asWebviewUri(
272+
Uri.joinPath(
273+
this.extensionContext.extensionUri,
274+
'dist',
275+
'webviews',
276+
'webview-side',
277+
'react-common',
278+
'codicon',
279+
'codicon.css'
280+
)
281+
);
282+
283+
const title = localize.BigNumberComparison.title;
284+
285+
return `<!DOCTYPE html>
286+
<html lang="en">
287+
<head>
288+
<meta charset="UTF-8">
289+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
290+
<meta http-equiv="Content-Security-Policy" content="default-src 'none'; style-src ${webview.cspSource} 'unsafe-inline'; script-src 'nonce-${nonce}'; font-src ${webview.cspSource};">
291+
<link rel="stylesheet" href="${codiconUri}">
292+
<title>${title}</title>
293+
</head>
294+
<body>
295+
<div id="root"></div>
296+
<script nonce="${nonce}" type="module" src="${scriptUri}"></script>
297+
</body>
298+
</html>`;
299+
}
300+
301+
private getNonce(): string {
302+
let text = '';
303+
const possible = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
304+
for (let i = 0; i < 32; i++) {
305+
text += possible.charAt(Math.floor(Math.random() * possible.length));
306+
}
307+
return text;
308+
}
309+
310+
public dispose(): void {
311+
this.currentPanel?.dispose();
312+
this.disposables.forEach((d) => d.dispose());
313+
}
314+
}

0 commit comments

Comments
 (0)