Skip to content

Commit 9240d01

Browse files
committed
big number UX
1 parent 53be9e5 commit 9240d01

File tree

14 files changed

+1201
-206
lines changed

14 files changed

+1201
-206
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: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -253,6 +253,18 @@ export type LocalizedMessages = {
253253
saveButton: string;
254254
cancelButton: string;
255255
failedToSave: string;
256+
// Big number comparison settings strings
257+
bigNumberComparisonTitle: string;
258+
enableComparison: string;
259+
comparisonTypeLabel: string;
260+
percentageChange: string;
261+
absoluteValue: string;
262+
comparisonValueLabel: string;
263+
comparisonValuePlaceholder: string;
264+
comparisonTitleLabel: string;
265+
comparisonTitlePlaceholder: string;
266+
comparisonFormatLabel: string;
267+
comparisonFormatHelp: string;
256268
};
257269
// Map all messages to specific payloads
258270
export class IInteractiveWindowMapping {
Lines changed: 310 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,310 @@
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
122+
await this.sendLocStrings();
123+
await this.sendInitialData();
124+
125+
// Return a promise that resolves when the user saves or cancels
126+
return new Promise((resolve) => {
127+
this.resolvePromise = resolve;
128+
});
129+
}
130+
131+
private async sendInitialData(): Promise<void> {
132+
if (!this.currentPanel || !this.currentCell) {
133+
return;
134+
}
135+
136+
const metadata = this.currentCell.metadata as Record<string, unknown> | undefined;
137+
138+
const settings: BigNumberComparisonSettings = {
139+
enabled: (metadata?.deepnote_big_number_comparison_enabled as boolean) ?? false,
140+
comparisonType:
141+
(metadata?.deepnote_big_number_comparison_type as 'percentage-change' | 'absolute-value' | '') ?? '',
142+
comparisonValue: (metadata?.deepnote_big_number_comparison_value as string) ?? '',
143+
comparisonTitle: (metadata?.deepnote_big_number_comparison_title as string) ?? '',
144+
comparisonFormat: (metadata?.deepnote_big_number_comparison_format as string) ?? ''
145+
};
146+
147+
await this.currentPanel.webview.postMessage({
148+
type: 'init',
149+
settings
150+
});
151+
}
152+
153+
private async sendLocStrings(): Promise<void> {
154+
if (!this.currentPanel) {
155+
return;
156+
}
157+
158+
const locStrings: Partial<LocalizedMessages> = {
159+
bigNumberComparisonTitle: localize.BigNumberComparison.title,
160+
enableComparison: localize.BigNumberComparison.enableComparison,
161+
comparisonTypeLabel: localize.BigNumberComparison.comparisonTypeLabel,
162+
percentageChange: localize.BigNumberComparison.percentageChange,
163+
absoluteValue: localize.BigNumberComparison.absoluteValue,
164+
comparisonValueLabel: localize.BigNumberComparison.comparisonValueLabel,
165+
comparisonValuePlaceholder: localize.BigNumberComparison.comparisonValuePlaceholder,
166+
comparisonTitleLabel: localize.BigNumberComparison.comparisonTitleLabel,
167+
comparisonTitlePlaceholder: localize.BigNumberComparison.comparisonTitlePlaceholder,
168+
comparisonFormatLabel: localize.BigNumberComparison.comparisonFormatLabel,
169+
comparisonFormatHelp: localize.BigNumberComparison.comparisonFormatHelp,
170+
saveButton: localize.BigNumberComparison.saveButton,
171+
cancelButton: localize.BigNumberComparison.cancelButton
172+
};
173+
174+
await this.currentPanel.webview.postMessage({
175+
type: 'locInit',
176+
locStrings
177+
});
178+
}
179+
180+
private async handleMessage(message: BigNumberComparisonWebviewMessage): Promise<void> {
181+
switch (message.type) {
182+
case 'save':
183+
if (this.currentCell) {
184+
try {
185+
await this.saveSettings(message.settings);
186+
if (this.resolvePromise) {
187+
this.resolvePromise(message.settings);
188+
this.resolvePromise = undefined;
189+
}
190+
this.currentPanel?.dispose();
191+
} catch (error) {
192+
// Error is already shown to user in saveSettings
193+
logger.error('BigNumberComparisonSettingsWebview: Failed to save settings', error);
194+
}
195+
}
196+
break;
197+
198+
case 'cancel':
199+
if (this.resolvePromise) {
200+
this.resolvePromise(null);
201+
this.resolvePromise = undefined;
202+
}
203+
this.currentPanel?.dispose();
204+
break;
205+
206+
case 'init':
207+
case 'locInit':
208+
// These messages are sent from extension to webview, not handled here
209+
break;
210+
}
211+
}
212+
213+
private async saveSettings(settings: BigNumberComparisonSettings): Promise<void> {
214+
if (!this.currentCell) {
215+
return;
216+
}
217+
218+
const edit = new WorkspaceEdit();
219+
const metadata = { ...(this.currentCell.metadata as Record<string, unknown>) };
220+
221+
metadata.deepnote_big_number_comparison_enabled = settings.enabled;
222+
metadata.deepnote_big_number_comparison_type = settings.comparisonType;
223+
metadata.deepnote_big_number_comparison_value = settings.comparisonValue;
224+
metadata.deepnote_big_number_comparison_title = settings.comparisonTitle;
225+
metadata.deepnote_big_number_comparison_format = settings.comparisonFormat;
226+
227+
// Update cell metadata
228+
edit.set(this.currentCell.notebook.uri, [NotebookEdit.updateCellMetadata(this.currentCell.index, metadata)]);
229+
230+
try {
231+
const success = await workspace.applyEdit(edit);
232+
if (!success) {
233+
const errorMessage = localize.BigNumberComparison.failedToSave;
234+
logger.error(errorMessage);
235+
void window.showErrorMessage(errorMessage);
236+
throw new WrappedError(errorMessage, undefined);
237+
}
238+
} catch (error) {
239+
const errorMessage = localize.BigNumberComparison.failedToSave;
240+
const cause = error instanceof Error ? error : undefined;
241+
const causeMessage = cause?.message || String(error);
242+
logger.error(`${errorMessage}: ${causeMessage}`, error);
243+
void window.showErrorMessage(errorMessage);
244+
throw new WrappedError(errorMessage, cause);
245+
}
246+
}
247+
248+
private getWebviewContent(): string {
249+
if (!this.currentPanel) {
250+
return '';
251+
}
252+
253+
const webview = this.currentPanel.webview;
254+
const nonce = this.getNonce();
255+
256+
// Get URIs for the React app
257+
const scriptUri = webview.asWebviewUri(
258+
Uri.joinPath(
259+
this.extensionContext.extensionUri,
260+
'dist',
261+
'webviews',
262+
'webview-side',
263+
'bigNumberComparisonSettings',
264+
'index.js'
265+
)
266+
);
267+
const codiconUri = webview.asWebviewUri(
268+
Uri.joinPath(
269+
this.extensionContext.extensionUri,
270+
'dist',
271+
'webviews',
272+
'webview-side',
273+
'react-common',
274+
'codicon',
275+
'codicon.css'
276+
)
277+
);
278+
279+
const title = localize.BigNumberComparison.title;
280+
281+
return `<!DOCTYPE html>
282+
<html lang="en">
283+
<head>
284+
<meta charset="UTF-8">
285+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
286+
<meta http-equiv="Content-Security-Policy" content="default-src 'none'; style-src ${webview.cspSource} 'unsafe-inline'; script-src 'nonce-${nonce}'; font-src ${webview.cspSource};">
287+
<link rel="stylesheet" href="${codiconUri}">
288+
<title>${title}</title>
289+
</head>
290+
<body>
291+
<div id="root"></div>
292+
<script nonce="${nonce}" type="module" src="${scriptUri}"></script>
293+
</body>
294+
</html>`;
295+
}
296+
297+
private getNonce(): string {
298+
let text = '';
299+
const possible = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
300+
for (let i = 0; i < 32; i++) {
301+
text += possible.charAt(Math.floor(Math.random() * possible.length));
302+
}
303+
return text;
304+
}
305+
306+
public dispose(): void {
307+
this.currentPanel?.dispose();
308+
this.disposables.forEach((d) => d.dispose());
309+
}
310+
}

0 commit comments

Comments
 (0)