From 24851bd7aff07e0645a20408a317ca7af32b344b Mon Sep 17 00:00:00 2001 From: jankuca Date: Thu, 23 Oct 2025 15:16:39 +0200 Subject: [PATCH 001/101] feat: input block code editors with variable name change button --- .../deepnote/converters/inputConverters.ts | 30 +-- .../converters/inputConverters.unit.test.ts | 237 +++++------------ ...deepnoteInputBlockCellStatusBarProvider.ts | 243 +++++++++++++++++- ...putBlockCellStatusBarProvider.unit.test.ts | 128 ++++----- 4 files changed, 381 insertions(+), 257 deletions(-) diff --git a/src/notebooks/deepnote/converters/inputConverters.ts b/src/notebooks/deepnote/converters/inputConverters.ts index 29ec1b9766..22885fa921 100644 --- a/src/notebooks/deepnote/converters/inputConverters.ts +++ b/src/notebooks/deepnote/converters/inputConverters.ts @@ -14,7 +14,6 @@ import { DeepnoteFileInputMetadataSchema, DeepnoteButtonMetadataSchema } from '../deepnoteSchemas'; -import { parseJsonWithFallback } from '../dataConversionUtils'; import { DEEPNOTE_VSCODE_RAW_CONTENT_KEY } from './constants'; export abstract class BaseInputBlockConverter implements BlockConverter { @@ -25,15 +24,12 @@ export abstract class BaseInputBlockConverter implements applyChangesToBlock(block: DeepnoteBlock, cell: NotebookCellData): void { block.content = ''; - const config = this.schema().safeParse(parseJsonWithFallback(cell.value)); + // The cell value now contains just the variable name + const variableName = cell.value.trim(); - if (config.success !== true) { - block.metadata = { - ...(block.metadata ?? {}), - [DEEPNOTE_VSCODE_RAW_CONTENT_KEY]: cell.value - }; - return; - } + // Preserve existing metadata and update only the variable name + const existingMetadata = this.schema().safeParse(block.metadata); + const baseMetadata = existingMetadata.success ? existingMetadata.data : this.defaultConfig(); if (block.metadata != null) { delete block.metadata[DEEPNOTE_VSCODE_RAW_CONTENT_KEY]; @@ -41,7 +37,8 @@ export abstract class BaseInputBlockConverter implements block.metadata = { ...(block.metadata ?? {}), - ...config.data + ...baseMetadata, + deepnote_variable_name: variableName }; } @@ -50,7 +47,6 @@ export abstract class BaseInputBlockConverter implements } convertToCell(block: DeepnoteBlock): NotebookCellData { - const deepnoteJupyterRawContentResult = z.string().safeParse(block.metadata?.[DEEPNOTE_VSCODE_RAW_CONTENT_KEY]); const deepnoteMetadataResult = this.schema().safeParse(block.metadata); if (deepnoteMetadataResult.error != null) { @@ -58,13 +54,13 @@ export abstract class BaseInputBlockConverter implements console.debug('Metadata:', JSON.stringify(block.metadata)); } - const configStr = deepnoteJupyterRawContentResult.success - ? deepnoteJupyterRawContentResult.data - : deepnoteMetadataResult.success - ? JSON.stringify(deepnoteMetadataResult.data, null, 2) - : JSON.stringify(this.defaultConfig(), null, 2); + // Extract the variable name from metadata + const variableName = deepnoteMetadataResult.success + ? (deepnoteMetadataResult.data as { deepnote_variable_name?: string }).deepnote_variable_name || '' + : ''; - const cell = new NotebookCellData(NotebookCellKind.Code, configStr, 'json'); + // Create a code cell with Python language showing just the variable name + const cell = new NotebookCellData(NotebookCellKind.Code, `# ${variableName}`, 'python'); return cell; } diff --git a/src/notebooks/deepnote/converters/inputConverters.unit.test.ts b/src/notebooks/deepnote/converters/inputConverters.unit.test.ts index efa1dbe6f2..92b6163dee 100644 --- a/src/notebooks/deepnote/converters/inputConverters.unit.test.ts +++ b/src/notebooks/deepnote/converters/inputConverters.unit.test.ts @@ -23,7 +23,7 @@ suite('InputTextBlockConverter', () => { }); suite('convertToCell', () => { - test('converts input-text block with metadata to JSON cell', () => { + test('converts input-text block with metadata to Python cell with variable name', () => { const block: DeepnoteBlock = { blockGroup: '92f21410c8c54ac0be7e4d2a544552ee', content: '', @@ -41,13 +41,8 @@ suite('InputTextBlockConverter', () => { const cell = converter.convertToCell(block); assert.strictEqual(cell.kind, NotebookCellKind.Code); - assert.strictEqual(cell.languageId, 'json'); - - const parsed = JSON.parse(cell.value); - assert.strictEqual(parsed.deepnote_input_label, 'some display name'); - assert.strictEqual(parsed.deepnote_variable_name, 'input_1'); - assert.strictEqual(parsed.deepnote_variable_value, 'some text input'); - assert.strictEqual(parsed.deepnote_variable_default_value, 'some default value'); + assert.strictEqual(cell.languageId, 'python'); + assert.strictEqual(cell.value, '# input_1'); }); test('handles missing metadata with default config', () => { @@ -62,23 +57,17 @@ suite('InputTextBlockConverter', () => { const cell = converter.convertToCell(block); assert.strictEqual(cell.kind, NotebookCellKind.Code); - assert.strictEqual(cell.languageId, 'json'); - - const parsed = JSON.parse(cell.value); - assert.strictEqual(parsed.deepnote_input_label, ''); - assert.strictEqual(parsed.deepnote_variable_name, ''); - assert.strictEqual(parsed.deepnote_variable_value, ''); - assert.isNull(parsed.deepnote_variable_default_value); + assert.strictEqual(cell.languageId, 'python'); + assert.strictEqual(cell.value, '# '); }); - test('uses raw content when available', () => { - const rawContent = '{"deepnote_variable_name": "custom"}'; + test('handles missing variable name', () => { const block: DeepnoteBlock = { blockGroup: 'test-group', content: '', id: 'block-123', metadata: { - [DEEPNOTE_VSCODE_RAW_CONTENT_KEY]: rawContent + deepnote_input_label: 'some label' }, sortingKey: 'a0', type: 'input-text' @@ -86,58 +75,69 @@ suite('InputTextBlockConverter', () => { const cell = converter.convertToCell(block); - assert.strictEqual(cell.value, rawContent); + assert.strictEqual(cell.value, '# '); }); }); suite('applyChangesToBlock', () => { - test('applies valid JSON to block metadata', () => { + test('applies variable name from cell value to block metadata', () => { const block: DeepnoteBlock = { blockGroup: 'test-group', content: 'old content', id: 'block-123', + metadata: { + deepnote_input_label: 'existing label', + deepnote_variable_value: 'existing value' + }, sortingKey: 'a0', type: 'input-text' }; - const cellValue = JSON.stringify( - { - deepnote_input_label: 'new label', - deepnote_variable_name: 'new_var', - deepnote_variable_value: 'new value', - deepnote_variable_default_value: 'new default' - }, - null, - 2 - ); - const cell = new NotebookCellData(NotebookCellKind.Code, cellValue, 'json'); + const cell = new NotebookCellData(NotebookCellKind.Code, 'new_var', 'python'); converter.applyChangesToBlock(block, cell); assert.strictEqual(block.content, ''); - assert.strictEqual(block.metadata?.deepnote_input_label, 'new label'); assert.strictEqual(block.metadata?.deepnote_variable_name, 'new_var'); - assert.strictEqual(block.metadata?.deepnote_variable_value, 'new value'); - assert.strictEqual(block.metadata?.deepnote_variable_default_value, 'new default'); + // Other metadata should be preserved + assert.strictEqual(block.metadata?.deepnote_input_label, 'existing label'); + assert.strictEqual(block.metadata?.deepnote_variable_value, 'existing value'); }); - test('handles invalid JSON by storing in raw content key', () => { + test('handles empty variable name', () => { const block: DeepnoteBlock = { blockGroup: 'test-group', content: 'old content', id: 'block-123', + metadata: { + deepnote_variable_name: 'old_var' + }, sortingKey: 'a0', type: 'input-text' }; - const invalidJson = '{invalid json}'; - const cell = new NotebookCellData(NotebookCellKind.Code, invalidJson, 'json'); + const cell = new NotebookCellData(NotebookCellKind.Code, '', 'python'); converter.applyChangesToBlock(block, cell); assert.strictEqual(block.content, ''); - assert.strictEqual(block.metadata?.[DEEPNOTE_VSCODE_RAW_CONTENT_KEY], invalidJson); + assert.strictEqual(block.metadata?.deepnote_variable_name, ''); }); - test('clears raw content key when valid JSON is applied', () => { + test('trims whitespace from variable name', () => { + const block: DeepnoteBlock = { + blockGroup: 'test-group', + content: '', + id: 'block-123', + sortingKey: 'a0', + type: 'input-text' + }; + const cell = new NotebookCellData(NotebookCellKind.Code, ' my_var \n', 'python'); + + converter.applyChangesToBlock(block, cell); + + assert.strictEqual(block.metadata?.deepnote_variable_name, 'my_var'); + }); + + test('clears raw content key when variable name is applied', () => { const block: DeepnoteBlock = { blockGroup: 'test-group', content: '', @@ -148,10 +148,7 @@ suite('InputTextBlockConverter', () => { sortingKey: 'a0', type: 'input-text' }; - const cellValue = JSON.stringify({ - deepnote_variable_name: 'var1' - }); - const cell = new NotebookCellData(NotebookCellKind.Code, cellValue, 'json'); + const cell = new NotebookCellData(NotebookCellKind.Code, 'var1', 'python'); converter.applyChangesToBlock(block, cell); @@ -167,8 +164,7 @@ suite('InputTextBlockConverter', () => { sortingKey: 'a0', type: 'input-text' }; - const cellValue = JSON.stringify({ deepnote_variable_name: 'var' }); - const cell = new NotebookCellData(NotebookCellKind.Code, cellValue, 'json'); + const cell = new NotebookCellData(NotebookCellKind.Code, 'var', 'python'); converter.applyChangesToBlock(block, cell); @@ -188,7 +184,7 @@ suite('InputTextareaBlockConverter', () => { }); suite('convertToCell', () => { - test('converts input-textarea block with multiline value to JSON cell', () => { + test('converts input-textarea block to Python cell with variable name', () => { const block: DeepnoteBlock = { blockGroup: '2b5f9340349f4baaa5a3237331214352', content: '', @@ -204,11 +200,8 @@ suite('InputTextareaBlockConverter', () => { const cell = converter.convertToCell(block); assert.strictEqual(cell.kind, NotebookCellKind.Code); - assert.strictEqual(cell.languageId, 'json'); - - const parsed = JSON.parse(cell.value); - assert.strictEqual(parsed.deepnote_variable_name, 'input_2'); - assert.strictEqual(parsed.deepnote_variable_value, 'some multiline\ntext input'); + assert.strictEqual(cell.languageId, 'python'); + assert.strictEqual(cell.value, '# input_2'); }); test('handles missing metadata with default config', () => { @@ -222,36 +215,33 @@ suite('InputTextareaBlockConverter', () => { const cell = converter.convertToCell(block); - const parsed = JSON.parse(cell.value); - assert.strictEqual(parsed.deepnote_variable_name, ''); - assert.strictEqual(parsed.deepnote_variable_value, ''); - assert.strictEqual(parsed.deepnote_input_label, ''); + assert.strictEqual(cell.value, '# '); }); }); suite('applyChangesToBlock', () => { - test('applies valid JSON with multiline value to block metadata', () => { + test('applies variable name from cell value', () => { const block: DeepnoteBlock = { blockGroup: 'test-group', content: '', id: 'block-123', + metadata: { + deepnote_variable_value: 'line1\nline2\nline3' + }, sortingKey: 'a0', type: 'input-textarea' }; - const cellValue = JSON.stringify({ - deepnote_variable_name: 'textarea_var', - deepnote_variable_value: 'line1\nline2\nline3' - }); - const cell = new NotebookCellData(NotebookCellKind.Code, cellValue, 'json'); + const cell = new NotebookCellData(NotebookCellKind.Code, 'textarea_var', 'python'); converter.applyChangesToBlock(block, cell); assert.strictEqual(block.content, ''); assert.strictEqual(block.metadata?.deepnote_variable_name, 'textarea_var'); + // Other metadata should be preserved assert.strictEqual(block.metadata?.deepnote_variable_value, 'line1\nline2\nline3'); }); - test('handles invalid JSON', () => { + test('handles empty variable name', () => { const block: DeepnoteBlock = { blockGroup: 'test-group', content: '', @@ -259,12 +249,11 @@ suite('InputTextareaBlockConverter', () => { sortingKey: 'a0', type: 'input-textarea' }; - const invalidJson = 'not json'; - const cell = new NotebookCellData(NotebookCellKind.Code, invalidJson, 'json'); + const cell = new NotebookCellData(NotebookCellKind.Code, '', 'python'); converter.applyChangesToBlock(block, cell); - assert.strictEqual(block.metadata?.[DEEPNOTE_VSCODE_RAW_CONTENT_KEY], invalidJson); + assert.strictEqual(block.metadata?.deepnote_variable_name, ''); }); }); }); @@ -277,7 +266,7 @@ suite('InputSelectBlockConverter', () => { }); suite('convertToCell', () => { - test('converts input-select block with single value', () => { + test('converts input-select block to Python cell with variable name', () => { const block: DeepnoteBlock = { blockGroup: 'ba248341bdd94b93a234777968bfedcf', content: '', @@ -298,65 +287,8 @@ suite('InputSelectBlockConverter', () => { const cell = converter.convertToCell(block); assert.strictEqual(cell.kind, NotebookCellKind.Code); - assert.strictEqual(cell.languageId, 'json'); - - const parsed = JSON.parse(cell.value); - assert.strictEqual(parsed.deepnote_variable_name, 'input_3'); - assert.strictEqual(parsed.deepnote_variable_value, 'Option 1'); - assert.deepStrictEqual(parsed.deepnote_variable_options, ['Option 1', 'Option 2']); - }); - - test('converts input-select block with multiple values', () => { - const block: DeepnoteBlock = { - blockGroup: '9f77387639cd432bb913890dea32b6c3', - content: '', - id: '748548e64442416bb9f3a9c5ec22c4de', - metadata: { - deepnote_input_label: 'some select display name', - deepnote_variable_name: 'input_4', - deepnote_variable_value: ['Option 1'], - deepnote_variable_options: ['Option 1', 'Option 2'], - deepnote_variable_select_type: 'from-options', - deepnote_allow_multiple_values: true, - deepnote_variable_custom_options: ['Option 1', 'Option 2'], - deepnote_variable_selected_variable: '' - }, - sortingKey: 'y', - type: 'input-select' - }; - - const cell = converter.convertToCell(block); - - const parsed = JSON.parse(cell.value); - assert.strictEqual(parsed.deepnote_allow_multiple_values, true); - assert.deepStrictEqual(parsed.deepnote_variable_value, ['Option 1']); - }); - - test('converts input-select block with allow empty values', () => { - const block: DeepnoteBlock = { - blockGroup: '146c4af1efb2448fa2d3b7cfbd30da77', - content: '', - id: 'a3521dd942d2407693b0202b55c935a7', - metadata: { - deepnote_input_label: 'allows empty value', - deepnote_variable_name: 'input_5', - deepnote_variable_value: 'Option 1', - deepnote_variable_options: ['Option 1', 'Option 2'], - deepnote_allow_empty_values: true, - deepnote_variable_select_type: 'from-options', - deepnote_variable_default_value: '', - deepnote_variable_custom_options: ['Option 1', 'Option 2'], - deepnote_variable_selected_variable: '' - }, - sortingKey: 'yU', - type: 'input-select' - }; - - const cell = converter.convertToCell(block); - - const parsed = JSON.parse(cell.value); - assert.strictEqual(parsed.deepnote_allow_empty_values, true); - assert.strictEqual(parsed.deepnote_variable_default_value, ''); + assert.strictEqual(cell.languageId, 'python'); + assert.strictEqual(cell.value, '# input_3'); }); test('handles missing metadata with default config', () => { @@ -370,70 +302,33 @@ suite('InputSelectBlockConverter', () => { const cell = converter.convertToCell(block); - const parsed = JSON.parse(cell.value); - assert.strictEqual(parsed.deepnote_variable_name, ''); - assert.strictEqual(parsed.deepnote_variable_value, 'Option 1'); + assert.strictEqual(cell.value, '# '); }); }); suite('applyChangesToBlock', () => { - test('applies valid JSON with single value', () => { + test('applies variable name from cell value', () => { const block: DeepnoteBlock = { blockGroup: 'test-group', content: '', id: 'block-123', + metadata: { + deepnote_variable_value: 'Option A', + deepnote_variable_options: ['Option A', 'Option B'] + }, sortingKey: 'a0', type: 'input-select' }; - const cellValue = JSON.stringify({ - deepnote_variable_name: 'select_var', - deepnote_variable_value: 'Option A', - deepnote_variable_options: ['Option A', 'Option B'] - }); - const cell = new NotebookCellData(NotebookCellKind.Code, cellValue, 'json'); + const cell = new NotebookCellData(NotebookCellKind.Code, 'select_var', 'python'); converter.applyChangesToBlock(block, cell); assert.strictEqual(block.content, ''); + assert.strictEqual(block.metadata?.deepnote_variable_name, 'select_var'); + // Other metadata should be preserved assert.strictEqual(block.metadata?.deepnote_variable_value, 'Option A'); assert.deepStrictEqual(block.metadata?.deepnote_variable_options, ['Option A', 'Option B']); }); - - test('applies valid JSON with array value', () => { - const block: DeepnoteBlock = { - blockGroup: 'test-group', - content: '', - id: 'block-123', - sortingKey: 'a0', - type: 'input-select' - }; - const cellValue = JSON.stringify({ - deepnote_variable_value: ['Option 1', 'Option 2'], - deepnote_allow_multiple_values: true - }); - const cell = new NotebookCellData(NotebookCellKind.Code, cellValue, 'json'); - - converter.applyChangesToBlock(block, cell); - - assert.deepStrictEqual(block.metadata?.deepnote_variable_value, ['Option 1', 'Option 2']); - assert.strictEqual(block.metadata?.deepnote_allow_multiple_values, true); - }); - - test('handles invalid JSON', () => { - const block: DeepnoteBlock = { - blockGroup: 'test-group', - content: '', - id: 'block-123', - sortingKey: 'a0', - type: 'input-select' - }; - const invalidJson = '{broken'; - const cell = new NotebookCellData(NotebookCellKind.Code, invalidJson, 'json'); - - converter.applyChangesToBlock(block, cell); - - assert.strictEqual(block.metadata?.[DEEPNOTE_VSCODE_RAW_CONTENT_KEY], invalidJson); - }); }); }); diff --git a/src/notebooks/deepnote/deepnoteInputBlockCellStatusBarProvider.ts b/src/notebooks/deepnote/deepnoteInputBlockCellStatusBarProvider.ts index 9b0bd542a0..5f06deb636 100644 --- a/src/notebooks/deepnote/deepnoteInputBlockCellStatusBarProvider.ts +++ b/src/notebooks/deepnote/deepnoteInputBlockCellStatusBarProvider.ts @@ -3,10 +3,19 @@ import { Disposable, - notebooks, + EventEmitter, NotebookCell, NotebookCellStatusBarItem, - NotebookCellStatusBarItemProvider + NotebookCellStatusBarItemProvider, + NotebookEdit, + Position, + Range, + WorkspaceEdit, + commands, + l10n, + notebooks, + window, + workspace } from 'vscode'; import { IExtensionSyncActivationService } from '../../platform/activation/types'; import { injectable } from 'inversify'; @@ -21,6 +30,9 @@ export class DeepnoteInputBlockCellStatusBarItemProvider implements NotebookCellStatusBarItemProvider, IExtensionSyncActivationService { private readonly disposables: Disposable[] = []; + private readonly _onDidChangeCellStatusBarItems = new EventEmitter(); + + public readonly onDidChangeCellStatusBarItems = this._onDidChangeCellStatusBarItems.event; // List of supported Deepnote input block types private readonly INPUT_BLOCK_TYPES = [ @@ -38,9 +50,41 @@ export class DeepnoteInputBlockCellStatusBarItemProvider activate(): void { // Register the status bar item provider for Deepnote notebooks this.disposables.push(notebooks.registerNotebookCellStatusBarItemProvider('deepnote', this)); + + // Listen for notebook changes to update status bar + this.disposables.push( + workspace.onDidChangeNotebookDocument((e) => { + if (e.notebook.notebookType === 'deepnote') { + this._onDidChangeCellStatusBarItems.fire(); + } + }) + ); + + // Register command to update input block variable name + this.disposables.push( + commands.registerCommand('deepnote.updateInputBlockVariableName', async (cell?: NotebookCell) => { + if (!cell) { + // Fall back to the active notebook cell + const activeEditor = window.activeNotebookEditor; + if (activeEditor && activeEditor.selection) { + cell = activeEditor.notebook.cellAt(activeEditor.selection.start); + } + } + + if (!cell) { + void window.showErrorMessage(l10n.t('No active notebook cell')); + return; + } + + await this.updateVariableName(cell); + }) + ); + + // Dispose our emitter with the extension + this.disposables.push(this._onDidChangeCellStatusBarItems); } - provideCellStatusBarItems(cell: NotebookCell): NotebookCellStatusBarItem | undefined { + provideCellStatusBarItems(cell: NotebookCell): NotebookCellStatusBarItem[] | undefined { // Check if this cell is a Deepnote input block // Get the block type from the __deepnotePocket metadata field const pocket = cell.metadata?.__deepnotePocket as Pocket | undefined; @@ -50,17 +94,196 @@ export class DeepnoteInputBlockCellStatusBarItemProvider return undefined; } + const items: NotebookCellStatusBarItem[] = []; + const formattedName = this.formatBlockTypeName(blockType); - // Create a status bar item showing the block type - // Using alignment value 2 (NotebookCellStatusBarAlignment.Right) - const statusBarItem: NotebookCellStatusBarItem = { - text: formattedName, - alignment: 2, // NotebookCellStatusBarAlignment.Right - tooltip: `Deepnote ${formattedName}` + // Extract additional metadata for display + const metadata = cell.metadata as Record | undefined; + const label = metadata?.deepnote_input_label as string | undefined; + const buttonTitle = metadata?.deepnote_button_title as string | undefined; + + // Build status bar text with additional info + let statusText = formattedName; + if (label) { + statusText += `: ${label}`; + } else if (buttonTitle) { + statusText += `: ${buttonTitle}`; + } + + // Build detailed tooltip + const tooltipLines = [`Deepnote ${formattedName}`]; + if (label) { + tooltipLines.push(`Label: ${label}`); + } + if (buttonTitle) { + tooltipLines.push(`Title: ${buttonTitle}`); + } + + // Add type-specific metadata to tooltip + this.addTypeSpecificTooltip(tooltipLines, blockType, metadata); + + // Create a status bar item showing the block type and metadata on the left + items.push({ + text: statusText, + alignment: 1, // NotebookCellStatusBarAlignment.Left + priority: 100, + tooltip: tooltipLines.join('\n') + }); + + // Add variable name status bar item (clickable) + items.push(this.createVariableStatusBarItem(cell)); + + return items; + } + + /** + * Adds type-specific metadata to the tooltip + */ + private addTypeSpecificTooltip( + tooltipLines: string[], + blockType: string, + metadata: Record | undefined + ): void { + if (!metadata) { + return; + } + + switch (blockType) { + case 'input-slider': + const min = metadata.deepnote_slider_min_value; + const max = metadata.deepnote_slider_max_value; + const step = metadata.deepnote_slider_step; + if (min !== undefined && max !== undefined) { + tooltipLines.push(`Range: ${min} - ${max}${step !== undefined ? ` (step: ${step})` : ''}`); + } + break; + + case 'input-select': + const options = metadata.deepnote_variable_options as string[] | undefined; + if (options && options.length > 0) { + tooltipLines.push(`Options: ${options.slice(0, 3).join(', ')}${options.length > 3 ? '...' : ''}`); + } + break; + + case 'input-file': + const extensions = metadata.deepnote_allowed_file_extensions as string | undefined; + if (extensions) { + tooltipLines.push(`Allowed extensions: ${extensions}`); + } + break; + + case 'button': + const behavior = metadata.deepnote_button_behavior as string | undefined; + const colorScheme = metadata.deepnote_button_color_scheme as string | undefined; + if (behavior) { + tooltipLines.push(`Behavior: ${behavior}`); + } + if (colorScheme) { + tooltipLines.push(`Color: ${colorScheme}`); + } + break; + } + + // Add default value if present + const defaultValue = metadata.deepnote_variable_default_value; + if (defaultValue !== undefined && defaultValue !== null) { + tooltipLines.push(`Default: ${defaultValue}`); + } + } + + /** + * Creates a status bar item for the variable name with a clickable command + */ + private createVariableStatusBarItem(cell: NotebookCell): NotebookCellStatusBarItem { + const variableName = this.getVariableName(cell); + + return { + text: l10n.t('Variable: {0}', variableName), + alignment: 1, // NotebookCellStatusBarAlignment.Left + priority: 90, + tooltip: l10n.t('Variable name for input block\nClick to change'), + command: { + title: l10n.t('Change Variable Name'), + command: 'deepnote.updateInputBlockVariableName', + arguments: [cell] + } + }; + } + + /** + * Gets the variable name from cell metadata or cell content + */ + private getVariableName(cell: NotebookCell): string { + const metadata = cell.metadata; + if (metadata && typeof metadata === 'object') { + const variableName = (metadata as Record).deepnote_variable_name; + if (typeof variableName === 'string' && variableName) { + return variableName; + } + } + + // Fall back to cell content (which should contain the variable name) + const cellContent = cell.document.getText().trim(); + if (cellContent) { + return cellContent; + } + + return ''; + } + + /** + * Updates the variable name for an input block cell + */ + private async updateVariableName(cell: NotebookCell): Promise { + const currentVariableName = this.getVariableName(cell); + + const newVariableNameInput = await window.showInputBox({ + prompt: l10n.t('Enter variable name for input block'), + value: currentVariableName, + ignoreFocusOut: true, + validateInput: (value) => { + const trimmed = value.trim(); + if (!trimmed) { + return l10n.t('Variable name cannot be empty'); + } + if (!/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(trimmed)) { + return l10n.t('Variable name must be a valid Python identifier'); + } + return undefined; + } + }); + + const newVariableName = newVariableNameInput?.trim(); + if (newVariableName === undefined || newVariableName === currentVariableName) { + return; + } + + // Update both cell metadata and cell content + const edit = new WorkspaceEdit(); + const updatedMetadata = { + ...cell.metadata, + deepnote_variable_name: newVariableName }; - return statusBarItem; + // Update cell metadata + edit.set(cell.notebook.uri, [NotebookEdit.updateCellMetadata(cell.index, updatedMetadata)]); + + // Update cell content (replace entire cell text with just the variable name) + const fullRange = new Range( + new Position(0, 0), + new Position(cell.document.lineCount - 1, cell.document.lineAt(cell.document.lineCount - 1).text.length) + ); + edit.replace(cell.document.uri, fullRange, newVariableName); + + const success = await workspace.applyEdit(edit); + if (!success) { + void window.showErrorMessage(l10n.t('Failed to update variable name')); + return; + } + + // Trigger status bar update + this._onDidChangeCellStatusBarItems.fire(); } /** diff --git a/src/notebooks/deepnote/deepnoteInputBlockCellStatusBarProvider.unit.test.ts b/src/notebooks/deepnote/deepnoteInputBlockCellStatusBarProvider.unit.test.ts index 0870f8225f..1f9c3f2361 100644 --- a/src/notebooks/deepnote/deepnoteInputBlockCellStatusBarProvider.unit.test.ts +++ b/src/notebooks/deepnote/deepnoteInputBlockCellStatusBarProvider.unit.test.ts @@ -47,155 +47,165 @@ suite('DeepnoteInputBlockCellStatusBarItemProvider', () => { } suite('Input Block Type Detection', () => { - test('Should return status bar item for input-text block', () => { + test('Should return status bar items for input-text block', () => { const cell = createMockCell({ __deepnotePocket: { type: 'input-text' } }); - const item = provider.provideCellStatusBarItems(cell); + const items = provider.provideCellStatusBarItems(cell); - expect(item).to.not.be.undefined; - expect(item?.text).to.equal('Input Text'); + expect(items).to.not.be.undefined; + expect(items).to.have.lengthOf(2); + expect(items?.[0].text).to.equal('Input Text'); + expect(items?.[0].alignment).to.equal(1); // Left }); - test('Should return status bar item for input-textarea block', () => { + test('Should return status bar items for input-textarea block', () => { const cell = createMockCell({ __deepnotePocket: { type: 'input-textarea' } }); - const item = provider.provideCellStatusBarItems(cell); + const items = provider.provideCellStatusBarItems(cell); - expect(item).to.not.be.undefined; - expect(item?.text).to.equal('Input Textarea'); + expect(items).to.not.be.undefined; + expect(items).to.have.lengthOf(2); + expect(items?.[0].text).to.equal('Input Textarea'); }); - test('Should return status bar item for input-select block', () => { + test('Should return status bar items for input-select block', () => { const cell = createMockCell({ __deepnotePocket: { type: 'input-select' } }); - const item = provider.provideCellStatusBarItems(cell); + const items = provider.provideCellStatusBarItems(cell); - expect(item).to.not.be.undefined; - expect(item?.text).to.equal('Input Select'); + expect(items).to.not.be.undefined; + expect(items).to.have.lengthOf(2); + expect(items?.[0].text).to.equal('Input Select'); }); - test('Should return status bar item for input-slider block', () => { + test('Should return status bar items for input-slider block', () => { const cell = createMockCell({ __deepnotePocket: { type: 'input-slider' } }); - const item = provider.provideCellStatusBarItems(cell); + const items = provider.provideCellStatusBarItems(cell); - expect(item).to.not.be.undefined; - expect(item?.text).to.equal('Input Slider'); + expect(items).to.not.be.undefined; + expect(items).to.have.lengthOf(2); + expect(items?.[0].text).to.equal('Input Slider'); }); - test('Should return status bar item for input-checkbox block', () => { + test('Should return status bar items for input-checkbox block', () => { const cell = createMockCell({ __deepnotePocket: { type: 'input-checkbox' } }); - const item = provider.provideCellStatusBarItems(cell); + const items = provider.provideCellStatusBarItems(cell); - expect(item).to.not.be.undefined; - expect(item?.text).to.equal('Input Checkbox'); + expect(items).to.not.be.undefined; + expect(items).to.have.lengthOf(2); + expect(items?.[0].text).to.equal('Input Checkbox'); }); - test('Should return status bar item for input-date block', () => { + test('Should return status bar items for input-date block', () => { const cell = createMockCell({ __deepnotePocket: { type: 'input-date' } }); - const item = provider.provideCellStatusBarItems(cell); + const items = provider.provideCellStatusBarItems(cell); - expect(item).to.not.be.undefined; - expect(item?.text).to.equal('Input Date'); + expect(items).to.not.be.undefined; + expect(items).to.have.lengthOf(2); + expect(items?.[0].text).to.equal('Input Date'); }); - test('Should return status bar item for input-date-range block', () => { + test('Should return status bar items for input-date-range block', () => { const cell = createMockCell({ __deepnotePocket: { type: 'input-date-range' } }); - const item = provider.provideCellStatusBarItems(cell); + const items = provider.provideCellStatusBarItems(cell); - expect(item).to.not.be.undefined; - expect(item?.text).to.equal('Input Date Range'); + expect(items).to.not.be.undefined; + expect(items).to.have.lengthOf(2); + expect(items?.[0].text).to.equal('Input Date Range'); }); - test('Should return status bar item for input-file block', () => { + test('Should return status bar items for input-file block', () => { const cell = createMockCell({ __deepnotePocket: { type: 'input-file' } }); - const item = provider.provideCellStatusBarItems(cell); + const items = provider.provideCellStatusBarItems(cell); - expect(item).to.not.be.undefined; - expect(item?.text).to.equal('Input File'); + expect(items).to.not.be.undefined; + expect(items).to.have.lengthOf(2); + expect(items?.[0].text).to.equal('Input File'); }); - test('Should return status bar item for button block', () => { + test('Should return status bar items for button block', () => { const cell = createMockCell({ __deepnotePocket: { type: 'button' } }); - const item = provider.provideCellStatusBarItems(cell); + const items = provider.provideCellStatusBarItems(cell); - expect(item).to.not.be.undefined; - expect(item?.text).to.equal('Button'); + expect(items).to.not.be.undefined; + expect(items).to.have.lengthOf(2); + expect(items?.[0].text).to.equal('Button'); }); }); suite('Non-Input Block Types', () => { test('Should return undefined for code block', () => { const cell = createMockCell({ __deepnotePocket: { type: 'code' } }); - const item = provider.provideCellStatusBarItems(cell); + const items = provider.provideCellStatusBarItems(cell); - expect(item).to.be.undefined; + expect(items).to.be.undefined; }); test('Should return undefined for sql block', () => { const cell = createMockCell({ __deepnotePocket: { type: 'sql' } }); - const item = provider.provideCellStatusBarItems(cell); + const items = provider.provideCellStatusBarItems(cell); - expect(item).to.be.undefined; + expect(items).to.be.undefined; }); test('Should return undefined for markdown block', () => { const cell = createMockCell({ __deepnotePocket: { type: 'text-cell-p' } }); - const item = provider.provideCellStatusBarItems(cell); + const items = provider.provideCellStatusBarItems(cell); - expect(item).to.be.undefined; + expect(items).to.be.undefined; }); test('Should return undefined for cell with no type metadata', () => { const cell = createMockCell({}); - const item = provider.provideCellStatusBarItems(cell); + const items = provider.provideCellStatusBarItems(cell); - expect(item).to.be.undefined; + expect(items).to.be.undefined; }); test('Should return undefined for cell with undefined metadata', () => { const cell = createMockCell(undefined); - const item = provider.provideCellStatusBarItems(cell); + const items = provider.provideCellStatusBarItems(cell); - expect(item).to.be.undefined; + expect(items).to.be.undefined; }); }); suite('Status Bar Item Properties', () => { test('Should have correct tooltip for input-text', () => { const cell = createMockCell({ __deepnotePocket: { type: 'input-text' } }); - const item = provider.provideCellStatusBarItems(cell); + const items = provider.provideCellStatusBarItems(cell); - expect(item?.tooltip).to.equal('Deepnote Input Text'); + expect(items?.[0].tooltip).to.equal('Deepnote Input Text'); }); test('Should have correct tooltip for button', () => { const cell = createMockCell({ __deepnotePocket: { type: 'button' } }); - const item = provider.provideCellStatusBarItems(cell); + const items = provider.provideCellStatusBarItems(cell); - expect(item?.tooltip).to.equal('Deepnote Button'); + expect(items?.[0].tooltip).to.equal('Deepnote Button'); }); test('Should format multi-word block types correctly', () => { const cell = createMockCell({ __deepnotePocket: { type: 'input-date-range' } }); - const item = provider.provideCellStatusBarItems(cell); + const items = provider.provideCellStatusBarItems(cell); - expect(item?.text).to.equal('Input Date Range'); - expect(item?.tooltip).to.equal('Deepnote Input Date Range'); + expect(items?.[0].text).to.equal('Input Date Range'); + expect(items?.[0].tooltip).to.equal('Deepnote Input Date Range'); }); }); suite('Case Insensitivity', () => { test('Should handle uppercase block type', () => { const cell = createMockCell({ __deepnotePocket: { type: 'INPUT-TEXT' } }); - const item = provider.provideCellStatusBarItems(cell); + const items = provider.provideCellStatusBarItems(cell); - expect(item).to.not.be.undefined; - expect(item?.text).to.equal('INPUT TEXT'); + expect(items).to.not.be.undefined; + expect(items?.[0].text).to.equal('INPUT TEXT'); }); test('Should handle mixed case block type', () => { const cell = createMockCell({ __deepnotePocket: { type: 'Input-Text' } }); - const item = provider.provideCellStatusBarItems(cell); + const items = provider.provideCellStatusBarItems(cell); - expect(item).to.not.be.undefined; - expect(item?.text).to.equal('Input Text'); + expect(items).to.not.be.undefined; + expect(items?.[0].text).to.equal('Input Text'); }); }); }); From 3aefe9da62a70d7b400857b7ea78355bb23060c1 Mon Sep 17 00:00:00 2001 From: jankuca Date: Thu, 23 Oct 2025 15:22:11 +0200 Subject: [PATCH 002/101] add options to input blocks --- ...deepnoteInputBlockCellStatusBarProvider.ts | 560 +++++++++++++++++- ...putBlockCellStatusBarProvider.unit.test.ts | 10 +- 2 files changed, 561 insertions(+), 9 deletions(-) diff --git a/src/notebooks/deepnote/deepnoteInputBlockCellStatusBarProvider.ts b/src/notebooks/deepnote/deepnoteInputBlockCellStatusBarProvider.ts index 5f06deb636..199f2ef830 100644 --- a/src/notebooks/deepnote/deepnoteInputBlockCellStatusBarProvider.ts +++ b/src/notebooks/deepnote/deepnoteInputBlockCellStatusBarProvider.ts @@ -80,10 +80,93 @@ export class DeepnoteInputBlockCellStatusBarItemProvider }) ); + // Register commands for type-specific actions + this.registerTypeSpecificCommands(); + // Dispose our emitter with the extension this.disposables.push(this._onDidChangeCellStatusBarItems); } + private registerTypeSpecificCommands(): void { + // Select input: choose option(s) + this.disposables.push( + commands.registerCommand('deepnote.selectInputChooseOption', async (cell?: NotebookCell) => { + const activeCell = cell || this.getActiveCell(); + if (activeCell) { + await this.selectInputChooseOption(activeCell); + } + }) + ); + + // Slider: set min value + this.disposables.push( + commands.registerCommand('deepnote.sliderSetMin', async (cell?: NotebookCell) => { + const activeCell = cell || this.getActiveCell(); + if (activeCell) { + await this.sliderSetMin(activeCell); + } + }) + ); + + // Slider: set max value + this.disposables.push( + commands.registerCommand('deepnote.sliderSetMax', async (cell?: NotebookCell) => { + const activeCell = cell || this.getActiveCell(); + if (activeCell) { + await this.sliderSetMax(activeCell); + } + }) + ); + + // Checkbox: toggle value + this.disposables.push( + commands.registerCommand('deepnote.checkboxToggle', async (cell?: NotebookCell) => { + const activeCell = cell || this.getActiveCell(); + if (activeCell) { + await this.checkboxToggle(activeCell); + } + }) + ); + + // Date input: choose date + this.disposables.push( + commands.registerCommand('deepnote.dateInputChooseDate', async (cell?: NotebookCell) => { + const activeCell = cell || this.getActiveCell(); + if (activeCell) { + await this.dateInputChooseDate(activeCell); + } + }) + ); + + // Date range: choose start date + this.disposables.push( + commands.registerCommand('deepnote.dateRangeChooseStart', async (cell?: NotebookCell) => { + const activeCell = cell || this.getActiveCell(); + if (activeCell) { + await this.dateRangeChooseStart(activeCell); + } + }) + ); + + // Date range: choose end date + this.disposables.push( + commands.registerCommand('deepnote.dateRangeChooseEnd', async (cell?: NotebookCell) => { + const activeCell = cell || this.getActiveCell(); + if (activeCell) { + await this.dateRangeChooseEnd(activeCell); + } + }) + ); + } + + private getActiveCell(): NotebookCell | undefined { + const activeEditor = window.activeNotebookEditor; + if (activeEditor && activeEditor.selection) { + return activeEditor.notebook.cellAt(activeEditor.selection.start); + } + return undefined; + } + provideCellStatusBarItems(cell: NotebookCell): NotebookCellStatusBarItem[] | undefined { // Check if this cell is a Deepnote input block // Get the block type from the __deepnotePocket metadata field @@ -134,9 +217,207 @@ export class DeepnoteInputBlockCellStatusBarItemProvider // Add variable name status bar item (clickable) items.push(this.createVariableStatusBarItem(cell)); + // Add type-specific status bar items + this.addTypeSpecificStatusBarItems(items, blockType, cell, metadata); + return items; } + /** + * Adds type-specific status bar items based on the block type + */ + private addTypeSpecificStatusBarItems( + items: NotebookCellStatusBarItem[], + blockType: string, + cell: NotebookCell, + metadata: Record | undefined + ): void { + if (!metadata) { + return; + } + + switch (blockType) { + case 'input-select': + this.addSelectInputStatusBarItems(items, cell, metadata); + break; + + case 'input-slider': + this.addSliderInputStatusBarItems(items, cell, metadata); + break; + + case 'input-checkbox': + this.addCheckboxInputStatusBarItems(items, cell, metadata); + break; + + case 'input-date': + this.addDateInputStatusBarItems(items, cell, metadata); + break; + + case 'input-date-range': + this.addDateRangeInputStatusBarItems(items, cell, metadata); + break; + + // input-text, input-textarea, input-file, and button don't have additional buttons + } + } + + private addSelectInputStatusBarItems( + items: NotebookCellStatusBarItem[], + cell: NotebookCell, + metadata: Record + ): void { + const selectType = metadata.deepnote_variable_select_type as string | undefined; + const sourceVariable = metadata.deepnote_variable_selected_variable as string | undefined; + const value = metadata.deepnote_variable_value; + const allowMultiple = metadata.deepnote_allow_multiple_values as boolean | undefined; + + // Show current selection + let selectionText = ''; + if (Array.isArray(value)) { + selectionText = value.length > 0 ? value.join(', ') : l10n.t('None'); + } else if (typeof value === 'string') { + selectionText = value || l10n.t('None'); + } + + items.push({ + text: l10n.t('Selection: {0}', selectionText), + alignment: 1, + priority: 80, + tooltip: allowMultiple + ? l10n.t('Current selection (multi-select)\nClick to change') + : l10n.t('Current selection\nClick to change'), + command: { + title: l10n.t('Choose Option'), + command: 'deepnote.selectInputChooseOption', + arguments: [cell] + } + }); + + // Show source variable if select type is from-variable + if (selectType === 'from-variable' && sourceVariable) { + items.push({ + text: l10n.t('Source: {0}', sourceVariable), + alignment: 1, + priority: 75, + tooltip: l10n.t('Variable containing options') + }); + } + } + + private addSliderInputStatusBarItems( + items: NotebookCellStatusBarItem[], + cell: NotebookCell, + metadata: Record + ): void { + const min = metadata.deepnote_slider_min_value as number | undefined; + const max = metadata.deepnote_slider_max_value as number | undefined; + + items.push({ + text: l10n.t('Min: {0}', min ?? 0), + alignment: 1, + priority: 80, + tooltip: l10n.t('Minimum value\nClick to change'), + command: { + title: l10n.t('Set Min'), + command: 'deepnote.sliderSetMin', + arguments: [cell] + } + }); + + items.push({ + text: l10n.t('Max: {0}', max ?? 10), + alignment: 1, + priority: 79, + tooltip: l10n.t('Maximum value\nClick to change'), + command: { + title: l10n.t('Set Max'), + command: 'deepnote.sliderSetMax', + arguments: [cell] + } + }); + } + + private addCheckboxInputStatusBarItems( + items: NotebookCellStatusBarItem[], + cell: NotebookCell, + metadata: Record + ): void { + const value = metadata.deepnote_variable_value as boolean | undefined; + const checked = value ?? false; + + items.push({ + text: checked ? l10n.t('$(check) Checked') : l10n.t('$(close) Unchecked'), + alignment: 1, + priority: 80, + tooltip: l10n.t('Click to toggle'), + command: { + title: l10n.t('Toggle'), + command: 'deepnote.checkboxToggle', + arguments: [cell] + } + }); + } + + private addDateInputStatusBarItems( + items: NotebookCellStatusBarItem[], + cell: NotebookCell, + metadata: Record + ): void { + const value = metadata.deepnote_variable_value as string | undefined; + const dateStr = value ? new Date(value).toLocaleDateString() : l10n.t('Not set'); + + items.push({ + text: l10n.t('Date: {0}', dateStr), + alignment: 1, + priority: 80, + tooltip: l10n.t('Click to choose date'), + command: { + title: l10n.t('Choose Date'), + command: 'deepnote.dateInputChooseDate', + arguments: [cell] + } + }); + } + + private addDateRangeInputStatusBarItems( + items: NotebookCellStatusBarItem[], + cell: NotebookCell, + metadata: Record + ): void { + const value = metadata.deepnote_variable_value; + let startDate = l10n.t('Not set'); + let endDate = l10n.t('Not set'); + + if (Array.isArray(value) && value.length === 2) { + startDate = new Date(value[0]).toLocaleDateString(); + endDate = new Date(value[1]).toLocaleDateString(); + } + + items.push({ + text: l10n.t('Start: {0}', startDate), + alignment: 1, + priority: 80, + tooltip: l10n.t('Click to choose start date'), + command: { + title: l10n.t('Choose Start Date'), + command: 'deepnote.dateRangeChooseStart', + arguments: [cell] + } + }); + + items.push({ + text: l10n.t('End: {0}', endDate), + alignment: 1, + priority: 79, + tooltip: l10n.t('Click to choose end date'), + command: { + title: l10n.t('Choose End Date'), + command: 'deepnote.dateRangeChooseEnd', + arguments: [cell] + } + }); + } + /** * Adds type-specific metadata to the tooltip */ @@ -223,10 +504,11 @@ export class DeepnoteInputBlockCellStatusBarItemProvider } } - // Fall back to cell content (which should contain the variable name) + // Fall back to cell content (which should contain the variable name with "# " prefix) const cellContent = cell.document.getText().trim(); if (cellContent) { - return cellContent; + // Remove "# " prefix if present + return cellContent.startsWith('# ') ? cellContent.substring(2) : cellContent; } return ''; @@ -269,12 +551,12 @@ export class DeepnoteInputBlockCellStatusBarItemProvider // Update cell metadata edit.set(cell.notebook.uri, [NotebookEdit.updateCellMetadata(cell.index, updatedMetadata)]); - // Update cell content (replace entire cell text with just the variable name) + // Update cell content (replace entire cell text with "# " + variable name) const fullRange = new Range( new Position(0, 0), new Position(cell.document.lineCount - 1, cell.document.lineAt(cell.document.lineCount - 1).text.length) ); - edit.replace(cell.document.uri, fullRange, newVariableName); + edit.replace(cell.document.uri, fullRange, `# ${newVariableName}`); const success = await workspace.applyEdit(edit); if (!success) { @@ -303,6 +585,276 @@ export class DeepnoteInputBlockCellStatusBarItemProvider .join(' '); } + /** + * Handler for select input: choose option(s) + */ + private async selectInputChooseOption(cell: NotebookCell): Promise { + const metadata = cell.metadata as Record | undefined; + if (!metadata) { + return; + } + + const selectType = metadata.deepnote_variable_select_type as string | undefined; + const allowMultiple = metadata.deepnote_allow_multiple_values as boolean | undefined; + const currentValue = metadata.deepnote_variable_value; + + // Get options based on select type + let options: string[] = []; + if (selectType === 'from-variable') { + // For from-variable type, we can't easily get the options here + // Show a message to the user + void window.showInformationMessage( + l10n.t('This select input uses options from a variable. Edit the source variable to change options.') + ); + return; + } else { + // from-options type + const optionsArray = metadata.deepnote_variable_options as string[] | undefined; + options = optionsArray || []; + } + + if (options.length === 0) { + void window.showWarningMessage(l10n.t('No options available')); + return; + } + + if (allowMultiple) { + // Multi-select using QuickPick + const currentSelection = Array.isArray(currentValue) ? currentValue : []; + const selected = await window.showQuickPick( + options.map((opt) => ({ + label: opt, + picked: currentSelection.includes(opt) + })), + { + canPickMany: true, + placeHolder: l10n.t('Select one or more options') + } + ); + + if (selected === undefined) { + return; + } + + const newValue = selected.map((item) => item.label); + await this.updateCellMetadata(cell, { deepnote_variable_value: newValue }); + } else { + // Single select + const selected = await window.showQuickPick(options, { + placeHolder: l10n.t('Select an option'), + canPickMany: false + }); + + if (selected === undefined) { + return; + } + + await this.updateCellMetadata(cell, { deepnote_variable_value: selected }); + } + } + + /** + * Handler for slider: set min value + */ + private async sliderSetMin(cell: NotebookCell): Promise { + const metadata = cell.metadata as Record | undefined; + const currentMin = (metadata?.deepnote_slider_min_value as number) ?? 0; + + const input = await window.showInputBox({ + prompt: l10n.t('Enter minimum value'), + value: String(currentMin), + validateInput: (value) => { + const num = parseFloat(value); + if (isNaN(num)) { + return l10n.t('Please enter a valid number'); + } + return undefined; + } + }); + + if (input === undefined) { + return; + } + + const newMin = parseFloat(input); + await this.updateCellMetadata(cell, { deepnote_slider_min_value: newMin }); + } + + /** + * Handler for slider: set max value + */ + private async sliderSetMax(cell: NotebookCell): Promise { + const metadata = cell.metadata as Record | undefined; + const currentMax = (metadata?.deepnote_slider_max_value as number) ?? 10; + + const input = await window.showInputBox({ + prompt: l10n.t('Enter maximum value'), + value: String(currentMax), + validateInput: (value) => { + const num = parseFloat(value); + if (isNaN(num)) { + return l10n.t('Please enter a valid number'); + } + return undefined; + } + }); + + if (input === undefined) { + return; + } + + const newMax = parseFloat(input); + await this.updateCellMetadata(cell, { deepnote_slider_max_value: newMax }); + } + + /** + * Handler for checkbox: toggle value + */ + private async checkboxToggle(cell: NotebookCell): Promise { + const metadata = cell.metadata as Record | undefined; + const currentValue = (metadata?.deepnote_variable_value as boolean) ?? false; + + await this.updateCellMetadata(cell, { deepnote_variable_value: !currentValue }); + } + + /** + * Handler for date input: choose date + */ + private async dateInputChooseDate(cell: NotebookCell): Promise { + const metadata = cell.metadata as Record | undefined; + const currentValue = metadata?.deepnote_variable_value as string | undefined; + const currentDate = currentValue ? new Date(currentValue).toISOString().split('T')[0] : ''; + + const input = await window.showInputBox({ + prompt: l10n.t('Enter date (YYYY-MM-DD)'), + value: currentDate, + validateInput: (value) => { + if (!value) { + return l10n.t('Date cannot be empty'); + } + if (!/^\d{4}-\d{2}-\d{2}$/.test(value)) { + return l10n.t('Please enter date in YYYY-MM-DD format'); + } + const date = new Date(value); + if (isNaN(date.getTime())) { + return l10n.t('Invalid date'); + } + return undefined; + } + }); + + if (input === undefined) { + return; + } + + const newDate = new Date(input); + await this.updateCellMetadata(cell, { deepnote_variable_value: newDate.toISOString() }); + } + + /** + * Handler for date range: choose start date + */ + private async dateRangeChooseStart(cell: NotebookCell): Promise { + const metadata = cell.metadata as Record | undefined; + const currentValue = metadata?.deepnote_variable_value; + let currentStart = ''; + let currentEnd = ''; + + if (Array.isArray(currentValue) && currentValue.length === 2) { + currentStart = new Date(currentValue[0]).toISOString().split('T')[0]; + currentEnd = new Date(currentValue[1]).toISOString().split('T')[0]; + } + + const input = await window.showInputBox({ + prompt: l10n.t('Enter start date (YYYY-MM-DD)'), + value: currentStart, + validateInput: (value) => { + if (!value) { + return l10n.t('Date cannot be empty'); + } + if (!/^\d{4}-\d{2}-\d{2}$/.test(value)) { + return l10n.t('Please enter date in YYYY-MM-DD format'); + } + const date = new Date(value); + if (isNaN(date.getTime())) { + return l10n.t('Invalid date'); + } + return undefined; + } + }); + + if (input === undefined) { + return; + } + + const newStart = new Date(input).toISOString(); + const newValue = currentEnd ? [newStart, new Date(currentEnd).toISOString()] : [newStart, newStart]; + await this.updateCellMetadata(cell, { deepnote_variable_value: newValue }); + } + + /** + * Handler for date range: choose end date + */ + private async dateRangeChooseEnd(cell: NotebookCell): Promise { + const metadata = cell.metadata as Record | undefined; + const currentValue = metadata?.deepnote_variable_value; + let currentStart = ''; + let currentEnd = ''; + + if (Array.isArray(currentValue) && currentValue.length === 2) { + currentStart = new Date(currentValue[0]).toISOString().split('T')[0]; + currentEnd = new Date(currentValue[1]).toISOString().split('T')[0]; + } + + const input = await window.showInputBox({ + prompt: l10n.t('Enter end date (YYYY-MM-DD)'), + value: currentEnd, + validateInput: (value) => { + if (!value) { + return l10n.t('Date cannot be empty'); + } + if (!/^\d{4}-\d{2}-\d{2}$/.test(value)) { + return l10n.t('Please enter date in YYYY-MM-DD format'); + } + const date = new Date(value); + if (isNaN(date.getTime())) { + return l10n.t('Invalid date'); + } + return undefined; + } + }); + + if (input === undefined) { + return; + } + + const newEnd = new Date(input).toISOString(); + const newValue = currentStart ? [new Date(currentStart).toISOString(), newEnd] : [newEnd, newEnd]; + await this.updateCellMetadata(cell, { deepnote_variable_value: newValue }); + } + + /** + * Helper method to update cell metadata + */ + private async updateCellMetadata(cell: NotebookCell, updates: Record): Promise { + const edit = new WorkspaceEdit(); + const updatedMetadata = { + ...cell.metadata, + ...updates + }; + + edit.set(cell.notebook.uri, [NotebookEdit.updateCellMetadata(cell.index, updatedMetadata)]); + + const success = await workspace.applyEdit(edit); + if (!success) { + void window.showErrorMessage(l10n.t('Failed to update cell metadata')); + return; + } + + // Trigger status bar update + this._onDidChangeCellStatusBarItems.fire(); + } + dispose(): void { this.disposables.forEach((d) => d.dispose()); } diff --git a/src/notebooks/deepnote/deepnoteInputBlockCellStatusBarProvider.unit.test.ts b/src/notebooks/deepnote/deepnoteInputBlockCellStatusBarProvider.unit.test.ts index 1f9c3f2361..7f7ae0c64d 100644 --- a/src/notebooks/deepnote/deepnoteInputBlockCellStatusBarProvider.unit.test.ts +++ b/src/notebooks/deepnote/deepnoteInputBlockCellStatusBarProvider.unit.test.ts @@ -71,7 +71,7 @@ suite('DeepnoteInputBlockCellStatusBarItemProvider', () => { const items = provider.provideCellStatusBarItems(cell); expect(items).to.not.be.undefined; - expect(items).to.have.lengthOf(2); + expect(items).to.have.length.at.least(2); // Type label, variable, and selection button expect(items?.[0].text).to.equal('Input Select'); }); @@ -80,7 +80,7 @@ suite('DeepnoteInputBlockCellStatusBarItemProvider', () => { const items = provider.provideCellStatusBarItems(cell); expect(items).to.not.be.undefined; - expect(items).to.have.lengthOf(2); + expect(items).to.have.length.at.least(2); // Type label, variable, min, and max buttons expect(items?.[0].text).to.equal('Input Slider'); }); @@ -89,7 +89,7 @@ suite('DeepnoteInputBlockCellStatusBarItemProvider', () => { const items = provider.provideCellStatusBarItems(cell); expect(items).to.not.be.undefined; - expect(items).to.have.lengthOf(2); + expect(items).to.have.length.at.least(2); // Type label, variable, and toggle button expect(items?.[0].text).to.equal('Input Checkbox'); }); @@ -98,7 +98,7 @@ suite('DeepnoteInputBlockCellStatusBarItemProvider', () => { const items = provider.provideCellStatusBarItems(cell); expect(items).to.not.be.undefined; - expect(items).to.have.lengthOf(2); + expect(items).to.have.length.at.least(2); // Type label, variable, and date button expect(items?.[0].text).to.equal('Input Date'); }); @@ -107,7 +107,7 @@ suite('DeepnoteInputBlockCellStatusBarItemProvider', () => { const items = provider.provideCellStatusBarItems(cell); expect(items).to.not.be.undefined; - expect(items).to.have.lengthOf(2); + expect(items).to.have.length.at.least(2); // Type label, variable, start, and end buttons expect(items?.[0].text).to.equal('Input Date Range'); }); From 378ccda6992e71055748bd8cba2cafb6c8f7808e Mon Sep 17 00:00:00 2001 From: jankuca Date: Thu, 23 Oct 2025 15:29:52 +0200 Subject: [PATCH 003/101] show input values in editors --- .../deepnote/converters/inputConverters.ts | 306 ++++++++++++++++++ .../converters/inputConverters.unit.test.ts | 39 +-- 2 files changed, 326 insertions(+), 19 deletions(-) diff --git a/src/notebooks/deepnote/converters/inputConverters.ts b/src/notebooks/deepnote/converters/inputConverters.ts index 22885fa921..cdc1217657 100644 --- a/src/notebooks/deepnote/converters/inputConverters.ts +++ b/src/notebooks/deepnote/converters/inputConverters.ts @@ -82,6 +82,37 @@ export class InputTextBlockConverter extends BaseInputBlockConverter { @@ -96,6 +127,37 @@ export class InputTextareaBlockConverter extends BaseInputBlockConverter { @@ -110,6 +172,57 @@ export class InputSelectBlockConverter extends BaseInputBlockConverter `"${v}"`).join(', ')}]`; + } else if (typeof value === 'string') { + // Single select: show as quoted string + cellValue = `"${value}"`; + } + + const cell = new NotebookCellData(NotebookCellKind.Code, cellValue, 'python'); + return cell; + } + + override applyChangesToBlock(block: DeepnoteBlock, cell: NotebookCellData): void { + block.content = ''; + + // Parse the cell value to extract the selection + const cellValue = cell.value.trim(); + let value: string | string[]; + + if (cellValue.startsWith('[') && cellValue.endsWith(']')) { + // Multi-select: parse array + const arrayContent = cellValue.slice(1, -1); + value = arrayContent + .split(',') + .map((v) => v.trim()) + .filter((v) => v) + .map((v) => v.replace(/^["']|["']$/g, '')); + } else { + // Single select: remove quotes + value = cellValue.replace(/^["']|["']$/g, ''); + } + + const existingMetadata = this.schema().safeParse(block.metadata); + const baseMetadata = existingMetadata.success ? existingMetadata.data : this.defaultConfig(); + + if (block.metadata != null) { + delete block.metadata[DEEPNOTE_VSCODE_RAW_CONTENT_KEY]; + } + + block.metadata = { + ...(block.metadata ?? {}), + ...baseMetadata, + deepnote_variable_value: value + }; + } } export class InputSliderBlockConverter extends BaseInputBlockConverter { @@ -124,6 +237,37 @@ export class InputSliderBlockConverter extends BaseInputBlockConverter { @@ -138,6 +282,38 @@ export class InputCheckboxBlockConverter extends BaseInputBlockConverter { @@ -152,6 +328,38 @@ export class InputDateBlockConverter extends BaseInputBlockConverter { @@ -166,6 +374,49 @@ export class InputDateRangeBlockConverter extends BaseInputBlockConverter { @@ -180,6 +431,38 @@ export class InputFileBlockConverter extends BaseInputBlockConverter { @@ -194,4 +477,27 @@ export class ButtonBlockConverter extends BaseInputBlockConverter { }); suite('convertToCell', () => { - test('converts input-text block with metadata to Python cell with variable name', () => { + test('converts input-text block with metadata to plaintext cell with value', () => { const block: DeepnoteBlock = { blockGroup: '92f21410c8c54ac0be7e4d2a544552ee', content: '', @@ -41,8 +41,8 @@ suite('InputTextBlockConverter', () => { const cell = converter.convertToCell(block); assert.strictEqual(cell.kind, NotebookCellKind.Code); - assert.strictEqual(cell.languageId, 'python'); - assert.strictEqual(cell.value, '# input_1'); + assert.strictEqual(cell.languageId, 'plaintext'); + assert.strictEqual(cell.value, 'some text input'); }); test('handles missing metadata with default config', () => { @@ -57,11 +57,11 @@ suite('InputTextBlockConverter', () => { const cell = converter.convertToCell(block); assert.strictEqual(cell.kind, NotebookCellKind.Code); - assert.strictEqual(cell.languageId, 'python'); - assert.strictEqual(cell.value, '# '); + assert.strictEqual(cell.languageId, 'plaintext'); + assert.strictEqual(cell.value, ''); }); - test('handles missing variable name', () => { + test('handles missing variable value', () => { const block: DeepnoteBlock = { blockGroup: 'test-group', content: '', @@ -75,54 +75,55 @@ suite('InputTextBlockConverter', () => { const cell = converter.convertToCell(block); - assert.strictEqual(cell.value, '# '); + assert.strictEqual(cell.value, ''); }); }); suite('applyChangesToBlock', () => { - test('applies variable name from cell value to block metadata', () => { + test('applies text value from cell to block metadata', () => { const block: DeepnoteBlock = { blockGroup: 'test-group', content: 'old content', id: 'block-123', metadata: { deepnote_input_label: 'existing label', - deepnote_variable_value: 'existing value' + deepnote_variable_name: 'input_1', + deepnote_variable_value: 'old value' }, sortingKey: 'a0', type: 'input-text' }; - const cell = new NotebookCellData(NotebookCellKind.Code, 'new_var', 'python'); + const cell = new NotebookCellData(NotebookCellKind.Code, 'new text value', 'plaintext'); converter.applyChangesToBlock(block, cell); assert.strictEqual(block.content, ''); - assert.strictEqual(block.metadata?.deepnote_variable_name, 'new_var'); + assert.strictEqual(block.metadata?.deepnote_variable_value, 'new text value'); // Other metadata should be preserved assert.strictEqual(block.metadata?.deepnote_input_label, 'existing label'); - assert.strictEqual(block.metadata?.deepnote_variable_value, 'existing value'); + assert.strictEqual(block.metadata?.deepnote_variable_name, 'input_1'); }); - test('handles empty variable name', () => { + test('handles empty value', () => { const block: DeepnoteBlock = { blockGroup: 'test-group', content: 'old content', id: 'block-123', metadata: { - deepnote_variable_name: 'old_var' + deepnote_variable_value: 'old value' }, sortingKey: 'a0', type: 'input-text' }; - const cell = new NotebookCellData(NotebookCellKind.Code, '', 'python'); + const cell = new NotebookCellData(NotebookCellKind.Code, '', 'plaintext'); converter.applyChangesToBlock(block, cell); assert.strictEqual(block.content, ''); - assert.strictEqual(block.metadata?.deepnote_variable_name, ''); + assert.strictEqual(block.metadata?.deepnote_variable_value, ''); }); - test('trims whitespace from variable name', () => { + test('preserves whitespace in value', () => { const block: DeepnoteBlock = { blockGroup: 'test-group', content: '', @@ -130,11 +131,11 @@ suite('InputTextBlockConverter', () => { sortingKey: 'a0', type: 'input-text' }; - const cell = new NotebookCellData(NotebookCellKind.Code, ' my_var \n', 'python'); + const cell = new NotebookCellData(NotebookCellKind.Code, ' text with spaces \n', 'plaintext'); converter.applyChangesToBlock(block, cell); - assert.strictEqual(block.metadata?.deepnote_variable_name, 'my_var'); + assert.strictEqual(block.metadata?.deepnote_variable_value, ' text with spaces \n'); }); test('clears raw content key when variable name is applied', () => { From 0704de491ac6f50e07ad21a03a833d56d02dbc0f Mon Sep 17 00:00:00 2001 From: jankuca Date: Thu, 23 Oct 2025 15:33:44 +0200 Subject: [PATCH 004/101] live update of input editor when value changes --- ...deepnoteInputBlockCellStatusBarProvider.ts | 77 ++++++++++++++++++- 1 file changed, 76 insertions(+), 1 deletion(-) diff --git a/src/notebooks/deepnote/deepnoteInputBlockCellStatusBarProvider.ts b/src/notebooks/deepnote/deepnoteInputBlockCellStatusBarProvider.ts index 199f2ef830..1a7b223f49 100644 --- a/src/notebooks/deepnote/deepnoteInputBlockCellStatusBarProvider.ts +++ b/src/notebooks/deepnote/deepnoteInputBlockCellStatusBarProvider.ts @@ -834,7 +834,7 @@ export class DeepnoteInputBlockCellStatusBarItemProvider } /** - * Helper method to update cell metadata + * Helper method to update cell metadata and cell content */ private async updateCellMetadata(cell: NotebookCell, updates: Record): Promise { const edit = new WorkspaceEdit(); @@ -843,8 +843,24 @@ export class DeepnoteInputBlockCellStatusBarItemProvider ...updates }; + // Update cell metadata edit.set(cell.notebook.uri, [NotebookEdit.updateCellMetadata(cell.index, updatedMetadata)]); + // Update cell content if the value changed + if ('deepnote_variable_value' in updates) { + const newCellContent = this.formatCellContent(cell, updatedMetadata); + if (newCellContent !== null) { + const fullRange = new Range( + new Position(0, 0), + new Position( + cell.document.lineCount - 1, + cell.document.lineAt(cell.document.lineCount - 1).text.length + ) + ); + edit.replace(cell.document.uri, fullRange, newCellContent); + } + } + const success = await workspace.applyEdit(edit); if (!success) { void window.showErrorMessage(l10n.t('Failed to update cell metadata')); @@ -855,6 +871,65 @@ export class DeepnoteInputBlockCellStatusBarItemProvider this._onDidChangeCellStatusBarItems.fire(); } + /** + * Formats the cell content based on the block type and value + */ + private formatCellContent(_cell: NotebookCell, metadata: Record): string | null { + const pocket = metadata.__deepnotePocket as Pocket | undefined; + const blockType = pocket?.type; + const value = metadata.deepnote_variable_value; + + if (!blockType) { + return null; + } + + switch (blockType) { + case 'input-text': + case 'input-textarea': + // Plain text value + return typeof value === 'string' ? value : ''; + + case 'input-select': + // Quoted string or array of quoted strings + if (Array.isArray(value)) { + return `[${value.map((v) => `"${v}"`).join(', ')}]`; + } else if (typeof value === 'string') { + return `"${value}"`; + } + return '""'; + + case 'input-slider': + // Numeric value + return typeof value === 'string' ? value : String(value ?? '5'); + + case 'input-checkbox': + // Boolean value + return value ? 'True' : 'False'; + + case 'input-date': + // Quoted ISO date string + return typeof value === 'string' && value ? `"${value}"` : '""'; + + case 'input-date-range': + // Tuple of quoted ISO date strings + if (Array.isArray(value) && value.length === 2) { + return `("${value[0]}", "${value[1]}")`; + } + return '("", "")'; + + case 'input-file': + // Quoted file path + return typeof value === 'string' && value ? `"${value}"` : '""'; + + case 'button': + // No content + return ''; + + default: + return null; + } + } + dispose(): void { this.disposables.forEach((d) => d.dispose()); } From 66f832a39381d6e4bed0d3ffa0765f6350fb61ee Mon Sep 17 00:00:00 2001 From: jankuca Date: Thu, 23 Oct 2025 16:25:44 +0200 Subject: [PATCH 005/101] fix date values --- .../deepnote/converters/inputConverters.ts | 62 ++++++++++++++++--- ...deepnoteInputBlockCellStatusBarProvider.ts | 59 +++++++++++++++--- 2 files changed, 104 insertions(+), 17 deletions(-) diff --git a/src/notebooks/deepnote/converters/inputConverters.ts b/src/notebooks/deepnote/converters/inputConverters.ts index cdc1217657..226112c8d2 100644 --- a/src/notebooks/deepnote/converters/inputConverters.ts +++ b/src/notebooks/deepnote/converters/inputConverters.ts @@ -330,10 +330,21 @@ export class InputDateBlockConverter extends BaseInputBlockConverter { + if (!val) { + return ''; + } + if (typeof val === 'string') { + return val; + } + if (val instanceof Date) { + // Convert Date to YYYY-MM-DD format + return val.toISOString().split('T')[0]; + } + return String(val); + }; + + // Check raw value first (before schema transformation) + if (Array.isArray(rawValue) && rawValue.length === 2) { // Show as tuple of quoted strings - cellValue = `("${value[0]}", "${value[1]}")`; - } else { - cellValue = '("", "")'; + const start = formatDateValue(rawValue[0]); + const end = formatDateValue(rawValue[1]); + if (start || end) { + cellValue = `("${start}", "${end}")`; + } + } else if (Array.isArray(rawDefaultValue) && rawDefaultValue.length === 2) { + // Use default value if available + const start = formatDateValue(rawDefaultValue[0]); + const end = formatDateValue(rawDefaultValue[1]); + if (start || end) { + cellValue = `("${start}", "${end}")`; + } + } else if (typeof rawValue === 'string' && rawValue) { + // Single date string (shouldn't happen but handle it) + cellValue = `"${rawValue}"`; } + // If no value, cellValue remains empty string const cell = new NotebookCellData(NotebookCellKind.Code, cellValue, 'python'); return cell; diff --git a/src/notebooks/deepnote/deepnoteInputBlockCellStatusBarProvider.ts b/src/notebooks/deepnote/deepnoteInputBlockCellStatusBarProvider.ts index 1a7b223f49..5cf5aeff20 100644 --- a/src/notebooks/deepnote/deepnoteInputBlockCellStatusBarProvider.ts +++ b/src/notebooks/deepnote/deepnoteInputBlockCellStatusBarProvider.ts @@ -906,16 +906,61 @@ export class DeepnoteInputBlockCellStatusBarItemProvider // Boolean value return value ? 'True' : 'False'; - case 'input-date': - // Quoted ISO date string - return typeof value === 'string' && value ? `"${value}"` : '""'; + case 'input-date': { + // Quoted date string (format as YYYY-MM-DD) + let dateStr = ''; + if (value) { + if (typeof value === 'string') { + dateStr = value; + } else if (value instanceof Date) { + dateStr = value.toISOString().split('T')[0]; + } else { + dateStr = String(value); + } + } + return dateStr ? `"${dateStr}"` : '""'; + } + + case 'input-date-range': { + // Tuple of quoted date strings + // Helper to format date value (could be string or Date object) + const formatDateValue = (val: unknown): string => { + if (!val) { + return ''; + } + if (typeof val === 'string') { + return val; + } + if (val instanceof Date) { + // Convert Date to YYYY-MM-DD format + return val.toISOString().split('T')[0]; + } + return String(val); + }; - case 'input-date-range': - // Tuple of quoted ISO date strings if (Array.isArray(value) && value.length === 2) { - return `("${value[0]}", "${value[1]}")`; + const start = formatDateValue(value[0]); + const end = formatDateValue(value[1]); + if (start || end) { + return `("${start}", "${end}")`; + } + } else { + // Check for default value + const defaultValue = metadata.deepnote_variable_default_value; + if (Array.isArray(defaultValue) && defaultValue.length === 2) { + const start = formatDateValue(defaultValue[0]); + const end = formatDateValue(defaultValue[1]); + if (start || end) { + return `("${start}", "${end}")`; + } + } else if (typeof value === 'string' && value) { + // Single date string (shouldn't happen but handle it) + return `"${value}"`; + } } - return '("", "")'; + // No value, return empty string + return ''; + } case 'input-file': // Quoted file path From bf199daa2ccec6f8f519affb5573bddf29b7c2fb Mon Sep 17 00:00:00 2001 From: jankuca Date: Thu, 23 Oct 2025 17:07:57 +0200 Subject: [PATCH 006/101] feat: make non-text input block editors readonly --- .../deepnote/converters/inputConverters.ts | 126 +++--------------- .../deepnote/deepnoteActivationService.ts | 5 + ...deepnoteInputBlockCellStatusBarProvider.ts | 94 +------------ .../deepnoteInputBlockEditProtection.ts | 83 ++++++++++++ .../deepnote/inputBlockContentFormatter.ts | 103 ++++++++++++++ 5 files changed, 210 insertions(+), 201 deletions(-) create mode 100644 src/notebooks/deepnote/deepnoteInputBlockEditProtection.ts create mode 100644 src/notebooks/deepnote/inputBlockContentFormatter.ts diff --git a/src/notebooks/deepnote/converters/inputConverters.ts b/src/notebooks/deepnote/converters/inputConverters.ts index 226112c8d2..7fa7b70031 100644 --- a/src/notebooks/deepnote/converters/inputConverters.ts +++ b/src/notebooks/deepnote/converters/inputConverters.ts @@ -15,6 +15,7 @@ import { DeepnoteButtonMetadataSchema } from '../deepnoteSchemas'; import { DEEPNOTE_VSCODE_RAW_CONTENT_KEY } from './constants'; +import { formatInputBlockCellContent } from '../inputBlockContentFormatter'; export abstract class BaseInputBlockConverter implements BlockConverter { abstract schema(): T; @@ -84,13 +85,8 @@ export class InputTextBlockConverter extends BaseInputBlockConverter `"${v}"`).join(', ')}]`; - } else if (typeof value === 'string') { - // Single select: show as quoted string - cellValue = `"${value}"`; - } - + const cellValue = formatInputBlockCellContent('input-select', block.metadata ?? {}); const cell = new NotebookCellData(NotebookCellKind.Code, cellValue, 'python'); return cell; } @@ -239,13 +219,8 @@ export class InputSliderBlockConverter extends BaseInputBlockConverter { - if (!val) { - return ''; - } - if (typeof val === 'string') { - return val; - } - if (val instanceof Date) { - // Convert Date to YYYY-MM-DD format - return val.toISOString().split('T')[0]; - } - return String(val); - }; - - // Check raw value first (before schema transformation) - if (Array.isArray(rawValue) && rawValue.length === 2) { - // Show as tuple of quoted strings - const start = formatDateValue(rawValue[0]); - const end = formatDateValue(rawValue[1]); - if (start || end) { - cellValue = `("${start}", "${end}")`; - } - } else if (Array.isArray(rawDefaultValue) && rawDefaultValue.length === 2) { - // Use default value if available - const start = formatDateValue(rawDefaultValue[0]); - const end = formatDateValue(rawDefaultValue[1]); - if (start || end) { - cellValue = `("${start}", "${end}")`; - } - } else if (typeof rawValue === 'string' && rawValue) { - // Single date string (shouldn't happen but handle it) - cellValue = `"${rawValue}"`; - } - // If no value, cellValue remains empty string - + const cellValue = formatInputBlockCellContent('input-date-range', block.metadata ?? {}); const cell = new NotebookCellData(NotebookCellKind.Code, cellValue, 'python'); return cell; } @@ -475,13 +387,7 @@ export class InputFileBlockConverter extends BaseInputBlockConverter): string | null { const pocket = metadata.__deepnotePocket as Pocket | undefined; const blockType = pocket?.type; - const value = metadata.deepnote_variable_value; if (!blockType) { return null; } - switch (blockType) { - case 'input-text': - case 'input-textarea': - // Plain text value - return typeof value === 'string' ? value : ''; - - case 'input-select': - // Quoted string or array of quoted strings - if (Array.isArray(value)) { - return `[${value.map((v) => `"${v}"`).join(', ')}]`; - } else if (typeof value === 'string') { - return `"${value}"`; - } - return '""'; - - case 'input-slider': - // Numeric value - return typeof value === 'string' ? value : String(value ?? '5'); - - case 'input-checkbox': - // Boolean value - return value ? 'True' : 'False'; - - case 'input-date': { - // Quoted date string (format as YYYY-MM-DD) - let dateStr = ''; - if (value) { - if (typeof value === 'string') { - dateStr = value; - } else if (value instanceof Date) { - dateStr = value.toISOString().split('T')[0]; - } else { - dateStr = String(value); - } - } - return dateStr ? `"${dateStr}"` : '""'; - } - - case 'input-date-range': { - // Tuple of quoted date strings - // Helper to format date value (could be string or Date object) - const formatDateValue = (val: unknown): string => { - if (!val) { - return ''; - } - if (typeof val === 'string') { - return val; - } - if (val instanceof Date) { - // Convert Date to YYYY-MM-DD format - return val.toISOString().split('T')[0]; - } - return String(val); - }; - - if (Array.isArray(value) && value.length === 2) { - const start = formatDateValue(value[0]); - const end = formatDateValue(value[1]); - if (start || end) { - return `("${start}", "${end}")`; - } - } else { - // Check for default value - const defaultValue = metadata.deepnote_variable_default_value; - if (Array.isArray(defaultValue) && defaultValue.length === 2) { - const start = formatDateValue(defaultValue[0]); - const end = formatDateValue(defaultValue[1]); - if (start || end) { - return `("${start}", "${end}")`; - } - } else if (typeof value === 'string' && value) { - // Single date string (shouldn't happen but handle it) - return `"${value}"`; - } - } - // No value, return empty string - return ''; - } - - case 'input-file': - // Quoted file path - return typeof value === 'string' && value ? `"${value}"` : '""'; - - case 'button': - // No content - return ''; - - default: - return null; - } + // Use shared formatter + return formatInputBlockCellContent(blockType, metadata); } dispose(): void { diff --git a/src/notebooks/deepnote/deepnoteInputBlockEditProtection.ts b/src/notebooks/deepnote/deepnoteInputBlockEditProtection.ts new file mode 100644 index 0000000000..21cff80305 --- /dev/null +++ b/src/notebooks/deepnote/deepnoteInputBlockEditProtection.ts @@ -0,0 +1,83 @@ +import { + Disposable, + NotebookCell, + NotebookDocumentChangeEvent, + Position, + Range, + workspace, + WorkspaceEdit +} from 'vscode'; +import { formatInputBlockCellContent } from './inputBlockContentFormatter'; + +/** + * Protects readonly input blocks from being edited by reverting changes. + * This is needed because VSCode doesn't support the `editable: false` metadata property. + */ +export class DeepnoteInputBlockEditProtection implements Disposable { + private readonly disposables: Disposable[] = []; + + // Input types that should be readonly (controlled via status bar) + private readonly readonlyInputTypes = new Set([ + 'input-select', + 'input-checkbox', + 'input-date', + 'input-date-range', + 'button' + ]); + + constructor() { + // Listen for notebook document changes + this.disposables.push( + workspace.onDidChangeNotebookDocument((e) => { + void this.handleNotebookChange(e); + }) + ); + } + + private async handleNotebookChange(e: NotebookDocumentChangeEvent): Promise { + // Check if this is a Deepnote notebook + if (e.notebook.notebookType !== 'deepnote') { + return; + } + + // Process cell changes (content edits) + for (const cellChange of e.cellChanges) { + const cell = cellChange.cell; + + // Check if this is a readonly input block + const blockType = cell.metadata?.__deepnotePocket?.type || cell.metadata?.type; + if (!blockType || !this.readonlyInputTypes.has(blockType)) { + continue; + } + + // Check if the document (content) changed + if (cellChange.document) { + // Revert the change by restoring content from metadata + await this.revertCellContent(cell); + } + } + } + + private async revertCellContent(cell: NotebookCell): Promise { + const blockType = cell.metadata?.__deepnotePocket?.type || cell.metadata?.type; + const metadata = cell.metadata; + + // Use shared formatter to get correct content + const correctContent = formatInputBlockCellContent(blockType, metadata); + + // Only revert if content actually changed + if (cell.document.getText() !== correctContent) { + const edit = new WorkspaceEdit(); + const fullRange = new Range( + new Position(0, 0), + new Position(cell.document.lineCount - 1, cell.document.lineAt(cell.document.lineCount - 1).text.length) + ); + edit.replace(cell.document.uri, fullRange, correctContent); + await workspace.applyEdit(edit); + } + } + + dispose(): void { + this.disposables.forEach((d) => d.dispose()); + } +} diff --git a/src/notebooks/deepnote/inputBlockContentFormatter.ts b/src/notebooks/deepnote/inputBlockContentFormatter.ts new file mode 100644 index 0000000000..5384d29b51 --- /dev/null +++ b/src/notebooks/deepnote/inputBlockContentFormatter.ts @@ -0,0 +1,103 @@ +/** + * Utility for formatting input block cell content based on block type and metadata. + * This is the single source of truth for how input block values are displayed in cells. + */ + +/** + * Formats the cell content for an input block based on its type and metadata. + * @param blockType The type of the input block (e.g., 'input-text', 'input-select') + * @param metadata The cell metadata containing the value and other configuration + * @returns The formatted cell content string + */ +export function formatInputBlockCellContent(blockType: string, metadata: Record): string { + switch (blockType) { + case 'input-text': + case 'input-textarea': { + const value = metadata.deepnote_variable_value; + return typeof value === 'string' ? value : ''; + } + + case 'input-select': { + const value = metadata.deepnote_variable_value; + if (Array.isArray(value)) { + // Multi-select: show as array of quoted strings + return `[${value.map((v) => `"${v}"`).join(', ')}]`; + } else if (typeof value === 'string') { + // Single select: show as quoted string + return `"${value}"`; + } + return ''; + } + + case 'input-slider': { + const value = metadata.deepnote_variable_value; + return typeof value === 'number' ? String(value) : ''; + } + + case 'input-checkbox': { + const value = metadata.deepnote_variable_value ?? false; + return value ? 'True' : 'False'; + } + + case 'input-date': { + const value = metadata.deepnote_variable_value; + if (value) { + const dateStr = formatDateValue(value); + return dateStr ? `"${dateStr}"` : '""'; + } + return '""'; + } + + case 'input-date-range': { + const value = metadata.deepnote_variable_value; + if (Array.isArray(value) && value.length === 2) { + const start = formatDateValue(value[0]); + const end = formatDateValue(value[1]); + if (start || end) { + return `("${start}", "${end}")`; + } + } else { + const defaultValue = metadata.deepnote_variable_default_value; + if (Array.isArray(defaultValue) && defaultValue.length === 2) { + const start = formatDateValue(defaultValue[0]); + const end = formatDateValue(defaultValue[1]); + if (start || end) { + return `("${start}", "${end}")`; + } + } + } + return ''; + } + + case 'input-file': { + const value = metadata.deepnote_variable_value; + return typeof value === 'string' && value ? `"${value}"` : ''; + } + + case 'button': { + return ''; + } + + default: + return ''; + } +} + +/** + * Helper to format date value (could be string or Date object). + * Converts to YYYY-MM-DD format. + */ +function formatDateValue(val: unknown): string { + if (!val) { + return ''; + } + if (typeof val === 'string') { + return val; + } + if (val instanceof Date) { + // Convert Date to YYYY-MM-DD format + return val.toISOString().split('T')[0]; + } + return String(val); +} + From e953e7b0771f056d6c651f52d33fd424e1ac47c5 Mon Sep 17 00:00:00 2001 From: jankuca Date: Fri, 24 Oct 2025 11:12:14 +0200 Subject: [PATCH 007/101] fix date format --- .../deepnoteInputBlockCellStatusBarProvider.ts | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/notebooks/deepnote/deepnoteInputBlockCellStatusBarProvider.ts b/src/notebooks/deepnote/deepnoteInputBlockCellStatusBarProvider.ts index 32fe87642b..96627efdec 100644 --- a/src/notebooks/deepnote/deepnoteInputBlockCellStatusBarProvider.ts +++ b/src/notebooks/deepnote/deepnoteInputBlockCellStatusBarProvider.ts @@ -748,8 +748,8 @@ export class DeepnoteInputBlockCellStatusBarItemProvider return; } - const newDate = new Date(input); - await this.updateCellMetadata(cell, { deepnote_variable_value: newDate.toISOString() }); + // Store as YYYY-MM-DD format (not full ISO string) + await this.updateCellMetadata(cell, { deepnote_variable_value: input }); } /** @@ -788,8 +788,8 @@ export class DeepnoteInputBlockCellStatusBarItemProvider return; } - const newStart = new Date(input).toISOString(); - const newValue = currentEnd ? [newStart, new Date(currentEnd).toISOString()] : [newStart, newStart]; + // Store as YYYY-MM-DD format (not full ISO string) + const newValue = currentEnd ? [input, currentEnd] : [input, input]; await this.updateCellMetadata(cell, { deepnote_variable_value: newValue }); } @@ -829,8 +829,8 @@ export class DeepnoteInputBlockCellStatusBarItemProvider return; } - const newEnd = new Date(input).toISOString(); - const newValue = currentStart ? [new Date(currentStart).toISOString(), newEnd] : [newEnd, newEnd]; + // Store as YYYY-MM-DD format (not full ISO string) + const newValue = currentStart ? [currentStart, input] : [input, input]; await this.updateCellMetadata(cell, { deepnote_variable_value: newValue }); } From 4ce5ae78b02694b571d1dd8d6036773a988576e3 Mon Sep 17 00:00:00 2001 From: jankuca Date: Fri, 24 Oct 2025 11:35:29 +0200 Subject: [PATCH 008/101] fix: prevent language change of input block editors --- .../deepnoteInputBlockEditProtection.ts | 104 +++++++++++++++++- 1 file changed, 98 insertions(+), 6 deletions(-) diff --git a/src/notebooks/deepnote/deepnoteInputBlockEditProtection.ts b/src/notebooks/deepnote/deepnoteInputBlockEditProtection.ts index 21cff80305..243a21e799 100644 --- a/src/notebooks/deepnote/deepnoteInputBlockEditProtection.ts +++ b/src/notebooks/deepnote/deepnoteInputBlockEditProtection.ts @@ -2,6 +2,8 @@ import { Disposable, NotebookCell, NotebookDocumentChangeEvent, + NotebookEdit, + NotebookRange, Position, Range, workspace, @@ -11,6 +13,7 @@ import { formatInputBlockCellContent } from './inputBlockContentFormatter'; /** * Protects readonly input blocks from being edited by reverting changes. + * Also protects the language ID of all input blocks. * This is needed because VSCode doesn't support the `editable: false` metadata property. */ export class DeepnoteInputBlockEditProtection implements Disposable { @@ -25,6 +28,32 @@ export class DeepnoteInputBlockEditProtection implements Disposable { 'button' ]); + // All input block types (for language protection) + private readonly allInputTypes = new Set([ + 'input-text', + 'input-textarea', + 'input-select', + 'input-slider', + 'input-checkbox', + 'input-date', + 'input-date-range', + 'input-file', + 'button' + ]); + + // Map of block types to their expected language IDs + private readonly expectedLanguages = new Map([ + ['input-text', 'plaintext'], + ['input-textarea', 'plaintext'], + ['input-select', 'python'], + ['input-slider', 'python'], + ['input-checkbox', 'python'], + ['input-date', 'python'], + ['input-date-range', 'python'], + ['input-file', 'python'], + ['button', 'python'] + ]); + constructor() { // Listen for notebook document changes this.disposables.push( @@ -40,22 +69,43 @@ export class DeepnoteInputBlockEditProtection implements Disposable { return; } - // Process cell changes (content edits) + // Collect all cells that need language fixes in a single batch + const cellsToFix: Array<{ cell: NotebookCell; blockType: string }> = []; + + // Process content changes (cell edits) for (const cellChange of e.cellChanges) { const cell = cellChange.cell; - - // Check if this is a readonly input block const blockType = cell.metadata?.__deepnotePocket?.type || cell.metadata?.type; - if (!blockType || !this.readonlyInputTypes.has(blockType)) { + + if (!blockType || !this.allInputTypes.has(blockType)) { continue; } - // Check if the document (content) changed - if (cellChange.document) { + // Check if the document (content) changed for readonly blocks + if (cellChange.document && this.readonlyInputTypes.has(blockType)) { // Revert the change by restoring content from metadata await this.revertCellContent(cell); } } + + // Check all cells in the notebook for language changes + // We need to check all cells because language changes don't reliably appear in cellChanges or contentChanges + for (const cell of e.notebook.getCells()) { + const blockType = cell.metadata?.__deepnotePocket?.type || cell.metadata?.type; + + if (blockType && this.allInputTypes.has(blockType)) { + const expectedLanguage = this.expectedLanguages.get(blockType); + // Only add to fix list if language is actually wrong + if (expectedLanguage && cell.document.languageId !== expectedLanguage) { + cellsToFix.push({ cell, blockType }); + } + } + } + + // Apply all language fixes in a single batch to minimize flickering + if (cellsToFix.length > 0) { + await this.protectCellLanguages(cellsToFix); + } } private async revertCellContent(cell: NotebookCell): Promise { @@ -77,6 +127,48 @@ export class DeepnoteInputBlockEditProtection implements Disposable { } } + private async protectCellLanguages(cellsToFix: Array<{ cell: NotebookCell; blockType: string }>): Promise { + if (cellsToFix.length === 0) { + return; + } + + // Group cells by notebook to apply edits efficiently + const editsByNotebook = new Map(); + + for (const { cell, blockType } of cellsToFix) { + const expectedLanguage = this.expectedLanguages.get(blockType); + + if (!expectedLanguage) { + continue; + } + + const notebookUriStr = cell.notebook.uri.toString(); + if (!editsByNotebook.has(notebookUriStr)) { + editsByNotebook.set(notebookUriStr, { uri: cell.notebook.uri, edits: [] }); + } + + // Add the cell replacement edit + editsByNotebook.get(notebookUriStr)!.edits.push( + NotebookEdit.replaceCells(new NotebookRange(cell.index, cell.index + 1), [ + { + kind: cell.kind, + languageId: expectedLanguage, + value: cell.document.getText(), + metadata: cell.metadata + } + ]) + ); + } + + // Apply all edits in a single workspace edit to minimize flickering + const workspaceEdit = new WorkspaceEdit(); + for (const { uri, edits } of editsByNotebook.values()) { + workspaceEdit.set(uri, edits); + } + + await workspace.applyEdit(workspaceEdit); + } + dispose(): void { this.disposables.forEach((d) => d.dispose()); } From 6d71fc0306b748715c671fe8318b6de64987c34f Mon Sep 17 00:00:00 2001 From: jankuca Date: Fri, 24 Oct 2025 11:37:59 +0200 Subject: [PATCH 009/101] feat: add Step option to slider inputs --- ...deepnoteInputBlockCellStatusBarProvider.ts | 53 +++++++++++++++++++ 1 file changed, 53 insertions(+) diff --git a/src/notebooks/deepnote/deepnoteInputBlockCellStatusBarProvider.ts b/src/notebooks/deepnote/deepnoteInputBlockCellStatusBarProvider.ts index 96627efdec..b48c4d06e8 100644 --- a/src/notebooks/deepnote/deepnoteInputBlockCellStatusBarProvider.ts +++ b/src/notebooks/deepnote/deepnoteInputBlockCellStatusBarProvider.ts @@ -119,6 +119,16 @@ export class DeepnoteInputBlockCellStatusBarItemProvider }) ); + // Slider: set step value + this.disposables.push( + commands.registerCommand('deepnote.sliderSetStep', async (cell?: NotebookCell) => { + const activeCell = cell || this.getActiveCell(); + if (activeCell) { + await this.sliderSetStep(activeCell); + } + }) + ); + // Checkbox: toggle value this.disposables.push( commands.registerCommand('deepnote.checkboxToggle', async (cell?: NotebookCell) => { @@ -312,6 +322,7 @@ export class DeepnoteInputBlockCellStatusBarItemProvider ): void { const min = metadata.deepnote_slider_min_value as number | undefined; const max = metadata.deepnote_slider_max_value as number | undefined; + const step = metadata.deepnote_slider_step as number | undefined; items.push({ text: l10n.t('Min: {0}', min ?? 0), @@ -336,6 +347,18 @@ export class DeepnoteInputBlockCellStatusBarItemProvider arguments: [cell] } }); + + items.push({ + text: l10n.t('Step: {0}', step ?? 1), + alignment: 1, + priority: 78, + tooltip: l10n.t('Step size\nClick to change'), + command: { + title: l10n.t('Set Step'), + command: 'deepnote.sliderSetStep', + arguments: [cell] + } + }); } private addCheckboxInputStatusBarItems( @@ -708,6 +731,36 @@ export class DeepnoteInputBlockCellStatusBarItemProvider await this.updateCellMetadata(cell, { deepnote_slider_max_value: newMax }); } + /** + * Handler for slider: set step value + */ + private async sliderSetStep(cell: NotebookCell): Promise { + const metadata = cell.metadata as Record | undefined; + const currentStep = (metadata?.deepnote_slider_step as number) ?? 1; + + const input = await window.showInputBox({ + prompt: l10n.t('Enter step size'), + value: String(currentStep), + validateInput: (value) => { + const num = parseFloat(value); + if (isNaN(num)) { + return l10n.t('Please enter a valid number'); + } + if (num <= 0) { + return l10n.t('Step size must be greater than 0'); + } + return undefined; + } + }); + + if (input === undefined) { + return; + } + + const newStep = parseFloat(input); + await this.updateCellMetadata(cell, { deepnote_slider_step: newStep }); + } + /** * Handler for checkbox: toggle value */ From 28e205f4c0d55990cf4b31d963df767d67f0d106 Mon Sep 17 00:00:00 2001 From: jankuca Date: Fri, 24 Oct 2025 11:41:11 +0200 Subject: [PATCH 010/101] feat: add file picker button to file inputs --- ...deepnoteInputBlockCellStatusBarProvider.ts | 76 ++++++++++++++++++- 1 file changed, 75 insertions(+), 1 deletion(-) diff --git a/src/notebooks/deepnote/deepnoteInputBlockCellStatusBarProvider.ts b/src/notebooks/deepnote/deepnoteInputBlockCellStatusBarProvider.ts index b48c4d06e8..a3311eedee 100644 --- a/src/notebooks/deepnote/deepnoteInputBlockCellStatusBarProvider.ts +++ b/src/notebooks/deepnote/deepnoteInputBlockCellStatusBarProvider.ts @@ -129,6 +129,16 @@ export class DeepnoteInputBlockCellStatusBarItemProvider }) ); + // File input: choose file + this.disposables.push( + commands.registerCommand('deepnote.fileInputChooseFile', async (cell?: NotebookCell) => { + const activeCell = cell || this.getActiveCell(); + if (activeCell) { + await this.fileInputChooseFile(activeCell); + } + }) + ); + // Checkbox: toggle value this.disposables.push( commands.registerCommand('deepnote.checkboxToggle', async (cell?: NotebookCell) => { @@ -268,7 +278,11 @@ export class DeepnoteInputBlockCellStatusBarItemProvider this.addDateRangeInputStatusBarItems(items, cell, metadata); break; - // input-text, input-textarea, input-file, and button don't have additional buttons + case 'input-file': + this.addFileInputStatusBarItems(items, cell, metadata); + break; + + // input-text, input-textarea, and button don't have additional buttons } } @@ -382,6 +396,24 @@ export class DeepnoteInputBlockCellStatusBarItemProvider }); } + private addFileInputStatusBarItems( + items: NotebookCellStatusBarItem[], + cell: NotebookCell, + _metadata: Record + ): void { + items.push({ + text: l10n.t('$(folder-opened) Choose File'), + alignment: 1, + priority: 80, + tooltip: l10n.t('Choose a file\nClick to browse'), + command: { + title: l10n.t('Choose File'), + command: 'deepnote.fileInputChooseFile', + arguments: [cell] + } + }); + } + private addDateInputStatusBarItems( items: NotebookCellStatusBarItem[], cell: NotebookCell, @@ -771,6 +803,48 @@ export class DeepnoteInputBlockCellStatusBarItemProvider await this.updateCellMetadata(cell, { deepnote_variable_value: !currentValue }); } + /** + * Handler for file input: choose file + */ + private async fileInputChooseFile(cell: NotebookCell): Promise { + const metadata = cell.metadata as Record | undefined; + const allowedExtensions = metadata?.deepnote_allowed_file_extensions as string | undefined; + + // Parse allowed extensions if provided + const filters: { [name: string]: string[] } = {}; + if (allowedExtensions) { + // Split by comma and clean up + const extensions = allowedExtensions + .split(',') + .map((ext) => ext.trim()) + .filter((ext) => ext.length > 0); + + if (extensions.length > 0) { + filters['Allowed Files'] = extensions; + } + } + + // Add "All Files" option + filters['All Files'] = ['*']; + + const uris = await window.showOpenDialog({ + canSelectFiles: true, + canSelectFolders: false, + canSelectMany: false, + filters: Object.keys(filters).length > 1 ? filters : undefined, + openLabel: l10n.t('Select File') + }); + + if (!uris || uris.length === 0) { + return; + } + + // Get the file path + const filePath = uris[0].path; + + await this.updateCellMetadata(cell, { deepnote_variable_value: filePath }); + } + /** * Handler for date input: choose date */ From cf64ac0d9833ac9ce049f35b5d29e3bfc447522e Mon Sep 17 00:00:00 2001 From: jankuca Date: Fri, 24 Oct 2025 12:03:00 +0200 Subject: [PATCH 011/101] add select input settings screen --- build/esbuild/build.ts | 5 + ...deepnoteInputBlockCellStatusBarProvider.ts | 42 ++- .../deepnote/selectInputSettingsWebview.ts | 250 ++++++++++++++++++ .../SelectInputSettingsPanel.tsx | 243 +++++++++++++++++ .../selectInputSettings/index.html | 24 ++ .../selectInputSettings/index.tsx | 24 ++ .../selectInputSettings.css | 228 ++++++++++++++++ .../webview-side/selectInputSettings/types.ts | 17 ++ 8 files changed, 832 insertions(+), 1 deletion(-) create mode 100644 src/notebooks/deepnote/selectInputSettingsWebview.ts create mode 100644 src/webviews/webview-side/selectInputSettings/SelectInputSettingsPanel.tsx create mode 100644 src/webviews/webview-side/selectInputSettings/index.html create mode 100644 src/webviews/webview-side/selectInputSettings/index.tsx create mode 100644 src/webviews/webview-side/selectInputSettings/selectInputSettings.css create mode 100644 src/webviews/webview-side/selectInputSettings/types.ts diff --git a/build/esbuild/build.ts b/build/esbuild/build.ts index 2ffacf0601..c2bc379e9e 100644 --- a/build/esbuild/build.ts +++ b/build/esbuild/build.ts @@ -379,6 +379,11 @@ async function buildAll() { path.join(extensionFolder, 'src', 'webviews', 'webview-side', 'integrations', 'index.tsx'), path.join(extensionFolder, 'dist', 'webviews', 'webview-side', 'integrations', 'index.js'), { target: 'web', watch: watchAll } + ), + build( + path.join(extensionFolder, 'src', 'webviews', 'webview-side', 'selectInputSettings', 'index.tsx'), + path.join(extensionFolder, 'dist', 'webviews', 'webview-side', 'selectInputSettings', 'index.js'), + { target: 'web', watch: watchAll } ) ); diff --git a/src/notebooks/deepnote/deepnoteInputBlockCellStatusBarProvider.ts b/src/notebooks/deepnote/deepnoteInputBlockCellStatusBarProvider.ts index a3311eedee..d9d952a8b1 100644 --- a/src/notebooks/deepnote/deepnoteInputBlockCellStatusBarProvider.ts +++ b/src/notebooks/deepnote/deepnoteInputBlockCellStatusBarProvider.ts @@ -18,9 +18,11 @@ import { workspace } from 'vscode'; import { IExtensionSyncActivationService } from '../../platform/activation/types'; -import { injectable } from 'inversify'; +import { inject, injectable } from 'inversify'; import type { Pocket } from '../../platform/deepnote/pocket'; import { formatInputBlockCellContent } from './inputBlockContentFormatter'; +import { SelectInputSettingsWebviewProvider } from './selectInputSettingsWebview'; +import { IExtensionContext } from '../../platform/common/types'; /** * Provides status bar items for Deepnote input block cells to display their block type. @@ -32,9 +34,14 @@ export class DeepnoteInputBlockCellStatusBarItemProvider { private readonly disposables: Disposable[] = []; private readonly _onDidChangeCellStatusBarItems = new EventEmitter(); + private readonly selectInputSettingsWebview: SelectInputSettingsWebviewProvider; public readonly onDidChangeCellStatusBarItems = this._onDidChangeCellStatusBarItems.event; + constructor(@inject(IExtensionContext) extensionContext: IExtensionContext) { + this.selectInputSettingsWebview = new SelectInputSettingsWebviewProvider(extensionContext); + } + // List of supported Deepnote input block types private readonly INPUT_BLOCK_TYPES = [ 'input-text', @@ -139,6 +146,16 @@ export class DeepnoteInputBlockCellStatusBarItemProvider }) ); + // Select input: configure settings + this.disposables.push( + commands.registerCommand('deepnote.selectInputSettings', async (cell?: NotebookCell) => { + const activeCell = cell || this.getActiveCell(); + if (activeCell) { + await this.selectInputSettings(activeCell); + } + }) + ); + // Checkbox: toggle value this.disposables.push( commands.registerCommand('deepnote.checkboxToggle', async (cell?: NotebookCell) => { @@ -318,6 +335,19 @@ export class DeepnoteInputBlockCellStatusBarItemProvider } }); + // Settings button + items.push({ + text: l10n.t('$(gear) Settings'), + alignment: 1, + priority: 79, + tooltip: l10n.t('Configure select input settings\nClick to open'), + command: { + title: l10n.t('Settings'), + command: 'deepnote.selectInputSettings', + arguments: [cell] + } + }); + // Show source variable if select type is from-variable if (selectType === 'from-variable' && sourceVariable) { items.push({ @@ -845,6 +875,16 @@ export class DeepnoteInputBlockCellStatusBarItemProvider await this.updateCellMetadata(cell, { deepnote_variable_value: filePath }); } + /** + * Handler for select input: configure settings + */ + private async selectInputSettings(cell: NotebookCell): Promise { + await this.selectInputSettingsWebview.show(cell); + // The webview will handle saving the settings + // Trigger a status bar refresh after the webview closes + this._onDidChangeCellStatusBarItems.fire(); + } + /** * Handler for date input: choose date */ diff --git a/src/notebooks/deepnote/selectInputSettingsWebview.ts b/src/notebooks/deepnote/selectInputSettingsWebview.ts new file mode 100644 index 0000000000..8f7d2b3b20 --- /dev/null +++ b/src/notebooks/deepnote/selectInputSettingsWebview.ts @@ -0,0 +1,250 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +import { + Disposable, + NotebookCell, + NotebookEdit, + NotebookRange, + Uri, + ViewColumn, + WebviewPanel, + window, + workspace, + WorkspaceEdit +} from 'vscode'; +import { inject, injectable } from 'inversify'; +import { IExtensionContext } from '../../platform/common/types'; + +interface SelectInputSettings { + allowMultipleValues: boolean; + allowEmptyValue: boolean; + selectType: 'from-options' | 'from-variable'; + options: string[]; + selectedVariable: string; +} + +/** + * Manages the webview panel for select input settings + */ +@injectable() +export class SelectInputSettingsWebviewProvider { + private currentPanel: WebviewPanel | undefined; + private readonly disposables: Disposable[] = []; + private currentCell: NotebookCell | undefined; + private resolvePromise: ((settings: SelectInputSettings | null) => void) | undefined; + + constructor(@inject(IExtensionContext) private readonly extensionContext: IExtensionContext) {} + + /** + * Show the select input settings webview + */ + public async show(cell: NotebookCell): Promise { + this.currentCell = cell; + + const column = window.activeTextEditor ? window.activeTextEditor.viewColumn : ViewColumn.One; + + // If we already have a panel, dispose it and create a new one + if (this.currentPanel) { + this.currentPanel.dispose(); + } + + // Create a new panel + this.currentPanel = window.createWebviewPanel( + 'deepnoteSelectInputSettings', + 'Select Input Settings', + column || ViewColumn.One, + { + enableScripts: true, + retainContextWhenHidden: true, + localResourceRoots: [this.extensionContext.extensionUri] + } + ); + + // Set the webview's initial html content + this.currentPanel.webview.html = this.getWebviewContent(); + + // Handle messages from the webview + this.currentPanel.webview.onDidReceiveMessage( + async (message) => { + await this.handleMessage(message); + }, + null, + this.disposables + ); + + // Reset when the current panel is closed + this.currentPanel.onDidDispose( + () => { + this.currentPanel = undefined; + this.currentCell = undefined; + if (this.resolvePromise) { + this.resolvePromise(null); + this.resolvePromise = undefined; + } + this.disposables.forEach((d) => d.dispose()); + this.disposables.length = 0; + }, + null, + this.disposables + ); + + // Send initial data + await this.sendInitialData(); + + // Return a promise that resolves when the user saves or cancels + return new Promise((resolve) => { + this.resolvePromise = resolve; + }); + } + + private async sendInitialData(): Promise { + if (!this.currentPanel || !this.currentCell) { + return; + } + + const metadata = this.currentCell.metadata as Record | undefined; + + const settings: SelectInputSettings = { + allowMultipleValues: (metadata?.deepnote_allow_multiple_values as boolean) ?? false, + allowEmptyValue: (metadata?.deepnote_allow_empty_values as boolean) ?? false, + selectType: (metadata?.deepnote_variable_select_type as 'from-options' | 'from-variable') ?? 'from-options', + options: (metadata?.deepnote_variable_custom_options as string[]) ?? [], + selectedVariable: (metadata?.deepnote_variable_selected_variable as string) ?? '' + }; + + await this.currentPanel.webview.postMessage({ + type: 'init', + settings + }); + } + + private async handleMessage(message: { type: string; settings?: SelectInputSettings }): Promise { + switch (message.type) { + case 'save': + if (message.settings && this.currentCell) { + await this.saveSettings(message.settings); + if (this.resolvePromise) { + this.resolvePromise(message.settings); + this.resolvePromise = undefined; + } + this.currentPanel?.dispose(); + } + break; + + case 'cancel': + if (this.resolvePromise) { + this.resolvePromise(null); + this.resolvePromise = undefined; + } + this.currentPanel?.dispose(); + break; + } + } + + private async saveSettings(settings: SelectInputSettings): Promise { + if (!this.currentCell) { + return; + } + + const edit = new WorkspaceEdit(); + const metadata = { ...(this.currentCell.metadata as Record) }; + + metadata.deepnote_allow_multiple_values = settings.allowMultipleValues; + metadata.deepnote_allow_empty_values = settings.allowEmptyValue; + metadata.deepnote_variable_select_type = settings.selectType; + metadata.deepnote_variable_custom_options = settings.options; + metadata.deepnote_variable_selected_variable = settings.selectedVariable; + + // Update the options field based on the select type + if (settings.selectType === 'from-options') { + metadata.deepnote_variable_options = settings.options; + } + + // Replace the cell with updated metadata + const cellData = { + kind: this.currentCell.kind, + languageId: this.currentCell.document.languageId, + value: this.currentCell.document.getText(), + metadata + }; + + edit.set(this.currentCell.notebook.uri, [ + NotebookEdit.replaceCells(new NotebookRange(this.currentCell.index, this.currentCell.index + 1), [cellData]) + ]); + + await workspace.applyEdit(edit); + } + + private getWebviewContent(): string { + if (!this.currentPanel) { + return ''; + } + + const webview = this.currentPanel.webview; + const nonce = this.getNonce(); + + // Get URIs for the React app + const scriptUri = webview.asWebviewUri( + Uri.joinPath( + this.extensionContext.extensionUri, + 'dist', + 'webviews', + 'webview-side', + 'selectInputSettings', + 'index.js' + ) + ); + const styleUri = webview.asWebviewUri( + Uri.joinPath( + this.extensionContext.extensionUri, + 'dist', + 'webviews', + 'webview-side', + 'selectInputSettings', + 'selectInputSettings.css' + ) + ); + const codiconUri = webview.asWebviewUri( + Uri.joinPath( + this.extensionContext.extensionUri, + 'dist', + 'webviews', + 'webview-side', + 'react-common', + 'codicon', + 'codicon.css' + ) + ); + + return ` + + + + + + + + Select Input Settings + + +
+ + +`; + } + + private getNonce(): string { + let text = ''; + const possible = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'; + for (let i = 0; i < 32; i++) { + text += possible.charAt(Math.floor(Math.random() * possible.length)); + } + return text; + } + + public dispose(): void { + this.currentPanel?.dispose(); + this.disposables.forEach((d) => d.dispose()); + } +} diff --git a/src/webviews/webview-side/selectInputSettings/SelectInputSettingsPanel.tsx b/src/webviews/webview-side/selectInputSettings/SelectInputSettingsPanel.tsx new file mode 100644 index 0000000000..d9549c249d --- /dev/null +++ b/src/webviews/webview-side/selectInputSettings/SelectInputSettingsPanel.tsx @@ -0,0 +1,243 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +import * as React from 'react'; +import { IVsCodeApi } from '../react-common/postOffice'; +import { getLocString, storeLocStrings } from '../react-common/locReactSide'; +import { SelectInputSettings, WebviewMessage } from './types'; + +export interface ISelectInputSettingsPanelProps { + baseTheme: string; + vscodeApi: IVsCodeApi; +} + +export const SelectInputSettingsPanel: React.FC = ({ baseTheme, vscodeApi }) => { + const [settings, setSettings] = React.useState({ + allowMultipleValues: false, + allowEmptyValue: false, + selectType: 'from-options', + options: [], + selectedVariable: '' + }); + + const [newOption, setNewOption] = React.useState(''); + + React.useEffect(() => { + const handleMessage = (event: MessageEvent) => { + const message = event.data as WebviewMessage; + + switch (message.type) { + case 'init': + if (message.settings) { + setSettings(message.settings); + } + break; + + case 'locInit': + if (message.locStrings) { + storeLocStrings(message.locStrings); + } + break; + } + }; + + window.addEventListener('message', handleMessage); + return () => window.removeEventListener('message', handleMessage); + }, []); + + const handleToggle = (field: 'allowMultipleValues' | 'allowEmptyValue') => { + setSettings((prev) => ({ + ...prev, + [field]: !prev[field] + })); + }; + + const handleSelectTypeChange = (selectType: 'from-options' | 'from-variable') => { + setSettings((prev) => ({ + ...prev, + selectType + })); + }; + + const handleAddOption = () => { + if (newOption.trim()) { + setSettings((prev) => ({ + ...prev, + options: [...prev.options, newOption.trim()] + })); + setNewOption(''); + } + }; + + const handleRemoveOption = (index: number) => { + setSettings((prev) => ({ + ...prev, + options: prev.options.filter((_, i) => i !== index) + })); + }; + + const handleVariableChange = (e: React.ChangeEvent) => { + setSettings((prev) => ({ + ...prev, + selectedVariable: e.target.value + })); + }; + + const handleSave = () => { + vscodeApi.postMessage({ + type: 'save', + settings + }); + }; + + const handleCancel = () => { + vscodeApi.postMessage({ + type: 'cancel' + }); + }; + + return ( +
+

{getLocString('selectInputSettingsTitle', 'Settings')}

+ +
+
+ + +
+ +
+ + +
+
+ +

{getLocString('valueSourceTitle', 'Value')}

+ +
+
handleSelectTypeChange('from-options')} + > + handleSelectTypeChange('from-options')} + /> +
+
{getLocString('fromOptions', 'From options')}
+
+ {getLocString('fromOptionsDescription', 'A set of defined options.')} +
+ + {settings.selectType === 'from-options' && ( +
+ {settings.options.map((option, index) => ( + + {option} + + + ))} + +
+ setNewOption(e.target.value)} + onKeyPress={(e) => { + if (e.key === 'Enter') { + e.preventDefault(); + handleAddOption(); + } + }} + placeholder={getLocString('addOptionPlaceholder', 'Add option...')} + onClick={(e) => e.stopPropagation()} + /> + +
+
+ )} +
+
+ +
handleSelectTypeChange('from-variable')} + > + handleSelectTypeChange('from-variable')} + /> +
+
{getLocString('fromVariable', 'From variable')}
+
+ {getLocString( + 'fromVariableDescription', + 'A list or Series that contains only strings, numbers or booleans.' + )} +
+ + {settings.selectType === 'from-variable' && ( + e.stopPropagation()} + /> + )} +
+
+
+ +
+ + +
+
+ ); +}; + diff --git a/src/webviews/webview-side/selectInputSettings/index.html b/src/webviews/webview-side/selectInputSettings/index.html new file mode 100644 index 0000000000..b711a372ec --- /dev/null +++ b/src/webviews/webview-side/selectInputSettings/index.html @@ -0,0 +1,24 @@ + + + + + + + + Select Input Settings + + +
+ + + + diff --git a/src/webviews/webview-side/selectInputSettings/index.tsx b/src/webviews/webview-side/selectInputSettings/index.tsx new file mode 100644 index 0000000000..be65d7fa9a --- /dev/null +++ b/src/webviews/webview-side/selectInputSettings/index.tsx @@ -0,0 +1,24 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +import * as React from 'react'; +import * as ReactDOM from 'react-dom'; + +import { IVsCodeApi } from '../react-common/postOffice'; +import { detectBaseTheme } from '../react-common/themeDetector'; +import { SelectInputSettingsPanel } from './SelectInputSettingsPanel'; + +import '../common/index.css'; +import './selectInputSettings.css'; + +// This special function talks to vscode from a web panel +declare function acquireVsCodeApi(): IVsCodeApi; + +const baseTheme = detectBaseTheme(); +const vscodeApi = acquireVsCodeApi(); + +ReactDOM.render( + , + document.getElementById('root') as HTMLElement +); + diff --git a/src/webviews/webview-side/selectInputSettings/selectInputSettings.css b/src/webviews/webview-side/selectInputSettings/selectInputSettings.css new file mode 100644 index 0000000000..4c3411e00d --- /dev/null +++ b/src/webviews/webview-side/selectInputSettings/selectInputSettings.css @@ -0,0 +1,228 @@ +.select-input-settings-panel { + padding: 20px; + max-width: 600px; + margin: 0 auto; +} + +.select-input-settings-panel h1 { + font-size: 24px; + margin-bottom: 20px; +} + +.select-input-settings-panel h2 { + font-size: 18px; + margin-top: 30px; + margin-bottom: 15px; + padding-bottom: 10px; + border-bottom: 1px solid var(--vscode-panel-border); +} + +.settings-section { + margin-bottom: 30px; +} + +.toggle-option { + display: flex; + align-items: center; + justify-content: space-between; + padding: 12px 0; +} + +.toggle-option label { + font-size: 14px; + cursor: pointer; +} + +.toggle-switch { + position: relative; + width: 44px; + height: 24px; +} + +.toggle-switch input { + opacity: 0; + width: 0; + height: 0; +} + +.toggle-slider { + position: absolute; + cursor: pointer; + top: 0; + left: 0; + right: 0; + bottom: 0; + background-color: var(--vscode-input-background); + border: 1px solid var(--vscode-input-border); + transition: 0.2s; + border-radius: 24px; +} + +.toggle-slider:before { + position: absolute; + content: ''; + height: 16px; + width: 16px; + left: 3px; + bottom: 3px; + background-color: var(--vscode-input-foreground); + transition: 0.2s; + border-radius: 50%; +} + +.toggle-switch input:checked + .toggle-slider { + background-color: var(--vscode-button-background); + border-color: var(--vscode-button-background); +} + +.toggle-switch input:checked + .toggle-slider:before { + transform: translateX(20px); + background-color: var(--vscode-button-foreground); +} + +.value-source-section { + margin-top: 20px; +} + +.radio-option { + display: flex; + align-items: flex-start; + padding: 15px; + margin-bottom: 10px; + border: 1px solid var(--vscode-panel-border); + border-radius: 4px; + cursor: pointer; + transition: background-color 0.2s; +} + +.radio-option:hover { + background-color: var(--vscode-list-hoverBackground); +} + +.radio-option.selected { + border-color: var(--vscode-focusBorder); + background-color: var(--vscode-list-activeSelectionBackground); +} + +.radio-option input[type='radio'] { + margin-right: 12px; + margin-top: 2px; +} + +.radio-content { + flex: 1; +} + +.radio-title { + font-weight: 600; + margin-bottom: 5px; +} + +.radio-description { + font-size: 12px; + color: var(--vscode-descriptionForeground); + margin-bottom: 10px; +} + +.options-list { + margin-top: 10px; +} + +.option-tag { + display: inline-flex; + align-items: center; + background-color: var(--vscode-badge-background); + color: var(--vscode-badge-foreground); + padding: 4px 8px; + margin: 4px 4px 4px 0; + border-radius: 3px; + font-size: 12px; +} + +.option-tag button { + background: none; + border: none; + color: inherit; + margin-left: 6px; + cursor: pointer; + padding: 0; + font-size: 14px; + line-height: 1; +} + +.option-tag button:hover { + opacity: 0.7; +} + +.add-option-form { + display: flex; + gap: 8px; + margin-top: 10px; +} + +.add-option-form input { + flex: 1; + padding: 6px 8px; + background-color: var(--vscode-input-background); + color: var(--vscode-input-foreground); + border: 1px solid var(--vscode-input-border); + border-radius: 2px; +} + +.add-option-form button { + padding: 6px 12px; + background-color: var(--vscode-button-background); + color: var(--vscode-button-foreground); + border: none; + border-radius: 2px; + cursor: pointer; +} + +.add-option-form button:hover { + background-color: var(--vscode-button-hoverBackground); +} + +.variable-input { + width: 100%; + padding: 6px 8px; + background-color: var(--vscode-input-background); + color: var(--vscode-input-foreground); + border: 1px solid var(--vscode-input-border); + border-radius: 2px; + margin-top: 10px; +} + +.actions { + display: flex; + gap: 10px; + margin-top: 30px; + padding-top: 20px; + border-top: 1px solid var(--vscode-panel-border); +} + +.actions button { + padding: 8px 16px; + border: none; + border-radius: 2px; + cursor: pointer; + font-size: 13px; +} + +.btn-primary { + background-color: var(--vscode-button-background); + color: var(--vscode-button-foreground); +} + +.btn-primary:hover { + background-color: var(--vscode-button-hoverBackground); +} + +.btn-secondary { + background-color: var(--vscode-button-secondaryBackground); + color: var(--vscode-button-secondaryForeground); +} + +.btn-secondary:hover { + background-color: var(--vscode-button-secondaryHoverBackground); +} + diff --git a/src/webviews/webview-side/selectInputSettings/types.ts b/src/webviews/webview-side/selectInputSettings/types.ts new file mode 100644 index 0000000000..e7dc1eb467 --- /dev/null +++ b/src/webviews/webview-side/selectInputSettings/types.ts @@ -0,0 +1,17 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +export interface SelectInputSettings { + allowMultipleValues: boolean; + allowEmptyValue: boolean; + selectType: 'from-options' | 'from-variable'; + options: string[]; + selectedVariable: string; +} + +export interface WebviewMessage { + type: 'init' | 'save' | 'locInit'; + settings?: SelectInputSettings; + locStrings?: Record; +} + From 62628b86821ad7d3ef0330cd9b1cfb513b3ba128 Mon Sep 17 00:00:00 2001 From: jankuca Date: Fri, 24 Oct 2025 13:11:29 +0200 Subject: [PATCH 012/101] feat: add multi-select support to select inputs --- ...deepnoteInputBlockCellStatusBarProvider.ts | 96 +++++++++++++++---- .../deepnote/inputBlockContentFormatter.ts | 8 +- 2 files changed, 85 insertions(+), 19 deletions(-) diff --git a/src/notebooks/deepnote/deepnoteInputBlockCellStatusBarProvider.ts b/src/notebooks/deepnote/deepnoteInputBlockCellStatusBarProvider.ts index d9d952a8b1..8394e4b621 100644 --- a/src/notebooks/deepnote/deepnoteInputBlockCellStatusBarProvider.ts +++ b/src/notebooks/deepnote/deepnoteInputBlockCellStatusBarProvider.ts @@ -682,6 +682,7 @@ export class DeepnoteInputBlockCellStatusBarItemProvider const selectType = metadata.deepnote_variable_select_type as string | undefined; const allowMultiple = metadata.deepnote_allow_multiple_values as boolean | undefined; + const allowEmpty = metadata.deepnote_allow_empty_values as boolean | undefined; const currentValue = metadata.deepnote_variable_value; // Get options based on select type @@ -705,29 +706,82 @@ export class DeepnoteInputBlockCellStatusBarItemProvider } if (allowMultiple) { - // Multi-select using QuickPick + // Multi-select using QuickPick with custom behavior for clear selection const currentSelection = Array.isArray(currentValue) ? currentValue : []; - const selected = await window.showQuickPick( - options.map((opt) => ({ - label: opt, - picked: currentSelection.includes(opt) - })), - { - canPickMany: true, - placeHolder: l10n.t('Select one or more options') - } - ); + const clearSelectionLabel = l10n.t('$(circle-slash) Clear selection'); + + // Create quick pick items + const optionItems = options.map((opt) => ({ + label: opt, + picked: currentSelection.includes(opt) + })); + + // Add empty option if allowed + if (allowEmpty) { + optionItems.unshift({ + label: clearSelectionLabel, + picked: false + }); + } - if (selected === undefined) { - return; + // Use createQuickPick for more control + const quickPick = window.createQuickPick(); + quickPick.items = optionItems; + quickPick.selectedItems = optionItems.filter((item) => item.picked); + quickPick.canSelectMany = true; + quickPick.placeholder = allowEmpty + ? l10n.t('Select one or more options (or clear selection)') + : l10n.t('Select one or more options'); + + // Listen for selection changes to handle "Clear selection" specially + if (allowEmpty) { + quickPick.onDidChangeSelection((selectedItems) => { + const hasClearSelection = selectedItems.some((item) => item.label === clearSelectionLabel); + if (hasClearSelection) { + // If "Clear selection" is selected, immediately clear and close + quickPick.selectedItems = []; + quickPick.hide(); + void this.updateCellMetadata(cell, { deepnote_variable_value: [] }); + } + }); } - const newValue = selected.map((item) => item.label); - await this.updateCellMetadata(cell, { deepnote_variable_value: newValue }); + quickPick.onDidAccept(() => { + const selected = quickPick.selectedItems; + const hasClearSelection = selected.some((item) => item.label === clearSelectionLabel); + + if (!hasClearSelection) { + const newValue = selected.map((item) => item.label); + void this.updateCellMetadata(cell, { deepnote_variable_value: newValue }); + } + quickPick.hide(); + }); + + quickPick.onDidHide(() => { + quickPick.dispose(); + }); + + quickPick.show(); } else { // Single select - const selected = await window.showQuickPick(options, { - placeHolder: l10n.t('Select an option'), + const quickPickItems = options.map((opt) => ({ + label: opt, + description: typeof currentValue === 'string' && currentValue === opt ? l10n.t('(current)') : undefined + })); + + // Add empty option if allowed + if (allowEmpty) { + quickPickItems.unshift({ + label: l10n.t('$(circle-slash) None'), + description: + currentValue === null || currentValue === undefined || currentValue === '' + ? l10n.t('(current)') + : undefined + }); + } + + const selected = await window.showQuickPick(quickPickItems, { + placeHolder: allowEmpty ? l10n.t('Select an option or none') : l10n.t('Select an option'), canPickMany: false }); @@ -735,7 +789,13 @@ export class DeepnoteInputBlockCellStatusBarItemProvider return; } - await this.updateCellMetadata(cell, { deepnote_variable_value: selected }); + // Check if "None" was chosen + const noneLabel = l10n.t('$(circle-slash) None'); + if (selected.label === noneLabel) { + await this.updateCellMetadata(cell, { deepnote_variable_value: null }); + } else { + await this.updateCellMetadata(cell, { deepnote_variable_value: selected.label }); + } } } diff --git a/src/notebooks/deepnote/inputBlockContentFormatter.ts b/src/notebooks/deepnote/inputBlockContentFormatter.ts index 5384d29b51..e4a7ea83a9 100644 --- a/src/notebooks/deepnote/inputBlockContentFormatter.ts +++ b/src/notebooks/deepnote/inputBlockContentFormatter.ts @@ -21,10 +21,17 @@ export function formatInputBlockCellContent(blockType: string, metadata: Record< const value = metadata.deepnote_variable_value; if (Array.isArray(value)) { // Multi-select: show as array of quoted strings + if (value.length === 0) { + // Empty array for multi-select + return '[]'; + } return `[${value.map((v) => `"${v}"`).join(', ')}]`; } else if (typeof value === 'string') { // Single select: show as quoted string return `"${value}"`; + } else if (value === null || value === undefined) { + // Empty/null value + return 'None'; } return ''; } @@ -100,4 +107,3 @@ function formatDateValue(val: unknown): string { } return String(val); } - From a3fb7074480ec9431529589f6fa33ff72477e75d Mon Sep 17 00:00:00 2001 From: jankuca Date: Fri, 24 Oct 2025 13:44:00 +0200 Subject: [PATCH 013/101] fix: improve UX of multi-select --- ...deepnoteInputBlockCellStatusBarProvider.ts | 40 +++++++------------ ...putBlockCellStatusBarProvider.unit.test.ts | 7 +++- 2 files changed, 21 insertions(+), 26 deletions(-) diff --git a/src/notebooks/deepnote/deepnoteInputBlockCellStatusBarProvider.ts b/src/notebooks/deepnote/deepnoteInputBlockCellStatusBarProvider.ts index 8394e4b621..82579b187a 100644 --- a/src/notebooks/deepnote/deepnoteInputBlockCellStatusBarProvider.ts +++ b/src/notebooks/deepnote/deepnoteInputBlockCellStatusBarProvider.ts @@ -10,6 +10,7 @@ import { NotebookEdit, Position, Range, + ThemeIcon, WorkspaceEdit, commands, l10n, @@ -708,38 +709,31 @@ export class DeepnoteInputBlockCellStatusBarItemProvider if (allowMultiple) { // Multi-select using QuickPick with custom behavior for clear selection const currentSelection = Array.isArray(currentValue) ? currentValue : []; - const clearSelectionLabel = l10n.t('$(circle-slash) Clear selection'); - // Create quick pick items + // Create quick pick items (only actual options, not "Clear selection") const optionItems = options.map((opt) => ({ label: opt, picked: currentSelection.includes(opt) })); - // Add empty option if allowed - if (allowEmpty) { - optionItems.unshift({ - label: clearSelectionLabel, - picked: false - }); - } - // Use createQuickPick for more control const quickPick = window.createQuickPick(); quickPick.items = optionItems; quickPick.selectedItems = optionItems.filter((item) => item.picked); quickPick.canSelectMany = true; - quickPick.placeholder = allowEmpty - ? l10n.t('Select one or more options (or clear selection)') - : l10n.t('Select one or more options'); + quickPick.placeholder = l10n.t('Select one or more options'); - // Listen for selection changes to handle "Clear selection" specially + // Add "Clear selection" as a button if empty values are allowed if (allowEmpty) { - quickPick.onDidChangeSelection((selectedItems) => { - const hasClearSelection = selectedItems.some((item) => item.label === clearSelectionLabel); - if (hasClearSelection) { - // If "Clear selection" is selected, immediately clear and close - quickPick.selectedItems = []; + const clearButton = { + iconPath: new ThemeIcon('clear-all'), + tooltip: l10n.t('Clear selection') + }; + quickPick.buttons = [clearButton]; + + quickPick.onDidTriggerButton((button) => { + if (button === clearButton) { + // Clear selection and close quickPick.hide(); void this.updateCellMetadata(cell, { deepnote_variable_value: [] }); } @@ -748,12 +742,8 @@ export class DeepnoteInputBlockCellStatusBarItemProvider quickPick.onDidAccept(() => { const selected = quickPick.selectedItems; - const hasClearSelection = selected.some((item) => item.label === clearSelectionLabel); - - if (!hasClearSelection) { - const newValue = selected.map((item) => item.label); - void this.updateCellMetadata(cell, { deepnote_variable_value: newValue }); - } + const newValue = selected.map((item) => item.label); + void this.updateCellMetadata(cell, { deepnote_variable_value: newValue }); quickPick.hide(); }); diff --git a/src/notebooks/deepnote/deepnoteInputBlockCellStatusBarProvider.unit.test.ts b/src/notebooks/deepnote/deepnoteInputBlockCellStatusBarProvider.unit.test.ts index 7f7ae0c64d..6254517230 100644 --- a/src/notebooks/deepnote/deepnoteInputBlockCellStatusBarProvider.unit.test.ts +++ b/src/notebooks/deepnote/deepnoteInputBlockCellStatusBarProvider.unit.test.ts @@ -5,12 +5,17 @@ import { expect } from 'chai'; import { DeepnoteInputBlockCellStatusBarItemProvider } from './deepnoteInputBlockCellStatusBarProvider'; import { NotebookCell, NotebookCellKind, NotebookDocument } from 'vscode'; import { Uri } from 'vscode'; +import type { IExtensionContext } from '../../platform/common/types'; suite('DeepnoteInputBlockCellStatusBarItemProvider', () => { let provider: DeepnoteInputBlockCellStatusBarItemProvider; + let mockExtensionContext: IExtensionContext; setup(() => { - provider = new DeepnoteInputBlockCellStatusBarItemProvider(); + mockExtensionContext = { + subscriptions: [] + } as any; + provider = new DeepnoteInputBlockCellStatusBarItemProvider(mockExtensionContext); }); teardown(() => { From b04f5f26eda7212acffe5204861fa28927ff28a6 Mon Sep 17 00:00:00 2001 From: jankuca Date: Fri, 24 Oct 2025 13:47:49 +0200 Subject: [PATCH 014/101] fix empty value handling for multi-select inputs --- .../deepnoteInputBlockCellStatusBarProvider.ts | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/src/notebooks/deepnote/deepnoteInputBlockCellStatusBarProvider.ts b/src/notebooks/deepnote/deepnoteInputBlockCellStatusBarProvider.ts index 82579b187a..93883f5466 100644 --- a/src/notebooks/deepnote/deepnoteInputBlockCellStatusBarProvider.ts +++ b/src/notebooks/deepnote/deepnoteInputBlockCellStatusBarProvider.ts @@ -738,10 +738,25 @@ export class DeepnoteInputBlockCellStatusBarItemProvider void this.updateCellMetadata(cell, { deepnote_variable_value: [] }); } }); + } else { + // If empty values are not allowed, ensure at least one item is always selected + quickPick.onDidChangeSelection((selectedItems) => { + if (selectedItems.length === 0) { + // Prevent deselecting the last item - restore previous selection + quickPick.selectedItems = optionItems.filter((item) => item.picked); + } + }); } quickPick.onDidAccept(() => { const selected = quickPick.selectedItems; + + // If empty values are not allowed, ensure at least one item is selected + if (!allowEmpty && selected.length === 0) { + void window.showWarningMessage(l10n.t('At least one option must be selected')); + return; + } + const newValue = selected.map((item) => item.label); void this.updateCellMetadata(cell, { deepnote_variable_value: newValue }); quickPick.hide(); From 9854628af7a9eb675b6571ac80d0555e22716c0c Mon Sep 17 00:00:00 2001 From: jankuca Date: Fri, 24 Oct 2025 13:54:21 +0200 Subject: [PATCH 015/101] feat: add warning comment to button blocks --- .../converters/inputConverters.unit.test.ts | 61 +++++++++---------- .../deepnote/inputBlockContentFormatter.ts | 2 +- 2 files changed, 31 insertions(+), 32 deletions(-) diff --git a/src/notebooks/deepnote/converters/inputConverters.unit.test.ts b/src/notebooks/deepnote/converters/inputConverters.unit.test.ts index c496c96470..45f1b57d1a 100644 --- a/src/notebooks/deepnote/converters/inputConverters.unit.test.ts +++ b/src/notebooks/deepnote/converters/inputConverters.unit.test.ts @@ -923,7 +923,7 @@ suite('ButtonBlockConverter', () => { }); suite('convertToCell', () => { - test('converts button block with set_variable behavior', () => { + test('converts button block to Python cell with comment', () => { const block: DeepnoteBlock = { blockGroup: '22e563550e734e75b35252e4975c3110', content: '', @@ -941,15 +941,11 @@ suite('ButtonBlockConverter', () => { const cell = converter.convertToCell(block); assert.strictEqual(cell.kind, NotebookCellKind.Code); - assert.strictEqual(cell.languageId, 'json'); - - const parsed = JSON.parse(cell.value); - assert.strictEqual(parsed.deepnote_button_title, 'Run'); - assert.strictEqual(parsed.deepnote_button_behavior, 'set_variable'); - assert.strictEqual(parsed.deepnote_button_color_scheme, 'blue'); + assert.strictEqual(cell.languageId, 'python'); + assert.strictEqual(cell.value, '# Buttons only work in Deepnote apps'); }); - test('converts button block with run behavior', () => { + test('converts button block with run behavior to Python cell with comment', () => { const block: DeepnoteBlock = { blockGroup: '2a1e97120eb24494adff278264625a4f', content: '', @@ -966,9 +962,9 @@ suite('ButtonBlockConverter', () => { const cell = converter.convertToCell(block); - const parsed = JSON.parse(cell.value); - assert.strictEqual(parsed.deepnote_button_title, 'Run notebook button'); - assert.strictEqual(parsed.deepnote_button_behavior, 'run'); + assert.strictEqual(cell.kind, NotebookCellKind.Code); + assert.strictEqual(cell.languageId, 'python'); + assert.strictEqual(cell.value, '# Buttons only work in Deepnote apps'); }); test('handles missing metadata with default config', () => { @@ -982,27 +978,27 @@ suite('ButtonBlockConverter', () => { const cell = converter.convertToCell(block); - const parsed = JSON.parse(cell.value); - assert.strictEqual(parsed.deepnote_button_title, 'Run'); - assert.strictEqual(parsed.deepnote_button_behavior, 'set_variable'); + assert.strictEqual(cell.kind, NotebookCellKind.Code); + assert.strictEqual(cell.languageId, 'python'); + assert.strictEqual(cell.value, '# Buttons only work in Deepnote apps'); }); }); suite('applyChangesToBlock', () => { - test('applies valid JSON with button configuration', () => { + test('preserves existing metadata and ignores cell content', () => { const block: DeepnoteBlock = { blockGroup: 'test-group', content: '', id: 'block-123', + metadata: { + deepnote_button_title: 'Click Me', + deepnote_button_behavior: 'run', + deepnote_button_color_scheme: 'red' + }, sortingKey: 'a0', type: 'button' }; - const cellValue = JSON.stringify({ - deepnote_button_title: 'Click Me', - deepnote_button_behavior: 'run', - deepnote_button_color_scheme: 'red' - }); - const cell = new NotebookCellData(NotebookCellKind.Code, cellValue, 'json'); + const cell = new NotebookCellData(NotebookCellKind.Code, '# Buttons only work in Deepnote apps', 'python'); converter.applyChangesToBlock(block, cell); @@ -1012,7 +1008,7 @@ suite('ButtonBlockConverter', () => { assert.strictEqual(block.metadata?.deepnote_button_color_scheme, 'red'); }); - test('applies different color schemes', () => { + test('applies default config when metadata is missing', () => { const block: DeepnoteBlock = { blockGroup: 'test-group', content: '', @@ -1020,30 +1016,33 @@ suite('ButtonBlockConverter', () => { sortingKey: 'a0', type: 'button' }; - const cellValue = JSON.stringify({ - deepnote_button_color_scheme: 'green' - }); - const cell = new NotebookCellData(NotebookCellKind.Code, cellValue, 'json'); + const cell = new NotebookCellData(NotebookCellKind.Code, '# Buttons only work in Deepnote apps', 'python'); converter.applyChangesToBlock(block, cell); - assert.strictEqual(block.metadata?.deepnote_button_color_scheme, 'green'); + assert.strictEqual(block.content, ''); + assert.strictEqual(block.metadata?.deepnote_button_title, 'Run'); + assert.strictEqual(block.metadata?.deepnote_button_behavior, 'set_variable'); + assert.strictEqual(block.metadata?.deepnote_button_color_scheme, 'blue'); }); - test('handles invalid JSON', () => { + test('removes raw content key from metadata', () => { const block: DeepnoteBlock = { blockGroup: 'test-group', content: '', id: 'block-123', + metadata: { + [DEEPNOTE_VSCODE_RAW_CONTENT_KEY]: 'some raw content', + deepnote_button_title: 'Test' + }, sortingKey: 'a0', type: 'button' }; - const invalidJson = '}{'; - const cell = new NotebookCellData(NotebookCellKind.Code, invalidJson, 'json'); + const cell = new NotebookCellData(NotebookCellKind.Code, '# Buttons only work in Deepnote apps', 'python'); converter.applyChangesToBlock(block, cell); - assert.strictEqual(block.metadata?.[DEEPNOTE_VSCODE_RAW_CONTENT_KEY], invalidJson); + assert.strictEqual(block.metadata?.[DEEPNOTE_VSCODE_RAW_CONTENT_KEY], undefined); }); }); }); diff --git a/src/notebooks/deepnote/inputBlockContentFormatter.ts b/src/notebooks/deepnote/inputBlockContentFormatter.ts index e4a7ea83a9..ebd52e67da 100644 --- a/src/notebooks/deepnote/inputBlockContentFormatter.ts +++ b/src/notebooks/deepnote/inputBlockContentFormatter.ts @@ -82,7 +82,7 @@ export function formatInputBlockCellContent(blockType: string, metadata: Record< } case 'button': { - return ''; + return '# Buttons only work in Deepnote apps'; } default: From 4b1b9627a46fe2e4eb9665a925ddef1416942679 Mon Sep 17 00:00:00 2001 From: jankuca Date: Fri, 24 Oct 2025 14:47:31 +0200 Subject: [PATCH 016/101] fix ts issues: add localization for select box webview --- src/messageTypes.ts | 14 +++++++++ .../deepnote/selectInputSettingsWebview.ts | 30 +++++++++++++++++++ src/platform/common/utils/localize.ts | 16 ++++++++++ 3 files changed, 60 insertions(+) diff --git a/src/messageTypes.ts b/src/messageTypes.ts index 4398a4d655..c615139e46 100644 --- a/src/messageTypes.ts +++ b/src/messageTypes.ts @@ -204,6 +204,20 @@ export type LocalizedMessages = { // Common form strings integrationsRequiredField: string; integrationsOptionalField: string; + // Select input settings strings + selectInputSettingsTitle: string; + allowMultipleValues: string; + allowEmptyValue: string; + valueSourceTitle: string; + fromOptions: string; + fromOptionsDescription: string; + addOptionPlaceholder: string; + addButton: string; + fromVariable: string; + fromVariableDescription: string; + variablePlaceholder: string; + saveButton: string; + cancelButton: string; }; // Map all messages to specific payloads export class IInteractiveWindowMapping { diff --git a/src/notebooks/deepnote/selectInputSettingsWebview.ts b/src/notebooks/deepnote/selectInputSettingsWebview.ts index 8f7d2b3b20..20f0ed15a4 100644 --- a/src/notebooks/deepnote/selectInputSettingsWebview.ts +++ b/src/notebooks/deepnote/selectInputSettingsWebview.ts @@ -15,6 +15,8 @@ import { } from 'vscode'; import { inject, injectable } from 'inversify'; import { IExtensionContext } from '../../platform/common/types'; +import { LocalizedMessages } from '../../messageTypes'; +import * as localize from '../../platform/common/utils/localize'; interface SelectInputSettings { allowMultipleValues: boolean; @@ -91,6 +93,7 @@ export class SelectInputSettingsWebviewProvider { // Send initial data await this.sendInitialData(); + await this.sendLocStrings(); // Return a promise that resolves when the user saves or cancels return new Promise((resolve) => { @@ -119,6 +122,33 @@ export class SelectInputSettingsWebviewProvider { }); } + private async sendLocStrings(): Promise { + if (!this.currentPanel) { + return; + } + + const locStrings: Partial = { + selectInputSettingsTitle: localize.SelectInputSettings.title, + allowMultipleValues: localize.SelectInputSettings.allowMultipleValues, + allowEmptyValue: localize.SelectInputSettings.allowEmptyValue, + valueSourceTitle: localize.SelectInputSettings.valueSourceTitle, + fromOptions: localize.SelectInputSettings.fromOptions, + fromOptionsDescription: localize.SelectInputSettings.fromOptionsDescription, + addOptionPlaceholder: localize.SelectInputSettings.addOptionPlaceholder, + addButton: localize.SelectInputSettings.addButton, + fromVariable: localize.SelectInputSettings.fromVariable, + fromVariableDescription: localize.SelectInputSettings.fromVariableDescription, + variablePlaceholder: localize.SelectInputSettings.variablePlaceholder, + saveButton: localize.SelectInputSettings.saveButton, + cancelButton: localize.SelectInputSettings.cancelButton + }; + + await this.currentPanel.webview.postMessage({ + type: 'locInit', + locStrings + }); + } + private async handleMessage(message: { type: string; settings?: SelectInputSettings }): Promise { switch (message.type) { case 'save': diff --git a/src/platform/common/utils/localize.ts b/src/platform/common/utils/localize.ts index ae961c9442..13118e08c8 100644 --- a/src/platform/common/utils/localize.ts +++ b/src/platform/common/utils/localize.ts @@ -859,6 +859,22 @@ export namespace Integrations { export const bigQueryUnnamedIntegration = (id: string) => l10n.t('Unnamed BigQuery Integration ({0})', id); } +export namespace SelectInputSettings { + export const title = l10n.t('Settings'); + export const allowMultipleValues = l10n.t('Allow to select multiple values'); + export const allowEmptyValue = l10n.t('Allow empty value'); + export const valueSourceTitle = l10n.t('Value'); + export const fromOptions = l10n.t('From options'); + export const fromOptionsDescription = l10n.t('A set of defined options.'); + export const addOptionPlaceholder = l10n.t('Add option...'); + export const addButton = l10n.t('Add'); + export const fromVariable = l10n.t('From variable'); + export const fromVariableDescription = l10n.t('A list or Series that contains only strings, numbers or booleans.'); + export const variablePlaceholder = l10n.t('Variable name...'); + export const saveButton = l10n.t('Save'); + export const cancelButton = l10n.t('Cancel'); +} + export namespace Deprecated { export const SHOW_DEPRECATED_FEATURE_PROMPT_FORMAT_ON_SAVE = l10n.t({ message: "The setting 'python.formatting.formatOnSave' is deprecated, please use 'editor.formatOnSave'.", From 06d2d7c9bea33bf195616f7e523072b155bbba71 Mon Sep 17 00:00:00 2001 From: jankuca Date: Fri, 24 Oct 2025 15:00:49 +0200 Subject: [PATCH 017/101] fix: normalize 'None' to null in select input parsing --- .../deepnote/converters/inputConverters.ts | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/src/notebooks/deepnote/converters/inputConverters.ts b/src/notebooks/deepnote/converters/inputConverters.ts index 7fa7b70031..8b331cd589 100644 --- a/src/notebooks/deepnote/converters/inputConverters.ts +++ b/src/notebooks/deepnote/converters/inputConverters.ts @@ -1,6 +1,7 @@ import { NotebookCellData, NotebookCellKind } from 'vscode'; import { z } from 'zod'; +import { logger } from '../../../platform/logging'; import type { BlockConverter } from './blockConverter'; import type { DeepnoteBlock } from '../../../platform/deepnote/deepnoteTypes'; import { @@ -51,8 +52,7 @@ export abstract class BaseInputBlockConverter implements const deepnoteMetadataResult = this.schema().safeParse(block.metadata); if (deepnoteMetadataResult.error != null) { - console.error('Error parsing deepnote input metadata:', deepnoteMetadataResult.error); - console.debug('Metadata:', JSON.stringify(block.metadata)); + logger.error('Error parsing deepnote input metadata', deepnoteMetadataResult.error); } // Extract the variable name from metadata @@ -175,10 +175,10 @@ export class InputSelectBlockConverter extends BaseInputBlockConverter v) .map((v) => v.replace(/^["']|["']$/g, '')); } else { - // Single select: remove quotes - value = cellValue.replace(/^["']|["']$/g, ''); + // Single select: 'None' => null, else strip quotes + const stripped = cellValue.replace(/^["']|["']$/g, ''); + if (stripped === 'None') { + // Represent empty selection + value = null; + } else { + value = stripped; + } } const existingMetadata = this.schema().safeParse(block.metadata); From 131a37732e38d3350d2021bb9d450c451f74bc47 Mon Sep 17 00:00:00 2001 From: jankuca Date: Fri, 24 Oct 2025 15:01:15 +0200 Subject: [PATCH 018/101] fix: parse slider value as number instead of string --- src/notebooks/deepnote/converters/inputConverters.ts | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/src/notebooks/deepnote/converters/inputConverters.ts b/src/notebooks/deepnote/converters/inputConverters.ts index 8b331cd589..cda9f675d5 100644 --- a/src/notebooks/deepnote/converters/inputConverters.ts +++ b/src/notebooks/deepnote/converters/inputConverters.ts @@ -233,12 +233,19 @@ export class InputSliderBlockConverter extends BaseInputBlockConverter Date: Fri, 24 Oct 2025 15:01:44 +0200 Subject: [PATCH 019/101] fix: use null instead of empty string for date-range parse failures --- src/notebooks/deepnote/converters/inputConverters.ts | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/notebooks/deepnote/converters/inputConverters.ts b/src/notebooks/deepnote/converters/inputConverters.ts index cda9f675d5..bce3464d58 100644 --- a/src/notebooks/deepnote/converters/inputConverters.ts +++ b/src/notebooks/deepnote/converters/inputConverters.ts @@ -363,7 +363,7 @@ export class InputDateRangeBlockConverter extends BaseInputBlockConverter Date: Fri, 24 Oct 2025 15:02:44 +0200 Subject: [PATCH 020/101] fix: localize tooltip strings and wrap switch-case declarations in blocks --- ...deepnoteInputBlockCellStatusBarProvider.ts | 40 +++++++++++++------ 1 file changed, 27 insertions(+), 13 deletions(-) diff --git a/src/notebooks/deepnote/deepnoteInputBlockCellStatusBarProvider.ts b/src/notebooks/deepnote/deepnoteInputBlockCellStatusBarProvider.ts index 93883f5466..33227deacb 100644 --- a/src/notebooks/deepnote/deepnoteInputBlockCellStatusBarProvider.ts +++ b/src/notebooks/deepnote/deepnoteInputBlockCellStatusBarProvider.ts @@ -234,12 +234,12 @@ export class DeepnoteInputBlockCellStatusBarItemProvider } // Build detailed tooltip - const tooltipLines = [`Deepnote ${formattedName}`]; + const tooltipLines = [l10n.t('Deepnote {0}', formattedName)]; if (label) { - tooltipLines.push(`Label: ${label}`); + tooltipLines.push(l10n.t('Label: {0}', label)); } if (buttonTitle) { - tooltipLines.push(`Title: ${buttonTitle}`); + tooltipLines.push(l10n.t('Title: {0}', buttonTitle)); } // Add type-specific metadata to tooltip @@ -518,45 +518,59 @@ export class DeepnoteInputBlockCellStatusBarItemProvider } switch (blockType) { - case 'input-slider': + case 'input-slider': { const min = metadata.deepnote_slider_min_value; const max = metadata.deepnote_slider_max_value; const step = metadata.deepnote_slider_step; if (min !== undefined && max !== undefined) { - tooltipLines.push(`Range: ${min} - ${max}${step !== undefined ? ` (step: ${step})` : ''}`); + tooltipLines.push( + l10n.t( + 'Range: {0} - {1}{2}', + String(min), + String(max), + step !== undefined ? l10n.t(' (step: {0})', String(step)) : '' + ) + ); } break; + } - case 'input-select': + case 'input-select': { const options = metadata.deepnote_variable_options as string[] | undefined; if (options && options.length > 0) { - tooltipLines.push(`Options: ${options.slice(0, 3).join(', ')}${options.length > 3 ? '...' : ''}`); + tooltipLines.push( + l10n.t('Options: {0}', `${options.slice(0, 3).join(', ')}${options.length > 3 ? '...' : ''}`) + ); } break; + } - case 'input-file': + case 'input-file': { const extensions = metadata.deepnote_allowed_file_extensions as string | undefined; if (extensions) { - tooltipLines.push(`Allowed extensions: ${extensions}`); + tooltipLines.push(l10n.t('Allowed extensions: {0}', extensions)); } break; + } - case 'button': + case 'button': { const behavior = metadata.deepnote_button_behavior as string | undefined; const colorScheme = metadata.deepnote_button_color_scheme as string | undefined; if (behavior) { - tooltipLines.push(`Behavior: ${behavior}`); + tooltipLines.push(l10n.t('Behavior: {0}', behavior)); } if (colorScheme) { - tooltipLines.push(`Color: ${colorScheme}`); + tooltipLines.push(l10n.t('Color: {0}', colorScheme)); } break; + } } // Add default value if present const defaultValue = metadata.deepnote_variable_default_value; if (defaultValue !== undefined && defaultValue !== null) { - tooltipLines.push(`Default: ${defaultValue}`); + const dv = typeof defaultValue === 'object' ? JSON.stringify(defaultValue) : String(defaultValue); + tooltipLines.push(l10n.t('Default: {0}', dv)); } } From 5618b5024144263865382ad03a50dc32e1d40e5c Mon Sep 17 00:00:00 2001 From: jankuca Date: Fri, 24 Oct 2025 15:04:18 +0200 Subject: [PATCH 021/101] fix: replace deprecated onKeyPress with onKeyDown --- .../selectInputSettings/SelectInputSettingsPanel.tsx | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/webviews/webview-side/selectInputSettings/SelectInputSettingsPanel.tsx b/src/webviews/webview-side/selectInputSettings/SelectInputSettingsPanel.tsx index d9549c249d..61f7f030c9 100644 --- a/src/webviews/webview-side/selectInputSettings/SelectInputSettingsPanel.tsx +++ b/src/webviews/webview-side/selectInputSettings/SelectInputSettingsPanel.tsx @@ -172,7 +172,7 @@ export const SelectInputSettingsPanel: React.FC type="text" value={newOption} onChange={(e) => setNewOption(e.target.value)} - onKeyPress={(e) => { + onKeyDown={(e) => { if (e.key === 'Enter') { e.preventDefault(); handleAddOption(); @@ -240,4 +240,3 @@ export const SelectInputSettingsPanel: React.FC ); }; - From e1a61cfb7480f2bbf58e03c5c31f6496e82530f4 Mon Sep 17 00:00:00 2001 From: jankuca Date: Fri, 24 Oct 2025 15:04:43 +0200 Subject: [PATCH 022/101] fix: add 'cancel' to WebviewMessage type union --- src/webviews/webview-side/selectInputSettings/types.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/webviews/webview-side/selectInputSettings/types.ts b/src/webviews/webview-side/selectInputSettings/types.ts index e7dc1eb467..6fd9b3961d 100644 --- a/src/webviews/webview-side/selectInputSettings/types.ts +++ b/src/webviews/webview-side/selectInputSettings/types.ts @@ -10,8 +10,7 @@ export interface SelectInputSettings { } export interface WebviewMessage { - type: 'init' | 'save' | 'locInit'; + type: 'init' | 'save' | 'locInit' | 'cancel'; settings?: SelectInputSettings; locStrings?: Record; } - From 8659ead3a151d69865a02b2b6ce05714fa7c800b Mon Sep 17 00:00:00 2001 From: jankuca Date: Fri, 24 Oct 2025 15:05:10 +0200 Subject: [PATCH 023/101] fix: remove duplicate SelectInputSettings interface --- src/notebooks/deepnote/selectInputSettingsWebview.ts | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/src/notebooks/deepnote/selectInputSettingsWebview.ts b/src/notebooks/deepnote/selectInputSettingsWebview.ts index 20f0ed15a4..94c2ca5f7e 100644 --- a/src/notebooks/deepnote/selectInputSettingsWebview.ts +++ b/src/notebooks/deepnote/selectInputSettingsWebview.ts @@ -17,14 +17,7 @@ import { inject, injectable } from 'inversify'; import { IExtensionContext } from '../../platform/common/types'; import { LocalizedMessages } from '../../messageTypes'; import * as localize from '../../platform/common/utils/localize'; - -interface SelectInputSettings { - allowMultipleValues: boolean; - allowEmptyValue: boolean; - selectType: 'from-options' | 'from-variable'; - options: string[]; - selectedVariable: string; -} +import { SelectInputSettings } from '../../webviews/webview-side/selectInputSettings/types'; /** * Manages the webview panel for select input settings From 267aca686bf22309fe2fbb39f546a8c723b51d7e Mon Sep 17 00:00:00 2001 From: jankuca Date: Fri, 24 Oct 2025 15:12:20 +0200 Subject: [PATCH 024/101] fix: add accessibility improvements to SelectInputSettingsPanel --- .../SelectInputSettingsPanel.tsx | 22 +++++++++++++++++-- 1 file changed, 20 insertions(+), 2 deletions(-) diff --git a/src/webviews/webview-side/selectInputSettings/SelectInputSettingsPanel.tsx b/src/webviews/webview-side/selectInputSettings/SelectInputSettingsPanel.tsx index 61f7f030c9..cbe3ca0f84 100644 --- a/src/webviews/webview-side/selectInputSettings/SelectInputSettingsPanel.tsx +++ b/src/webviews/webview-side/selectInputSettings/SelectInputSettingsPanel.tsx @@ -136,6 +136,14 @@ export const SelectInputSettingsPanel: React.FC
handleSelectTypeChange('from-options')} + onKeyDown={(e) => { + if (e.key === 'Enter' || e.key === ' ') { + e.preventDefault(); + handleSelectTypeChange('from-options'); + } + }} + role="button" + tabIndex={0} > {option} -
From d0efc2d03e6fff05277e3eb41b6ac044c388ca0a Mon Sep 17 00:00:00 2001 From: jankuca Date: Fri, 24 Oct 2025 15:13:24 +0200 Subject: [PATCH 025/101] fix: improve webview content loading and localization --- .../deepnote/selectInputSettingsWebview.ts | 20 ++++++------------- 1 file changed, 6 insertions(+), 14 deletions(-) diff --git a/src/notebooks/deepnote/selectInputSettingsWebview.ts b/src/notebooks/deepnote/selectInputSettingsWebview.ts index 94c2ca5f7e..b0a20d87cc 100644 --- a/src/notebooks/deepnote/selectInputSettingsWebview.ts +++ b/src/notebooks/deepnote/selectInputSettingsWebview.ts @@ -3,6 +3,7 @@ import { Disposable, + l10n, NotebookCell, NotebookEdit, NotebookRange, @@ -47,7 +48,7 @@ export class SelectInputSettingsWebviewProvider { // Create a new panel this.currentPanel = window.createWebviewPanel( 'deepnoteSelectInputSettings', - 'Select Input Settings', + l10n.t('Select Input Settings'), column || ViewColumn.One, { enableScripts: true, @@ -218,16 +219,6 @@ export class SelectInputSettingsWebviewProvider { 'index.js' ) ); - const styleUri = webview.asWebviewUri( - Uri.joinPath( - this.extensionContext.extensionUri, - 'dist', - 'webviews', - 'webview-side', - 'selectInputSettings', - 'selectInputSettings.css' - ) - ); const codiconUri = webview.asWebviewUri( Uri.joinPath( this.extensionContext.extensionUri, @@ -240,6 +231,8 @@ export class SelectInputSettingsWebviewProvider { ) ); + const title = l10n.t('Select Input Settings'); + return ` @@ -247,12 +240,11 @@ export class SelectInputSettingsWebviewProvider { - - Select Input Settings + ${title}
- + `; } From 8d4966ae0635e538ee7d22e3d7b639ebd1bd2c01 Mon Sep 17 00:00:00 2001 From: jankuca Date: Fri, 24 Oct 2025 15:13:55 +0200 Subject: [PATCH 026/101] fix: use updateCellMetadata to preserve cell outputs and attachments --- .../deepnote/selectInputSettingsWebview.ts | 14 ++------------ 1 file changed, 2 insertions(+), 12 deletions(-) diff --git a/src/notebooks/deepnote/selectInputSettingsWebview.ts b/src/notebooks/deepnote/selectInputSettingsWebview.ts index b0a20d87cc..7f9174a2c4 100644 --- a/src/notebooks/deepnote/selectInputSettingsWebview.ts +++ b/src/notebooks/deepnote/selectInputSettingsWebview.ts @@ -6,7 +6,6 @@ import { l10n, NotebookCell, NotebookEdit, - NotebookRange, Uri, ViewColumn, WebviewPanel, @@ -185,17 +184,8 @@ export class SelectInputSettingsWebviewProvider { metadata.deepnote_variable_options = settings.options; } - // Replace the cell with updated metadata - const cellData = { - kind: this.currentCell.kind, - languageId: this.currentCell.document.languageId, - value: this.currentCell.document.getText(), - metadata - }; - - edit.set(this.currentCell.notebook.uri, [ - NotebookEdit.replaceCells(new NotebookRange(this.currentCell.index, this.currentCell.index + 1), [cellData]) - ]); + // Update cell metadata to preserve outputs and attachments + edit.set(this.currentCell.notebook.uri, [NotebookEdit.updateCellMetadata(this.currentCell.index, metadata)]); await workspace.applyEdit(edit); } From 43fe8afa669d248d96f2fec41a5d73f17381388e Mon Sep 17 00:00:00 2001 From: jankuca Date: Fri, 24 Oct 2025 15:30:56 +0200 Subject: [PATCH 027/101] 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. --- .../deepnote/converters/inputConverters.ts | 18 +- .../converters/inputConverters.unit.test.ts | 298 ++++++++---------- 2 files changed, 153 insertions(+), 163 deletions(-) diff --git a/src/notebooks/deepnote/converters/inputConverters.ts b/src/notebooks/deepnote/converters/inputConverters.ts index bce3464d58..36fb9b4227 100644 --- a/src/notebooks/deepnote/converters/inputConverters.ts +++ b/src/notebooks/deepnote/converters/inputConverters.ts @@ -240,11 +240,19 @@ export class InputSliderBlockConverter extends BaseInputBlockConverter { }); suite('convertToCell', () => { - test('converts input-textarea block to Python cell with variable name', () => { + test('converts input-textarea block to plaintext cell with value', () => { const block: DeepnoteBlock = { blockGroup: '2b5f9340349f4baaa5a3237331214352', content: '', @@ -201,8 +201,8 @@ suite('InputTextareaBlockConverter', () => { const cell = converter.convertToCell(block); assert.strictEqual(cell.kind, NotebookCellKind.Code); - assert.strictEqual(cell.languageId, 'python'); - assert.strictEqual(cell.value, '# input_2'); + assert.strictEqual(cell.languageId, 'plaintext'); + assert.strictEqual(cell.value, 'some multiline\ntext input'); }); test('handles missing metadata with default config', () => { @@ -216,33 +216,33 @@ suite('InputTextareaBlockConverter', () => { const cell = converter.convertToCell(block); - assert.strictEqual(cell.value, '# '); + assert.strictEqual(cell.value, ''); }); }); suite('applyChangesToBlock', () => { - test('applies variable name from cell value', () => { + test('applies text value from cell to block metadata', () => { const block: DeepnoteBlock = { blockGroup: 'test-group', content: '', id: 'block-123', metadata: { - deepnote_variable_value: 'line1\nline2\nline3' + deepnote_variable_name: 'input_2' }, sortingKey: 'a0', type: 'input-textarea' }; - const cell = new NotebookCellData(NotebookCellKind.Code, 'textarea_var', 'python'); + const cell = new NotebookCellData(NotebookCellKind.Code, 'new multiline\ntext value', 'plaintext'); converter.applyChangesToBlock(block, cell); assert.strictEqual(block.content, ''); - assert.strictEqual(block.metadata?.deepnote_variable_name, 'textarea_var'); - // Other metadata should be preserved - assert.strictEqual(block.metadata?.deepnote_variable_value, 'line1\nline2\nline3'); + assert.strictEqual(block.metadata?.deepnote_variable_value, 'new multiline\ntext value'); + // Variable name should be preserved + assert.strictEqual(block.metadata?.deepnote_variable_name, 'input_2'); }); - test('handles empty variable name', () => { + test('handles empty value', () => { const block: DeepnoteBlock = { blockGroup: 'test-group', content: '', @@ -250,11 +250,11 @@ suite('InputTextareaBlockConverter', () => { sortingKey: 'a0', type: 'input-textarea' }; - const cell = new NotebookCellData(NotebookCellKind.Code, '', 'python'); + const cell = new NotebookCellData(NotebookCellKind.Code, '', 'plaintext'); converter.applyChangesToBlock(block, cell); - assert.strictEqual(block.metadata?.deepnote_variable_name, ''); + assert.strictEqual(block.metadata?.deepnote_variable_value, ''); }); }); }); @@ -267,7 +267,7 @@ suite('InputSelectBlockConverter', () => { }); suite('convertToCell', () => { - test('converts input-select block to Python cell with variable name', () => { + test('converts input-select block to Python cell with quoted value', () => { const block: DeepnoteBlock = { blockGroup: 'ba248341bdd94b93a234777968bfedcf', content: '', @@ -289,7 +289,7 @@ suite('InputSelectBlockConverter', () => { assert.strictEqual(cell.kind, NotebookCellKind.Code); assert.strictEqual(cell.languageId, 'python'); - assert.strictEqual(cell.value, '# input_3'); + assert.strictEqual(cell.value, '"Option 1"'); }); test('handles missing metadata with default config', () => { @@ -303,31 +303,31 @@ suite('InputSelectBlockConverter', () => { const cell = converter.convertToCell(block); - assert.strictEqual(cell.value, '# '); + assert.strictEqual(cell.value, 'None'); }); }); suite('applyChangesToBlock', () => { - test('applies variable name from cell value', () => { + test('applies selected value from cell', () => { const block: DeepnoteBlock = { blockGroup: 'test-group', content: '', id: 'block-123', metadata: { - deepnote_variable_value: 'Option A', + deepnote_variable_name: 'input_3', deepnote_variable_options: ['Option A', 'Option B'] }, sortingKey: 'a0', type: 'input-select' }; - const cell = new NotebookCellData(NotebookCellKind.Code, 'select_var', 'python'); + const cell = new NotebookCellData(NotebookCellKind.Code, '"Option B"', 'python'); converter.applyChangesToBlock(block, cell); assert.strictEqual(block.content, ''); - assert.strictEqual(block.metadata?.deepnote_variable_name, 'select_var'); - // Other metadata should be preserved - assert.strictEqual(block.metadata?.deepnote_variable_value, 'Option A'); + assert.strictEqual(block.metadata?.deepnote_variable_value, 'Option B'); + // Variable name should be preserved + assert.strictEqual(block.metadata?.deepnote_variable_name, 'input_3'); assert.deepStrictEqual(block.metadata?.deepnote_variable_options, ['Option A', 'Option B']); }); }); @@ -350,10 +350,10 @@ suite('InputSliderBlockConverter', () => { deepnote_input_label: 'slider input value', deepnote_slider_step: 1, deepnote_variable_name: 'input_6', - deepnote_variable_value: '5', + deepnote_variable_value: 5, deepnote_slider_max_value: 10, deepnote_slider_min_value: 0, - deepnote_variable_default_value: '5' + deepnote_variable_default_value: 5 }, sortingKey: 'yj', type: 'input-slider' @@ -362,14 +362,8 @@ suite('InputSliderBlockConverter', () => { const cell = converter.convertToCell(block); assert.strictEqual(cell.kind, NotebookCellKind.Code); - assert.strictEqual(cell.languageId, 'json'); - - const parsed = JSON.parse(cell.value); - assert.strictEqual(parsed.deepnote_variable_name, 'input_6'); - assert.strictEqual(parsed.deepnote_variable_value, '5'); - assert.strictEqual(parsed.deepnote_slider_min_value, 0); - assert.strictEqual(parsed.deepnote_slider_max_value, 10); - assert.strictEqual(parsed.deepnote_slider_step, 1); + assert.strictEqual(cell.languageId, 'python'); + assert.strictEqual(cell.value, '5'); }); test('converts input-slider block with custom step size', () => { @@ -381,7 +375,7 @@ suite('InputSliderBlockConverter', () => { deepnote_input_label: 'step size 2', deepnote_slider_step: 2, deepnote_variable_name: 'input_7', - deepnote_variable_value: '6', + deepnote_variable_value: 6, deepnote_slider_max_value: 10, deepnote_slider_min_value: 4 }, @@ -391,10 +385,7 @@ suite('InputSliderBlockConverter', () => { const cell = converter.convertToCell(block); - const parsed = JSON.parse(cell.value); - assert.strictEqual(parsed.deepnote_slider_step, 2); - assert.strictEqual(parsed.deepnote_slider_min_value, 4); - assert.strictEqual(parsed.deepnote_variable_value, '6'); + assert.strictEqual(cell.value, '6'); }); test('handles missing metadata with default config', () => { @@ -408,35 +399,33 @@ suite('InputSliderBlockConverter', () => { const cell = converter.convertToCell(block); - const parsed = JSON.parse(cell.value); - assert.strictEqual(parsed.deepnote_variable_name, ''); - assert.strictEqual(parsed.deepnote_slider_min_value, 0); - assert.strictEqual(parsed.deepnote_slider_max_value, 10); + assert.strictEqual(cell.value, ''); }); }); suite('applyChangesToBlock', () => { - test('applies valid JSON with slider configuration', () => { + test('applies numeric value from cell', () => { const block: DeepnoteBlock = { blockGroup: 'test-group', content: '', id: 'block-123', + metadata: { + deepnote_variable_name: 'slider1', + deepnote_slider_min_value: 0, + deepnote_slider_max_value: 100, + deepnote_slider_step: 5 + }, sortingKey: 'a0', type: 'input-slider' }; - const cellValue = JSON.stringify({ - deepnote_variable_name: 'slider1', - deepnote_variable_value: '7', - deepnote_slider_min_value: 0, - deepnote_slider_max_value: 100, - deepnote_slider_step: 5 - }); - const cell = new NotebookCellData(NotebookCellKind.Code, cellValue, 'json'); + const cell = new NotebookCellData(NotebookCellKind.Code, '7', 'python'); converter.applyChangesToBlock(block, cell); assert.strictEqual(block.content, ''); - assert.strictEqual(block.metadata?.deepnote_variable_value, '7'); + assert.strictEqual(block.metadata?.deepnote_variable_value, 7); + // Other metadata should be preserved + assert.strictEqual(block.metadata?.deepnote_variable_name, 'slider1'); assert.strictEqual(block.metadata?.deepnote_slider_min_value, 0); assert.strictEqual(block.metadata?.deepnote_slider_max_value, 100); assert.strictEqual(block.metadata?.deepnote_slider_step, 5); @@ -447,34 +436,37 @@ suite('InputSliderBlockConverter', () => { blockGroup: 'test-group', content: '', id: 'block-123', + metadata: { + deepnote_variable_name: 'slider1' + }, sortingKey: 'a0', type: 'input-slider' }; - const cellValue = JSON.stringify({ - deepnote_variable_value: 42 - }); - const cell = new NotebookCellData(NotebookCellKind.Code, cellValue, 'json'); + const cell = new NotebookCellData(NotebookCellKind.Code, '42', 'python'); converter.applyChangesToBlock(block, cell); - // Numeric values fail string schema validation, so stored in raw content - assert.strictEqual(block.metadata?.[DEEPNOTE_VSCODE_RAW_CONTENT_KEY], cellValue); + assert.strictEqual(block.metadata?.deepnote_variable_value, 42); }); - test('handles invalid JSON', () => { + test('handles invalid numeric value', () => { const block: DeepnoteBlock = { blockGroup: 'test-group', content: '', id: 'block-123', + metadata: { + deepnote_variable_name: 'slider1', + deepnote_variable_value: '5' + }, sortingKey: 'a0', type: 'input-slider' }; - const invalidJson = 'invalid'; - const cell = new NotebookCellData(NotebookCellKind.Code, invalidJson, 'json'); + const cell = new NotebookCellData(NotebookCellKind.Code, 'invalid', 'python'); converter.applyChangesToBlock(block, cell); - assert.strictEqual(block.metadata?.[DEEPNOTE_VSCODE_RAW_CONTENT_KEY], invalidJson); + // Should fall back to existing value (parsed as number) + assert.strictEqual(block.metadata?.deepnote_variable_value, 5); }); }); }); @@ -487,7 +479,7 @@ suite('InputCheckboxBlockConverter', () => { }); suite('convertToCell', () => { - test('converts input-checkbox block to JSON cell', () => { + test('converts input-checkbox block to Python cell with boolean value', () => { const block: DeepnoteBlock = { blockGroup: '5dd57f6bb90b49ebb954f6247b26427d', content: '', @@ -504,11 +496,8 @@ suite('InputCheckboxBlockConverter', () => { const cell = converter.convertToCell(block); assert.strictEqual(cell.kind, NotebookCellKind.Code); - assert.strictEqual(cell.languageId, 'json'); - - const parsed = JSON.parse(cell.value); - assert.strictEqual(parsed.deepnote_variable_name, 'input_8'); - assert.strictEqual(parsed.deepnote_variable_value, false); + assert.strictEqual(cell.languageId, 'python'); + assert.strictEqual(cell.value, 'False'); }); test('handles checkbox with true value', () => { @@ -526,8 +515,7 @@ suite('InputCheckboxBlockConverter', () => { const cell = converter.convertToCell(block); - const parsed = JSON.parse(cell.value); - assert.strictEqual(parsed.deepnote_variable_value, true); + assert.strictEqual(cell.value, 'True'); }); test('handles missing metadata with default config', () => { @@ -541,46 +529,45 @@ suite('InputCheckboxBlockConverter', () => { const cell = converter.convertToCell(block); - const parsed = JSON.parse(cell.value); - assert.strictEqual(parsed.deepnote_variable_name, ''); - assert.strictEqual(parsed.deepnote_variable_value, false); + assert.strictEqual(cell.value, 'False'); }); }); suite('applyChangesToBlock', () => { - test('applies valid JSON with boolean value', () => { + test('applies True value from cell', () => { const block: DeepnoteBlock = { blockGroup: 'test-group', content: '', id: 'block-123', + metadata: { + deepnote_variable_name: 'checkbox1' + }, sortingKey: 'a0', type: 'input-checkbox' }; - const cellValue = JSON.stringify({ - deepnote_variable_name: 'checkbox1', - deepnote_variable_value: true - }); - const cell = new NotebookCellData(NotebookCellKind.Code, cellValue, 'json'); + const cell = new NotebookCellData(NotebookCellKind.Code, 'True', 'python'); converter.applyChangesToBlock(block, cell); assert.strictEqual(block.content, ''); assert.strictEqual(block.metadata?.deepnote_variable_value, true); + // Variable name should be preserved + assert.strictEqual(block.metadata?.deepnote_variable_name, 'checkbox1'); }); - test('applies false value', () => { + test('applies False value', () => { const block: DeepnoteBlock = { blockGroup: 'test-group', content: '', id: 'block-123', + metadata: { + deepnote_variable_name: 'checkbox1', + deepnote_variable_default_value: true + }, sortingKey: 'a0', type: 'input-checkbox' }; - const cellValue = JSON.stringify({ - deepnote_variable_value: false, - deepnote_variable_default_value: true - }); - const cell = new NotebookCellData(NotebookCellKind.Code, cellValue, 'json'); + const cell = new NotebookCellData(NotebookCellKind.Code, 'False', 'python'); converter.applyChangesToBlock(block, cell); @@ -588,7 +575,7 @@ suite('InputCheckboxBlockConverter', () => { assert.strictEqual(block.metadata?.deepnote_variable_default_value, true); }); - test('handles invalid JSON', () => { + test('handles lowercase true', () => { const block: DeepnoteBlock = { blockGroup: 'test-group', content: '', @@ -596,12 +583,11 @@ suite('InputCheckboxBlockConverter', () => { sortingKey: 'a0', type: 'input-checkbox' }; - const invalidJson = '{]'; - const cell = new NotebookCellData(NotebookCellKind.Code, invalidJson, 'json'); + const cell = new NotebookCellData(NotebookCellKind.Code, 'true', 'python'); converter.applyChangesToBlock(block, cell); - assert.strictEqual(block.metadata?.[DEEPNOTE_VSCODE_RAW_CONTENT_KEY], invalidJson); + assert.strictEqual(block.metadata?.deepnote_variable_value, true); }); }); }); @@ -614,7 +600,7 @@ suite('InputDateBlockConverter', () => { }); suite('convertToCell', () => { - test('converts input-date block to JSON cell', () => { + test('converts input-date block to Python cell with quoted date', () => { const block: DeepnoteBlock = { blockGroup: 'e84010446b844a86a1f6bbe5d89dc798', content: '', @@ -632,12 +618,8 @@ suite('InputDateBlockConverter', () => { const cell = converter.convertToCell(block); assert.strictEqual(cell.kind, NotebookCellKind.Code); - assert.strictEqual(cell.languageId, 'json'); - - const parsed = JSON.parse(cell.value); - assert.strictEqual(parsed.deepnote_variable_name, 'input_9'); - assert.strictEqual(parsed.deepnote_variable_value, '2025-10-13T00:00:00.000Z'); - assert.strictEqual(parsed.deepnote_input_date_version, 2); + assert.strictEqual(cell.languageId, 'python'); + assert.strictEqual(cell.value, '"2025-10-13T00:00:00.000Z"'); }); test('handles missing metadata with default config', () => { @@ -651,50 +633,50 @@ suite('InputDateBlockConverter', () => { const cell = converter.convertToCell(block); - const parsed = JSON.parse(cell.value); - assert.strictEqual(parsed.deepnote_variable_name, ''); - // Default value should be an ISO date string - assert.match(parsed.deepnote_variable_value, /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/); + assert.strictEqual(cell.value, '""'); }); }); suite('applyChangesToBlock', () => { - test('applies valid JSON with date value', () => { + test('applies date value from cell', () => { const block: DeepnoteBlock = { blockGroup: 'test-group', content: '', id: 'block-123', + metadata: { + deepnote_variable_name: 'date1', + deepnote_input_date_version: 2 + }, sortingKey: 'a0', type: 'input-date' }; - const cellValue = JSON.stringify({ - deepnote_variable_name: 'date1', - deepnote_variable_value: '2025-12-31T00:00:00.000Z', - deepnote_input_date_version: 2 - }); - const cell = new NotebookCellData(NotebookCellKind.Code, cellValue, 'json'); + const cell = new NotebookCellData(NotebookCellKind.Code, '"2025-12-31T00:00:00.000Z"', 'python'); converter.applyChangesToBlock(block, cell); assert.strictEqual(block.content, ''); assert.strictEqual(block.metadata?.deepnote_variable_value, '2025-12-31T00:00:00.000Z'); + // Other metadata should be preserved + assert.strictEqual(block.metadata?.deepnote_variable_name, 'date1'); assert.strictEqual(block.metadata?.deepnote_input_date_version, 2); }); - test('handles invalid JSON', () => { + test('handles unquoted date value', () => { const block: DeepnoteBlock = { blockGroup: 'test-group', content: '', id: 'block-123', + metadata: { + deepnote_variable_name: 'date1' + }, sortingKey: 'a0', type: 'input-date' }; - const invalidJson = 'not valid'; - const cell = new NotebookCellData(NotebookCellKind.Code, invalidJson, 'json'); + const cell = new NotebookCellData(NotebookCellKind.Code, '2025-12-31', 'python'); converter.applyChangesToBlock(block, cell); - assert.strictEqual(block.metadata?.[DEEPNOTE_VSCODE_RAW_CONTENT_KEY], invalidJson); + assert.strictEqual(block.metadata?.deepnote_variable_value, '2025-12-31'); }); }); }); @@ -723,11 +705,8 @@ suite('InputDateRangeBlockConverter', () => { const cell = converter.convertToCell(block); assert.strictEqual(cell.kind, NotebookCellKind.Code); - assert.strictEqual(cell.languageId, 'json'); - - const parsed = JSON.parse(cell.value); - assert.strictEqual(parsed.deepnote_variable_name, 'input_10'); - assert.deepStrictEqual(parsed.deepnote_variable_value, ['2025-10-06', '2025-10-16']); + assert.strictEqual(cell.languageId, 'python'); + assert.strictEqual(cell.value, '("2025-10-06", "2025-10-16")'); }); test('converts input-date-range block with relative date', () => { @@ -746,9 +725,8 @@ suite('InputDateRangeBlockConverter', () => { const cell = converter.convertToCell(block); - const parsed = JSON.parse(cell.value); - assert.strictEqual(parsed.deepnote_input_label, 'relative past 3 months'); - assert.strictEqual(parsed.deepnote_variable_value, 'past3months'); + // Relative dates are not formatted as tuples + assert.strictEqual(cell.value, ''); }); test('handles missing metadata with default config', () => { @@ -762,65 +740,70 @@ suite('InputDateRangeBlockConverter', () => { const cell = converter.convertToCell(block); - const parsed = JSON.parse(cell.value); - assert.strictEqual(parsed.deepnote_variable_name, ''); - assert.strictEqual(parsed.deepnote_variable_value, ''); + assert.strictEqual(cell.value, ''); }); }); suite('applyChangesToBlock', () => { - test('applies valid JSON with date range array', () => { + test('applies date range tuple from cell', () => { const block: DeepnoteBlock = { blockGroup: 'test-group', content: '', id: 'block-123', + metadata: { + deepnote_variable_name: 'range1' + }, sortingKey: 'a0', type: 'input-date-range' }; - const cellValue = JSON.stringify({ - deepnote_variable_name: 'range1', - deepnote_variable_value: ['2025-01-01', '2025-12-31'] - }); - const cell = new NotebookCellData(NotebookCellKind.Code, cellValue, 'json'); + const cell = new NotebookCellData(NotebookCellKind.Code, '("2025-01-01", "2025-12-31")', 'python'); converter.applyChangesToBlock(block, cell); assert.strictEqual(block.content, ''); assert.deepStrictEqual(block.metadata?.deepnote_variable_value, ['2025-01-01', '2025-12-31']); + // Variable name should be preserved + assert.strictEqual(block.metadata?.deepnote_variable_name, 'range1'); }); - test('applies valid JSON with relative date string', () => { + test('handles invalid tuple format', () => { const block: DeepnoteBlock = { blockGroup: 'test-group', content: '', id: 'block-123', + metadata: { + deepnote_variable_name: 'range1', + deepnote_variable_value: ['2025-01-01', '2025-12-31'] + }, sortingKey: 'a0', type: 'input-date-range' }; - const cellValue = JSON.stringify({ - deepnote_variable_value: 'past7days' - }); - const cell = new NotebookCellData(NotebookCellKind.Code, cellValue, 'json'); + const cell = new NotebookCellData(NotebookCellKind.Code, 'invalid', 'python'); converter.applyChangesToBlock(block, cell); - assert.strictEqual(block.metadata?.deepnote_variable_value, 'past7days'); + // Should preserve existing value when parse fails + assert.deepStrictEqual(block.metadata?.deepnote_variable_value, ['2025-01-01', '2025-12-31']); }); - test('handles invalid JSON', () => { + test('handles empty value', () => { const block: DeepnoteBlock = { blockGroup: 'test-group', content: '', id: 'block-123', + metadata: { + deepnote_variable_name: 'range1', + deepnote_variable_value: ['2025-01-01', '2025-12-31'] + }, sortingKey: 'a0', type: 'input-date-range' }; - const invalidJson = '{{bad}}'; - const cell = new NotebookCellData(NotebookCellKind.Code, invalidJson, 'json'); + const cell = new NotebookCellData(NotebookCellKind.Code, '', 'python'); converter.applyChangesToBlock(block, cell); - assert.strictEqual(block.metadata?.[DEEPNOTE_VSCODE_RAW_CONTENT_KEY], invalidJson); + // Should preserve existing value when cell is empty + assert.deepStrictEqual(block.metadata?.deepnote_variable_value, ['2025-01-01', '2025-12-31']); }); }); }); @@ -833,7 +816,7 @@ suite('InputFileBlockConverter', () => { }); suite('convertToCell', () => { - test('converts input-file block to JSON cell', () => { + test('converts input-file block to Python cell with quoted file path', () => { const block: DeepnoteBlock = { blockGroup: '651f4f5db96b43d5a6a1a492935fa08d', content: '', @@ -841,7 +824,7 @@ suite('InputFileBlockConverter', () => { metadata: { deepnote_input_label: 'csv file input', deepnote_variable_name: 'input_12', - deepnote_variable_value: '', + deepnote_variable_value: 'data.csv', deepnote_allowed_file_extensions: '.csv' }, sortingKey: 'yyj', @@ -851,12 +834,8 @@ suite('InputFileBlockConverter', () => { const cell = converter.convertToCell(block); assert.strictEqual(cell.kind, NotebookCellKind.Code); - assert.strictEqual(cell.languageId, 'json'); - - const parsed = JSON.parse(cell.value); - assert.strictEqual(parsed.deepnote_input_label, 'csv file input'); - assert.strictEqual(parsed.deepnote_variable_name, 'input_12'); - assert.strictEqual(parsed.deepnote_allowed_file_extensions, '.csv'); + assert.strictEqual(cell.languageId, 'python'); + assert.strictEqual(cell.value, '"data.csv"'); }); test('handles missing metadata with default config', () => { @@ -870,47 +849,50 @@ suite('InputFileBlockConverter', () => { const cell = converter.convertToCell(block); - const parsed = JSON.parse(cell.value); - assert.strictEqual(parsed.deepnote_variable_name, ''); - assert.isNull(parsed.deepnote_allowed_file_extensions); + assert.strictEqual(cell.value, ''); }); }); suite('applyChangesToBlock', () => { - test('applies valid JSON with file extension', () => { + test('applies file path from cell', () => { const block: DeepnoteBlock = { blockGroup: 'test-group', content: '', id: 'block-123', + metadata: { + deepnote_variable_name: 'file1', + deepnote_allowed_file_extensions: '.pdf,.docx' + }, sortingKey: 'a0', type: 'input-file' }; - const cellValue = JSON.stringify({ - deepnote_variable_name: 'file1', - deepnote_allowed_file_extensions: '.pdf,.docx' - }); - const cell = new NotebookCellData(NotebookCellKind.Code, cellValue, 'json'); + const cell = new NotebookCellData(NotebookCellKind.Code, '"document.pdf"', 'python'); converter.applyChangesToBlock(block, cell); assert.strictEqual(block.content, ''); + assert.strictEqual(block.metadata?.deepnote_variable_value, 'document.pdf'); + // Other metadata should be preserved + assert.strictEqual(block.metadata?.deepnote_variable_name, 'file1'); assert.strictEqual(block.metadata?.deepnote_allowed_file_extensions, '.pdf,.docx'); }); - test('handles invalid JSON', () => { + test('handles unquoted file path', () => { const block: DeepnoteBlock = { blockGroup: 'test-group', content: '', id: 'block-123', + metadata: { + deepnote_variable_name: 'file1' + }, sortingKey: 'a0', type: 'input-file' }; - const invalidJson = 'bad json'; - const cell = new NotebookCellData(NotebookCellKind.Code, invalidJson, 'json'); + const cell = new NotebookCellData(NotebookCellKind.Code, 'file.txt', 'python'); converter.applyChangesToBlock(block, cell); - assert.strictEqual(block.metadata?.[DEEPNOTE_VSCODE_RAW_CONTENT_KEY], invalidJson); + assert.strictEqual(block.metadata?.deepnote_variable_value, 'file.txt'); }); }); }); From 6a5982b30b9ae8ed90623d1806eb180367dfedae Mon Sep 17 00:00:00 2001 From: jankuca Date: Fri, 24 Oct 2025 15:34:00 +0200 Subject: [PATCH 028/101] 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 --- .../deepnoteInputBlockCellStatusBarProvider.unit.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/notebooks/deepnote/deepnoteInputBlockCellStatusBarProvider.unit.test.ts b/src/notebooks/deepnote/deepnoteInputBlockCellStatusBarProvider.unit.test.ts index 6254517230..c4c6cad27e 100644 --- a/src/notebooks/deepnote/deepnoteInputBlockCellStatusBarProvider.unit.test.ts +++ b/src/notebooks/deepnote/deepnoteInputBlockCellStatusBarProvider.unit.test.ts @@ -121,7 +121,7 @@ suite('DeepnoteInputBlockCellStatusBarItemProvider', () => { const items = provider.provideCellStatusBarItems(cell); expect(items).to.not.be.undefined; - expect(items).to.have.lengthOf(2); + expect(items).to.have.lengthOf(3); // Type label, variable, and choose file button expect(items?.[0].text).to.equal('Input File'); }); From 09266cb303117b76805c85146604a0720f31e9be Mon Sep 17 00:00:00 2001 From: jankuca Date: Fri, 24 Oct 2025 15:49:55 +0200 Subject: [PATCH 029/101] 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 --- .../deepnote/converters/inputConverters.ts | 10 +++------- .../deepnoteInputBlockEditProtection.ts | 3 ++- .../deepnote/selectInputSettingsWebview.ts | 2 +- src/platform/notebooks/deepnote/types.ts | 20 +++++++++++++++++++ .../webview-side/selectInputSettings/types.ts | 18 +++++------------ 5 files changed, 31 insertions(+), 22 deletions(-) diff --git a/src/notebooks/deepnote/converters/inputConverters.ts b/src/notebooks/deepnote/converters/inputConverters.ts index 36fb9b4227..4bc49845a8 100644 --- a/src/notebooks/deepnote/converters/inputConverters.ts +++ b/src/notebooks/deepnote/converters/inputConverters.ts @@ -245,11 +245,11 @@ export class InputSliderBlockConverter extends BaseInputBlockConverter(); + const editsByNotebook = new Map(); for (const { cell, blockType } of cellsToFix) { const expectedLanguage = this.expectedLanguages.get(blockType); diff --git a/src/notebooks/deepnote/selectInputSettingsWebview.ts b/src/notebooks/deepnote/selectInputSettingsWebview.ts index 7f9174a2c4..8c2674d68b 100644 --- a/src/notebooks/deepnote/selectInputSettingsWebview.ts +++ b/src/notebooks/deepnote/selectInputSettingsWebview.ts @@ -17,7 +17,7 @@ import { inject, injectable } from 'inversify'; import { IExtensionContext } from '../../platform/common/types'; import { LocalizedMessages } from '../../messageTypes'; import * as localize from '../../platform/common/utils/localize'; -import { SelectInputSettings } from '../../webviews/webview-side/selectInputSettings/types'; +import { SelectInputSettings } from '../../platform/notebooks/deepnote/types'; /** * Manages the webview panel for select input settings diff --git a/src/platform/notebooks/deepnote/types.ts b/src/platform/notebooks/deepnote/types.ts index 6036737e2c..1c8f5f44d1 100644 --- a/src/platform/notebooks/deepnote/types.ts +++ b/src/platform/notebooks/deepnote/types.ts @@ -3,6 +3,26 @@ import { IDisposable, Resource } from '../../common/types'; import { EnvironmentVariables } from '../../common/variables/types'; import { IntegrationConfig } from './integrationTypes'; +/** + * Settings for select input blocks + */ +export interface SelectInputSettings { + allowMultipleValues: boolean; + allowEmptyValue: boolean; + selectType: 'from-options' | 'from-variable'; + options: string[]; + selectedVariable: string; +} + +/** + * Message types for select input settings webview + */ +export interface SelectInputWebviewMessage { + type: 'init' | 'save' | 'locInit' | 'cancel'; + settings?: SelectInputSettings; + locStrings?: Record; +} + export const IIntegrationStorage = Symbol('IIntegrationStorage'); export interface IIntegrationStorage extends IDisposable { /** diff --git a/src/webviews/webview-side/selectInputSettings/types.ts b/src/webviews/webview-side/selectInputSettings/types.ts index 6fd9b3961d..5f25b5d3a5 100644 --- a/src/webviews/webview-side/selectInputSettings/types.ts +++ b/src/webviews/webview-side/selectInputSettings/types.ts @@ -1,16 +1,8 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -export interface SelectInputSettings { - allowMultipleValues: boolean; - allowEmptyValue: boolean; - selectType: 'from-options' | 'from-variable'; - options: string[]; - selectedVariable: string; -} - -export interface WebviewMessage { - type: 'init' | 'save' | 'locInit' | 'cancel'; - settings?: SelectInputSettings; - locStrings?: Record; -} +// Re-export types from platform for use in webview +export type { + SelectInputSettings, + SelectInputWebviewMessage as WebviewMessage +} from '../../../platform/notebooks/deepnote/types'; From 760fdd0fa6d38274e8f3830962bf86755b931f86 Mon Sep 17 00:00:00 2001 From: jankuca Date: Fri, 24 Oct 2025 16:07:08 +0200 Subject: [PATCH 030/101] fix: remove invalid content parsing for variable names --- .../deepnote/deepnoteInputBlockCellStatusBarProvider.ts | 7 ------- 1 file changed, 7 deletions(-) diff --git a/src/notebooks/deepnote/deepnoteInputBlockCellStatusBarProvider.ts b/src/notebooks/deepnote/deepnoteInputBlockCellStatusBarProvider.ts index 33227deacb..3aa7c7ac75 100644 --- a/src/notebooks/deepnote/deepnoteInputBlockCellStatusBarProvider.ts +++ b/src/notebooks/deepnote/deepnoteInputBlockCellStatusBarProvider.ts @@ -605,13 +605,6 @@ export class DeepnoteInputBlockCellStatusBarItemProvider } } - // Fall back to cell content (which should contain the variable name with "# " prefix) - const cellContent = cell.document.getText().trim(); - if (cellContent) { - // Remove "# " prefix if present - return cellContent.startsWith('# ') ? cellContent.substring(2) : cellContent; - } - return ''; } From 3202b3a8ba8adc870306008eb84b89177742a71f Mon Sep 17 00:00:00 2001 From: jankuca Date: Fri, 24 Oct 2025 16:10:21 +0200 Subject: [PATCH 031/101] refactor: remove dead input value parsing logic --- .../deepnote/converters/inputConverters.ts | 69 +++------- .../converters/inputConverters.unit.test.ts | 118 ++++-------------- 2 files changed, 43 insertions(+), 144 deletions(-) diff --git a/src/notebooks/deepnote/converters/inputConverters.ts b/src/notebooks/deepnote/converters/inputConverters.ts index 4bc49845a8..9f483dd839 100644 --- a/src/notebooks/deepnote/converters/inputConverters.ts +++ b/src/notebooks/deepnote/converters/inputConverters.ts @@ -170,32 +170,11 @@ export class InputSelectBlockConverter extends BaseInputBlockConverter v.trim()) - .filter((v) => v) - .map((v) => v.replace(/^["']|["']$/g, '')); - } else { - // Single select: 'None' => null, else strip quotes - const stripped = cellValue.replace(/^["']|["']$/g, ''); - if (stripped === 'None') { - // Represent empty selection - value = null; - } else { - value = stripped; - } - } - + // Select blocks are readonly - edits are reverted by DeepnoteInputBlockEditProtection + // Just preserve existing metadata const existingMetadata = this.schema().safeParse(block.metadata); const baseMetadata = existingMetadata.success ? existingMetadata.data : this.defaultConfig(); @@ -205,8 +184,7 @@ export class InputSelectBlockConverter extends BaseInputBlockConverter { }); suite('applyChangesToBlock', () => { - test('applies selected value from cell', () => { + test('preserves existing metadata (select blocks are readonly)', () => { const block: DeepnoteBlock = { blockGroup: 'test-group', content: '', id: 'block-123', metadata: { deepnote_variable_name: 'input_3', - deepnote_variable_options: ['Option A', 'Option B'] + deepnote_variable_options: ['Option A', 'Option B'], + deepnote_variable_value: 'Option A' }, sortingKey: 'a0', type: 'input-select' }; + // Cell content is ignored since select blocks are readonly const cell = new NotebookCellData(NotebookCellKind.Code, '"Option B"', 'python'); converter.applyChangesToBlock(block, cell); assert.strictEqual(block.content, ''); - assert.strictEqual(block.metadata?.deepnote_variable_value, 'Option B'); - // Variable name should be preserved + // Value should be preserved from metadata, not parsed from cell + assert.strictEqual(block.metadata?.deepnote_variable_value, 'Option A'); assert.strictEqual(block.metadata?.deepnote_variable_name, 'input_3'); assert.deepStrictEqual(block.metadata?.deepnote_variable_options, ['Option A', 'Option B']); }); @@ -534,34 +536,37 @@ suite('InputCheckboxBlockConverter', () => { }); suite('applyChangesToBlock', () => { - test('applies True value from cell', () => { + test('preserves existing metadata (checkbox blocks are readonly)', () => { const block: DeepnoteBlock = { blockGroup: 'test-group', content: '', id: 'block-123', metadata: { - deepnote_variable_name: 'checkbox1' + deepnote_variable_name: 'checkbox1', + deepnote_variable_value: false }, sortingKey: 'a0', type: 'input-checkbox' }; + // Cell content is ignored since checkbox blocks are readonly const cell = new NotebookCellData(NotebookCellKind.Code, 'True', 'python'); converter.applyChangesToBlock(block, cell); assert.strictEqual(block.content, ''); - assert.strictEqual(block.metadata?.deepnote_variable_value, true); - // Variable name should be preserved + // Value should be preserved from metadata, not parsed from cell + assert.strictEqual(block.metadata?.deepnote_variable_value, false); assert.strictEqual(block.metadata?.deepnote_variable_name, 'checkbox1'); }); - test('applies False value', () => { + test('preserves default value', () => { const block: DeepnoteBlock = { blockGroup: 'test-group', content: '', id: 'block-123', metadata: { deepnote_variable_name: 'checkbox1', + deepnote_variable_value: true, deepnote_variable_default_value: true }, sortingKey: 'a0', @@ -571,23 +576,8 @@ suite('InputCheckboxBlockConverter', () => { converter.applyChangesToBlock(block, cell); - assert.strictEqual(block.metadata?.deepnote_variable_value, false); - assert.strictEqual(block.metadata?.deepnote_variable_default_value, true); - }); - - test('handles lowercase true', () => { - const block: DeepnoteBlock = { - blockGroup: 'test-group', - content: '', - id: 'block-123', - sortingKey: 'a0', - type: 'input-checkbox' - }; - const cell = new NotebookCellData(NotebookCellKind.Code, 'true', 'python'); - - converter.applyChangesToBlock(block, cell); - assert.strictEqual(block.metadata?.deepnote_variable_value, true); + assert.strictEqual(block.metadata?.deepnote_variable_default_value, true); }); }); }); @@ -638,46 +628,30 @@ suite('InputDateBlockConverter', () => { }); suite('applyChangesToBlock', () => { - test('applies date value from cell', () => { + test('preserves existing metadata (date blocks are readonly)', () => { const block: DeepnoteBlock = { blockGroup: 'test-group', content: '', id: 'block-123', metadata: { deepnote_variable_name: 'date1', - deepnote_input_date_version: 2 + deepnote_input_date_version: 2, + deepnote_variable_value: '2025-01-01T00:00:00.000Z' }, sortingKey: 'a0', type: 'input-date' }; + // Cell content is ignored since date blocks are readonly const cell = new NotebookCellData(NotebookCellKind.Code, '"2025-12-31T00:00:00.000Z"', 'python'); converter.applyChangesToBlock(block, cell); assert.strictEqual(block.content, ''); - assert.strictEqual(block.metadata?.deepnote_variable_value, '2025-12-31T00:00:00.000Z'); - // Other metadata should be preserved + // Value should be preserved from metadata, not parsed from cell + assert.strictEqual(block.metadata?.deepnote_variable_value, '2025-01-01T00:00:00.000Z'); assert.strictEqual(block.metadata?.deepnote_variable_name, 'date1'); assert.strictEqual(block.metadata?.deepnote_input_date_version, 2); }); - - test('handles unquoted date value', () => { - const block: DeepnoteBlock = { - blockGroup: 'test-group', - content: '', - id: 'block-123', - metadata: { - deepnote_variable_name: 'date1' - }, - sortingKey: 'a0', - type: 'input-date' - }; - const cell = new NotebookCellData(NotebookCellKind.Code, '2025-12-31', 'python'); - - converter.applyChangesToBlock(block, cell); - - assert.strictEqual(block.metadata?.deepnote_variable_value, '2025-12-31'); - }); }); }); @@ -745,66 +719,28 @@ suite('InputDateRangeBlockConverter', () => { }); suite('applyChangesToBlock', () => { - test('applies date range tuple from cell', () => { + test('preserves existing metadata (date range blocks are readonly)', () => { const block: DeepnoteBlock = { blockGroup: 'test-group', content: '', id: 'block-123', metadata: { - deepnote_variable_name: 'range1' + deepnote_variable_name: 'range1', + deepnote_variable_value: ['2025-06-01', '2025-06-30'] }, sortingKey: 'a0', type: 'input-date-range' }; + // Cell content is ignored since date range blocks are readonly const cell = new NotebookCellData(NotebookCellKind.Code, '("2025-01-01", "2025-12-31")', 'python'); converter.applyChangesToBlock(block, cell); assert.strictEqual(block.content, ''); - assert.deepStrictEqual(block.metadata?.deepnote_variable_value, ['2025-01-01', '2025-12-31']); - // Variable name should be preserved + // Value should be preserved from metadata, not parsed from cell + assert.deepStrictEqual(block.metadata?.deepnote_variable_value, ['2025-06-01', '2025-06-30']); assert.strictEqual(block.metadata?.deepnote_variable_name, 'range1'); }); - - test('handles invalid tuple format', () => { - const block: DeepnoteBlock = { - blockGroup: 'test-group', - content: '', - id: 'block-123', - metadata: { - deepnote_variable_name: 'range1', - deepnote_variable_value: ['2025-01-01', '2025-12-31'] - }, - sortingKey: 'a0', - type: 'input-date-range' - }; - const cell = new NotebookCellData(NotebookCellKind.Code, 'invalid', 'python'); - - converter.applyChangesToBlock(block, cell); - - // Should preserve existing value when parse fails - assert.deepStrictEqual(block.metadata?.deepnote_variable_value, ['2025-01-01', '2025-12-31']); - }); - - test('handles empty value', () => { - const block: DeepnoteBlock = { - blockGroup: 'test-group', - content: '', - id: 'block-123', - metadata: { - deepnote_variable_name: 'range1', - deepnote_variable_value: ['2025-01-01', '2025-12-31'] - }, - sortingKey: 'a0', - type: 'input-date-range' - }; - const cell = new NotebookCellData(NotebookCellKind.Code, '', 'python'); - - converter.applyChangesToBlock(block, cell); - - // Should preserve existing value when cell is empty - assert.deepStrictEqual(block.metadata?.deepnote_variable_value, ['2025-01-01', '2025-12-31']); - }); }); }); From 8a9ede81d6186714f433230dcad41ef8dcae9d9c Mon Sep 17 00:00:00 2001 From: jankuca Date: Fri, 24 Oct 2025 16:14:03 +0200 Subject: [PATCH 032/101] fix: handle lineCount=0 --- src/notebooks/deepnote/deepnoteInputBlockEditProtection.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/notebooks/deepnote/deepnoteInputBlockEditProtection.ts b/src/notebooks/deepnote/deepnoteInputBlockEditProtection.ts index 2d2e13fbb2..95afe0205c 100644 --- a/src/notebooks/deepnote/deepnoteInputBlockEditProtection.ts +++ b/src/notebooks/deepnote/deepnoteInputBlockEditProtection.ts @@ -119,9 +119,10 @@ export class DeepnoteInputBlockEditProtection implements Disposable { // Only revert if content actually changed if (cell.document.getText() !== correctContent) { const edit = new WorkspaceEdit(); + const lastLine = Math.max(0, cell.document.lineCount - 1); const fullRange = new Range( new Position(0, 0), - new Position(cell.document.lineCount - 1, cell.document.lineAt(cell.document.lineCount - 1).text.length) + new Position(lastLine, cell.document.lineAt(lastLine).text.length) ); edit.replace(cell.document.uri, fullRange, correctContent); await workspace.applyEdit(edit); From d1a6abc0275a996a2339e5cfc0a12b87e986f8b0 Mon Sep 17 00:00:00 2001 From: jankuca Date: Fri, 24 Oct 2025 16:23:48 +0200 Subject: [PATCH 033/101] add logs for failed reverts --- .../deepnote/deepnoteActivationService.ts | 6 +- .../deepnoteActivationService.unit.test.ts | 57 +++++++++++++++++-- .../deepnoteInputBlockEditProtection.ts | 24 +++++++- 3 files changed, 77 insertions(+), 10 deletions(-) diff --git a/src/notebooks/deepnote/deepnoteActivationService.ts b/src/notebooks/deepnote/deepnoteActivationService.ts index e860132b70..c707e309c8 100644 --- a/src/notebooks/deepnote/deepnoteActivationService.ts +++ b/src/notebooks/deepnote/deepnoteActivationService.ts @@ -2,6 +2,7 @@ import { injectable, inject } from 'inversify'; import { workspace } from 'vscode'; import { IExtensionSyncActivationService } from '../../platform/activation/types'; import { IExtensionContext } from '../../platform/common/types'; +import { ILogger } from '../../platform/logging/types'; import { IDeepnoteNotebookManager } from '../types'; import { DeepnoteNotebookSerializer } from './deepnoteSerializer'; import { DeepnoteExplorerView } from './deepnoteExplorerView'; @@ -25,7 +26,8 @@ export class DeepnoteActivationService implements IExtensionSyncActivationServic constructor( @inject(IExtensionContext) private extensionContext: IExtensionContext, @inject(IDeepnoteNotebookManager) private readonly notebookManager: IDeepnoteNotebookManager, - @inject(IIntegrationManager) integrationManager: IIntegrationManager + @inject(IIntegrationManager) integrationManager: IIntegrationManager, + @inject(ILogger) private readonly logger: ILogger ) { this.integrationManager = integrationManager; } @@ -37,7 +39,7 @@ export class DeepnoteActivationService implements IExtensionSyncActivationServic public activate() { this.serializer = new DeepnoteNotebookSerializer(this.notebookManager); this.explorerView = new DeepnoteExplorerView(this.extensionContext, this.notebookManager); - this.editProtection = new DeepnoteInputBlockEditProtection(); + this.editProtection = new DeepnoteInputBlockEditProtection(this.logger); this.extensionContext.subscriptions.push(workspace.registerNotebookSerializer('deepnote', this.serializer)); this.extensionContext.subscriptions.push(this.editProtection); diff --git a/src/notebooks/deepnote/deepnoteActivationService.unit.test.ts b/src/notebooks/deepnote/deepnoteActivationService.unit.test.ts index c7ee287329..89374f53a6 100644 --- a/src/notebooks/deepnote/deepnoteActivationService.unit.test.ts +++ b/src/notebooks/deepnote/deepnoteActivationService.unit.test.ts @@ -3,6 +3,7 @@ import { assert } from 'chai'; import { DeepnoteActivationService } from './deepnoteActivationService'; import { DeepnoteNotebookManager } from './deepnoteNotebookManager'; import { IExtensionContext } from '../../platform/common/types'; +import { ILogger } from '../../platform/logging/types'; import { IIntegrationManager } from './integrations/types'; suite('DeepnoteActivationService', () => { @@ -10,6 +11,7 @@ suite('DeepnoteActivationService', () => { let mockExtensionContext: IExtensionContext; let manager: DeepnoteNotebookManager; let mockIntegrationManager: IIntegrationManager; + let mockLogger: ILogger; setup(() => { mockExtensionContext = { @@ -22,7 +24,20 @@ suite('DeepnoteActivationService', () => { return; } }; - activationService = new DeepnoteActivationService(mockExtensionContext, manager, mockIntegrationManager); + mockLogger = { + error: () => {}, + warn: () => {}, + info: () => {}, + debug: () => {}, + trace: () => {}, + ci: () => {} + } as ILogger; + activationService = new DeepnoteActivationService( + mockExtensionContext, + manager, + mockIntegrationManager, + mockLogger + ); }); suite('constructor', () => { @@ -92,8 +107,24 @@ suite('DeepnoteActivationService', () => { return; } }; - const service1 = new DeepnoteActivationService(context1, manager1, mockIntegrationManager1); - const service2 = new DeepnoteActivationService(context2, manager2, mockIntegrationManager2); + const mockLogger1: ILogger = { + error: () => {}, + warn: () => {}, + info: () => {}, + debug: () => {}, + trace: () => {}, + ci: () => {} + } as ILogger; + const mockLogger2: ILogger = { + error: () => {}, + warn: () => {}, + info: () => {}, + debug: () => {}, + trace: () => {}, + ci: () => {} + } as ILogger; + const service1 = new DeepnoteActivationService(context1, manager1, mockIntegrationManager1, mockLogger1); + const service2 = new DeepnoteActivationService(context2, manager2, mockIntegrationManager2, mockLogger2); // Verify each service has its own context assert.strictEqual((service1 as any).extensionContext, context1); @@ -128,8 +159,24 @@ suite('DeepnoteActivationService', () => { return; } }; - new DeepnoteActivationService(context1, manager1, mockIntegrationManager1); - new DeepnoteActivationService(context2, manager2, mockIntegrationManager2); + const mockLogger3: ILogger = { + error: () => {}, + warn: () => {}, + info: () => {}, + debug: () => {}, + trace: () => {}, + ci: () => {} + } as ILogger; + const mockLogger4: ILogger = { + error: () => {}, + warn: () => {}, + info: () => {}, + debug: () => {}, + trace: () => {}, + ci: () => {} + } as ILogger; + new DeepnoteActivationService(context1, manager1, mockIntegrationManager1, mockLogger3); + new DeepnoteActivationService(context2, manager2, mockIntegrationManager2, mockLogger4); assert.strictEqual(context1.subscriptions.length, 0); assert.strictEqual(context2.subscriptions.length, 1); diff --git a/src/notebooks/deepnote/deepnoteInputBlockEditProtection.ts b/src/notebooks/deepnote/deepnoteInputBlockEditProtection.ts index 95afe0205c..4ea23d2dd3 100644 --- a/src/notebooks/deepnote/deepnoteInputBlockEditProtection.ts +++ b/src/notebooks/deepnote/deepnoteInputBlockEditProtection.ts @@ -1,3 +1,4 @@ +import { injectable, inject } from 'inversify'; import { Disposable, NotebookCell, @@ -10,6 +11,7 @@ import { workspace, WorkspaceEdit } from 'vscode'; +import { ILogger } from '../../platform/logging/types'; import { formatInputBlockCellContent } from './inputBlockContentFormatter'; /** @@ -17,6 +19,7 @@ import { formatInputBlockCellContent } from './inputBlockContentFormatter'; * Also protects the language ID of all input blocks. * This is needed because VSCode doesn't support the `editable: false` metadata property. */ +@injectable() export class DeepnoteInputBlockEditProtection implements Disposable { private readonly disposables: Disposable[] = []; @@ -55,7 +58,7 @@ export class DeepnoteInputBlockEditProtection implements Disposable { ['button', 'python'] ]); - constructor() { + constructor(@inject(ILogger) private readonly logger: ILogger) { // Listen for notebook document changes this.disposables.push( workspace.onDidChangeNotebookDocument((e) => { @@ -125,7 +128,14 @@ export class DeepnoteInputBlockEditProtection implements Disposable { new Position(lastLine, cell.document.lineAt(lastLine).text.length) ); edit.replace(cell.document.uri, fullRange, correctContent); - await workspace.applyEdit(edit); + const success = await workspace.applyEdit(edit); + if (!success) { + this.logger.error( + `Failed to revert cell content for input block type '${blockType}' at cell index ${ + cell.index + } in notebook ${cell.notebook.uri.toString()}` + ); + } } } @@ -168,7 +178,15 @@ export class DeepnoteInputBlockEditProtection implements Disposable { workspaceEdit.set(uri, edits); } - await workspace.applyEdit(workspaceEdit); + const success = await workspace.applyEdit(workspaceEdit); + if (!success) { + const cellInfo = cellsToFix + .map(({ cell, blockType }) => `cell ${cell.index} (type: ${blockType})`) + .join(', '); + this.logger.error( + `Failed to protect cell languages for ${cellsToFix.length} cell(s): ${cellInfo} in notebook(s)` + ); + } } dispose(): void { From 07e59fe49eaaabd2055e16fb506276a7b4b5b66e Mon Sep 17 00:00:00 2001 From: jankuca Date: Fri, 24 Oct 2025 16:27:34 +0200 Subject: [PATCH 034/101] remove unnecessary base logic --- src/notebooks/deepnote/converters/inputConverters.ts | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/src/notebooks/deepnote/converters/inputConverters.ts b/src/notebooks/deepnote/converters/inputConverters.ts index 9f483dd839..92e946703d 100644 --- a/src/notebooks/deepnote/converters/inputConverters.ts +++ b/src/notebooks/deepnote/converters/inputConverters.ts @@ -55,13 +55,8 @@ export abstract class BaseInputBlockConverter implements logger.error('Error parsing deepnote input metadata', deepnoteMetadataResult.error); } - // Extract the variable name from metadata - const variableName = deepnoteMetadataResult.success - ? (deepnoteMetadataResult.data as { deepnote_variable_name?: string }).deepnote_variable_name || '' - : ''; - // Create a code cell with Python language showing just the variable name - const cell = new NotebookCellData(NotebookCellKind.Code, `# ${variableName}`, 'python'); + const cell = new NotebookCellData(NotebookCellKind.Code, '', 'plaintext'); return cell; } From 0f2cb39e43db381ca999ebeb5ddd135501c09f02 Mon Sep 17 00:00:00 2001 From: jankuca Date: Fri, 24 Oct 2025 16:37:03 +0200 Subject: [PATCH 035/101] simplify input converters --- .../deepnote/converters/inputConverters.ts | 172 ++++-------------- 1 file changed, 32 insertions(+), 140 deletions(-) diff --git a/src/notebooks/deepnote/converters/inputConverters.ts b/src/notebooks/deepnote/converters/inputConverters.ts index 92e946703d..07b7698dbb 100644 --- a/src/notebooks/deepnote/converters/inputConverters.ts +++ b/src/notebooks/deepnote/converters/inputConverters.ts @@ -23,13 +23,14 @@ export abstract class BaseInputBlockConverter implements abstract getSupportedType(): string; abstract defaultConfig(): z.infer; - applyChangesToBlock(block: DeepnoteBlock, cell: NotebookCellData): void { + /** + * Helper method to update block metadata with common logic. + * Clears block.content, parses schema, deletes DEEPNOTE_VSCODE_RAW_CONTENT_KEY, + * and merges metadata with updates. + */ + protected updateBlockMetadata(block: DeepnoteBlock, updates: Record): void { block.content = ''; - // The cell value now contains just the variable name - const variableName = cell.value.trim(); - - // Preserve existing metadata and update only the variable name const existingMetadata = this.schema().safeParse(block.metadata); const baseMetadata = existingMetadata.success ? existingMetadata.data : this.defaultConfig(); @@ -40,10 +41,17 @@ export abstract class BaseInputBlockConverter implements block.metadata = { ...(block.metadata ?? {}), ...baseMetadata, - deepnote_variable_name: variableName + ...updates }; } + applyChangesToBlock(block: DeepnoteBlock, _cell: NotebookCellData): void { + // Default implementation: preserve existing metadata + // Readonly blocks (select, checkbox, date, date-range, button) use this default behavior + // Editable blocks override this method to update specific metadata fields + this.updateBlockMetadata(block, {}); + } + canConvert(blockType: string): boolean { return blockType.toLowerCase() === this.getSupportedType(); } @@ -86,23 +94,12 @@ export class InputTextBlockConverter extends BaseInputBlockConverter { @@ -204,14 +175,11 @@ export class InputSliderBlockConverter extends BaseInputBlockConverter { @@ -296,23 +243,8 @@ export class InputDateBlockConverter extends BaseInputBlockConverter { @@ -334,23 +266,8 @@ export class InputDateRangeBlockConverter extends BaseInputBlockConverter { @@ -373,23 +290,12 @@ export class InputFileBlockConverter extends BaseInputBlockConverter Date: Fri, 24 Oct 2025 16:41:47 +0200 Subject: [PATCH 036/101] fix lang in text input tests --- .../deepnote/converters/inputConverters.ts | 18 +++++------------- .../converters/inputConverters.unit.test.ts | 4 ++-- 2 files changed, 7 insertions(+), 15 deletions(-) diff --git a/src/notebooks/deepnote/converters/inputConverters.ts b/src/notebooks/deepnote/converters/inputConverters.ts index 07b7698dbb..6672da0478 100644 --- a/src/notebooks/deepnote/converters/inputConverters.ts +++ b/src/notebooks/deepnote/converters/inputConverters.ts @@ -181,19 +181,11 @@ export class InputSliderBlockConverter extends BaseInputBlockConverter { sortingKey: 'a0', type: 'input-text' }; - const cell = new NotebookCellData(NotebookCellKind.Code, 'var1', 'python'); + const cell = new NotebookCellData(NotebookCellKind.Code, 'var1', 'plaintext'); converter.applyChangesToBlock(block, cell); @@ -165,7 +165,7 @@ suite('InputTextBlockConverter', () => { sortingKey: 'a0', type: 'input-text' }; - const cell = new NotebookCellData(NotebookCellKind.Code, 'var', 'python'); + const cell = new NotebookCellData(NotebookCellKind.Code, 'var', 'plaintext'); converter.applyChangesToBlock(block, cell); From bae175984b462524230cf5373b46bfa20e342255 Mon Sep 17 00:00:00 2001 From: jankuca Date: Fri, 24 Oct 2025 16:45:24 +0200 Subject: [PATCH 037/101] remove outdated input value logic --- .../deepnote/deepnoteInputBlockCellStatusBarProvider.ts | 7 ------- 1 file changed, 7 deletions(-) diff --git a/src/notebooks/deepnote/deepnoteInputBlockCellStatusBarProvider.ts b/src/notebooks/deepnote/deepnoteInputBlockCellStatusBarProvider.ts index 3aa7c7ac75..899e086f5b 100644 --- a/src/notebooks/deepnote/deepnoteInputBlockCellStatusBarProvider.ts +++ b/src/notebooks/deepnote/deepnoteInputBlockCellStatusBarProvider.ts @@ -645,13 +645,6 @@ export class DeepnoteInputBlockCellStatusBarItemProvider // Update cell metadata edit.set(cell.notebook.uri, [NotebookEdit.updateCellMetadata(cell.index, updatedMetadata)]); - // Update cell content (replace entire cell text with "# " + variable name) - const fullRange = new Range( - new Position(0, 0), - new Position(cell.document.lineCount - 1, cell.document.lineAt(cell.document.lineCount - 1).text.length) - ); - edit.replace(cell.document.uri, fullRange, `# ${newVariableName}`); - const success = await workspace.applyEdit(edit); if (!success) { void window.showErrorMessage(l10n.t('Failed to update variable name')); From 9297504d80638bd2d74dcbf5218e2b08d7d3ed97 Mon Sep 17 00:00:00 2001 From: jankuca Date: Fri, 24 Oct 2025 16:45:36 +0200 Subject: [PATCH 038/101] add handling for start>end date ranges --- .../deepnoteInputBlockCellStatusBarProvider.ts | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/src/notebooks/deepnote/deepnoteInputBlockCellStatusBarProvider.ts b/src/notebooks/deepnote/deepnoteInputBlockCellStatusBarProvider.ts index 899e086f5b..ade07bc73f 100644 --- a/src/notebooks/deepnote/deepnoteInputBlockCellStatusBarProvider.ts +++ b/src/notebooks/deepnote/deepnoteInputBlockCellStatusBarProvider.ts @@ -1021,7 +1021,11 @@ export class DeepnoteInputBlockCellStatusBarItemProvider } // Store as YYYY-MM-DD format (not full ISO string) - const newValue = currentEnd ? [input, currentEnd] : [input, input]; + let newValue = currentEnd ? [input, currentEnd] : [input, input]; + if (newValue[1] && newValue[0] > newValue[1]) { + newValue = [newValue[1], newValue[0]]; + void window.showWarningMessage(l10n.t('Start date was after end date; the range was adjusted.')); + } await this.updateCellMetadata(cell, { deepnote_variable_value: newValue }); } @@ -1062,7 +1066,11 @@ export class DeepnoteInputBlockCellStatusBarItemProvider } // Store as YYYY-MM-DD format (not full ISO string) - const newValue = currentStart ? [currentStart, input] : [input, input]; + let newValue = currentStart ? [currentStart, input] : [input, input]; + if (newValue[0] > newValue[1]) { + newValue = [newValue[1], newValue[0]]; + void window.showWarningMessage(l10n.t('End date was before start date; the range was adjusted.')); + } await this.updateCellMetadata(cell, { deepnote_variable_value: newValue }); } From c2c6f450d678291c8f558f39c28333112ca55857 Mon Sep 17 00:00:00 2001 From: jankuca Date: Fri, 24 Oct 2025 16:48:10 +0200 Subject: [PATCH 039/101] improve empty selection handling when not allowed --- .../deepnoteInputBlockCellStatusBarProvider.ts | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/src/notebooks/deepnote/deepnoteInputBlockCellStatusBarProvider.ts b/src/notebooks/deepnote/deepnoteInputBlockCellStatusBarProvider.ts index ade07bc73f..20978d5f19 100644 --- a/src/notebooks/deepnote/deepnoteInputBlockCellStatusBarProvider.ts +++ b/src/notebooks/deepnote/deepnoteInputBlockCellStatusBarProvider.ts @@ -9,6 +9,7 @@ import { NotebookCellStatusBarItemProvider, NotebookEdit, Position, + QuickPickItem, Range, ThemeIcon, WorkspaceEdit, @@ -740,10 +741,16 @@ export class DeepnoteInputBlockCellStatusBarItemProvider }); } else { // If empty values are not allowed, ensure at least one item is always selected + // Track the most recent non-empty selection + let lastNonEmptySelection: readonly QuickPickItem[] = optionItems.filter((item) => item.picked); + quickPick.onDidChangeSelection((selectedItems) => { if (selectedItems.length === 0) { - // Prevent deselecting the last item - restore previous selection - quickPick.selectedItems = optionItems.filter((item) => item.picked); + // Prevent deselecting the last item - restore most recent selection + quickPick.selectedItems = lastNonEmptySelection; + } else { + // Update the last non-empty selection + lastNonEmptySelection = selectedItems; } }); } From 6b021cd27e59748dde86e44e0579cfad21e71a47 Mon Sep 17 00:00:00 2001 From: jankuca Date: Fri, 24 Oct 2025 16:50:13 +0200 Subject: [PATCH 040/101] fix file input paths --- .../deepnote/deepnoteInputBlockCellStatusBarProvider.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/notebooks/deepnote/deepnoteInputBlockCellStatusBarProvider.ts b/src/notebooks/deepnote/deepnoteInputBlockCellStatusBarProvider.ts index 20978d5f19..90fb4021e6 100644 --- a/src/notebooks/deepnote/deepnoteInputBlockCellStatusBarProvider.ts +++ b/src/notebooks/deepnote/deepnoteInputBlockCellStatusBarProvider.ts @@ -25,6 +25,7 @@ import type { Pocket } from '../../platform/deepnote/pocket'; import { formatInputBlockCellContent } from './inputBlockContentFormatter'; import { SelectInputSettingsWebviewProvider } from './selectInputSettingsWebview'; import { IExtensionContext } from '../../platform/common/types'; +import { getFilePath } from '../../platform/common/platform/fs-paths'; /** * Provides status bar items for Deepnote input block cells to display their block type. @@ -941,8 +942,8 @@ export class DeepnoteInputBlockCellStatusBarItemProvider return; } - // Get the file path - const filePath = uris[0].path; + // Get the file path (using getFilePath for platform-correct path separators) + const filePath = getFilePath(uris[0]); await this.updateCellMetadata(cell, { deepnote_variable_value: filePath }); } From 3663f998f169505d4dd8106eaf6cfac07a608bfe Mon Sep 17 00:00:00 2001 From: jankuca Date: Fri, 24 Oct 2025 16:51:07 +0200 Subject: [PATCH 041/101] ease test --- .../deepnoteInputBlockCellStatusBarProvider.unit.test.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/notebooks/deepnote/deepnoteInputBlockCellStatusBarProvider.unit.test.ts b/src/notebooks/deepnote/deepnoteInputBlockCellStatusBarProvider.unit.test.ts index c4c6cad27e..6a4fbb2a66 100644 --- a/src/notebooks/deepnote/deepnoteInputBlockCellStatusBarProvider.unit.test.ts +++ b/src/notebooks/deepnote/deepnoteInputBlockCellStatusBarProvider.unit.test.ts @@ -57,7 +57,7 @@ suite('DeepnoteInputBlockCellStatusBarItemProvider', () => { const items = provider.provideCellStatusBarItems(cell); expect(items).to.not.be.undefined; - expect(items).to.have.lengthOf(2); + expect(items).to.have.length.at.least(2); expect(items?.[0].text).to.equal('Input Text'); expect(items?.[0].alignment).to.equal(1); // Left }); @@ -67,7 +67,7 @@ suite('DeepnoteInputBlockCellStatusBarItemProvider', () => { const items = provider.provideCellStatusBarItems(cell); expect(items).to.not.be.undefined; - expect(items).to.have.lengthOf(2); + expect(items).to.have.length.at.least(2); expect(items?.[0].text).to.equal('Input Textarea'); }); @@ -130,7 +130,7 @@ suite('DeepnoteInputBlockCellStatusBarItemProvider', () => { const items = provider.provideCellStatusBarItems(cell); expect(items).to.not.be.undefined; - expect(items).to.have.lengthOf(2); + expect(items).to.have.length.at.least(2); expect(items?.[0].text).to.equal('Button'); }); }); From f41cd577f18119313a7e9ed6e11dac0a2815b7f1 Mon Sep 17 00:00:00 2001 From: jankuca Date: Fri, 24 Oct 2025 16:53:31 +0200 Subject: [PATCH 042/101] improve uri typing --- .../deepnoteInputBlockEditProtection.ts | 17 +++++++---------- 1 file changed, 7 insertions(+), 10 deletions(-) diff --git a/src/notebooks/deepnote/deepnoteInputBlockEditProtection.ts b/src/notebooks/deepnote/deepnoteInputBlockEditProtection.ts index 4ea23d2dd3..8cceda7ae9 100644 --- a/src/notebooks/deepnote/deepnoteInputBlockEditProtection.ts +++ b/src/notebooks/deepnote/deepnoteInputBlockEditProtection.ts @@ -2,6 +2,7 @@ import { injectable, inject } from 'inversify'; import { Disposable, NotebookCell, + NotebookCellData, NotebookDocumentChangeEvent, NotebookEdit, NotebookRange, @@ -160,16 +161,12 @@ export class DeepnoteInputBlockEditProtection implements Disposable { } // Add the cell replacement edit - editsByNotebook.get(notebookUriStr)!.edits.push( - NotebookEdit.replaceCells(new NotebookRange(cell.index, cell.index + 1), [ - { - kind: cell.kind, - languageId: expectedLanguage, - value: cell.document.getText(), - metadata: cell.metadata - } - ]) - ); + const cellData = new NotebookCellData(cell.kind, cell.document.getText(), expectedLanguage); + cellData.metadata = cell.metadata; + + editsByNotebook + .get(notebookUriStr)! + .edits.push(NotebookEdit.replaceCells(new NotebookRange(cell.index, cell.index + 1), [cellData])); } // Apply all edits in a single workspace edit to minimize flickering From 378c9cf4596f7ac37e5f3412b797fbd2b0de6dd5 Mon Sep 17 00:00:00 2001 From: jankuca Date: Fri, 24 Oct 2025 16:57:35 +0200 Subject: [PATCH 043/101] handle special chars in input values --- .../converters/inputConverters.unit.test.ts | 102 ++++++++++++++++++ .../deepnote/inputBlockContentFormatter.ts | 12 +-- 2 files changed, 108 insertions(+), 6 deletions(-) diff --git a/src/notebooks/deepnote/converters/inputConverters.unit.test.ts b/src/notebooks/deepnote/converters/inputConverters.unit.test.ts index dc08f57a01..86c0e09bd6 100644 --- a/src/notebooks/deepnote/converters/inputConverters.unit.test.ts +++ b/src/notebooks/deepnote/converters/inputConverters.unit.test.ts @@ -305,6 +305,74 @@ suite('InputSelectBlockConverter', () => { assert.strictEqual(cell.value, 'None'); }); + + test('escapes quotes in single select value', () => { + const block: DeepnoteBlock = { + blockGroup: 'test-group', + content: '', + id: 'block-123', + metadata: { + deepnote_variable_value: 'Option with "quotes"' + }, + sortingKey: 'a0', + type: 'input-select' + }; + + const cell = converter.convertToCell(block); + + assert.strictEqual(cell.value, '"Option with \\"quotes\\""'); + }); + + test('escapes backslashes in single select value', () => { + const block: DeepnoteBlock = { + blockGroup: 'test-group', + content: '', + id: 'block-123', + metadata: { + deepnote_variable_value: 'Path\\to\\file' + }, + sortingKey: 'a0', + type: 'input-select' + }; + + const cell = converter.convertToCell(block); + + assert.strictEqual(cell.value, '"Path\\\\to\\\\file"'); + }); + + test('escapes quotes in multi-select array values', () => { + const block: DeepnoteBlock = { + blockGroup: 'test-group', + content: '', + id: 'block-123', + metadata: { + deepnote_variable_value: ['Option "A"', 'Option "B"'] + }, + sortingKey: 'a0', + type: 'input-select' + }; + + const cell = converter.convertToCell(block); + + assert.strictEqual(cell.value, '["Option \\"A\\"", "Option \\"B\\""]'); + }); + + test('handles empty array for multi-select', () => { + const block: DeepnoteBlock = { + blockGroup: 'test-group', + content: '', + id: 'block-123', + metadata: { + deepnote_variable_value: [] + }, + sortingKey: 'a0', + type: 'input-select' + }; + + const cell = converter.convertToCell(block); + + assert.strictEqual(cell.value, '[]'); + }); }); suite('applyChangesToBlock', () => { @@ -787,6 +855,40 @@ suite('InputFileBlockConverter', () => { assert.strictEqual(cell.value, ''); }); + + test('escapes backslashes in Windows file paths', () => { + const block: DeepnoteBlock = { + blockGroup: 'test-group', + content: '', + id: 'block-123', + metadata: { + deepnote_variable_value: 'C:\\Users\\Documents\\file.txt' + }, + sortingKey: 'a0', + type: 'input-file' + }; + + const cell = converter.convertToCell(block); + + assert.strictEqual(cell.value, '"C:\\\\Users\\\\Documents\\\\file.txt"'); + }); + + test('escapes quotes in file paths', () => { + const block: DeepnoteBlock = { + blockGroup: 'test-group', + content: '', + id: 'block-123', + metadata: { + deepnote_variable_value: 'file "with quotes".txt' + }, + sortingKey: 'a0', + type: 'input-file' + }; + + const cell = converter.convertToCell(block); + + assert.strictEqual(cell.value, '"file \\"with quotes\\".txt"'); + }); }); suite('applyChangesToBlock', () => { diff --git a/src/notebooks/deepnote/inputBlockContentFormatter.ts b/src/notebooks/deepnote/inputBlockContentFormatter.ts index ebd52e67da..9aaab6ac56 100644 --- a/src/notebooks/deepnote/inputBlockContentFormatter.ts +++ b/src/notebooks/deepnote/inputBlockContentFormatter.ts @@ -25,10 +25,10 @@ export function formatInputBlockCellContent(blockType: string, metadata: Record< // Empty array for multi-select return '[]'; } - return `[${value.map((v) => `"${v}"`).join(', ')}]`; + return `[${value.map((v) => JSON.stringify(v)).join(', ')}]`; } else if (typeof value === 'string') { // Single select: show as quoted string - return `"${value}"`; + return JSON.stringify(value); } else if (value === null || value === undefined) { // Empty/null value return 'None'; @@ -50,7 +50,7 @@ export function formatInputBlockCellContent(blockType: string, metadata: Record< const value = metadata.deepnote_variable_value; if (value) { const dateStr = formatDateValue(value); - return dateStr ? `"${dateStr}"` : '""'; + return dateStr ? JSON.stringify(dateStr) : '""'; } return '""'; } @@ -61,7 +61,7 @@ export function formatInputBlockCellContent(blockType: string, metadata: Record< const start = formatDateValue(value[0]); const end = formatDateValue(value[1]); if (start || end) { - return `("${start}", "${end}")`; + return `(${JSON.stringify(start)}, ${JSON.stringify(end)})`; } } else { const defaultValue = metadata.deepnote_variable_default_value; @@ -69,7 +69,7 @@ export function formatInputBlockCellContent(blockType: string, metadata: Record< const start = formatDateValue(defaultValue[0]); const end = formatDateValue(defaultValue[1]); if (start || end) { - return `("${start}", "${end}")`; + return `(${JSON.stringify(start)}, ${JSON.stringify(end)})`; } } } @@ -78,7 +78,7 @@ export function formatInputBlockCellContent(blockType: string, metadata: Record< case 'input-file': { const value = metadata.deepnote_variable_value; - return typeof value === 'string' && value ? `"${value}"` : ''; + return typeof value === 'string' && value ? JSON.stringify(value) : ''; } case 'button': { From 7c83d1e0d6b4f281de11ce205d0af82bc5d5844c Mon Sep 17 00:00:00 2001 From: jankuca Date: Fri, 24 Oct 2025 16:59:47 +0200 Subject: [PATCH 044/101] delete unused html --- .../selectInputSettings/index.html | 24 ------------------- 1 file changed, 24 deletions(-) delete mode 100644 src/webviews/webview-side/selectInputSettings/index.html diff --git a/src/webviews/webview-side/selectInputSettings/index.html b/src/webviews/webview-side/selectInputSettings/index.html deleted file mode 100644 index b711a372ec..0000000000 --- a/src/webviews/webview-side/selectInputSettings/index.html +++ /dev/null @@ -1,24 +0,0 @@ - - - - - - - - Select Input Settings - - -
- - - - From a035cd85a2ebfc6fd2f7f9289fc7a8f1912f4c8f Mon Sep 17 00:00:00 2001 From: jankuca Date: Fri, 24 Oct 2025 17:03:30 +0200 Subject: [PATCH 045/101] add focus outlines --- .../selectInputSettings.css | 21 ++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/src/webviews/webview-side/selectInputSettings/selectInputSettings.css b/src/webviews/webview-side/selectInputSettings/selectInputSettings.css index 4c3411e00d..a8b98f72e2 100644 --- a/src/webviews/webview-side/selectInputSettings/selectInputSettings.css +++ b/src/webviews/webview-side/selectInputSettings/selectInputSettings.css @@ -39,6 +39,11 @@ height: 24px; } +.toggle-switch input:focus-visible + .toggle-slider { + outline: 2px solid var(--vscode-focusBorder); + outline-offset: 2px; +} + .toggle-switch input { opacity: 0; width: 0; @@ -109,6 +114,11 @@ margin-top: 2px; } +.radio-option input[type='radio']:focus-visible { + outline: 2px solid var(--vscode-focusBorder); + outline-offset: 2px; +} + .radio-content { flex: 1; } @@ -154,6 +164,11 @@ opacity: 0.7; } +.option-tag button:focus-visible { + outline: 2px solid var(--vscode-focusBorder); + outline-offset: 2px; +} + .add-option-form { display: flex; gap: 8px; @@ -208,6 +223,11 @@ font-size: 13px; } +.actions button:focus-visible { + outline: 2px solid var(--vscode-focusBorder); + outline-offset: 2px; +} + .btn-primary { background-color: var(--vscode-button-background); color: var(--vscode-button-foreground); @@ -225,4 +245,3 @@ .btn-secondary:hover { background-color: var(--vscode-button-secondaryHoverBackground); } - From ebf8518a37021d600a8655b2d4c238e0a08b4f03 Mon Sep 17 00:00:00 2001 From: jankuca Date: Fri, 24 Oct 2025 17:04:57 +0200 Subject: [PATCH 046/101] aria for inputs --- .../selectInputSettings/SelectInputSettingsPanel.tsx | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/webviews/webview-side/selectInputSettings/SelectInputSettingsPanel.tsx b/src/webviews/webview-side/selectInputSettings/SelectInputSettingsPanel.tsx index cbe3ca0f84..a66e4629f6 100644 --- a/src/webviews/webview-side/selectInputSettings/SelectInputSettingsPanel.tsx +++ b/src/webviews/webview-side/selectInputSettings/SelectInputSettingsPanel.tsx @@ -142,8 +142,9 @@ export const SelectInputSettingsPanel: React.FC handleSelectTypeChange('from-options'); } }} - role="button" + role="radio" tabIndex={0} + aria-checked={settings.selectType === 'from-options'} > handleSelectTypeChange('from-variable'); } }} - role="button" + role="radio" tabIndex={0} + aria-checked={settings.selectType === 'from-variable'} > Date: Fri, 24 Oct 2025 17:05:56 +0200 Subject: [PATCH 047/101] tighten typing in converotrs --- src/notebooks/deepnote/converters/inputConverters.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/notebooks/deepnote/converters/inputConverters.ts b/src/notebooks/deepnote/converters/inputConverters.ts index 6672da0478..672efac35e 100644 --- a/src/notebooks/deepnote/converters/inputConverters.ts +++ b/src/notebooks/deepnote/converters/inputConverters.ts @@ -28,7 +28,7 @@ export abstract class BaseInputBlockConverter implements * Clears block.content, parses schema, deletes DEEPNOTE_VSCODE_RAW_CONTENT_KEY, * and merges metadata with updates. */ - protected updateBlockMetadata(block: DeepnoteBlock, updates: Record): void { + protected updateBlockMetadata(block: DeepnoteBlock, updates: Partial>): void { block.content = ''; const existingMetadata = this.schema().safeParse(block.metadata); From b7f2dee4b38e35205d480d52c82a0aef34d16443 Mon Sep 17 00:00:00 2001 From: jankuca Date: Fri, 24 Oct 2025 17:06:04 +0200 Subject: [PATCH 048/101] refactor mock logger --- .../deepnoteActivationService.unit.test.ts | 56 ++++++------------- 1 file changed, 16 insertions(+), 40 deletions(-) diff --git a/src/notebooks/deepnote/deepnoteActivationService.unit.test.ts b/src/notebooks/deepnote/deepnoteActivationService.unit.test.ts index 89374f53a6..8c2fd9a265 100644 --- a/src/notebooks/deepnote/deepnoteActivationService.unit.test.ts +++ b/src/notebooks/deepnote/deepnoteActivationService.unit.test.ts @@ -6,6 +6,17 @@ import { IExtensionContext } from '../../platform/common/types'; import { ILogger } from '../../platform/logging/types'; import { IIntegrationManager } from './integrations/types'; +function createMockLogger(): ILogger { + return { + error: () => void 0, + warn: () => void 0, + info: () => void 0, + debug: () => void 0, + trace: () => void 0, + ci: () => void 0 + } as ILogger; +} + suite('DeepnoteActivationService', () => { let activationService: DeepnoteActivationService; let mockExtensionContext: IExtensionContext; @@ -24,14 +35,7 @@ suite('DeepnoteActivationService', () => { return; } }; - mockLogger = { - error: () => {}, - warn: () => {}, - info: () => {}, - debug: () => {}, - trace: () => {}, - ci: () => {} - } as ILogger; + mockLogger = createMockLogger(); activationService = new DeepnoteActivationService( mockExtensionContext, manager, @@ -107,22 +111,8 @@ suite('DeepnoteActivationService', () => { return; } }; - const mockLogger1: ILogger = { - error: () => {}, - warn: () => {}, - info: () => {}, - debug: () => {}, - trace: () => {}, - ci: () => {} - } as ILogger; - const mockLogger2: ILogger = { - error: () => {}, - warn: () => {}, - info: () => {}, - debug: () => {}, - trace: () => {}, - ci: () => {} - } as ILogger; + const mockLogger1 = createMockLogger(); + const mockLogger2 = createMockLogger(); const service1 = new DeepnoteActivationService(context1, manager1, mockIntegrationManager1, mockLogger1); const service2 = new DeepnoteActivationService(context2, manager2, mockIntegrationManager2, mockLogger2); @@ -159,22 +149,8 @@ suite('DeepnoteActivationService', () => { return; } }; - const mockLogger3: ILogger = { - error: () => {}, - warn: () => {}, - info: () => {}, - debug: () => {}, - trace: () => {}, - ci: () => {} - } as ILogger; - const mockLogger4: ILogger = { - error: () => {}, - warn: () => {}, - info: () => {}, - debug: () => {}, - trace: () => {}, - ci: () => {} - } as ILogger; + const mockLogger3 = createMockLogger(); + const mockLogger4 = createMockLogger(); new DeepnoteActivationService(context1, manager1, mockIntegrationManager1, mockLogger3); new DeepnoteActivationService(context2, manager2, mockIntegrationManager2, mockLogger4); From 3c421dd7e02b5c62e6fb768dc7ca71b993ce1fd0 Mon Sep 17 00:00:00 2001 From: jankuca Date: Fri, 24 Oct 2025 17:06:38 +0200 Subject: [PATCH 049/101] refactor canConvert --- src/notebooks/deepnote/converters/inputConverters.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/notebooks/deepnote/converters/inputConverters.ts b/src/notebooks/deepnote/converters/inputConverters.ts index 672efac35e..8ca0584741 100644 --- a/src/notebooks/deepnote/converters/inputConverters.ts +++ b/src/notebooks/deepnote/converters/inputConverters.ts @@ -53,7 +53,7 @@ export abstract class BaseInputBlockConverter implements } canConvert(blockType: string): boolean { - return blockType.toLowerCase() === this.getSupportedType(); + return this.getSupportedTypes().includes(blockType.toLowerCase()); } convertToCell(block: DeepnoteBlock): NotebookCellData { From f74b2210628dd2919dc0e165a890bb24d0c0cd45 Mon Sep 17 00:00:00 2001 From: jankuca Date: Fri, 24 Oct 2025 17:07:12 +0200 Subject: [PATCH 050/101] fix comment --- src/notebooks/deepnote/converters/inputConverters.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/notebooks/deepnote/converters/inputConverters.ts b/src/notebooks/deepnote/converters/inputConverters.ts index 8ca0584741..65260994ad 100644 --- a/src/notebooks/deepnote/converters/inputConverters.ts +++ b/src/notebooks/deepnote/converters/inputConverters.ts @@ -63,7 +63,7 @@ export abstract class BaseInputBlockConverter implements logger.error('Error parsing deepnote input metadata', deepnoteMetadataResult.error); } - // Create a code cell with Python language showing just the variable name + // Default fallback: empty plaintext cell; subclasses render content/language const cell = new NotebookCellData(NotebookCellKind.Code, '', 'plaintext'); return cell; From c8ccc0247e08eb30c621d75aa57eb05d1ca73f8c Mon Sep 17 00:00:00 2001 From: jankuca Date: Fri, 24 Oct 2025 17:10:49 +0200 Subject: [PATCH 051/101] fix type --- src/notebooks/deepnote/converters/inputConverters.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/notebooks/deepnote/converters/inputConverters.ts b/src/notebooks/deepnote/converters/inputConverters.ts index 65260994ad..f3f219ba84 100644 --- a/src/notebooks/deepnote/converters/inputConverters.ts +++ b/src/notebooks/deepnote/converters/inputConverters.ts @@ -188,7 +188,7 @@ export class InputSliderBlockConverter extends BaseInputBlockConverter Date: Fri, 24 Oct 2025 17:10:55 +0200 Subject: [PATCH 052/101] void->undefined --- .../deepnote/deepnoteActivationService.unit.test.ts | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/notebooks/deepnote/deepnoteActivationService.unit.test.ts b/src/notebooks/deepnote/deepnoteActivationService.unit.test.ts index 8c2fd9a265..d1f6780a30 100644 --- a/src/notebooks/deepnote/deepnoteActivationService.unit.test.ts +++ b/src/notebooks/deepnote/deepnoteActivationService.unit.test.ts @@ -8,12 +8,12 @@ import { IIntegrationManager } from './integrations/types'; function createMockLogger(): ILogger { return { - error: () => void 0, - warn: () => void 0, - info: () => void 0, - debug: () => void 0, - trace: () => void 0, - ci: () => void 0 + error: () => undefined, + warn: () => undefined, + info: () => undefined, + debug: () => undefined, + trace: () => undefined, + ci: () => undefined } as ILogger; } From eff2419d1738339c9bbb4fc01909dd66345fa861 Mon Sep 17 00:00:00 2001 From: jankuca Date: Fri, 24 Oct 2025 17:15:35 +0200 Subject: [PATCH 053/101] remove copyright --- src/webviews/webview-side/selectInputSettings/index.tsx | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/webviews/webview-side/selectInputSettings/index.tsx b/src/webviews/webview-side/selectInputSettings/index.tsx index be65d7fa9a..3595441219 100644 --- a/src/webviews/webview-side/selectInputSettings/index.tsx +++ b/src/webviews/webview-side/selectInputSettings/index.tsx @@ -1,6 +1,3 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - import * as React from 'react'; import * as ReactDOM from 'react-dom'; @@ -21,4 +18,3 @@ ReactDOM.render( , document.getElementById('root') as HTMLElement ); - From 684e765e718886f5536bf15cf8d3793a853aaeea Mon Sep 17 00:00:00 2001 From: jankuca Date: Fri, 24 Oct 2025 17:16:55 +0200 Subject: [PATCH 054/101] fix tests of numeric values --- .../deepnote/converters/inputConverters.unit.test.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/notebooks/deepnote/converters/inputConverters.unit.test.ts b/src/notebooks/deepnote/converters/inputConverters.unit.test.ts index 86c0e09bd6..29e3e24e5a 100644 --- a/src/notebooks/deepnote/converters/inputConverters.unit.test.ts +++ b/src/notebooks/deepnote/converters/inputConverters.unit.test.ts @@ -493,7 +493,7 @@ suite('InputSliderBlockConverter', () => { converter.applyChangesToBlock(block, cell); assert.strictEqual(block.content, ''); - assert.strictEqual(block.metadata?.deepnote_variable_value, 7); + assert.strictEqual(block.metadata?.deepnote_variable_value, '7'); // Other metadata should be preserved assert.strictEqual(block.metadata?.deepnote_variable_name, 'slider1'); assert.strictEqual(block.metadata?.deepnote_slider_min_value, 0); @@ -516,7 +516,7 @@ suite('InputSliderBlockConverter', () => { converter.applyChangesToBlock(block, cell); - assert.strictEqual(block.metadata?.deepnote_variable_value, 42); + assert.strictEqual(block.metadata?.deepnote_variable_value, '42'); }); test('handles invalid numeric value', () => { @@ -536,7 +536,7 @@ suite('InputSliderBlockConverter', () => { converter.applyChangesToBlock(block, cell); // Should fall back to existing value (parsed as number) - assert.strictEqual(block.metadata?.deepnote_variable_value, 5); + assert.strictEqual(block.metadata?.deepnote_variable_value, '5'); }); }); }); From 57e826afeb4763ed5331ae312db8b0590d0d4dbd Mon Sep 17 00:00:00 2001 From: jankuca Date: Fri, 24 Oct 2025 17:19:13 +0200 Subject: [PATCH 055/101] show "Set variable name" when not set --- .../deepnote/deepnoteInputBlockCellStatusBarProvider.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/notebooks/deepnote/deepnoteInputBlockCellStatusBarProvider.ts b/src/notebooks/deepnote/deepnoteInputBlockCellStatusBarProvider.ts index 90fb4021e6..fc8e2651cb 100644 --- a/src/notebooks/deepnote/deepnoteInputBlockCellStatusBarProvider.ts +++ b/src/notebooks/deepnote/deepnoteInputBlockCellStatusBarProvider.ts @@ -582,8 +582,10 @@ export class DeepnoteInputBlockCellStatusBarItemProvider private createVariableStatusBarItem(cell: NotebookCell): NotebookCellStatusBarItem { const variableName = this.getVariableName(cell); + const text = variableName ? l10n.t('Variable: {0}', variableName) : l10n.t('$(edit) Set variable name'); + return { - text: l10n.t('Variable: {0}', variableName), + text, alignment: 1, // NotebookCellStatusBarAlignment.Left priority: 90, tooltip: l10n.t('Variable name for input block\nClick to change'), From 7c3e74cafe57aed443dc2407b18fe9edd39fbb69 Mon Sep 17 00:00:00 2001 From: jankuca Date: Fri, 24 Oct 2025 17:19:19 +0200 Subject: [PATCH 056/101] fix extension filter --- .../deepnote/deepnoteInputBlockCellStatusBarProvider.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/notebooks/deepnote/deepnoteInputBlockCellStatusBarProvider.ts b/src/notebooks/deepnote/deepnoteInputBlockCellStatusBarProvider.ts index fc8e2651cb..f36bf003f1 100644 --- a/src/notebooks/deepnote/deepnoteInputBlockCellStatusBarProvider.ts +++ b/src/notebooks/deepnote/deepnoteInputBlockCellStatusBarProvider.ts @@ -921,7 +921,7 @@ export class DeepnoteInputBlockCellStatusBarItemProvider // Split by comma and clean up const extensions = allowedExtensions .split(',') - .map((ext) => ext.trim()) + .map((ext) => ext.trim().replace(/^\./, '')) .filter((ext) => ext.length > 0); if (extensions.length > 0) { From c090db7429fe3a50d948f3ddcf6482d3c274dcd1 Mon Sep 17 00:00:00 2001 From: jankuca Date: Fri, 24 Oct 2025 17:21:13 +0200 Subject: [PATCH 057/101] 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 --- ...deepnoteInputBlockCellStatusBarProvider.ts | 33 ++++++++++++++++--- 1 file changed, 28 insertions(+), 5 deletions(-) diff --git a/src/notebooks/deepnote/deepnoteInputBlockCellStatusBarProvider.ts b/src/notebooks/deepnote/deepnoteInputBlockCellStatusBarProvider.ts index f36bf003f1..5911afc0a6 100644 --- a/src/notebooks/deepnote/deepnoteInputBlockCellStatusBarProvider.ts +++ b/src/notebooks/deepnote/deepnoteInputBlockCellStatusBarProvider.ts @@ -960,13 +960,36 @@ export class DeepnoteInputBlockCellStatusBarItemProvider this._onDidChangeCellStatusBarItems.fire(); } + /** + * Convert a date value to YYYY-MM-DD format without timezone shifts. + * If the value already matches YYYY-MM-DD, use it directly. + * Otherwise, use local date components to construct the string. + */ + private formatDateToYYYYMMDD(dateValue: string): string { + if (!dateValue) { + return ''; + } + + // If already in YYYY-MM-DD format, use it directly + if (/^\d{4}-\d{2}-\d{2}$/.test(dateValue)) { + return dateValue; + } + + // Otherwise, construct from local date components + const date = new Date(dateValue); + const year = date.getFullYear(); + const month = String(date.getMonth() + 1).padStart(2, '0'); + const day = String(date.getDate()).padStart(2, '0'); + return `${year}-${month}-${day}`; + } + /** * Handler for date input: choose date */ private async dateInputChooseDate(cell: NotebookCell): Promise { const metadata = cell.metadata as Record | undefined; const currentValue = metadata?.deepnote_variable_value as string | undefined; - const currentDate = currentValue ? new Date(currentValue).toISOString().split('T')[0] : ''; + const currentDate = currentValue ? this.formatDateToYYYYMMDD(currentValue) : ''; const input = await window.showInputBox({ prompt: l10n.t('Enter date (YYYY-MM-DD)'), @@ -1004,8 +1027,8 @@ export class DeepnoteInputBlockCellStatusBarItemProvider let currentEnd = ''; if (Array.isArray(currentValue) && currentValue.length === 2) { - currentStart = new Date(currentValue[0]).toISOString().split('T')[0]; - currentEnd = new Date(currentValue[1]).toISOString().split('T')[0]; + currentStart = this.formatDateToYYYYMMDD(currentValue[0]); + currentEnd = this.formatDateToYYYYMMDD(currentValue[1]); } const input = await window.showInputBox({ @@ -1049,8 +1072,8 @@ export class DeepnoteInputBlockCellStatusBarItemProvider let currentEnd = ''; if (Array.isArray(currentValue) && currentValue.length === 2) { - currentStart = new Date(currentValue[0]).toISOString().split('T')[0]; - currentEnd = new Date(currentValue[1]).toISOString().split('T')[0]; + currentStart = this.formatDateToYYYYMMDD(currentValue[0]); + currentEnd = this.formatDateToYYYYMMDD(currentValue[1]); } const input = await window.showInputBox({ From 5913802fcb3e026657f9e4a7994df40c02b38cfb Mon Sep 17 00:00:00 2001 From: jankuca Date: Fri, 24 Oct 2025 17:23:49 +0200 Subject: [PATCH 058/101] Fix accessibility issues in SelectInputSettingsPanel radio buttons - Replace non-semantic div with role='radio' with semantic -
handleSelectTypeChange('from-variable')} - onKeyDown={(e) => { - if (e.key === 'Enter' || e.key === ' ') { - e.preventDefault(); - handleSelectTypeChange('from-variable'); - } - }} - role="radio" - tabIndex={0} - aria-checked={settings.selectType === 'from-variable'} - > +
- +
From 67c471bb1a13cf8d5c1dc3d0a1802f016c49f321 Mon Sep 17 00:00:00 2001 From: jankuca Date: Fri, 24 Oct 2025 17:47:42 +0200 Subject: [PATCH 059/101] fix css importing --- src/notebooks/deepnote/integrations/integrationWebview.ts | 2 +- src/notebooks/deepnote/selectInputSettingsWebview.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/notebooks/deepnote/integrations/integrationWebview.ts b/src/notebooks/deepnote/integrations/integrationWebview.ts index bbcbd27660..a91d0f9128 100644 --- a/src/notebooks/deepnote/integrations/integrationWebview.ts +++ b/src/notebooks/deepnote/integrations/integrationWebview.ts @@ -327,7 +327,7 @@ export class IntegrationWebviewProvider implements IIntegrationWebviewProvider { - + Deepnote Integrations diff --git a/src/notebooks/deepnote/selectInputSettingsWebview.ts b/src/notebooks/deepnote/selectInputSettingsWebview.ts index 8c2674d68b..d5f6a1e939 100644 --- a/src/notebooks/deepnote/selectInputSettingsWebview.ts +++ b/src/notebooks/deepnote/selectInputSettingsWebview.ts @@ -228,7 +228,7 @@ export class SelectInputSettingsWebviewProvider { - + ${title} From 97c515082dd915d4b58f275dd84fe8474926de4e Mon Sep 17 00:00:00 2001 From: jankuca Date: Fri, 24 Oct 2025 17:52:55 +0200 Subject: [PATCH 060/101] add tests for command handlers in the input status bar provider --- ...putBlockCellStatusBarProvider.unit.test.ts | 464 +++++++++++++++++- 1 file changed, 459 insertions(+), 5 deletions(-) diff --git a/src/notebooks/deepnote/deepnoteInputBlockCellStatusBarProvider.unit.test.ts b/src/notebooks/deepnote/deepnoteInputBlockCellStatusBarProvider.unit.test.ts index 6a4fbb2a66..be068873d0 100644 --- a/src/notebooks/deepnote/deepnoteInputBlockCellStatusBarProvider.unit.test.ts +++ b/src/notebooks/deepnote/deepnoteInputBlockCellStatusBarProvider.unit.test.ts @@ -2,10 +2,12 @@ // Licensed under the MIT License. import { expect } from 'chai'; +import { anything, instance, mock, verify, when } from 'ts-mockito'; import { DeepnoteInputBlockCellStatusBarItemProvider } from './deepnoteInputBlockCellStatusBarProvider'; -import { NotebookCell, NotebookCellKind, NotebookDocument } from 'vscode'; +import { NotebookCell, NotebookCellKind, NotebookDocument, NotebookEdit, WorkspaceEdit } from 'vscode'; import { Uri } from 'vscode'; import type { IExtensionContext } from '../../platform/common/types'; +import { mockedVSCodeNamespaces, resetVSCodeMocks } from '../../test/vscode-mock'; suite('DeepnoteInputBlockCellStatusBarItemProvider', () => { let provider: DeepnoteInputBlockCellStatusBarItemProvider; @@ -23,13 +25,16 @@ suite('DeepnoteInputBlockCellStatusBarItemProvider', () => { }); function createMockCell(metadata?: Record): NotebookCell { + const notebookUri = Uri.file('/test/notebook.deepnote'); return { index: 0, - notebook: {} as NotebookDocument, + notebook: { + uri: notebookUri + } as NotebookDocument, kind: NotebookCellKind.Code, document: { - uri: Uri.file('/test/notebook.deepnote'), - fileName: '/test/notebook.deepnote', + uri: Uri.file('/test/notebook.deepnote#cell0'), + fileName: '/test/notebook.deepnote#cell0', isUntitled: false, languageId: 'json', version: 1, @@ -39,7 +44,7 @@ suite('DeepnoteInputBlockCellStatusBarItemProvider', () => { save: async () => true, eol: 1, lineCount: 1, - lineAt: () => ({}) as any, + lineAt: () => ({ text: '' }) as any, offsetAt: () => 0, positionAt: () => ({}) as any, validateRange: () => ({}) as any, @@ -213,4 +218,453 @@ suite('DeepnoteInputBlockCellStatusBarItemProvider', () => { expect(items?.[0].text).to.equal('Input Text'); }); }); + + suite('Command Handlers', () => { + setup(() => { + resetVSCodeMocks(); + }); + + teardown(() => { + resetVSCodeMocks(); + }); + + suite('deepnote.updateInputBlockVariableName', () => { + test('should update variable name when valid input is provided', async () => { + const cell = createMockCell({ + __deepnotePocket: { type: 'input-text' }, + deepnote_variable_name: 'old_var' + }); + + when(mockedVSCodeNamespaces.window.showInputBox(anything())).thenReturn(Promise.resolve('new_var')); + when(mockedVSCodeNamespaces.workspace.applyEdit(anything())).thenReturn(Promise.resolve(true)); + + await (provider as any).updateVariableName(cell); + + verify(mockedVSCodeNamespaces.window.showInputBox(anything())).once(); + verify(mockedVSCodeNamespaces.workspace.applyEdit(anything())).once(); + }); + + test('should not update if user cancels input', async () => { + const cell = createMockCell({ + __deepnotePocket: { type: 'input-text' }, + deepnote_variable_name: 'old_var' + }); + + when(mockedVSCodeNamespaces.window.showInputBox(anything())).thenReturn(Promise.resolve(undefined)); + + await (provider as any).updateVariableName(cell); + + verify(mockedVSCodeNamespaces.window.showInputBox(anything())).once(); + verify(mockedVSCodeNamespaces.workspace.applyEdit(anything())).never(); + }); + + test('should not update if variable name is unchanged', async () => { + const cell = createMockCell({ + __deepnotePocket: { type: 'input-text' }, + deepnote_variable_name: 'my_var' + }); + + when(mockedVSCodeNamespaces.window.showInputBox(anything())).thenReturn(Promise.resolve('my_var')); + + await (provider as any).updateVariableName(cell); + + verify(mockedVSCodeNamespaces.window.showInputBox(anything())).once(); + verify(mockedVSCodeNamespaces.workspace.applyEdit(anything())).never(); + }); + + test('should show error if workspace edit fails', async () => { + const cell = createMockCell({ + __deepnotePocket: { type: 'input-text' }, + deepnote_variable_name: 'old_var' + }); + + when(mockedVSCodeNamespaces.window.showInputBox(anything())).thenReturn(Promise.resolve('new_var')); + when(mockedVSCodeNamespaces.workspace.applyEdit(anything())).thenReturn(Promise.resolve(false)); + + await (provider as any).updateVariableName(cell); + + verify(mockedVSCodeNamespaces.window.showErrorMessage(anything())).once(); + }); + }); + + suite('deepnote.checkboxToggle', () => { + test('should toggle checkbox from false to true', async () => { + const cell = createMockCell({ + __deepnotePocket: { type: 'input-checkbox' }, + deepnote_variable_value: false + }); + + when(mockedVSCodeNamespaces.workspace.applyEdit(anything())).thenReturn(Promise.resolve(true)); + + await (provider as any).checkboxToggle(cell); + + verify(mockedVSCodeNamespaces.workspace.applyEdit(anything())).once(); + }); + + test('should toggle checkbox from true to false', async () => { + const cell = createMockCell({ + __deepnotePocket: { type: 'input-checkbox' }, + deepnote_variable_value: true + }); + + when(mockedVSCodeNamespaces.workspace.applyEdit(anything())).thenReturn(Promise.resolve(true)); + + await (provider as any).checkboxToggle(cell); + + verify(mockedVSCodeNamespaces.workspace.applyEdit(anything())).once(); + }); + + test('should default to false if value is undefined', async () => { + const cell = createMockCell({ + __deepnotePocket: { type: 'input-checkbox' } + }); + + when(mockedVSCodeNamespaces.workspace.applyEdit(anything())).thenReturn(Promise.resolve(true)); + + await (provider as any).checkboxToggle(cell); + + verify(mockedVSCodeNamespaces.workspace.applyEdit(anything())).once(); + }); + }); + + suite('deepnote.sliderSetMin', () => { + test('should update slider min value', async () => { + const cell = createMockCell({ + __deepnotePocket: { type: 'input-slider' }, + deepnote_slider_min_value: 0 + }); + + when(mockedVSCodeNamespaces.window.showInputBox(anything())).thenReturn(Promise.resolve('5')); + when(mockedVSCodeNamespaces.workspace.applyEdit(anything())).thenReturn(Promise.resolve(true)); + + await (provider as any).sliderSetMin(cell); + + verify(mockedVSCodeNamespaces.window.showInputBox(anything())).once(); + verify(mockedVSCodeNamespaces.workspace.applyEdit(anything())).once(); + }); + + test('should not update if user cancels', async () => { + const cell = createMockCell({ + __deepnotePocket: { type: 'input-slider' }, + deepnote_slider_min_value: 0 + }); + + when(mockedVSCodeNamespaces.window.showInputBox(anything())).thenReturn(Promise.resolve(undefined)); + + await (provider as any).sliderSetMin(cell); + + verify(mockedVSCodeNamespaces.workspace.applyEdit(anything())).never(); + }); + }); + + suite('deepnote.sliderSetMax', () => { + test('should update slider max value', async () => { + const cell = createMockCell({ + __deepnotePocket: { type: 'input-slider' }, + deepnote_slider_max_value: 10 + }); + + when(mockedVSCodeNamespaces.window.showInputBox(anything())).thenReturn(Promise.resolve('20')); + when(mockedVSCodeNamespaces.workspace.applyEdit(anything())).thenReturn(Promise.resolve(true)); + + await (provider as any).sliderSetMax(cell); + + verify(mockedVSCodeNamespaces.window.showInputBox(anything())).once(); + verify(mockedVSCodeNamespaces.workspace.applyEdit(anything())).once(); + }); + + test('should not update if user cancels', async () => { + const cell = createMockCell({ + __deepnotePocket: { type: 'input-slider' }, + deepnote_slider_max_value: 10 + }); + + when(mockedVSCodeNamespaces.window.showInputBox(anything())).thenReturn(Promise.resolve(undefined)); + + await (provider as any).sliderSetMax(cell); + + verify(mockedVSCodeNamespaces.workspace.applyEdit(anything())).never(); + }); + }); + + suite('deepnote.sliderSetStep', () => { + test('should update slider step value', async () => { + const cell = createMockCell({ + __deepnotePocket: { type: 'input-slider' }, + deepnote_slider_step: 1 + }); + + when(mockedVSCodeNamespaces.window.showInputBox(anything())).thenReturn(Promise.resolve('0.5')); + when(mockedVSCodeNamespaces.workspace.applyEdit(anything())).thenReturn(Promise.resolve(true)); + + await (provider as any).sliderSetStep(cell); + + verify(mockedVSCodeNamespaces.window.showInputBox(anything())).once(); + verify(mockedVSCodeNamespaces.workspace.applyEdit(anything())).once(); + }); + + test('should not update if user cancels', async () => { + const cell = createMockCell({ + __deepnotePocket: { type: 'input-slider' }, + deepnote_slider_step: 1 + }); + + when(mockedVSCodeNamespaces.window.showInputBox(anything())).thenReturn(Promise.resolve(undefined)); + + await (provider as any).sliderSetStep(cell); + + verify(mockedVSCodeNamespaces.workspace.applyEdit(anything())).never(); + }); + }); + + suite('deepnote.selectInputChooseOption', () => { + test('should update single select value', async () => { + const cell = createMockCell({ + __deepnotePocket: { type: 'input-select' }, + deepnote_variable_select_type: 'from-options', + deepnote_variable_options: ['option1', 'option2', 'option3'], + deepnote_variable_value: 'option1', + deepnote_allow_multiple_values: false + }); + + when(mockedVSCodeNamespaces.window.showQuickPick(anything(), anything())).thenReturn( + Promise.resolve({ label: 'option2' } as any) + ); + when(mockedVSCodeNamespaces.workspace.applyEdit(anything())).thenReturn(Promise.resolve(true)); + + await (provider as any).selectInputChooseOption(cell); + + verify(mockedVSCodeNamespaces.window.showQuickPick(anything(), anything())).once(); + verify(mockedVSCodeNamespaces.workspace.applyEdit(anything())).once(); + }); + + test('should not update if user cancels single select', async () => { + const cell = createMockCell({ + __deepnotePocket: { type: 'input-select' }, + deepnote_variable_select_type: 'from-options', + deepnote_variable_options: ['option1', 'option2'], + deepnote_allow_multiple_values: false + }); + + when(mockedVSCodeNamespaces.window.showQuickPick(anything(), anything())).thenReturn( + Promise.resolve(undefined) + ); + + await (provider as any).selectInputChooseOption(cell); + + verify(mockedVSCodeNamespaces.workspace.applyEdit(anything())).never(); + }); + + test('should show info message for from-variable select type', async () => { + const cell = createMockCell({ + __deepnotePocket: { type: 'input-select' }, + deepnote_variable_select_type: 'from-variable', + deepnote_variable_selected_variable: 'my_options' + }); + + await (provider as any).selectInputChooseOption(cell); + + verify(mockedVSCodeNamespaces.window.showInformationMessage(anything())).once(); + verify(mockedVSCodeNamespaces.workspace.applyEdit(anything())).never(); + }); + + test('should show warning if no options available', async () => { + const cell = createMockCell({ + __deepnotePocket: { type: 'input-select' }, + deepnote_variable_select_type: 'from-options', + deepnote_variable_options: [] + }); + + await (provider as any).selectInputChooseOption(cell); + + verify(mockedVSCodeNamespaces.window.showWarningMessage(anything())).once(); + }); + }); + + suite('deepnote.selectInputSettings', () => { + test('should open settings webview and fire status bar update', async () => { + const cell = createMockCell({ + __deepnotePocket: { type: 'input-select' } + }); + + // Mock the webview show method + const webview = (provider as any).selectInputSettingsWebview; + let showCalled = false; + webview.show = async () => { + showCalled = true; + }; + + // Track if the event was fired + let eventFired = false; + provider.onDidChangeCellStatusBarItems(() => { + eventFired = true; + }); + + await (provider as any).selectInputSettings(cell); + + expect(showCalled).to.be.true; + expect(eventFired).to.be.true; + }); + }); + + suite('deepnote.fileInputChooseFile', () => { + test('should update file path when file is selected', async () => { + const cell = createMockCell({ + __deepnotePocket: { type: 'input-file' } + }); + + const mockUri = Uri.file('/path/to/file.txt'); + when(mockedVSCodeNamespaces.window.showOpenDialog(anything())).thenReturn(Promise.resolve([mockUri])); + when(mockedVSCodeNamespaces.workspace.applyEdit(anything())).thenReturn(Promise.resolve(true)); + + await (provider as any).fileInputChooseFile(cell); + + verify(mockedVSCodeNamespaces.window.showOpenDialog(anything())).once(); + verify(mockedVSCodeNamespaces.workspace.applyEdit(anything())).once(); + }); + + test('should not update if user cancels file selection', async () => { + const cell = createMockCell({ + __deepnotePocket: { type: 'input-file' } + }); + + when(mockedVSCodeNamespaces.window.showOpenDialog(anything())).thenReturn(Promise.resolve(undefined)); + + await (provider as any).fileInputChooseFile(cell); + + verify(mockedVSCodeNamespaces.workspace.applyEdit(anything())).never(); + }); + + test('should not update if empty array is returned', async () => { + const cell = createMockCell({ + __deepnotePocket: { type: 'input-file' } + }); + + when(mockedVSCodeNamespaces.window.showOpenDialog(anything())).thenReturn(Promise.resolve([])); + + await (provider as any).fileInputChooseFile(cell); + + verify(mockedVSCodeNamespaces.workspace.applyEdit(anything())).never(); + }); + }); + + suite('deepnote.dateInputChooseDate', () => { + test('should update date value when valid date is provided', async () => { + const cell = createMockCell({ + __deepnotePocket: { type: 'input-date' }, + deepnote_variable_value: '2024-01-01' + }); + + when(mockedVSCodeNamespaces.window.showInputBox(anything())).thenReturn(Promise.resolve('2024-12-31')); + when(mockedVSCodeNamespaces.workspace.applyEdit(anything())).thenReturn(Promise.resolve(true)); + + await (provider as any).dateInputChooseDate(cell); + + verify(mockedVSCodeNamespaces.window.showInputBox(anything())).once(); + verify(mockedVSCodeNamespaces.workspace.applyEdit(anything())).once(); + }); + + test('should not update if user cancels date input', async () => { + const cell = createMockCell({ + __deepnotePocket: { type: 'input-date' } + }); + + when(mockedVSCodeNamespaces.window.showInputBox(anything())).thenReturn(Promise.resolve(undefined)); + + await (provider as any).dateInputChooseDate(cell); + + verify(mockedVSCodeNamespaces.workspace.applyEdit(anything())).never(); + }); + }); + + suite('deepnote.dateRangeChooseStart', () => { + test('should update start date in date range', async () => { + const cell = createMockCell({ + __deepnotePocket: { type: 'input-date-range' }, + deepnote_variable_value: ['2024-01-01', '2024-12-31'] + }); + + when(mockedVSCodeNamespaces.window.showInputBox(anything())).thenReturn(Promise.resolve('2024-02-01')); + when(mockedVSCodeNamespaces.workspace.applyEdit(anything())).thenReturn(Promise.resolve(true)); + + await (provider as any).dateRangeChooseStart(cell); + + verify(mockedVSCodeNamespaces.window.showInputBox(anything())).once(); + verify(mockedVSCodeNamespaces.workspace.applyEdit(anything())).once(); + }); + + test('should not update if user cancels start date input', async () => { + const cell = createMockCell({ + __deepnotePocket: { type: 'input-date-range' }, + deepnote_variable_value: ['2024-01-01', '2024-12-31'] + }); + + when(mockedVSCodeNamespaces.window.showInputBox(anything())).thenReturn(Promise.resolve(undefined)); + + await (provider as any).dateRangeChooseStart(cell); + + verify(mockedVSCodeNamespaces.workspace.applyEdit(anything())).never(); + }); + + test('should show warning if start date is after end date', async () => { + const cell = createMockCell({ + __deepnotePocket: { type: 'input-date-range' }, + deepnote_variable_value: ['2024-01-01', '2024-06-30'] + }); + + when(mockedVSCodeNamespaces.window.showInputBox(anything())).thenReturn(Promise.resolve('2024-12-31')); + when(mockedVSCodeNamespaces.workspace.applyEdit(anything())).thenReturn(Promise.resolve(true)); + + await (provider as any).dateRangeChooseStart(cell); + + verify(mockedVSCodeNamespaces.window.showWarningMessage(anything())).once(); + verify(mockedVSCodeNamespaces.workspace.applyEdit(anything())).once(); + }); + }); + + suite('deepnote.dateRangeChooseEnd', () => { + test('should update end date in date range', async () => { + const cell = createMockCell({ + __deepnotePocket: { type: 'input-date-range' }, + deepnote_variable_value: ['2024-01-01', '2024-12-31'] + }); + + when(mockedVSCodeNamespaces.window.showInputBox(anything())).thenReturn(Promise.resolve('2024-11-30')); + when(mockedVSCodeNamespaces.workspace.applyEdit(anything())).thenReturn(Promise.resolve(true)); + + await (provider as any).dateRangeChooseEnd(cell); + + verify(mockedVSCodeNamespaces.window.showInputBox(anything())).once(); + verify(mockedVSCodeNamespaces.workspace.applyEdit(anything())).once(); + }); + + test('should not update if user cancels end date input', async () => { + const cell = createMockCell({ + __deepnotePocket: { type: 'input-date-range' }, + deepnote_variable_value: ['2024-01-01', '2024-12-31'] + }); + + when(mockedVSCodeNamespaces.window.showInputBox(anything())).thenReturn(Promise.resolve(undefined)); + + await (provider as any).dateRangeChooseEnd(cell); + + verify(mockedVSCodeNamespaces.workspace.applyEdit(anything())).never(); + }); + + test('should show warning if end date is before start date', async () => { + const cell = createMockCell({ + __deepnotePocket: { type: 'input-date-range' }, + deepnote_variable_value: ['2024-06-01', '2024-12-31'] + }); + + when(mockedVSCodeNamespaces.window.showInputBox(anything())).thenReturn(Promise.resolve('2024-01-01')); + when(mockedVSCodeNamespaces.workspace.applyEdit(anything())).thenReturn(Promise.resolve(true)); + + await (provider as any).dateRangeChooseEnd(cell); + + verify(mockedVSCodeNamespaces.window.showWarningMessage(anything())).once(); + verify(mockedVSCodeNamespaces.workspace.applyEdit(anything())).once(); + }); + }); + }); }); From 459f6d6564982112752974c19f330e387101cb6e Mon Sep 17 00:00:00 2001 From: jankuca Date: Fri, 24 Oct 2025 17:56:38 +0200 Subject: [PATCH 061/101] remove unused imports --- .../deepnoteInputBlockCellStatusBarProvider.unit.test.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/notebooks/deepnote/deepnoteInputBlockCellStatusBarProvider.unit.test.ts b/src/notebooks/deepnote/deepnoteInputBlockCellStatusBarProvider.unit.test.ts index be068873d0..987db8f732 100644 --- a/src/notebooks/deepnote/deepnoteInputBlockCellStatusBarProvider.unit.test.ts +++ b/src/notebooks/deepnote/deepnoteInputBlockCellStatusBarProvider.unit.test.ts @@ -2,9 +2,9 @@ // Licensed under the MIT License. import { expect } from 'chai'; -import { anything, instance, mock, verify, when } from 'ts-mockito'; +import { anything, verify, when } from 'ts-mockito'; import { DeepnoteInputBlockCellStatusBarItemProvider } from './deepnoteInputBlockCellStatusBarProvider'; -import { NotebookCell, NotebookCellKind, NotebookDocument, NotebookEdit, WorkspaceEdit } from 'vscode'; +import { NotebookCell, NotebookCellKind, NotebookDocument } from 'vscode'; import { Uri } from 'vscode'; import type { IExtensionContext } from '../../platform/common/types'; import { mockedVSCodeNamespaces, resetVSCodeMocks } from '../../test/vscode-mock'; From dfacb82acfc0854982f612bf444b3e242113725a Mon Sep 17 00:00:00 2001 From: jankuca Date: Mon, 27 Oct 2025 14:38:34 +0100 Subject: [PATCH 062/101] polish input converter tests --- .../converters/inputConverters.unit.test.ts | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/src/notebooks/deepnote/converters/inputConverters.unit.test.ts b/src/notebooks/deepnote/converters/inputConverters.unit.test.ts index 29e3e24e5a..483f43b23d 100644 --- a/src/notebooks/deepnote/converters/inputConverters.unit.test.ts +++ b/src/notebooks/deepnote/converters/inputConverters.unit.test.ts @@ -75,6 +75,8 @@ suite('InputTextBlockConverter', () => { const cell = converter.convertToCell(block); + assert.strictEqual(cell.kind, NotebookCellKind.Code); + assert.strictEqual(cell.languageId, 'plaintext'); assert.strictEqual(cell.value, ''); }); }); @@ -98,10 +100,12 @@ suite('InputTextBlockConverter', () => { converter.applyChangesToBlock(block, cell); assert.strictEqual(block.content, ''); - assert.strictEqual(block.metadata?.deepnote_variable_value, 'new text value'); - // Other metadata should be preserved - assert.strictEqual(block.metadata?.deepnote_input_label, 'existing label'); - assert.strictEqual(block.metadata?.deepnote_variable_name, 'input_1'); + assert.deepStrictEqual(block.metadata, { + deepnote_variable_value: 'new text value', + // Other metadata should be preserved + deepnote_input_label: 'existing label', + deepnote_variable_name: 'input_1' + }); }); test('handles empty value', () => { @@ -535,7 +539,7 @@ suite('InputSliderBlockConverter', () => { converter.applyChangesToBlock(block, cell); - // Should fall back to existing value (parsed as number) + // Should fall back to existing value (string per metadata contract) assert.strictEqual(block.metadata?.deepnote_variable_value, '5'); }); }); From c5bb4ec2e6052483b4b8ef27fd6f543fab164246 Mon Sep 17 00:00:00 2001 From: jankuca Date: Mon, 27 Oct 2025 14:39:36 +0100 Subject: [PATCH 063/101] respect cancl token --- .../deepnote/deepnoteInputBlockCellStatusBarProvider.ts | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/notebooks/deepnote/deepnoteInputBlockCellStatusBarProvider.ts b/src/notebooks/deepnote/deepnoteInputBlockCellStatusBarProvider.ts index 5911afc0a6..f14192e4bf 100644 --- a/src/notebooks/deepnote/deepnoteInputBlockCellStatusBarProvider.ts +++ b/src/notebooks/deepnote/deepnoteInputBlockCellStatusBarProvider.ts @@ -2,6 +2,7 @@ // Licensed under the MIT License. import { + CancellationToken, Disposable, EventEmitter, NotebookCell, @@ -208,7 +209,11 @@ export class DeepnoteInputBlockCellStatusBarItemProvider return undefined; } - provideCellStatusBarItems(cell: NotebookCell): NotebookCellStatusBarItem[] | undefined { + provideCellStatusBarItems(cell: NotebookCell, token: CancellationToken): NotebookCellStatusBarItem[] | undefined { + if (token.isCancellationRequested) { + return undefined; + } + // Check if this cell is a Deepnote input block // Get the block type from the __deepnotePocket metadata field const pocket = cell.metadata?.__deepnotePocket as Pocket | undefined; From 2523fcd5f33d837fe50210566ce76ab30c4fbdcc Mon Sep 17 00:00:00 2001 From: jankuca Date: Mon, 27 Oct 2025 14:39:48 +0100 Subject: [PATCH 064/101] fix: avoid max = { deepnote_slider_min_value: newMin }; + const currentMax = metadata?.deepnote_slider_max_value as number | undefined; + if (currentMax !== undefined && newMin > currentMax) { + updates.deepnote_slider_max_value = newMin; + void window.showWarningMessage(l10n.t('Min exceeded max; max adjusted to {0}.', String(newMin))); + } + await this.updateCellMetadata(cell, updates); } /** @@ -870,7 +876,13 @@ export class DeepnoteInputBlockCellStatusBarItemProvider } const newMax = parseFloat(input); - await this.updateCellMetadata(cell, { deepnote_slider_max_value: newMax }); + const updates: Record = { deepnote_slider_max_value: newMax }; + const currentMin = metadata?.deepnote_slider_min_value as number | undefined; + if (currentMin !== undefined && newMax < currentMin) { + updates.deepnote_slider_min_value = newMax; + void window.showWarningMessage(l10n.t('Max exceeded min; min adjusted to {0}.', String(newMax))); + } + await this.updateCellMetadata(cell, updates); } /** From 9f6c5d357bd34873b0ec4fc74de48812036257b7 Mon Sep 17 00:00:00 2001 From: jankuca Date: Mon, 27 Oct 2025 14:41:39 +0100 Subject: [PATCH 065/101] localize file input block status bar --- .../deepnote/deepnoteInputBlockCellStatusBarProvider.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/notebooks/deepnote/deepnoteInputBlockCellStatusBarProvider.ts b/src/notebooks/deepnote/deepnoteInputBlockCellStatusBarProvider.ts index fe7a364cbc..25b3d12ae7 100644 --- a/src/notebooks/deepnote/deepnoteInputBlockCellStatusBarProvider.ts +++ b/src/notebooks/deepnote/deepnoteInputBlockCellStatusBarProvider.ts @@ -942,12 +942,12 @@ export class DeepnoteInputBlockCellStatusBarItemProvider .filter((ext) => ext.length > 0); if (extensions.length > 0) { - filters['Allowed Files'] = extensions; + filters[l10n.t('Allowed Files')] = extensions; } } // Add "All Files" option - filters['All Files'] = ['*']; + filters[l10n.t('All Files')] = ['*']; const uris = await window.showOpenDialog({ canSelectFiles: true, From cc33ea3585b15dfc2b4e3a6897239e4bfe75088c Mon Sep 17 00:00:00 2001 From: jankuca Date: Mon, 27 Oct 2025 14:44:04 +0100 Subject: [PATCH 066/101] fix: prevent invalid dates --- ...deepnoteInputBlockCellStatusBarProvider.ts | 54 +++++++++++++------ 1 file changed, 39 insertions(+), 15 deletions(-) diff --git a/src/notebooks/deepnote/deepnoteInputBlockCellStatusBarProvider.ts b/src/notebooks/deepnote/deepnoteInputBlockCellStatusBarProvider.ts index 25b3d12ae7..9cf039f82f 100644 --- a/src/notebooks/deepnote/deepnoteInputBlockCellStatusBarProvider.ts +++ b/src/notebooks/deepnote/deepnoteInputBlockCellStatusBarProvider.ts @@ -1000,6 +1000,42 @@ export class DeepnoteInputBlockCellStatusBarItemProvider return `${year}-${month}-${day}`; } + /** + * Strictly validate a YYYY-MM-DD date string. + * Returns the normalized string if valid, or null if invalid. + * Rejects out-of-range dates like "2023-02-30". + */ + private validateStrictDate(value: string): string | null { + // 1) Match YYYY-MM-DD format + const match = /^(\d{4})-(\d{2})-(\d{2})$/.exec(value); + if (!match) { + return null; + } + + // 2) Extract year, month, day as integers + const year = parseInt(match[1], 10); + const month = parseInt(match[2], 10); + const day = parseInt(match[3], 10); + + // 3) Check month is 1..12 + if (month < 1 || month > 12) { + return null; + } + + // 4) Compute correct days-in-month (accounting for leap years) + const isLeapYear = (year % 4 === 0 && year % 100 !== 0) || year % 400 === 0; + const daysInMonth = [31, isLeapYear ? 29 : 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]; + const maxDay = daysInMonth[month - 1]; + + // 5) Ensure day is within range + if (day < 1 || day > maxDay) { + return null; + } + + // Return the normalized string + return value; + } + /** * Handler for date input: choose date */ @@ -1015,11 +1051,7 @@ export class DeepnoteInputBlockCellStatusBarItemProvider if (!value) { return l10n.t('Date cannot be empty'); } - if (!/^\d{4}-\d{2}-\d{2}$/.test(value)) { - return l10n.t('Please enter date in YYYY-MM-DD format'); - } - const date = new Date(value); - if (isNaN(date.getTime())) { + if (this.validateStrictDate(value) === null) { return l10n.t('Invalid date'); } return undefined; @@ -1055,11 +1087,7 @@ export class DeepnoteInputBlockCellStatusBarItemProvider if (!value) { return l10n.t('Date cannot be empty'); } - if (!/^\d{4}-\d{2}-\d{2}$/.test(value)) { - return l10n.t('Please enter date in YYYY-MM-DD format'); - } - const date = new Date(value); - if (isNaN(date.getTime())) { + if (this.validateStrictDate(value) === null) { return l10n.t('Invalid date'); } return undefined; @@ -1100,11 +1128,7 @@ export class DeepnoteInputBlockCellStatusBarItemProvider if (!value) { return l10n.t('Date cannot be empty'); } - if (!/^\d{4}-\d{2}-\d{2}$/.test(value)) { - return l10n.t('Please enter date in YYYY-MM-DD format'); - } - const date = new Date(value); - if (isNaN(date.getTime())) { + if (this.validateStrictDate(value) === null) { return l10n.t('Invalid date'); } return undefined; From 2577c378980f7cb1ed75ba0bb816f66c0f7a8537 Mon Sep 17 00:00:00 2001 From: jankuca Date: Mon, 27 Oct 2025 14:49:05 +0100 Subject: [PATCH 067/101] polish select variable metadata cleanup --- .../deepnote/selectInputSettingsWebview.ts | 29 +++++++++++++++---- 1 file changed, 23 insertions(+), 6 deletions(-) diff --git a/src/notebooks/deepnote/selectInputSettingsWebview.ts b/src/notebooks/deepnote/selectInputSettingsWebview.ts index d5f6a1e939..9bb1b92526 100644 --- a/src/notebooks/deepnote/selectInputSettingsWebview.ts +++ b/src/notebooks/deepnote/selectInputSettingsWebview.ts @@ -146,12 +146,21 @@ export class SelectInputSettingsWebviewProvider { switch (message.type) { case 'save': if (message.settings && this.currentCell) { - await this.saveSettings(message.settings); - if (this.resolvePromise) { - this.resolvePromise(message.settings); - this.resolvePromise = undefined; + try { + await this.saveSettings(message.settings); + if (this.resolvePromise) { + this.resolvePromise(message.settings); + this.resolvePromise = undefined; + } + this.currentPanel?.dispose(); + } catch (error) { + // Error is already shown to user in saveSettings, just reject the promise + if (this.resolvePromise) { + this.resolvePromise(null); + this.resolvePromise = undefined; + } + // Keep panel open so user can retry or cancel } - this.currentPanel?.dispose(); } break; @@ -182,12 +191,20 @@ export class SelectInputSettingsWebviewProvider { // Update the options field based on the select type if (settings.selectType === 'from-options') { metadata.deepnote_variable_options = settings.options; + } else { + // Clear stale options when not using 'from-options' mode + delete metadata.deepnote_variable_options; } // Update cell metadata to preserve outputs and attachments edit.set(this.currentCell.notebook.uri, [NotebookEdit.updateCellMetadata(this.currentCell.index, metadata)]); - await workspace.applyEdit(edit); + const success = await workspace.applyEdit(edit); + if (!success) { + const errorMessage = l10n.t('Failed to save select input settings'); + void window.showErrorMessage(errorMessage); + throw new Error(errorMessage); + } } private getWebviewContent(): string { From f6567f33f3073a1c47a5fef896ab298495e66aae Mon Sep 17 00:00:00 2001 From: jankuca Date: Mon, 27 Oct 2025 14:49:48 +0100 Subject: [PATCH 068/101] replace event.data cast with type param --- .../selectInputSettings/SelectInputSettingsPanel.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/webviews/webview-side/selectInputSettings/SelectInputSettingsPanel.tsx b/src/webviews/webview-side/selectInputSettings/SelectInputSettingsPanel.tsx index 2242d58e17..3189050d49 100644 --- a/src/webviews/webview-side/selectInputSettings/SelectInputSettingsPanel.tsx +++ b/src/webviews/webview-side/selectInputSettings/SelectInputSettingsPanel.tsx @@ -23,8 +23,8 @@ export const SelectInputSettingsPanel: React.FC const [newOption, setNewOption] = React.useState(''); React.useEffect(() => { - const handleMessage = (event: MessageEvent) => { - const message = event.data as WebviewMessage; + const handleMessage = (event: MessageEvent) => { + const message = event.data; switch (message.type) { case 'init': From cbd0cc859a64a38b763335514a59ff3989f06266 Mon Sep 17 00:00:00 2001 From: jankuca Date: Mon, 27 Oct 2025 14:51:35 +0100 Subject: [PATCH 069/101] polish localization of select input settings webview --- src/notebooks/deepnote/selectInputSettingsWebview.ts | 4 ++-- src/platform/common/utils/localize.ts | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/notebooks/deepnote/selectInputSettingsWebview.ts b/src/notebooks/deepnote/selectInputSettingsWebview.ts index 9bb1b92526..7e7dc2e989 100644 --- a/src/notebooks/deepnote/selectInputSettingsWebview.ts +++ b/src/notebooks/deepnote/selectInputSettingsWebview.ts @@ -47,7 +47,7 @@ export class SelectInputSettingsWebviewProvider { // Create a new panel this.currentPanel = window.createWebviewPanel( 'deepnoteSelectInputSettings', - l10n.t('Select Input Settings'), + localize.SelectInputSettings.title, column || ViewColumn.One, { enableScripts: true, @@ -238,7 +238,7 @@ export class SelectInputSettingsWebviewProvider { ) ); - const title = l10n.t('Select Input Settings'); + const title = localize.SelectInputSettings.title; return ` diff --git a/src/platform/common/utils/localize.ts b/src/platform/common/utils/localize.ts index 13118e08c8..01316d7b03 100644 --- a/src/platform/common/utils/localize.ts +++ b/src/platform/common/utils/localize.ts @@ -860,7 +860,7 @@ export namespace Integrations { } export namespace SelectInputSettings { - export const title = l10n.t('Settings'); + export const title = l10n.t('Select Input Settings'); export const allowMultipleValues = l10n.t('Allow to select multiple values'); export const allowEmptyValue = l10n.t('Allow empty value'); export const valueSourceTitle = l10n.t('Value'); From f4783007660119b064408b3d6b5968ba7746a5ce Mon Sep 17 00:00:00 2001 From: jankuca Date: Mon, 27 Oct 2025 14:53:49 +0100 Subject: [PATCH 070/101] avoid dupe select box options --- .../SelectInputSettingsPanel.tsx | 28 +++++++++++++++---- 1 file changed, 22 insertions(+), 6 deletions(-) diff --git a/src/webviews/webview-side/selectInputSettings/SelectInputSettingsPanel.tsx b/src/webviews/webview-side/selectInputSettings/SelectInputSettingsPanel.tsx index 3189050d49..228563d828 100644 --- a/src/webviews/webview-side/selectInputSettings/SelectInputSettingsPanel.tsx +++ b/src/webviews/webview-side/selectInputSettings/SelectInputSettingsPanel.tsx @@ -60,13 +60,29 @@ export const SelectInputSettingsPanel: React.FC }; const handleAddOption = () => { - if (newOption.trim()) { - setSettings((prev) => ({ - ...prev, - options: [...prev.options, newOption.trim()] - })); - setNewOption(''); + const trimmedValue = newOption.trim(); + + // Check if the trimmed value is non-empty + if (!trimmedValue) { + return; + } + + // Normalize for comparison (case-insensitive) + const normalizedValue = trimmedValue.toLowerCase(); + + // Check if the normalized value is already present in options + const isDuplicate = settings.options.some((option) => option.toLowerCase() === normalizedValue); + + if (isDuplicate) { + return; } + + // Add the trimmed value and clear input + setSettings((prev) => ({ + ...prev, + options: [...prev.options, trimmedValue] + })); + setNewOption(''); }; const handleRemoveOption = (index: number) => { From 84f4404e7fcddb9b901269edae7e6dec6a97249b Mon Sep 17 00:00:00 2001 From: jankuca Date: Mon, 27 Oct 2025 14:56:32 +0100 Subject: [PATCH 071/101] add accessible labels to select input settings --- .../SelectInputSettingsPanel.tsx | 26 ++++++++++++++----- .../selectInputSettings.css | 12 +++++++++ 2 files changed, 31 insertions(+), 7 deletions(-) diff --git a/src/webviews/webview-side/selectInputSettings/SelectInputSettingsPanel.tsx b/src/webviews/webview-side/selectInputSettings/SelectInputSettingsPanel.tsx index 228563d828..6948ce3a79 100644 --- a/src/webviews/webview-side/selectInputSettings/SelectInputSettingsPanel.tsx +++ b/src/webviews/webview-side/selectInputSettings/SelectInputSettingsPanel.tsx @@ -179,8 +179,12 @@ export const SelectInputSettingsPanel: React.FC ))}
+ setNewOption(e.target.value)} onKeyDown={(e) => { @@ -190,6 +194,7 @@ export const SelectInputSettingsPanel: React.FC } }} placeholder={getLocString('addOptionPlaceholder', 'Add option...')} + aria-label="Option name" />
{settings.selectType === 'from-variable' && ( - + <> + + + )}
diff --git a/src/webviews/webview-side/selectInputSettings/selectInputSettings.css b/src/webviews/webview-side/selectInputSettings/selectInputSettings.css index a8b98f72e2..1897688ba3 100644 --- a/src/webviews/webview-side/selectInputSettings/selectInputSettings.css +++ b/src/webviews/webview-side/selectInputSettings/selectInputSettings.css @@ -4,6 +4,18 @@ margin: 0 auto; } +.visually-hidden { + position: absolute; + width: 1px; + height: 1px; + padding: 0; + margin: -1px; + overflow: hidden; + clip: rect(0, 0, 0, 0); + white-space: nowrap; + border-width: 0; +} + .select-input-settings-panel h1 { font-size: 24px; margin-bottom: 20px; From 18b3fafa4c96d0b31c5f899a95378400022889f2 Mon Sep 17 00:00:00 2001 From: jankuca Date: Mon, 27 Oct 2025 14:58:32 +0100 Subject: [PATCH 072/101] polish localization of select input settings --- src/messageTypes.ts | 1 + src/notebooks/deepnote/selectInputSettingsWebview.ts | 1 + src/platform/common/utils/localize.ts | 1 + .../selectInputSettings/SelectInputSettingsPanel.tsx | 2 +- 4 files changed, 4 insertions(+), 1 deletion(-) diff --git a/src/messageTypes.ts b/src/messageTypes.ts index c615139e46..2ce7d1adf6 100644 --- a/src/messageTypes.ts +++ b/src/messageTypes.ts @@ -216,6 +216,7 @@ export type LocalizedMessages = { fromVariable: string; fromVariableDescription: string; variablePlaceholder: string; + removeOptionAriaLabel: string; saveButton: string; cancelButton: string; }; diff --git a/src/notebooks/deepnote/selectInputSettingsWebview.ts b/src/notebooks/deepnote/selectInputSettingsWebview.ts index 7e7dc2e989..f706313908 100644 --- a/src/notebooks/deepnote/selectInputSettingsWebview.ts +++ b/src/notebooks/deepnote/selectInputSettingsWebview.ts @@ -132,6 +132,7 @@ export class SelectInputSettingsWebviewProvider { fromVariable: localize.SelectInputSettings.fromVariable, fromVariableDescription: localize.SelectInputSettings.fromVariableDescription, variablePlaceholder: localize.SelectInputSettings.variablePlaceholder, + removeOptionAriaLabel: localize.SelectInputSettings.removeOptionAriaLabel, saveButton: localize.SelectInputSettings.saveButton, cancelButton: localize.SelectInputSettings.cancelButton }; diff --git a/src/platform/common/utils/localize.ts b/src/platform/common/utils/localize.ts index 01316d7b03..8796708b7e 100644 --- a/src/platform/common/utils/localize.ts +++ b/src/platform/common/utils/localize.ts @@ -871,6 +871,7 @@ export namespace SelectInputSettings { export const fromVariable = l10n.t('From variable'); export const fromVariableDescription = l10n.t('A list or Series that contains only strings, numbers or booleans.'); export const variablePlaceholder = l10n.t('Variable name...'); + export const removeOptionAriaLabel = l10n.t('Remove option'); export const saveButton = l10n.t('Save'); export const cancelButton = l10n.t('Cancel'); } diff --git a/src/webviews/webview-side/selectInputSettings/SelectInputSettingsPanel.tsx b/src/webviews/webview-side/selectInputSettings/SelectInputSettingsPanel.tsx index 6948ce3a79..8afb22f227 100644 --- a/src/webviews/webview-side/selectInputSettings/SelectInputSettingsPanel.tsx +++ b/src/webviews/webview-side/selectInputSettings/SelectInputSettingsPanel.tsx @@ -171,7 +171,7 @@ export const SelectInputSettingsPanel: React.FC From 51acc124519f943061e135980b6b6de30e0098ae Mon Sep 17 00:00:00 2001 From: jankuca Date: Mon, 27 Oct 2025 15:00:06 +0100 Subject: [PATCH 073/101] disposable in test --- ...pnoteInputBlockCellStatusBarProvider.unit.test.ts | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/src/notebooks/deepnote/deepnoteInputBlockCellStatusBarProvider.unit.test.ts b/src/notebooks/deepnote/deepnoteInputBlockCellStatusBarProvider.unit.test.ts index 987db8f732..0ae6e828fe 100644 --- a/src/notebooks/deepnote/deepnoteInputBlockCellStatusBarProvider.unit.test.ts +++ b/src/notebooks/deepnote/deepnoteInputBlockCellStatusBarProvider.unit.test.ts @@ -496,14 +496,18 @@ suite('DeepnoteInputBlockCellStatusBarItemProvider', () => { // Track if the event was fired let eventFired = false; - provider.onDidChangeCellStatusBarItems(() => { + const disposable = provider.onDidChangeCellStatusBarItems(() => { eventFired = true; }); - await (provider as any).selectInputSettings(cell); + try { + await (provider as any).selectInputSettings(cell); - expect(showCalled).to.be.true; - expect(eventFired).to.be.true; + expect(showCalled).to.be.true; + expect(eventFired).to.be.true; + } finally { + disposable.dispose(); + } }); }); From 002e44a559f13ed57679737dd3547a01a8b026ad Mon Sep 17 00:00:00 2001 From: jankuca Date: Mon, 27 Oct 2025 15:00:31 +0100 Subject: [PATCH 074/101] reorganize imports in input block tests --- .../deepnoteInputBlockCellStatusBarProvider.unit.test.ts | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/notebooks/deepnote/deepnoteInputBlockCellStatusBarProvider.unit.test.ts b/src/notebooks/deepnote/deepnoteInputBlockCellStatusBarProvider.unit.test.ts index 0ae6e828fe..f2f75d4436 100644 --- a/src/notebooks/deepnote/deepnoteInputBlockCellStatusBarProvider.unit.test.ts +++ b/src/notebooks/deepnote/deepnoteInputBlockCellStatusBarProvider.unit.test.ts @@ -3,11 +3,12 @@ import { expect } from 'chai'; import { anything, verify, when } from 'ts-mockito'; -import { DeepnoteInputBlockCellStatusBarItemProvider } from './deepnoteInputBlockCellStatusBarProvider'; -import { NotebookCell, NotebookCellKind, NotebookDocument } from 'vscode'; -import { Uri } from 'vscode'; + +import { NotebookCell, NotebookCellKind, NotebookDocument, Uri } from 'vscode'; + import type { IExtensionContext } from '../../platform/common/types'; import { mockedVSCodeNamespaces, resetVSCodeMocks } from '../../test/vscode-mock'; +import { DeepnoteInputBlockCellStatusBarItemProvider } from './deepnoteInputBlockCellStatusBarProvider'; suite('DeepnoteInputBlockCellStatusBarItemProvider', () => { let provider: DeepnoteInputBlockCellStatusBarItemProvider; From bb7c4f58132efadeec9188300324c89eec771abb Mon Sep 17 00:00:00 2001 From: jankuca Date: Mon, 27 Oct 2025 15:06:23 +0100 Subject: [PATCH 075/101] fixup token --- ...putBlockCellStatusBarProvider.unit.test.ts | 45 ++++++++++--------- 1 file changed, 25 insertions(+), 20 deletions(-) diff --git a/src/notebooks/deepnote/deepnoteInputBlockCellStatusBarProvider.unit.test.ts b/src/notebooks/deepnote/deepnoteInputBlockCellStatusBarProvider.unit.test.ts index f2f75d4436..082c2fe3c4 100644 --- a/src/notebooks/deepnote/deepnoteInputBlockCellStatusBarProvider.unit.test.ts +++ b/src/notebooks/deepnote/deepnoteInputBlockCellStatusBarProvider.unit.test.ts @@ -4,7 +4,7 @@ import { expect } from 'chai'; import { anything, verify, when } from 'ts-mockito'; -import { NotebookCell, NotebookCellKind, NotebookDocument, Uri } from 'vscode'; +import { CancellationToken, NotebookCell, NotebookCellKind, NotebookDocument, Uri } from 'vscode'; import type { IExtensionContext } from '../../platform/common/types'; import { mockedVSCodeNamespaces, resetVSCodeMocks } from '../../test/vscode-mock'; @@ -13,11 +13,16 @@ import { DeepnoteInputBlockCellStatusBarItemProvider } from './deepnoteInputBloc suite('DeepnoteInputBlockCellStatusBarItemProvider', () => { let provider: DeepnoteInputBlockCellStatusBarItemProvider; let mockExtensionContext: IExtensionContext; + let mockToken: CancellationToken; setup(() => { mockExtensionContext = { subscriptions: [] } as any; + mockToken = { + isCancellationRequested: false, + onCancellationRequested: () => ({ dispose: () => {} }) + } as any; provider = new DeepnoteInputBlockCellStatusBarItemProvider(mockExtensionContext); }); @@ -60,7 +65,7 @@ suite('DeepnoteInputBlockCellStatusBarItemProvider', () => { suite('Input Block Type Detection', () => { test('Should return status bar items for input-text block', () => { const cell = createMockCell({ __deepnotePocket: { type: 'input-text' } }); - const items = provider.provideCellStatusBarItems(cell); + const items = provider.provideCellStatusBarItems(cell, mockToken); expect(items).to.not.be.undefined; expect(items).to.have.length.at.least(2); @@ -70,7 +75,7 @@ suite('DeepnoteInputBlockCellStatusBarItemProvider', () => { test('Should return status bar items for input-textarea block', () => { const cell = createMockCell({ __deepnotePocket: { type: 'input-textarea' } }); - const items = provider.provideCellStatusBarItems(cell); + const items = provider.provideCellStatusBarItems(cell, mockToken); expect(items).to.not.be.undefined; expect(items).to.have.length.at.least(2); @@ -79,7 +84,7 @@ suite('DeepnoteInputBlockCellStatusBarItemProvider', () => { test('Should return status bar items for input-select block', () => { const cell = createMockCell({ __deepnotePocket: { type: 'input-select' } }); - const items = provider.provideCellStatusBarItems(cell); + const items = provider.provideCellStatusBarItems(cell, mockToken); expect(items).to.not.be.undefined; expect(items).to.have.length.at.least(2); // Type label, variable, and selection button @@ -88,7 +93,7 @@ suite('DeepnoteInputBlockCellStatusBarItemProvider', () => { test('Should return status bar items for input-slider block', () => { const cell = createMockCell({ __deepnotePocket: { type: 'input-slider' } }); - const items = provider.provideCellStatusBarItems(cell); + const items = provider.provideCellStatusBarItems(cell, mockToken); expect(items).to.not.be.undefined; expect(items).to.have.length.at.least(2); // Type label, variable, min, and max buttons @@ -97,7 +102,7 @@ suite('DeepnoteInputBlockCellStatusBarItemProvider', () => { test('Should return status bar items for input-checkbox block', () => { const cell = createMockCell({ __deepnotePocket: { type: 'input-checkbox' } }); - const items = provider.provideCellStatusBarItems(cell); + const items = provider.provideCellStatusBarItems(cell, mockToken); expect(items).to.not.be.undefined; expect(items).to.have.length.at.least(2); // Type label, variable, and toggle button @@ -106,7 +111,7 @@ suite('DeepnoteInputBlockCellStatusBarItemProvider', () => { test('Should return status bar items for input-date block', () => { const cell = createMockCell({ __deepnotePocket: { type: 'input-date' } }); - const items = provider.provideCellStatusBarItems(cell); + const items = provider.provideCellStatusBarItems(cell, mockToken); expect(items).to.not.be.undefined; expect(items).to.have.length.at.least(2); // Type label, variable, and date button @@ -115,7 +120,7 @@ suite('DeepnoteInputBlockCellStatusBarItemProvider', () => { test('Should return status bar items for input-date-range block', () => { const cell = createMockCell({ __deepnotePocket: { type: 'input-date-range' } }); - const items = provider.provideCellStatusBarItems(cell); + const items = provider.provideCellStatusBarItems(cell, mockToken); expect(items).to.not.be.undefined; expect(items).to.have.length.at.least(2); // Type label, variable, start, and end buttons @@ -124,7 +129,7 @@ suite('DeepnoteInputBlockCellStatusBarItemProvider', () => { test('Should return status bar items for input-file block', () => { const cell = createMockCell({ __deepnotePocket: { type: 'input-file' } }); - const items = provider.provideCellStatusBarItems(cell); + const items = provider.provideCellStatusBarItems(cell, mockToken); expect(items).to.not.be.undefined; expect(items).to.have.lengthOf(3); // Type label, variable, and choose file button @@ -133,7 +138,7 @@ suite('DeepnoteInputBlockCellStatusBarItemProvider', () => { test('Should return status bar items for button block', () => { const cell = createMockCell({ __deepnotePocket: { type: 'button' } }); - const items = provider.provideCellStatusBarItems(cell); + const items = provider.provideCellStatusBarItems(cell, mockToken); expect(items).to.not.be.undefined; expect(items).to.have.length.at.least(2); @@ -144,35 +149,35 @@ suite('DeepnoteInputBlockCellStatusBarItemProvider', () => { suite('Non-Input Block Types', () => { test('Should return undefined for code block', () => { const cell = createMockCell({ __deepnotePocket: { type: 'code' } }); - const items = provider.provideCellStatusBarItems(cell); + const items = provider.provideCellStatusBarItems(cell, mockToken); expect(items).to.be.undefined; }); test('Should return undefined for sql block', () => { const cell = createMockCell({ __deepnotePocket: { type: 'sql' } }); - const items = provider.provideCellStatusBarItems(cell); + const items = provider.provideCellStatusBarItems(cell, mockToken); expect(items).to.be.undefined; }); test('Should return undefined for markdown block', () => { const cell = createMockCell({ __deepnotePocket: { type: 'text-cell-p' } }); - const items = provider.provideCellStatusBarItems(cell); + const items = provider.provideCellStatusBarItems(cell, mockToken); expect(items).to.be.undefined; }); test('Should return undefined for cell with no type metadata', () => { const cell = createMockCell({}); - const items = provider.provideCellStatusBarItems(cell); + const items = provider.provideCellStatusBarItems(cell, mockToken); expect(items).to.be.undefined; }); test('Should return undefined for cell with undefined metadata', () => { const cell = createMockCell(undefined); - const items = provider.provideCellStatusBarItems(cell); + const items = provider.provideCellStatusBarItems(cell, mockToken); expect(items).to.be.undefined; }); @@ -181,21 +186,21 @@ suite('DeepnoteInputBlockCellStatusBarItemProvider', () => { suite('Status Bar Item Properties', () => { test('Should have correct tooltip for input-text', () => { const cell = createMockCell({ __deepnotePocket: { type: 'input-text' } }); - const items = provider.provideCellStatusBarItems(cell); + const items = provider.provideCellStatusBarItems(cell, mockToken); expect(items?.[0].tooltip).to.equal('Deepnote Input Text'); }); test('Should have correct tooltip for button', () => { const cell = createMockCell({ __deepnotePocket: { type: 'button' } }); - const items = provider.provideCellStatusBarItems(cell); + const items = provider.provideCellStatusBarItems(cell, mockToken); expect(items?.[0].tooltip).to.equal('Deepnote Button'); }); test('Should format multi-word block types correctly', () => { const cell = createMockCell({ __deepnotePocket: { type: 'input-date-range' } }); - const items = provider.provideCellStatusBarItems(cell); + const items = provider.provideCellStatusBarItems(cell, mockToken); expect(items?.[0].text).to.equal('Input Date Range'); expect(items?.[0].tooltip).to.equal('Deepnote Input Date Range'); @@ -205,7 +210,7 @@ suite('DeepnoteInputBlockCellStatusBarItemProvider', () => { suite('Case Insensitivity', () => { test('Should handle uppercase block type', () => { const cell = createMockCell({ __deepnotePocket: { type: 'INPUT-TEXT' } }); - const items = provider.provideCellStatusBarItems(cell); + const items = provider.provideCellStatusBarItems(cell, mockToken); expect(items).to.not.be.undefined; expect(items?.[0].text).to.equal('INPUT TEXT'); @@ -213,7 +218,7 @@ suite('DeepnoteInputBlockCellStatusBarItemProvider', () => { test('Should handle mixed case block type', () => { const cell = createMockCell({ __deepnotePocket: { type: 'Input-Text' } }); - const items = provider.provideCellStatusBarItems(cell); + const items = provider.provideCellStatusBarItems(cell, mockToken); expect(items).to.not.be.undefined; expect(items?.[0].text).to.equal('Input Text'); From d948c28aca62303a7d71f26ebf7e62ca812141df Mon Sep 17 00:00:00 2001 From: jankuca Date: Wed, 29 Oct 2025 12:15:37 +0100 Subject: [PATCH 076/101] empty function -> return undefined --- .../deepnoteInputBlockCellStatusBarProvider.unit.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/notebooks/deepnote/deepnoteInputBlockCellStatusBarProvider.unit.test.ts b/src/notebooks/deepnote/deepnoteInputBlockCellStatusBarProvider.unit.test.ts index 082c2fe3c4..fb85496ee0 100644 --- a/src/notebooks/deepnote/deepnoteInputBlockCellStatusBarProvider.unit.test.ts +++ b/src/notebooks/deepnote/deepnoteInputBlockCellStatusBarProvider.unit.test.ts @@ -21,7 +21,7 @@ suite('DeepnoteInputBlockCellStatusBarItemProvider', () => { } as any; mockToken = { isCancellationRequested: false, - onCancellationRequested: () => ({ dispose: () => {} }) + onCancellationRequested: () => ({ dispose: () => undefined }) } as any; provider = new DeepnoteInputBlockCellStatusBarItemProvider(mockExtensionContext); }); From e09992880ec47f306b000a0f8d584ee12f56ad4c Mon Sep 17 00:00:00 2001 From: jankuca Date: Wed, 29 Oct 2025 12:24:03 +0100 Subject: [PATCH 077/101] fix package lock file --- package-lock.json | 162 +++++++++++++++++----------------------------- 1 file changed, 61 insertions(+), 101 deletions(-) diff --git a/package-lock.json b/package-lock.json index 01f09aecf3..852dcc923e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "vscode-deepnote", - "version": "0.1.0", + "version": "0.2.0", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "vscode-deepnote", - "version": "0.1.0", + "version": "0.2.0", "hasInstallScript": true, "license": "MIT", "dependencies": { @@ -222,7 +222,7 @@ "util": "^0.12.4" }, "engines": { - "vscode": "^1.103.0" + "vscode": "^1.95.0" }, "optionalDependencies": { "fsevents": "^2.3.2" @@ -458,7 +458,6 @@ "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.22.5.tgz", "integrity": "sha512-SBuTAjg91A3eKOvD+bPEz3LlhHZRNu1nFOVts9lzDJTXshHTjII0BAtDS3Y2DAkdZdDKWVZGVwkDfc4Clxn1dg==", "dev": true, - "peer": true, "dependencies": { "@ampproject/remapping": "^2.2.0", "@babel/code-frame": "^7.22.5", @@ -968,8 +967,7 @@ "resolved": "https://registry.npmjs.org/@cspell/dict-css/-/dict-css-4.0.18.tgz", "integrity": "sha512-EF77RqROHL+4LhMGW5NTeKqfUd/e4OOv6EDFQ/UQQiFyWuqkEKyEz0NDILxOFxWUEVdjT2GQ2cC7t12B6pESwg==", "dev": true, - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/@cspell/dict-dart": { "version": "2.3.1", @@ -1109,16 +1107,14 @@ "resolved": "https://registry.npmjs.org/@cspell/dict-html/-/dict-html-4.0.12.tgz", "integrity": "sha512-JFffQ1dDVEyJq6tCDWv0r/RqkdSnV43P2F/3jJ9rwLgdsOIXwQbXrz6QDlvQLVvNSnORH9KjDtenFTGDyzfCaA==", "dev": true, - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/@cspell/dict-html-symbol-entities": { "version": "4.0.4", "resolved": "https://registry.npmjs.org/@cspell/dict-html-symbol-entities/-/dict-html-symbol-entities-4.0.4.tgz", "integrity": "sha512-afea+0rGPDeOV9gdO06UW183Qg6wRhWVkgCFwiO3bDupAoyXRuvupbb5nUyqSTsLXIKL8u8uXQlJ9pkz07oVXw==", "dev": true, - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/@cspell/dict-java": { "version": "5.0.12", @@ -1316,8 +1312,7 @@ "resolved": "https://registry.npmjs.org/@cspell/dict-typescript/-/dict-typescript-3.2.3.tgz", "integrity": "sha512-zXh1wYsNljQZfWWdSPYwQhpwiuW0KPW1dSd8idjMRvSD0aSvWWHoWlrMsmZeRl4qM4QCEAjua8+cjflm41cQBg==", "dev": true, - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/@cspell/dict-vue": { "version": "3.0.5", @@ -2871,7 +2866,6 @@ "integrity": "sha512-dKYCMuPO1bmrpuogcjQ8z7ICCH3FP6WmxpwC03yjzGfZhj9fTJg6+bS1+UAplekbN2C+M61UNllGOOoAfGCrdQ==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@octokit/auth-token": "^4.0.0", "@octokit/graphql": "^7.1.0", @@ -3032,7 +3026,6 @@ "version": "1.4.1", "resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-1.4.1.tgz", "integrity": "sha512-O2yRJce1GOc6PAy3QxFM4NzFiWzvScDC1/5ihYBL6BUEVdq0XMWN01sppE+H6bBXbaFYipjwFLEWLg5PaSOThA==", - "peer": true, "engines": { "node": ">=8.0.0" } @@ -4102,7 +4095,6 @@ "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-6.9.0.tgz", "integrity": "sha512-GZmjMh4AJ/5gaH4XF2eXA8tMnHWP+Pm1mjQR2QN4Iz+j/zO04b9TOvJYOX2sCNIQHtRStKTxRY1FX7LhpJT4Gw==", "dev": true, - "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "6.9.0", "@typescript-eslint/types": "6.9.0", @@ -4817,7 +4809,6 @@ "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.9.0.tgz", "integrity": "sha512-jaVNAFBHNLXspO543WnNNPZFRtavh3skAkITqD0/2aeMkKZTN+254PyhwxFYrk3vQ1xfY+2wbesJMs/JC8/PwQ==", "dev": true, - "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -4958,6 +4949,7 @@ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", "dev": true, + "peer": true, "dependencies": { "color-convert": "^1.9.0" }, @@ -5630,7 +5622,6 @@ "integrity": "sha512-oxSAxTS1hRfnyit2CL5QpAOS5ixfBjj6ex3yTNvXyY/kE719jQ/IjuESJBK2w5v4wwQRAHGseVJXx9QBYOtFGQ==", "dev": true, "license": "Apache-2.0", - "peer": true, "peerDependencies": { "bare-abort-controller": "*" }, @@ -6038,7 +6029,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "baseline-browser-mapping": "^2.8.9", "caniuse-lite": "^1.0.30001746", @@ -6102,7 +6092,6 @@ "integrity": "sha512-jduaYOYtnio4aIAyc6UbvPCVcgq7nYpVnucyxr6eCYg/Woad9Hf/oxxBRDnGGjPfjUm6j5O/uBWhIu4iLebFaw==", "devOptional": true, "hasInstallScript": true, - "peer": true, "dependencies": { "node-gyp-build": "^4.3.0" }, @@ -6329,7 +6318,6 @@ "resolved": "https://registry.npmjs.org/chai/-/chai-4.3.10.tgz", "integrity": "sha512-0UXG04VuVbruMUYbJ6JctvH0YnC/4q3/AkT18q4NaITo91CUm0liMS9VqzT9vZhVQ/1eqPanMWjBM+Juhfb/9g==", "dev": true, - "peer": true, "dependencies": { "assertion-error": "^1.1.0", "check-error": "^1.0.3", @@ -6381,6 +6369,7 @@ "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", "dev": true, + "peer": true, "dependencies": { "ansi-styles": "^3.2.1", "escape-string-regexp": "^1.0.5", @@ -6766,6 +6755,7 @@ "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", "dev": true, + "peer": true, "dependencies": { "color-name": "1.1.3" } @@ -6774,7 +6764,8 @@ "version": "1.1.3", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=", - "dev": true + "dev": true, + "peer": true }, "node_modules/colorette": { "version": "2.0.20", @@ -8273,7 +8264,6 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/diagnostic-channel/-/diagnostic-channel-1.1.0.tgz", "integrity": "sha512-fwujyMe1gj6rk6dYi9hMZm0c8Mz8NDMVl2LB4iaYh3+LIAThZC8RKFGXWG0IML2OxAit/ZFRgZhMkhQ3d/bobQ==", - "peer": true, "dependencies": { "semver": "^5.3.0" } @@ -8575,7 +8565,6 @@ "version": "0.1.13", "resolved": "https://registry.npmjs.org/encoding/-/encoding-0.1.13.tgz", "integrity": "sha512-ETBauow1T35Y/WZMkio9jiM0Z5xjHHmJ4XmjZOq1l/dXz3lr2sRn87nJy20RupqSh1F2m3HHPSp8ShIPQJrJ3A==", - "peer": true, "dependencies": { "iconv-lite": "^0.6.2" } @@ -8880,7 +8869,6 @@ "dev": true, "hasInstallScript": true, "license": "MIT", - "peer": true, "bin": { "esbuild": "bin/esbuild" }, @@ -8973,6 +8961,7 @@ "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=", "dev": true, + "peer": true, "engines": { "node": ">=0.8.0" } @@ -9067,7 +9056,6 @@ "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.52.0.tgz", "integrity": "sha512-zh/JHnaixqHZsolRB/w9/02akBk9EPrOs9JwcTP2ek7yL5bVvXuRariiaAjjoJ5DvuwQ1WAE/HsMz+w17YgBCg==", "dev": true, - "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.6.1", @@ -9256,7 +9244,6 @@ "resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.29.0.tgz", "integrity": "sha512-QPOO5NO6Odv5lpoTkddtutccQjysJuFxoPS7fAHO+9m9udNHvTCPSAMW9zGAYj8lAIdr40I8yPCdUYrncXtrwg==", "dev": true, - "peer": true, "dependencies": { "array-includes": "^3.1.7", "array.prototype.findlastindex": "^1.2.3", @@ -9380,7 +9367,6 @@ "resolved": "https://registry.npmjs.org/eslint-plugin-jsx-a11y/-/eslint-plugin-jsx-a11y-6.7.1.tgz", "integrity": "sha512-63Bog4iIethyo8smBklORknVjB0T2dwB8Mr/hIC+fBS0uyHdYYpzM/Ed+YC8VxTjlXHEWFOdmgwcDn1U2L9VCA==", "dev": true, - "peer": true, "dependencies": { "@babel/runtime": "^7.20.7", "aria-query": "^5.1.3", @@ -9483,7 +9469,6 @@ "resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.33.2.tgz", "integrity": "sha512-73QQMKALArI8/7xGLNI/3LylrEYrlKZSb5C9+q3OtOewTnMQi5cT+aE9E41sLCmli3I9PGGmD1yiZydyo4FEPw==", "dev": true, - "peer": true, "dependencies": { "array-includes": "^3.1.6", "array.prototype.flatmap": "^1.3.1", @@ -9514,7 +9499,6 @@ "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-4.6.0.tgz", "integrity": "sha512-oFc7Itz9Qxh2x4gNHStv3BqJq54ExXmfC+a1NjAta66IAN87Wu0R/QArgIS9qKzX3dXKPI9H5crl9QchNMY9+g==", "dev": true, - "peer": true, "engines": { "node": ">=10" }, @@ -11199,6 +11183,7 @@ "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", "dev": true, + "peer": true, "engines": { "node": ">=4" } @@ -14207,7 +14192,6 @@ "integrity": "sha512-+3GkODfsDG71KSCQhc4IekSW+ItCK/kiez1Z28ksWvYhKXV/syxMlerR/sC7whDp7IyreZ4YxceMLdTs5hQE8A==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "ansi-colors": "^4.1.3", "browser-stdout": "^1.3.1", @@ -14941,7 +14925,6 @@ "resolved": "https://registry.npmjs.org/nyc/-/nyc-15.1.0.tgz", "integrity": "sha512-jMW04n9SxKdKi1ZMGhvUTHBN0EICCRkHemEoE5jm6mTYcqcdas0ATzgUgejlQUHMvpnOZqGB5Xxsv9KxJW1j8A==", "dev": true, - "peer": true, "dependencies": { "@istanbuljs/load-nyc-config": "^1.0.0", "@istanbuljs/schema": "^0.1.2", @@ -16181,7 +16164,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", @@ -16261,7 +16243,6 @@ "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.0.0.tgz", "integrity": "sha512-zBf5eHpwHOGPC47h0zrPyNn+eAEIdEzfywMoYn2XPi0P44Zp0tSq64rq0xAREh4auw2cJZHo9QUob+NqCQky4g==", "dev": true, - "peer": true, "bin": { "prettier": "bin/prettier.cjs" }, @@ -16327,7 +16308,6 @@ "version": "15.8.1", "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==", - "peer": true, "dependencies": { "loose-envify": "^1.4.0", "object-assign": "^4.1.1", @@ -16498,7 +16478,6 @@ "version": "16.14.0", "resolved": "https://registry.npmjs.org/react/-/react-16.14.0.tgz", "integrity": "sha512-0X2CImDkJGApiAlcf0ODKIneSwBPhqJawOa5wCtKbu7ZECrmS26NvtSILynQ66cgkT/RJ4LidJOc3bUESwmU8g==", - "peer": true, "dependencies": { "loose-envify": "^1.1.0", "object-assign": "^4.1.1", @@ -16526,7 +16505,6 @@ "version": "16.14.0", "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-16.14.0.tgz", "integrity": "sha512-1gCeQXDLoIqMgqD3IO2Ah9bnf0w9kzhwN5q4FGnHZ67hBm9yePzB5JJAIQCc8x3pFnNlwFq4RidZggNAAkzWWw==", - "peer": true, "dependencies": { "loose-envify": "^1.1.0", "object-assign": "^4.1.1", @@ -18260,6 +18238,7 @@ "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", "dev": true, + "peer": true, "dependencies": { "has-flag": "^3.0.0" }, @@ -18670,7 +18649,6 @@ "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=12" }, @@ -18859,8 +18837,7 @@ "version": "2.8.1", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", - "license": "0BSD", - "peer": true + "license": "0BSD" }, "node_modules/tslint": { "version": "6.1.3", @@ -18868,6 +18845,7 @@ "integrity": "sha512-IbR4nkT96EQOvKE2PW/djGz8iGNeJ4rF2mBfiYaR/nvUWYKJhLwimoJKgjIFEIDibBtOevj7BqCRL4oHeWWUCg==", "deprecated": "TSLint has been deprecated in favor of ESLint. Please see https://github.com/palantir/tslint/issues/4534 for more information.", "dev": true, + "peer": true, "dependencies": { "@babel/code-frame": "^7.0.0", "builtin-modules": "^1.1.1", @@ -18899,6 +18877,7 @@ "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "sprintf-js": "~1.0.2" } @@ -18908,6 +18887,7 @@ "resolved": "https://registry.npmjs.org/builtin-modules/-/builtin-modules-1.1.1.tgz", "integrity": "sha512-wxXCdllwGhI2kCC0MnvTGYTMvnVZTvqgypkiTI8Pa5tcz2i6VqsqwYGgqwXji+4RgCzms6EajE4IxiUH6HH8nQ==", "dev": true, + "peer": true, "engines": { "node": ">=0.10.0" } @@ -18917,6 +18897,7 @@ "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==", "dev": true, + "peer": true, "engines": { "node": ">=0.3.1" } @@ -18928,6 +18909,7 @@ "deprecated": "Glob versions prior to v9 are no longer supported", "dev": true, "license": "ISC", + "peer": true, "dependencies": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", @@ -18949,6 +18931,7 @@ "integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "argparse": "^1.0.7", "esprima": "^4.0.0" @@ -18961,7 +18944,8 @@ "version": "1.14.1", "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", - "dev": true + "dev": true, + "peer": true }, "node_modules/tsscmp": { "version": "1.0.6", @@ -18978,6 +18962,7 @@ "resolved": "https://registry.npmjs.org/tsutils/-/tsutils-2.29.0.tgz", "integrity": "sha512-g5JVHCIJwzfISaXpXE1qvNalca5Jwob6FjI4AoPlqMusJ6ftFE7IkkFoMhVLRgK+4Kx3gkzb8UZK5t5yTTvEmA==", "dev": true, + "peer": true, "dependencies": { "tslib": "^1.8.1" }, @@ -18989,7 +18974,8 @@ "version": "1.14.1", "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", - "dev": true + "dev": true, + "peer": true }, "node_modules/tsx": { "version": "4.19.4", @@ -19210,7 +19196,6 @@ "integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==", "dev": true, "license": "Apache-2.0", - "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -19448,7 +19433,6 @@ "integrity": "sha512-Yek7dAy0v3Kl0orwMlvi7TPtiCNrdfHNd7Gcc/pLq4BLXqfAmd0J7OWMizUQnTTJsyjKn02mU7anqwfmUP4J8Q==", "devOptional": true, "hasInstallScript": true, - "peer": true, "dependencies": { "node-gyp-build": "^4.3.0" }, @@ -19568,7 +19552,6 @@ "resolved": "https://registry.npmjs.org/vega/-/vega-5.33.0.tgz", "integrity": "sha512-jNAGa7TxLojOpMMMrKMXXBos4K6AaLJbCgGDOw1YEkLRjUkh12pcf65J2lMSdEHjcEK47XXjKiOUVZ8L+MniBA==", "license": "BSD-3-Clause", - "peer": true, "dependencies": { "vega-crossfilter": "~4.1.3", "vega-dataflow": "~5.7.7", @@ -19796,7 +19779,6 @@ "resolved": "https://registry.npmjs.org/vega-lite/-/vega-lite-5.23.0.tgz", "integrity": "sha512-l4J6+AWE3DIjvovEoHl2LdtCUkfm4zs8Xxx7INwZEAv+XVb6kR6vIN1gt3t2gN2gs/y4DYTs/RPoTeYAuEg6mA==", "license": "BSD-3-Clause", - "peer": true, "dependencies": { "json-stringify-pretty-compact": "~4.0.0", "tslib": "~2.8.1", @@ -20455,7 +20437,6 @@ "version": "6.2.3", "resolved": "https://registry.npmjs.org/ws/-/ws-6.2.3.tgz", "integrity": "sha512-jmTjYU0j60B+vHey6TfR3Z7RD61z/hmxBS3VMSGIrroOWXQEneK1zNuotOUrGyBHQj0yrpsLHPWtigEFd13ndA==", - "peer": true, "dependencies": { "async-limiter": "~1.0.0" } @@ -20612,7 +20593,6 @@ "version": "13.6.18", "resolved": "https://registry.npmjs.org/yjs/-/yjs-13.6.18.tgz", "integrity": "sha512-GBTjO4QCmv2HFKFkYIJl7U77hIB1o22vSCSQD1Ge8ZxWbIbn8AltI4gyXbtL+g5/GJep67HCMq3Y5AmNwDSyEg==", - "peer": true, "dependencies": { "lib0": "^0.2.86" }, @@ -20856,7 +20836,6 @@ "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.22.5.tgz", "integrity": "sha512-SBuTAjg91A3eKOvD+bPEz3LlhHZRNu1nFOVts9lzDJTXshHTjII0BAtDS3Y2DAkdZdDKWVZGVwkDfc4Clxn1dg==", "dev": true, - "peer": true, "requires": { "@ampproject/remapping": "^2.2.0", "@babel/code-frame": "^7.22.5", @@ -21258,8 +21237,7 @@ "version": "4.0.18", "resolved": "https://registry.npmjs.org/@cspell/dict-css/-/dict-css-4.0.18.tgz", "integrity": "sha512-EF77RqROHL+4LhMGW5NTeKqfUd/e4OOv6EDFQ/UQQiFyWuqkEKyEz0NDILxOFxWUEVdjT2GQ2cC7t12B6pESwg==", - "dev": true, - "peer": true + "dev": true }, "@cspell/dict-dart": { "version": "2.3.1", @@ -21379,15 +21357,13 @@ "version": "4.0.12", "resolved": "https://registry.npmjs.org/@cspell/dict-html/-/dict-html-4.0.12.tgz", "integrity": "sha512-JFffQ1dDVEyJq6tCDWv0r/RqkdSnV43P2F/3jJ9rwLgdsOIXwQbXrz6QDlvQLVvNSnORH9KjDtenFTGDyzfCaA==", - "dev": true, - "peer": true + "dev": true }, "@cspell/dict-html-symbol-entities": { "version": "4.0.4", "resolved": "https://registry.npmjs.org/@cspell/dict-html-symbol-entities/-/dict-html-symbol-entities-4.0.4.tgz", "integrity": "sha512-afea+0rGPDeOV9gdO06UW183Qg6wRhWVkgCFwiO3bDupAoyXRuvupbb5nUyqSTsLXIKL8u8uXQlJ9pkz07oVXw==", - "dev": true, - "peer": true + "dev": true }, "@cspell/dict-java": { "version": "5.0.12", @@ -21553,8 +21529,7 @@ "version": "3.2.3", "resolved": "https://registry.npmjs.org/@cspell/dict-typescript/-/dict-typescript-3.2.3.tgz", "integrity": "sha512-zXh1wYsNljQZfWWdSPYwQhpwiuW0KPW1dSd8idjMRvSD0aSvWWHoWlrMsmZeRl4qM4QCEAjua8+cjflm41cQBg==", - "dev": true, - "peer": true + "dev": true }, "@cspell/dict-vue": { "version": "3.0.5", @@ -22640,7 +22615,6 @@ "resolved": "https://registry.npmjs.org/@octokit/core/-/core-5.2.1.tgz", "integrity": "sha512-dKYCMuPO1bmrpuogcjQ8z7ICCH3FP6WmxpwC03yjzGfZhj9fTJg6+bS1+UAplekbN2C+M61UNllGOOoAfGCrdQ==", "dev": true, - "peer": true, "requires": { "@octokit/auth-token": "^4.0.0", "@octokit/graphql": "^7.1.0", @@ -22765,8 +22739,7 @@ "@opentelemetry/api": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-1.4.1.tgz", - "integrity": "sha512-O2yRJce1GOc6PAy3QxFM4NzFiWzvScDC1/5ihYBL6BUEVdq0XMWN01sppE+H6bBXbaFYipjwFLEWLg5PaSOThA==", - "peer": true + "integrity": "sha512-O2yRJce1GOc6PAy3QxFM4NzFiWzvScDC1/5ihYBL6BUEVdq0XMWN01sppE+H6bBXbaFYipjwFLEWLg5PaSOThA==" }, "@opentelemetry/core": { "version": "1.10.1", @@ -23598,7 +23571,6 @@ "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-6.9.0.tgz", "integrity": "sha512-GZmjMh4AJ/5gaH4XF2eXA8tMnHWP+Pm1mjQR2QN4Iz+j/zO04b9TOvJYOX2sCNIQHtRStKTxRY1FX7LhpJT4Gw==", "dev": true, - "peer": true, "requires": { "@typescript-eslint/scope-manager": "6.9.0", "@typescript-eslint/types": "6.9.0", @@ -24085,8 +24057,7 @@ "version": "8.9.0", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.9.0.tgz", "integrity": "sha512-jaVNAFBHNLXspO543WnNNPZFRtavh3skAkITqD0/2aeMkKZTN+254PyhwxFYrk3vQ1xfY+2wbesJMs/JC8/PwQ==", - "dev": true, - "peer": true + "dev": true }, "acorn-jsx": { "version": "5.3.2", @@ -24187,6 +24158,7 @@ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", "dev": true, + "peer": true, "requires": { "color-convert": "^1.9.0" } @@ -24686,7 +24658,6 @@ "resolved": "https://registry.npmjs.org/bare-events/-/bare-events-2.8.1.tgz", "integrity": "sha512-oxSAxTS1hRfnyit2CL5QpAOS5ixfBjj6ex3yTNvXyY/kE719jQ/IjuESJBK2w5v4wwQRAHGseVJXx9QBYOtFGQ==", "dev": true, - "peer": true, "requires": {} }, "bare-fs": { @@ -24995,7 +24966,6 @@ "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.26.3.tgz", "integrity": "sha512-lAUU+02RFBuCKQPj/P6NgjlbCnLBMp4UtgTx7vNHd3XSIJF87s9a5rA3aH2yw3GS9DqZAUbOtZdCCiZeVRqt0w==", "dev": true, - "peer": true, "requires": { "baseline-browser-mapping": "^2.8.9", "caniuse-lite": "^1.0.30001746", @@ -25035,7 +25005,6 @@ "resolved": "https://registry.npmjs.org/bufferutil/-/bufferutil-4.0.6.tgz", "integrity": "sha512-jduaYOYtnio4aIAyc6UbvPCVcgq7nYpVnucyxr6eCYg/Woad9Hf/oxxBRDnGGjPfjUm6j5O/uBWhIu4iLebFaw==", "devOptional": true, - "peer": true, "requires": { "node-gyp-build": "^4.3.0" } @@ -25182,7 +25151,6 @@ "resolved": "https://registry.npmjs.org/chai/-/chai-4.3.10.tgz", "integrity": "sha512-0UXG04VuVbruMUYbJ6JctvH0YnC/4q3/AkT18q4NaITo91CUm0liMS9VqzT9vZhVQ/1eqPanMWjBM+Juhfb/9g==", "dev": true, - "peer": true, "requires": { "assertion-error": "^1.1.0", "check-error": "^1.0.3", @@ -25222,6 +25190,7 @@ "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", "dev": true, + "peer": true, "requires": { "ansi-styles": "^3.2.1", "escape-string-regexp": "^1.0.5", @@ -25482,6 +25451,7 @@ "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", "dev": true, + "peer": true, "requires": { "color-name": "1.1.3" } @@ -25490,7 +25460,8 @@ "version": "1.1.3", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=", - "dev": true + "dev": true, + "peer": true }, "colorette": { "version": "2.0.20", @@ -26592,7 +26563,6 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/diagnostic-channel/-/diagnostic-channel-1.1.0.tgz", "integrity": "sha512-fwujyMe1gj6rk6dYi9hMZm0c8Mz8NDMVl2LB4iaYh3+LIAThZC8RKFGXWG0IML2OxAit/ZFRgZhMkhQ3d/bobQ==", - "peer": true, "requires": { "semver": "^5.3.0" } @@ -26850,7 +26820,6 @@ "version": "0.1.13", "resolved": "https://registry.npmjs.org/encoding/-/encoding-0.1.13.tgz", "integrity": "sha512-ETBauow1T35Y/WZMkio9jiM0Z5xjHHmJ4XmjZOq1l/dXz3lr2sRn87nJy20RupqSh1F2m3HHPSp8ShIPQJrJ3A==", - "peer": true, "requires": { "iconv-lite": "^0.6.2" } @@ -27100,7 +27069,6 @@ "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.1.tgz", "integrity": "sha512-BGO5LtrGC7vxnqucAe/rmvKdJllfGaYWdyABvyMoXQlfYMb2bbRuReWR5tEGE//4LcNJj9XrkovTqNYRFZHAMQ==", "dev": true, - "peer": true, "requires": { "@esbuild/aix-ppc64": "0.25.1", "@esbuild/android-arm": "0.25.1", @@ -27172,7 +27140,8 @@ "version": "1.0.5", "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=", - "dev": true + "dev": true, + "peer": true }, "escodegen": { "version": "1.14.3", @@ -27239,7 +27208,6 @@ "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.52.0.tgz", "integrity": "sha512-zh/JHnaixqHZsolRB/w9/02akBk9EPrOs9JwcTP2ek7yL5bVvXuRariiaAjjoJ5DvuwQ1WAE/HsMz+w17YgBCg==", "dev": true, - "peer": true, "requires": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.6.1", @@ -27478,7 +27446,6 @@ "resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.29.0.tgz", "integrity": "sha512-QPOO5NO6Odv5lpoTkddtutccQjysJuFxoPS7fAHO+9m9udNHvTCPSAMW9zGAYj8lAIdr40I8yPCdUYrncXtrwg==", "dev": true, - "peer": true, "requires": { "array-includes": "^3.1.7", "array.prototype.findlastindex": "^1.2.3", @@ -27573,7 +27540,6 @@ "resolved": "https://registry.npmjs.org/eslint-plugin-jsx-a11y/-/eslint-plugin-jsx-a11y-6.7.1.tgz", "integrity": "sha512-63Bog4iIethyo8smBklORknVjB0T2dwB8Mr/hIC+fBS0uyHdYYpzM/Ed+YC8VxTjlXHEWFOdmgwcDn1U2L9VCA==", "dev": true, - "peer": true, "requires": { "@babel/runtime": "^7.20.7", "aria-query": "^5.1.3", @@ -27639,7 +27605,6 @@ "resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.33.2.tgz", "integrity": "sha512-73QQMKALArI8/7xGLNI/3LylrEYrlKZSb5C9+q3OtOewTnMQi5cT+aE9E41sLCmli3I9PGGmD1yiZydyo4FEPw==", "dev": true, - "peer": true, "requires": { "array-includes": "^3.1.6", "array.prototype.flatmap": "^1.3.1", @@ -27692,7 +27657,6 @@ "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-4.6.0.tgz", "integrity": "sha512-oFc7Itz9Qxh2x4gNHStv3BqJq54ExXmfC+a1NjAta66IAN87Wu0R/QArgIS9qKzX3dXKPI9H5crl9QchNMY9+g==", "dev": true, - "peer": true, "requires": {} }, "eslint-visitor-keys": { @@ -28800,7 +28764,8 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", - "dev": true + "dev": true, + "peer": true }, "has-property-descriptors": { "version": "1.0.2", @@ -30890,7 +30855,6 @@ "resolved": "https://registry.npmjs.org/mocha/-/mocha-11.0.1.tgz", "integrity": "sha512-+3GkODfsDG71KSCQhc4IekSW+ItCK/kiez1Z28ksWvYhKXV/syxMlerR/sC7whDp7IyreZ4YxceMLdTs5hQE8A==", "dev": true, - "peer": true, "requires": { "ansi-colors": "^4.1.3", "browser-stdout": "^1.3.1", @@ -31430,7 +31394,6 @@ "resolved": "https://registry.npmjs.org/nyc/-/nyc-15.1.0.tgz", "integrity": "sha512-jMW04n9SxKdKi1ZMGhvUTHBN0EICCRkHemEoE5jm6mTYcqcdas0ATzgUgejlQUHMvpnOZqGB5Xxsv9KxJW1j8A==", "dev": true, - "peer": true, "requires": { "@istanbuljs/load-nyc-config": "^1.0.0", "@istanbuljs/schema": "^0.1.2", @@ -32324,7 +32287,6 @@ "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", "dev": true, - "peer": true, "requires": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", @@ -32371,8 +32333,7 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.0.0.tgz", "integrity": "sha512-zBf5eHpwHOGPC47h0zrPyNn+eAEIdEzfywMoYn2XPi0P44Zp0tSq64rq0xAREh4auw2cJZHo9QUob+NqCQky4g==", - "dev": true, - "peer": true + "dev": true }, "prettier-linter-helpers": { "version": "1.0.0", @@ -32417,7 +32378,6 @@ "version": "15.8.1", "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==", - "peer": true, "requires": { "loose-envify": "^1.4.0", "object-assign": "^4.1.1", @@ -32560,7 +32520,6 @@ "version": "16.14.0", "resolved": "https://registry.npmjs.org/react/-/react-16.14.0.tgz", "integrity": "sha512-0X2CImDkJGApiAlcf0ODKIneSwBPhqJawOa5wCtKbu7ZECrmS26NvtSILynQ66cgkT/RJ4LidJOc3bUESwmU8g==", - "peer": true, "requires": { "loose-envify": "^1.1.0", "object-assign": "^4.1.1", @@ -32581,7 +32540,6 @@ "version": "16.14.0", "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-16.14.0.tgz", "integrity": "sha512-1gCeQXDLoIqMgqD3IO2Ah9bnf0w9kzhwN5q4FGnHZ67hBm9yePzB5JJAIQCc8x3pFnNlwFq4RidZggNAAkzWWw==", - "peer": true, "requires": { "loose-envify": "^1.1.0", "object-assign": "^4.1.1", @@ -33464,7 +33422,7 @@ "integrity": "sha512-DF7ePE5bwitJrRdJSNrV+qAnQsfds0GbRA02ywy6TQrQewkm9DSHGDUxJaoJk2WUMlyQ7Odrf2o1PCZM50BcSg==", "requires": { "jquery": ">=1.8.0", - "jquery-ui": "1.13.2" + "jquery-ui": ">=1.8.0" } }, "smol-toml": { @@ -33896,6 +33854,7 @@ "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", "dev": true, + "peer": true, "requires": { "has-flag": "^3.0.0" } @@ -34220,8 +34179,7 @@ "version": "4.0.3", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", - "dev": true, - "peer": true + "dev": true } } }, @@ -34358,14 +34316,14 @@ "tslib": { "version": "2.8.1", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", - "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", - "peer": true + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==" }, "tslint": { "version": "6.1.3", "resolved": "https://registry.npmjs.org/tslint/-/tslint-6.1.3.tgz", "integrity": "sha512-IbR4nkT96EQOvKE2PW/djGz8iGNeJ4rF2mBfiYaR/nvUWYKJhLwimoJKgjIFEIDibBtOevj7BqCRL4oHeWWUCg==", "dev": true, + "peer": true, "requires": { "@babel/code-frame": "^7.0.0", "builtin-modules": "^1.1.1", @@ -34387,6 +34345,7 @@ "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", "dev": true, + "peer": true, "requires": { "sprintf-js": "~1.0.2" } @@ -34395,19 +34354,22 @@ "version": "1.1.1", "resolved": "https://registry.npmjs.org/builtin-modules/-/builtin-modules-1.1.1.tgz", "integrity": "sha512-wxXCdllwGhI2kCC0MnvTGYTMvnVZTvqgypkiTI8Pa5tcz2i6VqsqwYGgqwXji+4RgCzms6EajE4IxiUH6HH8nQ==", - "dev": true + "dev": true, + "peer": true }, "diff": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==", - "dev": true + "dev": true, + "peer": true }, "glob": { "version": "7.2.3", "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", "dev": true, + "peer": true, "requires": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", @@ -34422,6 +34384,7 @@ "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz", "integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==", "dev": true, + "peer": true, "requires": { "argparse": "^1.0.7", "esprima": "^4.0.0" @@ -34431,7 +34394,8 @@ "version": "1.14.1", "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", - "dev": true + "dev": true, + "peer": true } } }, @@ -34446,6 +34410,7 @@ "resolved": "https://registry.npmjs.org/tsutils/-/tsutils-2.29.0.tgz", "integrity": "sha512-g5JVHCIJwzfISaXpXE1qvNalca5Jwob6FjI4AoPlqMusJ6ftFE7IkkFoMhVLRgK+4Kx3gkzb8UZK5t5yTTvEmA==", "dev": true, + "peer": true, "requires": { "tslib": "^1.8.1" }, @@ -34454,7 +34419,8 @@ "version": "1.14.1", "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", - "dev": true + "dev": true, + "peer": true } } }, @@ -34616,8 +34582,7 @@ "version": "5.8.3", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz", "integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==", - "dev": true, - "peer": true + "dev": true }, "unbox-primitive": { "version": "1.0.2", @@ -34804,7 +34769,6 @@ "resolved": "https://registry.npmjs.org/utf-8-validate/-/utf-8-validate-5.0.9.tgz", "integrity": "sha512-Yek7dAy0v3Kl0orwMlvi7TPtiCNrdfHNd7Gcc/pLq4BLXqfAmd0J7OWMizUQnTTJsyjKn02mU7anqwfmUP4J8Q==", "devOptional": true, - "peer": true, "requires": { "node-gyp-build": "^4.3.0" } @@ -34904,7 +34868,6 @@ "version": "5.33.0", "resolved": "https://registry.npmjs.org/vega/-/vega-5.33.0.tgz", "integrity": "sha512-jNAGa7TxLojOpMMMrKMXXBos4K6AaLJbCgGDOw1YEkLRjUkh12pcf65J2lMSdEHjcEK47XXjKiOUVZ8L+MniBA==", - "peer": true, "requires": { "vega-crossfilter": "~4.1.3", "vega-dataflow": "~5.7.7", @@ -35106,7 +35069,6 @@ "version": "5.23.0", "resolved": "https://registry.npmjs.org/vega-lite/-/vega-lite-5.23.0.tgz", "integrity": "sha512-l4J6+AWE3DIjvovEoHl2LdtCUkfm4zs8Xxx7INwZEAv+XVb6kR6vIN1gt3t2gN2gs/y4DYTs/RPoTeYAuEg6mA==", - "peer": true, "requires": { "json-stringify-pretty-compact": "~4.0.0", "tslib": "~2.8.1", @@ -35642,7 +35604,6 @@ "version": "6.2.3", "resolved": "https://registry.npmjs.org/ws/-/ws-6.2.3.tgz", "integrity": "sha512-jmTjYU0j60B+vHey6TfR3Z7RD61z/hmxBS3VMSGIrroOWXQEneK1zNuotOUrGyBHQj0yrpsLHPWtigEFd13ndA==", - "peer": true, "requires": { "async-limiter": "~1.0.0" } @@ -35748,7 +35709,6 @@ "version": "13.6.18", "resolved": "https://registry.npmjs.org/yjs/-/yjs-13.6.18.tgz", "integrity": "sha512-GBTjO4QCmv2HFKFkYIJl7U77hIB1o22vSCSQD1Ge8ZxWbIbn8AltI4gyXbtL+g5/GJep67HCMq3Y5AmNwDSyEg==", - "peer": true, "requires": { "lib0": "^0.2.86" } From cec91d362200ff0614f8b0a9dcdbb80914401666 Mon Sep 17 00:00:00 2001 From: jankuca Date: Wed, 29 Oct 2025 13:21:05 +0100 Subject: [PATCH 078/101] fix: fix default metadata logic --- .../deepnote/converters/inputConverters.ts | 29 ++++++++++++++----- 1 file changed, 21 insertions(+), 8 deletions(-) diff --git a/src/notebooks/deepnote/converters/inputConverters.ts b/src/notebooks/deepnote/converters/inputConverters.ts index f3f219ba84..f2386a2fba 100644 --- a/src/notebooks/deepnote/converters/inputConverters.ts +++ b/src/notebooks/deepnote/converters/inputConverters.ts @@ -27,22 +27,35 @@ export abstract class BaseInputBlockConverter implements * Helper method to update block metadata with common logic. * Clears block.content, parses schema, deletes DEEPNOTE_VSCODE_RAW_CONTENT_KEY, * and merges metadata with updates. + * + * If metadata is missing or invalid, applies default config. + * Otherwise, preserves existing metadata and only applies updates. */ protected updateBlockMetadata(block: DeepnoteBlock, updates: Partial>): void { block.content = ''; - const existingMetadata = this.schema().safeParse(block.metadata); - const baseMetadata = existingMetadata.success ? existingMetadata.data : this.defaultConfig(); - if (block.metadata != null) { delete block.metadata[DEEPNOTE_VSCODE_RAW_CONTENT_KEY]; } - block.metadata = { - ...(block.metadata ?? {}), - ...baseMetadata, - ...updates - }; + // Check if existing metadata is valid + const existingMetadata = this.schema().safeParse(block.metadata); + const hasValidMetadata = + existingMetadata.success && block.metadata != null && Object.keys(block.metadata).length > 0; + + if (hasValidMetadata) { + // Preserve existing metadata and only apply updates + block.metadata = { + ...(block.metadata ?? {}), + ...updates + }; + } else { + // Apply defaults when metadata is missing or invalid + block.metadata = { + ...this.defaultConfig(), + ...updates + }; + } } applyChangesToBlock(block: DeepnoteBlock, _cell: NotebookCellData): void { From 0ed9e297d8ca72f1d2cce13e5161e6bf3268c92a Mon Sep 17 00:00:00 2001 From: jankuca Date: Wed, 29 Oct 2025 13:36:05 +0100 Subject: [PATCH 079/101] test: add more tests for sql block status bar provider --- .../sqlCellStatusBarProvider.unit.test.ts | 193 ++++++++++++++++++ 1 file changed, 193 insertions(+) diff --git a/src/notebooks/deepnote/sqlCellStatusBarProvider.unit.test.ts b/src/notebooks/deepnote/sqlCellStatusBarProvider.unit.test.ts index 906ff5d26d..85689cd301 100644 --- a/src/notebooks/deepnote/sqlCellStatusBarProvider.unit.test.ts +++ b/src/notebooks/deepnote/sqlCellStatusBarProvider.unit.test.ts @@ -347,6 +347,25 @@ suite('SqlCellStatusBarProvider', () => { // Verify the listener was registered by checking disposables assert.isTrue(activateDisposables.length > 0); }); + + test('registers workspace.onDidChangeNotebookDocument listener', () => { + const onDidChangeIntegrations = new EventEmitter(); + when(activateIntegrationStorage.onDidChangeIntegrations).thenReturn(onDidChangeIntegrations.event); + + activateProvider.activate(); + + verify(mockedVSCodeNamespaces.workspace.onDidChangeNotebookDocument(anything())).once(); + }); + + test('disposes the event emitter', () => { + const onDidChangeIntegrations = new EventEmitter(); + when(activateIntegrationStorage.onDidChangeIntegrations).thenReturn(onDidChangeIntegrations.event); + + activateProvider.activate(); + + // Verify the emitter is added to disposables + assert.isTrue(activateDisposables.length > 0); + }); }); suite('event listeners', () => { @@ -407,6 +426,72 @@ suite('SqlCellStatusBarProvider', () => { 'onDidChangeCellStatusBarItems should fire three times' ); }); + + test('fires onDidChangeCellStatusBarItems when deepnote notebook changes', () => { + const onDidChangeIntegrations = new EventEmitter(); + const onDidChangeNotebookDocument = new EventEmitter(); + when(eventIntegrationStorage.onDidChangeIntegrations).thenReturn(onDidChangeIntegrations.event); + when(mockedVSCodeNamespaces.workspace.onDidChangeNotebookDocument(anything())).thenCall((handler) => { + onDidChangeNotebookDocument.event(handler); + return { + dispose: () => { + return; + } + }; + }); + + eventProvider.activate(); + + const statusBarChangeHandler = createEventHandler( + eventProvider, + 'onDidChangeCellStatusBarItems', + eventDisposables + ); + + // Fire notebook document change event for deepnote notebook + onDidChangeNotebookDocument.fire({ + notebook: { + notebookType: 'deepnote' + } + }); + + assert.strictEqual(statusBarChangeHandler.count, 1, 'onDidChangeCellStatusBarItems should fire once'); + }); + + test('does not fire onDidChangeCellStatusBarItems when non-deepnote notebook changes', () => { + const onDidChangeIntegrations = new EventEmitter(); + const onDidChangeNotebookDocument = new EventEmitter(); + when(eventIntegrationStorage.onDidChangeIntegrations).thenReturn(onDidChangeIntegrations.event); + when(mockedVSCodeNamespaces.workspace.onDidChangeNotebookDocument(anything())).thenCall((handler) => { + onDidChangeNotebookDocument.event(handler); + return { + dispose: () => { + return; + } + }; + }); + + eventProvider.activate(); + + const statusBarChangeHandler = createEventHandler( + eventProvider, + 'onDidChangeCellStatusBarItems', + eventDisposables + ); + + // Fire notebook document change event for non-deepnote notebook + onDidChangeNotebookDocument.fire({ + notebook: { + notebookType: 'jupyter-notebook' + } + }); + + assert.strictEqual( + statusBarChangeHandler.count, + 0, + 'onDidChangeCellStatusBarItems should not fire for non-deepnote notebooks' + ); + }); }); suite('updateSqlVariableName command handler', () => { @@ -534,6 +619,52 @@ suite('SqlCellStatusBarProvider', () => { await updateVariableNameHandler(cell); }); + + test('falls back to active cell when no cell is provided', async () => { + const cell = createMockCell('sql', { deepnote_variable_name: 'old_name' }); + const newVariableName = 'new_name'; + + // Mock active notebook editor + when(mockedVSCodeNamespaces.window.activeNotebookEditor).thenReturn({ + selection: { start: 0 }, + notebook: { + cellAt: (_index: number) => cell + } + } as any); + + when(mockedVSCodeNamespaces.window.showInputBox(anything())).thenReturn(Promise.resolve(newVariableName)); + when(mockedVSCodeNamespaces.workspace.applyEdit(anything())).thenReturn(Promise.resolve(true)); + + // Call without providing a cell + await updateVariableNameHandler(); + + verify(mockedVSCodeNamespaces.window.showInputBox(anything())).once(); + verify(mockedVSCodeNamespaces.workspace.applyEdit(anything())).once(); + }); + + test('shows error when no cell is provided and no active editor', async () => { + // Mock no active notebook editor + when(mockedVSCodeNamespaces.window.activeNotebookEditor).thenReturn(undefined); + + // Call without providing a cell + await updateVariableNameHandler(); + + verify(mockedVSCodeNamespaces.window.showErrorMessage(anything())).once(); + verify(mockedVSCodeNamespaces.window.showInputBox(anything())).never(); + }); + + test('shows error when no cell is provided and active editor has no selection', async () => { + // Mock active notebook editor without selection + when(mockedVSCodeNamespaces.window.activeNotebookEditor).thenReturn({ + selection: undefined + } as any); + + // Call without providing a cell + await updateVariableNameHandler(); + + verify(mockedVSCodeNamespaces.window.showErrorMessage(anything())).once(); + verify(mockedVSCodeNamespaces.window.showInputBox(anything())).never(); + }); }); suite('switchSqlIntegration command handler', () => { @@ -813,6 +944,68 @@ suite('SqlCellStatusBarProvider', () => { const duckDbItem = quickPickItems.find((item) => item.id === DATAFRAME_SQL_INTEGRATION_ID); assert.isDefined(duckDbItem, 'DuckDB should still be in the list'); }); + + test('falls back to active cell when no cell is provided', async () => { + const notebookMetadata = { deepnoteProjectId: 'project-1' }; + const cell = createMockCell('sql', { sql_integration_id: 'old-integration' }, notebookMetadata); + const newIntegrationId = 'new-integration'; + + // Mock active notebook editor + when(mockedVSCodeNamespaces.window.activeNotebookEditor).thenReturn({ + selection: { start: 0 }, + notebook: { + cellAt: (_index: number) => cell + } + } as any); + + when(commandNotebookManager.getOriginalProject('project-1')).thenReturn({ + project: { + integrations: [ + { + id: newIntegrationId, + name: 'New Integration', + type: 'pgsql' + } + ] + } + } as any); + + when(mockedVSCodeNamespaces.window.showErrorMessage(anything())).thenReturn(Promise.resolve(undefined)); + when(mockedVSCodeNamespaces.window.showQuickPick(anything(), anything())).thenReturn( + Promise.resolve({ id: newIntegrationId, label: 'New Integration' } as any) + ); + when(mockedVSCodeNamespaces.workspace.applyEdit(anything())).thenReturn(Promise.resolve(true)); + + // Call without providing a cell + await switchIntegrationHandler(); + + verify(mockedVSCodeNamespaces.window.showQuickPick(anything(), anything())).once(); + verify(mockedVSCodeNamespaces.workspace.applyEdit(anything())).once(); + }); + + test('shows error when no cell is provided and no active editor', async () => { + // Mock no active notebook editor + when(mockedVSCodeNamespaces.window.activeNotebookEditor).thenReturn(undefined); + + // Call without providing a cell + await switchIntegrationHandler(); + + verify(mockedVSCodeNamespaces.window.showErrorMessage(anything())).once(); + verify(mockedVSCodeNamespaces.window.showQuickPick(anything(), anything())).never(); + }); + + test('shows error when no cell is provided and active editor has no selection', async () => { + // Mock active notebook editor without selection + when(mockedVSCodeNamespaces.window.activeNotebookEditor).thenReturn({ + selection: undefined + } as any); + + // Call without providing a cell + await switchIntegrationHandler(); + + verify(mockedVSCodeNamespaces.window.showErrorMessage(anything())).once(); + verify(mockedVSCodeNamespaces.window.showQuickPick(anything(), anything())).never(); + }); }); function createMockCell( From a7868abd23e00687280fc25c02851976ebc97b4a Mon Sep 17 00:00:00 2001 From: jankuca Date: Wed, 29 Oct 2025 13:41:49 +0100 Subject: [PATCH 080/101] localize select input strings --- src/messageTypes.ts | 2 ++ src/notebooks/deepnote/selectInputSettingsWebview.ts | 2 ++ src/platform/common/utils/localize.ts | 2 ++ .../selectInputSettings/SelectInputSettingsPanel.tsx | 8 ++++---- 4 files changed, 10 insertions(+), 4 deletions(-) diff --git a/src/messageTypes.ts b/src/messageTypes.ts index a1d5e49289..579aba04a2 100644 --- a/src/messageTypes.ts +++ b/src/messageTypes.ts @@ -219,6 +219,8 @@ export type LocalizedMessages = { fromVariable: string; fromVariableDescription: string; variablePlaceholder: string; + optionNameLabel: string; + variableNameLabel: string; removeOptionAriaLabel: string; saveButton: string; cancelButton: string; diff --git a/src/notebooks/deepnote/selectInputSettingsWebview.ts b/src/notebooks/deepnote/selectInputSettingsWebview.ts index f706313908..17592a99ec 100644 --- a/src/notebooks/deepnote/selectInputSettingsWebview.ts +++ b/src/notebooks/deepnote/selectInputSettingsWebview.ts @@ -132,6 +132,8 @@ export class SelectInputSettingsWebviewProvider { fromVariable: localize.SelectInputSettings.fromVariable, fromVariableDescription: localize.SelectInputSettings.fromVariableDescription, variablePlaceholder: localize.SelectInputSettings.variablePlaceholder, + optionNameLabel: localize.SelectInputSettings.optionNameLabel, + variableNameLabel: localize.SelectInputSettings.variableNameLabel, removeOptionAriaLabel: localize.SelectInputSettings.removeOptionAriaLabel, saveButton: localize.SelectInputSettings.saveButton, cancelButton: localize.SelectInputSettings.cancelButton diff --git a/src/platform/common/utils/localize.ts b/src/platform/common/utils/localize.ts index 5f51279da0..7168a22dd8 100644 --- a/src/platform/common/utils/localize.ts +++ b/src/platform/common/utils/localize.ts @@ -875,6 +875,8 @@ export namespace SelectInputSettings { export const fromVariable = l10n.t('From variable'); export const fromVariableDescription = l10n.t('A list or Series that contains only strings, numbers or booleans.'); export const variablePlaceholder = l10n.t('Variable name...'); + export const optionNameLabel = l10n.t('Option name'); + export const variableNameLabel = l10n.t('Variable name'); export const removeOptionAriaLabel = l10n.t('Remove option'); export const saveButton = l10n.t('Save'); export const cancelButton = l10n.t('Cancel'); diff --git a/src/webviews/webview-side/selectInputSettings/SelectInputSettingsPanel.tsx b/src/webviews/webview-side/selectInputSettings/SelectInputSettingsPanel.tsx index 8afb22f227..fb962fbece 100644 --- a/src/webviews/webview-side/selectInputSettings/SelectInputSettingsPanel.tsx +++ b/src/webviews/webview-side/selectInputSettings/SelectInputSettingsPanel.tsx @@ -180,7 +180,7 @@ export const SelectInputSettingsPanel: React.FC
} }} placeholder={getLocString('addOptionPlaceholder', 'Add option...')} - aria-label="Option name" + aria-label={getLocString('optionNameLabel', 'Option name')} />