Skip to content

Commit eec9df6

Browse files
data explorer: create "Copy as Code" modal (#8537)
This PR implements the actions and modal dialog for the `Copy as Code` data explorer button. A modal dialog was chosen over a modal popup since we cannot use a modal popup in the editor action bar at this point in time. Notes from @dhruvisompura: > The data explorer UI doesn't have the action bar component we created because we normally have it live in its own row below the VS Code editor action bar (the one that every editor tab has). We don't want the data explorer to have two action bars as that is confusing/annoying/bad ux for the user! We are relying on the VS Code editor action bar for the Data Explorer. We have to contribute all of our buttons via a command for them to show up in that editor action bar. There currently is no mechanism to show a popup that is anchored to the copy button via an action because commands have no way of knowing what DOM element to anchor a piece of UI to. Commands are pretty primitive and don't have UI context generally. Note that this is just the first implementation! Things like the "Copy to Clipboard" functionality, real code editor block, etc will come in a follow up PR. ### Release Notes <!-- Optionally, replace `N/A` with text to be included in the next release notes. The `N/A` bullets are ignored. If you refer to one or more Positron issues, these issues are used to collect information about the feature or bugfix, such as the relevant language pack as determined by Github labels of type `lang: `. The note will automatically be tagged with the language. These notes are typically filled by the Positron team. If you are an external contributor, you may ignore this section. --> #### New Features - N/A #### Bug Fixes - N/A ### QA Notes Intended behavior is that the modal has a dropdown that is populated with the name of a syntax, eg, `pandas`, + generated code. At this point, the code generation piece is not implemented, so we won't have to check the code is correct (yet). On the Python side, a `pandas` dataframe will fill the dropdown with `pandas`. A `polars` dataframe will fill the dropdown with `polars`. For the R side, unless you have a locally built Ark, the button should be greyed out since there are no syntaxes available. --------- Signed-off-by: Isabel Zimmerman <54685329+isabelizimm@users.noreply.github.com> Co-authored-by: Dhruvi Sompura <dhruvisompura@users.noreply.github.com>
1 parent 8f386df commit eec9df6

File tree

11 files changed

+499
-7
lines changed

11 files changed

+499
-7
lines changed

extensions/positron-duckdb/src/extension.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1056,6 +1056,9 @@ END`;
10561056
export_data_selection: {
10571057
support_status: SupportStatus.Unsupported,
10581058
supported_formats: []
1059+
},
1060+
convert_to_code: {
1061+
support_status: SupportStatus.Unsupported,
10591062
}
10601063
}
10611064
};
@@ -1179,6 +1182,9 @@ END`;
11791182
ExportFormat.Tsv,
11801183
ExportFormat.Html
11811184
]
1185+
},
1186+
convert_to_code: {
1187+
support_status: SupportStatus.Unsupported,
11821188
}
11831189
}
11841190
};

extensions/positron-duckdb/src/interfaces.ts

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -902,6 +902,11 @@ export interface SupportedFeatures {
902902
*/
903903
export_data_selection: ExportDataSelectionFeatures;
904904

905+
/**
906+
* Support for 'convert_to_code' RPC and its features
907+
*/
908+
convert_to_code: ConvertToCodeFeatures;
909+
905910
}
906911

907912
/**
@@ -1000,6 +1005,24 @@ export interface SetSortColumnsFeatures {
10001005

10011006
}
10021007

1008+
/**
1009+
* Feature flags for 'convert_to_code' RPC
1010+
*/
1011+
export interface ConvertToCodeFeatures {
1012+
/**
1013+
* The support status for this RPC method
1014+
* */
1015+
support_status: SupportStatus;
1016+
/**
1017+
* The supported code syntax names
1018+
*/
1019+
supported_code_syntaxes?: Array<CodeSyntaxName>;
1020+
}
1021+
1022+
export interface CodeSyntaxName {
1023+
code_syntax_name: string;
1024+
}
1025+
10031026
/**
10041027
* A selection on the data grid, for copying to the clipboard or other
10051028
* actions

extensions/positron-duckdb/src/test/extension.test.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -104,7 +104,7 @@ async function createTempTable(
104104
for (let i = 0; i < length; i++) {
105105
tuples.push(`(${columns.map(c => c.values[i]).join(', ')})`);
106106
}
107-
107+
108108
// Use explicit column names in INSERT to ensure proper ordering
109109
const columnNames = columns.map(c => quoteIdentifier(c.name)).join(', ');
110110
await runQuery(`INSERT INTO ${tableName} (${columnNames}) VALUES\n${tuples.join(',\n')};`);
@@ -243,7 +243,8 @@ suite('Positron DuckDB Extension Test Suite', () => {
243243
ExportFormat.Tsv,
244244
ExportFormat.Html
245245
]
246-
}
246+
},
247+
convert_to_code: { support_status: SupportStatus.Unsupported }
247248
}
248249
} satisfies BackendState);
249250

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
/*---------------------------------------------------------------------------------------------
2+
* Copyright (C) 2025 Posit Software, PBC. All rights reserved.
3+
* Licensed under the Elastic License 2.0. See LICENSE.txt for license information.
4+
*--------------------------------------------------------------------------------------------*/
5+
6+
// React.
7+
import React from 'react';
8+
9+
/**
10+
* DropdownEntryProps interface.
11+
*/
12+
interface DropdownEntryProps {
13+
title: string;
14+
subtitle?: string;
15+
group?: string;
16+
}
17+
18+
/**
19+
* DropdownEntry component.
20+
* @param props The dropdown entry props.
21+
* @returns The rendered component
22+
*/
23+
export const DropdownEntry = (props: DropdownEntryProps) => {
24+
// Render.
25+
return (
26+
<div className='dropdown-entry'>
27+
<div className='dropdown-entry-title'>
28+
{props.title}
29+
</div>
30+
{props.group ? <div className='dropdown-entry-group'>{props.group}</div> : null}
31+
</div>
32+
);
33+
};
Lines changed: 167 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,167 @@
1+
/*---------------------------------------------------------------------------------------------
2+
* Copyright (C) 2025 Posit Software, PBC. All rights reserved.
3+
* Licensed under the Elastic License 2.0. See LICENSE.txt for license information.
4+
*--------------------------------------------------------------------------------------------*/
5+
6+
// React.
7+
import React, { useEffect, useState } from 'react';
8+
9+
// Other dependencies.
10+
import { localize } from '../../../nls.js';
11+
import { VerticalStack } from '../positronComponents/positronModalDialog/components/verticalStack.js';
12+
import { OKCancelModalDialog } from '../positronComponents/positronModalDialog/positronOKCancelModalDialog.js';
13+
import { IPositronDataExplorerInstance } from '../../services/positronDataExplorer/browser/interfaces/positronDataExplorerInstance.js';
14+
import { PositronDataExplorerCommandId } from '../../contrib/positronDataExplorerEditor/browser/positronDataExplorerActions.js';
15+
import { DropDownListBox } from '../positronComponents/dropDownListBox/dropDownListBox.js';
16+
import { DropDownListBoxItem } from '../positronComponents/dropDownListBox/dropDownListBoxItem.js';
17+
import { DropdownEntry } from './components/dropdownEntry.js';
18+
import { CodeSyntaxName } from '../../services/languageRuntime/common/positronDataExplorerComm.js';
19+
import { PositronModalReactRenderer } from '../../../base/browser/positronModalReactRenderer.js';
20+
import { usePositronReactServicesContext } from '../../../base/browser/positronReactRendererContext.js';
21+
22+
/**
23+
* Shows the convert to code modal dialog.
24+
* @param dataExplorerClientInstance The data explorer client instance.
25+
* @returns A promise that resolves when the dialog is closed.
26+
*/
27+
export const showConvertToCodeModalDialog = async (
28+
dataExplorerClientInstance: IPositronDataExplorerInstance,
29+
): Promise<void> => {
30+
// Create the renderer.
31+
const renderer = new PositronModalReactRenderer()
32+
33+
// Show the copy as code dialog.
34+
renderer.render(
35+
<ConvertToCodeModalDialog
36+
dataExplorerClientInstance={dataExplorerClientInstance}
37+
renderer={renderer}
38+
/>
39+
);
40+
};
41+
42+
/**
43+
* ConvertToCodeDialogProps interface.
44+
*/
45+
interface ConvertToCodeDialogProps {
46+
dataExplorerClientInstance: IPositronDataExplorerInstance
47+
renderer: PositronModalReactRenderer;
48+
}
49+
50+
51+
/**
52+
* ConvertToCodeModalDialog component.
53+
* @param props The component properties.
54+
* @returns The rendered component.
55+
*/
56+
export const ConvertToCodeModalDialog = (props: ConvertToCodeDialogProps) => {
57+
// Service hooks.
58+
const services = usePositronReactServicesContext();
59+
60+
// State hooks.
61+
const instance = props.dataExplorerClientInstance.dataExplorerClientInstance;
62+
const codeSyntaxOptions = instance.cachedBackendState?.supported_features?.convert_to_code?.code_syntaxes ?? [];
63+
64+
const [selectedSyntax, setSelectedSyntax] = useState<CodeSyntaxName | undefined>(instance.suggestedSyntax);
65+
66+
const [codeString, setCodeString] = useState<string | undefined>(undefined);
67+
68+
useEffect(() => {
69+
const getCodeString = async () => {
70+
try {
71+
// Execute the command to get the code string based on the selected syntax.
72+
const result = await services.commandService.executeCommand(PositronDataExplorerCommandId.ConvertToCodeAction, selectedSyntax);
73+
setCodeString(result);
74+
} catch (error) {
75+
if (selectedSyntax) {
76+
setCodeString(localize(
77+
'positron.dataExplorer.getCodeStringWithSyntax',
78+
"Cannot generate code for type {0}",
79+
selectedSyntax.code_syntax_name
80+
));
81+
} else {
82+
setCodeString(localize(
83+
'positron.dataExplorer.getCodeStringNoSyntax',
84+
"Cannot generate code"
85+
));
86+
}
87+
}
88+
};
89+
90+
getCodeString(); // Call the async function
91+
}, [selectedSyntax, services.commandService]);
92+
93+
// Construct the syntax options dropdown entries
94+
const syntaxDropdownEntries = () => {
95+
return syntaxInfoToDropDownItems(codeSyntaxOptions);
96+
};
97+
98+
const syntaxInfoToDropDownItems = (
99+
syntaxes: CodeSyntaxName[]
100+
): DropDownListBoxItem<string, CodeSyntaxName>[] => {
101+
return syntaxes.map(
102+
(syntax) =>
103+
new DropDownListBoxItem<string, CodeSyntaxName>({
104+
identifier: syntax.code_syntax_name,
105+
value: syntax,
106+
})
107+
);
108+
};
109+
110+
const syntaxDropdownTitle = (): string => {
111+
// if selectedSyntax is an object with code_syntax_name, return that name
112+
if (typeof selectedSyntax === 'object' && 'code_syntax_name' in selectedSyntax) {
113+
return (selectedSyntax as CodeSyntaxName).code_syntax_name;
114+
}
115+
return localize('positron.dataExplorer.selectCodeSyntax', 'Select Code Syntax');
116+
}
117+
118+
const onSelectionChanged = async (item: DropDownListBoxItem<string, CodeSyntaxName>) => {
119+
const typedItem = item as DropDownListBoxItem<string, CodeSyntaxName>;
120+
setSelectedSyntax(typedItem.options.value);
121+
122+
// Execute the command to get the code string based on the selected syntax.
123+
try {
124+
const exc = await services.commandService.executeCommand(PositronDataExplorerCommandId.ConvertToCodeAction, typedItem.options.value);
125+
setCodeString(exc);
126+
} catch (error) {
127+
setCodeString(localize(
128+
'positron.dataExplorer.cannotGenerateCodeForType',
129+
"Cannot generate code for type {0}",
130+
typedItem.options.value.code_syntax_name
131+
));
132+
}
133+
};
134+
135+
// Render.
136+
return (
137+
<OKCancelModalDialog
138+
catchErrors
139+
height={300}
140+
renderer={props.renderer}
141+
title={(() => localize(
142+
'positronConvertToCodeModalDialogTitle',
143+
"Convert to Code"
144+
))()}
145+
width={400}
146+
onAccept={() => props.renderer.dispose()}
147+
onCancel={() => props.renderer.dispose()}
148+
>
149+
<VerticalStack>
150+
<DropDownListBox
151+
createItem={(item) => (
152+
<DropdownEntry
153+
title={item.options.identifier}
154+
/>
155+
)}
156+
entries={syntaxDropdownEntries()}
157+
title={syntaxDropdownTitle()}
158+
onSelectionChanged={onSelectionChanged}
159+
/>
160+
<pre>
161+
{codeString}
162+
</pre>
163+
</VerticalStack>
164+
165+
</OKCancelModalDialog>
166+
);
167+
};

src/vs/workbench/browser/positronNewFolderFlow/components/steps/dropdownEntry.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ export const DropdownEntry = (props: DropdownEntryProps) => {
3535
<div className='dropdown-entry-subtitle'>
3636
{props.subtitle}
3737
</div>
38-
{props.group ? <div className='dropdown-entry-group'>{props.group}</div> : null}
38+
{props.group && <div className='dropdown-entry-group'>{props.group}</div>}
3939
</div>
4040
);
4141
};

0 commit comments

Comments
 (0)