Skip to content

Commit b7065eb

Browse files
authored
feat: Better UX for input blocks (#115)
* feat: input block code editors with variable name change button * add options to input blocks * show input values in editors * live update of input editor when value changes * fix date values * feat: make non-text input block editors readonly * fix date format * fix: prevent language change of input block editors * feat: add Step option to slider inputs * feat: add file picker button to file inputs * add select input settings screen * feat: add multi-select support to select inputs * fix: improve UX of multi-select * fix empty value handling for multi-select inputs * feat: add warning comment to button blocks * fix ts issues: add localization for select box webview * fix: normalize 'None' to null in select input parsing * fix: parse slider value as number instead of string * fix: use null instead of empty string for date-range parse failures * fix: localize tooltip strings and wrap switch-case declarations in blocks * fix: replace deprecated onKeyPress with onKeyDown * fix: add 'cancel' to WebviewMessage type union * fix: remove duplicate SelectInputSettings interface * fix: add accessibility improvements to SelectInputSettingsPanel * fix: improve webview content loading and localization * fix: use updateCellMetadata to preserve cell outputs and attachments * fix: properly parse slider values as numbers in applyChangesToBlock When falling back to existing or default values, the slider converter now properly parses string values to numbers to ensure consistent numeric types. * fix: update input-file status bar test to expect 3 items The test was expecting 2 items but the provider now returns 3: - Type label - Variable name - Choose File button * fix: resolve all lint issues - Remove 'any' types in inputConverters.ts by using proper type inference from Zod schemas - Remove 'any' type in deepnoteInputBlockEditProtection.ts by using Uri type - Fix import restriction by moving SelectInputSettings and SelectInputWebviewMessage types to src/platform/notebooks/deepnote/types.ts - Re-export types from platform in webview-side types file for backward compatibility * fix: remove invalid content parsing for variable names * refactor: remove dead input value parsing logic * fix: handle lineCount=0 * add logs for failed reverts * remove unnecessary base logic * simplify input converters * fix lang in text input tests * remove outdated input value logic * add handling for start>end date ranges * improve empty selection handling when not allowed * fix file input paths * ease test * improve uri typing * handle special chars in input values * delete unused html * add focus outlines * aria for inputs * tighten typing in converotrs * refactor mock logger * refactor canConvert * fix comment * fix type * void->undefined * remove copyright * fix tests of numeric values * show "Set variable name" when not set * fix extension filter * Fix date input timezone shifts in input block status bar - Add formatDateToYYYYMMDD helper to avoid UTC conversion shifts - Check if date already matches YYYY-MM-DD pattern and use directly - Otherwise construct from local date components (year, month, day) - Apply fix to dateInputChooseDate, dateRangeChooseStart, dateRangeChooseEnd - Prevents dates from shifting by one day in non-UTC timezones * Fix accessibility issues in SelectInputSettingsPanel radio buttons - Replace non-semantic div with role='radio' with semantic <label> elements - Remove manual role, tabIndex, aria-checked attributes from wrapper - Remove manual keyboard handlers (Enter/Space) - native radio behavior - Associate labels with radio inputs for proper click handling - Remove stopPropagation calls - no longer needed with semantic structure - Radio inputs now work natively for screen readers and keyboard users - Maintains existing visual styling via .radio-option CSS class * fix css importing * add tests for command handlers in the input status bar provider * remove unused imports * polish input converter tests * respect cancl token * fix: avoid max<min in sliders * localize file input block status bar * fix: prevent invalid dates * polish select variable metadata cleanup * replace event.data cast with type param * polish localization of select input settings webview * avoid dupe select box options * add accessible labels to select input settings * polish localization of select input settings * disposable in test * reorganize imports in input block tests * fixup token * empty function -> return undefined * fix package lock file * fix: fix default metadata logic * test: add more tests for sql block status bar provider * localize select input strings * tighten typing of select box webview messages * test: add more tests for sql statu sbar * test: add more tests for input block status bar provider * convert to typed error * remove generated copyright * discriminated union for select box options * polish sql status bar test * swap data and localization string sending * Revert "discriminated union for select box options" d6cae8a I changed my mind, let's not mess with this. * refactor: centralize error localization and improve error handling in select input settings - Add failedToSave localization key to SelectInputSettings namespace - Update LocalizedMessages type to include failedToSave key - Replace hardcoded error message with centralized localization key - Wrap workspace.applyEdit in try-catch to capture underlying errors - Include error cause in logger.error, window.showErrorMessage, and WrappedError - Remove unused l10n import * improve test * remove generated copyright header * fix deduplication * Fix promise handling in select input settings save failure Remove promise resolution from the catch block when save fails so the promise stays pending until the user explicitly retries or cancels. Previously, calling resolvePromise(null) made callers think the flow finished even though the panel remained open and interactive. Now when save fails: - The error is logged via logger.error for debugging - The error is already shown to the user via saveSettings - The promise remains pending (not resolved) - The panel stays open so the user can retry or cancel - Only explicit cancel or successful save resolves the promise * fix select box option detection * fix dispose race conditions in select box settings webview * fix select box message type use * cancel token in the webview * hide error cause from ui * consolidate input block language options * fix text input block execution
1 parent b44951c commit b7065eb

22 files changed

+4125
-484
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/messageTypes.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -235,6 +235,24 @@ export type LocalizedMessages = {
235235
integrationsRequiredField: string;
236236
integrationsOptionalField: string;
237237
integrationsUnnamedIntegration: string;
238+
// Select input settings strings
239+
selectInputSettingsTitle: string;
240+
allowMultipleValues: string;
241+
allowEmptyValue: string;
242+
valueSourceTitle: string;
243+
fromOptions: string;
244+
fromOptionsDescription: string;
245+
addOptionPlaceholder: string;
246+
addButton: string;
247+
fromVariable: string;
248+
fromVariableDescription: string;
249+
variablePlaceholder: string;
250+
optionNameLabel: string;
251+
variableNameLabel: string;
252+
removeOptionAriaLabel: string;
253+
saveButton: string;
254+
cancelButton: string;
255+
failedToSave: string;
238256
};
239257
// Map all messages to specific payloads
240258
export class IInteractiveWindowMapping {

src/notebooks/deepnote/converters/inputConverters.ts

Lines changed: 152 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { NotebookCellData, NotebookCellKind } from 'vscode';
22
import { z } from 'zod';
33

4+
import { logger } from '../../../platform/logging';
45
import type { BlockConverter } from './blockConverter';
56
import type { DeepnoteBlock } from '../../../platform/deepnote/deepnoteTypes';
67
import {
@@ -14,57 +15,69 @@ import {
1415
DeepnoteFileInputMetadataSchema,
1516
DeepnoteButtonMetadataSchema
1617
} from '../deepnoteSchemas';
17-
import { parseJsonWithFallback } from '../dataConversionUtils';
1818
import { DEEPNOTE_VSCODE_RAW_CONTENT_KEY } from './constants';
19+
import { formatInputBlockCellContent } from '../inputBlockContentFormatter';
1920

2021
export abstract class BaseInputBlockConverter<T extends z.ZodObject> implements BlockConverter {
2122
abstract schema(): T;
2223
abstract getSupportedType(): string;
2324
abstract defaultConfig(): z.infer<T>;
2425

25-
applyChangesToBlock(block: DeepnoteBlock, cell: NotebookCellData): void {
26+
/**
27+
* Helper method to update block metadata with common logic.
28+
* Clears block.content, parses schema, deletes DEEPNOTE_VSCODE_RAW_CONTENT_KEY,
29+
* and merges metadata with updates.
30+
*
31+
* If metadata is missing or invalid, applies default config.
32+
* Otherwise, preserves existing metadata and only applies updates.
33+
*/
34+
protected updateBlockMetadata(block: DeepnoteBlock, updates: Partial<z.infer<T>>): void {
2635
block.content = '';
2736

28-
const config = this.schema().safeParse(parseJsonWithFallback(cell.value));
37+
if (block.metadata != null) {
38+
delete block.metadata[DEEPNOTE_VSCODE_RAW_CONTENT_KEY];
39+
}
40+
41+
// Check if existing metadata is valid
42+
const existingMetadata = this.schema().safeParse(block.metadata);
43+
const hasValidMetadata =
44+
existingMetadata.success && block.metadata != null && Object.keys(block.metadata).length > 0;
2945

30-
if (config.success !== true) {
46+
if (hasValidMetadata) {
47+
// Preserve existing metadata and only apply updates
3148
block.metadata = {
3249
...(block.metadata ?? {}),
33-
[DEEPNOTE_VSCODE_RAW_CONTENT_KEY]: cell.value
50+
...updates
51+
};
52+
} else {
53+
// Apply defaults when metadata is missing or invalid
54+
block.metadata = {
55+
...this.defaultConfig(),
56+
...updates
3457
};
35-
return;
36-
}
37-
38-
if (block.metadata != null) {
39-
delete block.metadata[DEEPNOTE_VSCODE_RAW_CONTENT_KEY];
4058
}
59+
}
4160

42-
block.metadata = {
43-
...(block.metadata ?? {}),
44-
...config.data
45-
};
61+
applyChangesToBlock(block: DeepnoteBlock, _cell: NotebookCellData): void {
62+
// Default implementation: preserve existing metadata
63+
// Readonly blocks (select, checkbox, date, date-range, button) use this default behavior
64+
// Editable blocks override this method to update specific metadata fields
65+
this.updateBlockMetadata(block, {});
4666
}
4767

4868
canConvert(blockType: string): boolean {
49-
return blockType.toLowerCase() === this.getSupportedType();
69+
return this.getSupportedTypes().includes(blockType.toLowerCase());
5070
}
5171

5272
convertToCell(block: DeepnoteBlock): NotebookCellData {
53-
const deepnoteJupyterRawContentResult = z.string().safeParse(block.metadata?.[DEEPNOTE_VSCODE_RAW_CONTENT_KEY]);
5473
const deepnoteMetadataResult = this.schema().safeParse(block.metadata);
5574

5675
if (deepnoteMetadataResult.error != null) {
57-
console.error('Error parsing deepnote input metadata:', deepnoteMetadataResult.error);
58-
console.debug('Metadata:', JSON.stringify(block.metadata));
76+
logger.error('Error parsing deepnote input metadata', deepnoteMetadataResult.error);
5977
}
6078

61-
const configStr = deepnoteJupyterRawContentResult.success
62-
? deepnoteJupyterRawContentResult.data
63-
: deepnoteMetadataResult.success
64-
? JSON.stringify(deepnoteMetadataResult.data, null, 2)
65-
: JSON.stringify(this.defaultConfig(), null, 2);
66-
67-
const cell = new NotebookCellData(NotebookCellKind.Code, configStr, 'json');
79+
// Default fallback: empty plaintext cell; subclasses render content/language
80+
const cell = new NotebookCellData(NotebookCellKind.Code, '', 'plaintext');
6881

6982
return cell;
7083
}
@@ -86,6 +99,21 @@ export class InputTextBlockConverter extends BaseInputBlockConverter<typeof Deep
8699
defaultConfig() {
87100
return this.DEFAULT_INPUT_TEXT_CONFIG;
88101
}
102+
103+
override convertToCell(block: DeepnoteBlock): NotebookCellData {
104+
const cellValue = formatInputBlockCellContent('input-text', block.metadata ?? {});
105+
const cell = new NotebookCellData(NotebookCellKind.Code, cellValue, 'plaintext');
106+
return cell;
107+
}
108+
109+
override applyChangesToBlock(block: DeepnoteBlock, cell: NotebookCellData): void {
110+
// The cell value contains the text value
111+
const value = cell.value;
112+
113+
this.updateBlockMetadata(block, {
114+
deepnote_variable_value: value
115+
});
116+
}
89117
}
90118

91119
export class InputTextareaBlockConverter extends BaseInputBlockConverter<typeof DeepnoteTextareaInputMetadataSchema> {
@@ -100,6 +128,21 @@ export class InputTextareaBlockConverter extends BaseInputBlockConverter<typeof
100128
defaultConfig() {
101129
return this.DEFAULT_INPUT_TEXTAREA_CONFIG;
102130
}
131+
132+
override convertToCell(block: DeepnoteBlock): NotebookCellData {
133+
const cellValue = formatInputBlockCellContent('input-textarea', block.metadata ?? {});
134+
const cell = new NotebookCellData(NotebookCellKind.Code, cellValue, 'plaintext');
135+
return cell;
136+
}
137+
138+
override applyChangesToBlock(block: DeepnoteBlock, cell: NotebookCellData): void {
139+
// The cell value contains the text value
140+
const value = cell.value;
141+
142+
this.updateBlockMetadata(block, {
143+
deepnote_variable_value: value
144+
});
145+
}
103146
}
104147

105148
export class InputSelectBlockConverter extends BaseInputBlockConverter<typeof DeepnoteSelectInputMetadataSchema> {
@@ -114,6 +157,15 @@ export class InputSelectBlockConverter extends BaseInputBlockConverter<typeof De
114157
defaultConfig() {
115158
return this.DEFAULT_INPUT_SELECT_CONFIG;
116159
}
160+
161+
override convertToCell(block: DeepnoteBlock): NotebookCellData {
162+
const cellValue = formatInputBlockCellContent('input-select', block.metadata ?? {});
163+
const cell = new NotebookCellData(NotebookCellKind.Code, cellValue, 'python');
164+
return cell;
165+
}
166+
167+
// Select blocks are readonly - edits are reverted by DeepnoteInputBlockEditProtection
168+
// Uses base class applyChangesToBlock which preserves existing metadata
117169
}
118170

119171
export class InputSliderBlockConverter extends BaseInputBlockConverter<typeof DeepnoteSliderInputMetadataSchema> {
@@ -128,6 +180,30 @@ export class InputSliderBlockConverter extends BaseInputBlockConverter<typeof De
128180
defaultConfig() {
129181
return this.DEFAULT_INPUT_SLIDER_CONFIG;
130182
}
183+
184+
override convertToCell(block: DeepnoteBlock): NotebookCellData {
185+
const cellValue = formatInputBlockCellContent('input-slider', block.metadata ?? {});
186+
const cell = new NotebookCellData(NotebookCellKind.Code, cellValue, 'python');
187+
return cell;
188+
}
189+
190+
override applyChangesToBlock(block: DeepnoteBlock, cell: NotebookCellData): void {
191+
// Parse numeric value; fall back to existing/default
192+
const str = cell.value.trim();
193+
const parsed = Number(str);
194+
195+
const existingMetadata = this.schema().safeParse(block.metadata);
196+
197+
const existingValue = existingMetadata.success
198+
? Number(existingMetadata.data.deepnote_variable_value)
199+
: Number(this.defaultConfig().deepnote_variable_value);
200+
const fallback = Number.isFinite(existingValue) ? existingValue : 0;
201+
const value = Number.isFinite(parsed) ? parsed : fallback;
202+
203+
this.updateBlockMetadata(block, {
204+
deepnote_variable_value: String(value)
205+
});
206+
}
131207
}
132208

133209
export class InputCheckboxBlockConverter extends BaseInputBlockConverter<typeof DeepnoteCheckboxInputMetadataSchema> {
@@ -142,6 +218,15 @@ export class InputCheckboxBlockConverter extends BaseInputBlockConverter<typeof
142218
defaultConfig() {
143219
return this.DEFAULT_INPUT_CHECKBOX_CONFIG;
144220
}
221+
222+
override convertToCell(block: DeepnoteBlock): NotebookCellData {
223+
const cellValue = formatInputBlockCellContent('input-checkbox', block.metadata ?? {});
224+
const cell = new NotebookCellData(NotebookCellKind.Code, cellValue, 'python');
225+
return cell;
226+
}
227+
228+
// Checkbox blocks are readonly - edits are reverted by DeepnoteInputBlockEditProtection
229+
// Uses base class applyChangesToBlock which preserves existing metadata
145230
}
146231

147232
export class InputDateBlockConverter extends BaseInputBlockConverter<typeof DeepnoteDateInputMetadataSchema> {
@@ -156,6 +241,15 @@ export class InputDateBlockConverter extends BaseInputBlockConverter<typeof Deep
156241
defaultConfig() {
157242
return this.DEFAULT_INPUT_DATE_CONFIG;
158243
}
244+
245+
override convertToCell(block: DeepnoteBlock): NotebookCellData {
246+
const cellValue = formatInputBlockCellContent('input-date', block.metadata ?? {});
247+
const cell = new NotebookCellData(NotebookCellKind.Code, cellValue, 'python');
248+
return cell;
249+
}
250+
251+
// Date blocks are readonly - edits are reverted by DeepnoteInputBlockEditProtection
252+
// Uses base class applyChangesToBlock which preserves existing metadata
159253
}
160254

161255
export class InputDateRangeBlockConverter extends BaseInputBlockConverter<typeof DeepnoteDateRangeInputMetadataSchema> {
@@ -170,6 +264,15 @@ export class InputDateRangeBlockConverter extends BaseInputBlockConverter<typeof
170264
defaultConfig() {
171265
return this.DEFAULT_INPUT_DATE_RANGE_CONFIG;
172266
}
267+
268+
override convertToCell(block: DeepnoteBlock): NotebookCellData {
269+
const cellValue = formatInputBlockCellContent('input-date-range', block.metadata ?? {});
270+
const cell = new NotebookCellData(NotebookCellKind.Code, cellValue, 'python');
271+
return cell;
272+
}
273+
274+
// Date range blocks are readonly - edits are reverted by DeepnoteInputBlockEditProtection
275+
// Uses base class applyChangesToBlock which preserves existing metadata
173276
}
174277

175278
export class InputFileBlockConverter extends BaseInputBlockConverter<typeof DeepnoteFileInputMetadataSchema> {
@@ -184,6 +287,21 @@ export class InputFileBlockConverter extends BaseInputBlockConverter<typeof Deep
184287
defaultConfig() {
185288
return this.DEFAULT_INPUT_FILE_CONFIG;
186289
}
290+
291+
override convertToCell(block: DeepnoteBlock): NotebookCellData {
292+
const cellValue = formatInputBlockCellContent('input-file', block.metadata ?? {});
293+
const cell = new NotebookCellData(NotebookCellKind.Code, cellValue, 'python');
294+
return cell;
295+
}
296+
297+
override applyChangesToBlock(block: DeepnoteBlock, cell: NotebookCellData): void {
298+
// Remove quotes from the cell value
299+
const value = cell.value.trim().replace(/^["']|["']$/g, '');
300+
301+
this.updateBlockMetadata(block, {
302+
deepnote_variable_value: value
303+
});
304+
}
187305
}
188306

189307
export class ButtonBlockConverter extends BaseInputBlockConverter<typeof DeepnoteButtonMetadataSchema> {
@@ -198,4 +316,13 @@ export class ButtonBlockConverter extends BaseInputBlockConverter<typeof Deepnot
198316
defaultConfig() {
199317
return this.DEFAULT_BUTTON_CONFIG;
200318
}
319+
320+
override convertToCell(block: DeepnoteBlock): NotebookCellData {
321+
const cellValue = formatInputBlockCellContent('button', block.metadata ?? {});
322+
const cell = new NotebookCellData(NotebookCellKind.Code, cellValue, 'python');
323+
return cell;
324+
}
325+
326+
// Button blocks don't store any value from the cell content
327+
// Uses base class applyChangesToBlock which preserves existing metadata
201328
}

0 commit comments

Comments
 (0)