Skip to content

Commit 9c166b0

Browse files
committed
feedback
1 parent 76b2381 commit 9c166b0

File tree

2 files changed

+278
-5
lines changed

2 files changed

+278
-5
lines changed

src/notebooks/deepnote/deepnoteExplorerView.ts

Lines changed: 69 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -984,6 +984,10 @@ export class DeepnoteExplorerView {
984984
* @param treeItem The tree item representing a project
985985
*/
986986
private async exportProject(treeItem: DeepnoteTreeItem): Promise<void> {
987+
if (treeItem.type !== DeepnoteTreeItemType.ProjectFile) {
988+
return;
989+
}
990+
987991
try {
988992
const format = await window.showQuickPick([{ label: 'Jupyter Notebook (.ipynb)', value: 'jupyter' }], {
989993
placeHolder: l10n.t('Select export format')
@@ -1016,6 +1020,32 @@ export class DeepnoteExplorerView {
10161020

10171021
const jupyterNotebooks = convertDeepnoteToJupyterNotebooks(projectData);
10181022

1023+
// Check for existing files before writing
1024+
const existingFiles: string[] = [];
1025+
for (const { filename } of jupyterNotebooks) {
1026+
const outputPath = Uri.joinPath(outputFolder[0], filename);
1027+
try {
1028+
await workspace.fs.stat(outputPath);
1029+
existingFiles.push(filename);
1030+
} catch {
1031+
// File doesn't exist, safe to write
1032+
}
1033+
}
1034+
1035+
if (existingFiles.length > 0) {
1036+
const fileList = existingFiles.join(', ');
1037+
const overwrite = l10n.t('Overwrite');
1038+
const result = await window.showWarningMessage(
1039+
l10n.t('The following files already exist: {0}. Do you want to overwrite them?', fileList),
1040+
{ modal: true },
1041+
overwrite
1042+
);
1043+
1044+
if (result !== overwrite) {
1045+
return;
1046+
}
1047+
}
1048+
10191049
for (const { filename, notebook } of jupyterNotebooks) {
10201050
const outputPath = Uri.joinPath(outputFolder[0], filename);
10211051

@@ -1040,6 +1070,10 @@ export class DeepnoteExplorerView {
10401070
* @param treeItem The tree item representing a notebook
10411071
*/
10421072
private async exportNotebook(treeItem: DeepnoteTreeItem): Promise<void> {
1073+
if (treeItem.type !== DeepnoteTreeItemType.Notebook) {
1074+
return;
1075+
}
1076+
10431077
try {
10441078
const format = await window.showQuickPick([{ label: 'Jupyter Notebook (.ipynb)', value: 'jupyter' }], {
10451079
placeHolder: l10n.t('Select export format')
@@ -1070,19 +1104,49 @@ export class DeepnoteExplorerView {
10701104
return;
10711105
}
10721106

1073-
const jupyterNotebooks = convertDeepnoteToJupyterNotebooks(projectData);
1074-
const notebookToExport = jupyterNotebooks.find(
1075-
({ notebook }) => notebook.metadata.deepnote_notebook_id === treeItem.context.notebookId
1076-
);
1107+
const targetNotebook = projectData.project.notebooks.find((nb) => nb.id === treeItem.context.notebookId);
10771108

1078-
if (!notebookToExport) {
1109+
if (!targetNotebook) {
10791110
await window.showErrorMessage(l10n.t('Notebook not found'));
10801111

10811112
return;
10821113
}
10831114

1115+
const filteredProject = {
1116+
...projectData,
1117+
project: {
1118+
...projectData.project,
1119+
notebooks: [targetNotebook]
1120+
}
1121+
};
1122+
1123+
const [notebookToExport] = convertDeepnoteToJupyterNotebooks(filteredProject);
10841124
const outputPath = Uri.joinPath(outputFolder[0], notebookToExport.filename);
10851125

1126+
let fileExists = false;
1127+
try {
1128+
await workspace.fs.stat(outputPath);
1129+
fileExists = true;
1130+
} catch {
1131+
// File doesn't exist, safe to write
1132+
}
1133+
1134+
if (fileExists) {
1135+
const overwrite = l10n.t('Overwrite');
1136+
const result = await window.showWarningMessage(
1137+
l10n.t(
1138+
'A file named "{0}" already exists. Do you want to overwrite it?',
1139+
notebookToExport.filename
1140+
),
1141+
{ modal: true },
1142+
overwrite
1143+
);
1144+
1145+
if (result !== overwrite) {
1146+
return;
1147+
}
1148+
}
1149+
10861150
await workspace.fs.writeFile(
10871151
outputPath,
10881152
new TextEncoder().encode(JSON.stringify(notebookToExport.notebook, null, 2))

src/notebooks/deepnote/deepnoteExplorerView.unit.test.ts

Lines changed: 209 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1886,6 +1886,7 @@ suite('DeepnoteExplorerView - Empty State Commands', () => {
18861886

18871887
const mockFS = mock<typeof workspace.fs>();
18881888
when(mockFS.readFile(anything())).thenReturn(Promise.resolve(Buffer.from(yaml.dump(projectData))));
1889+
when(mockFS.stat(anything())).thenReject(new Error('File not found'));
18891890
when(mockedVSCodeNamespaces.workspace.fs).thenReturn(instance(mockFS));
18901891

18911892
when(mockedVSCodeNamespaces.window.showQuickPick(anything(), anything())).thenReturn(
@@ -1941,6 +1942,7 @@ suite('DeepnoteExplorerView - Empty State Commands', () => {
19411942

19421943
const mockFS = mock<typeof workspace.fs>();
19431944
when(mockFS.readFile(anything())).thenReturn(Promise.resolve(Buffer.from(yaml.dump(projectData))));
1945+
when(mockFS.stat(anything())).thenReject(new Error('File not found'));
19441946
when(mockedVSCodeNamespaces.workspace.fs).thenReturn(instance(mockFS));
19451947

19461948
when(mockedVSCodeNamespaces.window.showQuickPick(anything(), anything())).thenReturn(
@@ -1994,6 +1996,7 @@ suite('DeepnoteExplorerView - Empty State Commands', () => {
19941996

19951997
const mockFS = mock<typeof workspace.fs>();
19961998
when(mockFS.readFile(anything())).thenReturn(Promise.resolve(Buffer.from(yaml.dump(projectData))));
1999+
when(mockFS.stat(anything())).thenReject(new Error('File not found'));
19972000
when(mockedVSCodeNamespaces.workspace.fs).thenReturn(instance(mockFS));
19982001

19992002
when(mockedVSCodeNamespaces.window.showQuickPick(anything(), anything())).thenReturn(
@@ -2043,6 +2046,7 @@ suite('DeepnoteExplorerView - Empty State Commands', () => {
20432046

20442047
const mockFS = mock<typeof workspace.fs>();
20452048
when(mockFS.readFile(anything())).thenReturn(Promise.resolve(Buffer.from(yaml.dump(projectData))));
2049+
when(mockFS.stat(anything())).thenReject(new Error('File not found'));
20462050
when(mockedVSCodeNamespaces.workspace.fs).thenReturn(instance(mockFS));
20472051

20482052
when(mockedVSCodeNamespaces.window.showQuickPick(anything(), anything())).thenReturn(
@@ -2069,6 +2073,108 @@ suite('DeepnoteExplorerView - Empty State Commands', () => {
20692073
// Verify error message was shown
20702074
verify(mockedVSCodeNamespaces.window.showErrorMessage(anything())).once();
20712075
});
2076+
2077+
test('should prompt for overwrite when files already exist and cancel if declined', async () => {
2078+
resetVSCodeMocks();
2079+
2080+
const projectData = {
2081+
version: '1.0.0',
2082+
metadata: { createdAt: '2024-01-01T00:00:00.000Z' },
2083+
project: {
2084+
id: 'project-id',
2085+
name: 'Test Project',
2086+
notebooks: [
2087+
{ id: 'nb-1', name: 'Notebook 1', blocks: [], executionMode: 'block' },
2088+
{ id: 'nb-2', name: 'Notebook 2', blocks: [], executionMode: 'block' }
2089+
]
2090+
}
2091+
};
2092+
2093+
const mockFS = mock<typeof workspace.fs>();
2094+
when(mockFS.readFile(anything())).thenReturn(Promise.resolve(Buffer.from(yaml.dump(projectData))));
2095+
// Files exist - stat returns successfully
2096+
when(mockFS.stat(anything())).thenReturn(Promise.resolve({} as any));
2097+
when(mockedVSCodeNamespaces.workspace.fs).thenReturn(instance(mockFS));
2098+
2099+
when(mockedVSCodeNamespaces.window.showQuickPick(anything(), anything())).thenReturn(
2100+
Promise.resolve({ label: 'Jupyter Notebook (.ipynb)', value: 'jupyter' }) as any
2101+
);
2102+
when(mockedVSCodeNamespaces.window.showOpenDialog(anything())).thenReturn(
2103+
Promise.resolve([Uri.file('/output/folder')])
2104+
);
2105+
// User cancels overwrite
2106+
when(mockedVSCodeNamespaces.window.showWarningMessage(anything(), anything(), anything())).thenReturn(
2107+
Promise.resolve(undefined)
2108+
);
2109+
2110+
const treeItem: Partial<DeepnoteTreeItem> = {
2111+
type: DeepnoteTreeItemType.ProjectFile,
2112+
context: {
2113+
filePath: '/test/project.deepnote',
2114+
projectId: 'project-id'
2115+
}
2116+
};
2117+
2118+
await (explorerView as any).exportProject(treeItem);
2119+
2120+
// Verify warning message was shown about files existing
2121+
verify(mockedVSCodeNamespaces.window.showWarningMessage(anything(), anything(), anything())).once();
2122+
// Verify no files were written
2123+
verify(mockFS.writeFile(anything(), anything())).never();
2124+
});
2125+
2126+
test('should overwrite files when user confirms', async () => {
2127+
resetVSCodeMocks();
2128+
2129+
const projectData = {
2130+
version: '1.0.0',
2131+
metadata: { createdAt: '2024-01-01T00:00:00.000Z' },
2132+
project: {
2133+
id: 'project-id',
2134+
name: 'Test Project',
2135+
notebooks: [{ id: 'nb-1', name: 'Notebook 1', blocks: [], executionMode: 'block' }]
2136+
}
2137+
};
2138+
2139+
const mockFS = mock<typeof workspace.fs>();
2140+
when(mockFS.readFile(anything())).thenReturn(Promise.resolve(Buffer.from(yaml.dump(projectData))));
2141+
// File exists - stat returns successfully
2142+
when(mockFS.stat(anything())).thenReturn(Promise.resolve({} as any));
2143+
when(mockedVSCodeNamespaces.workspace.fs).thenReturn(instance(mockFS));
2144+
2145+
when(mockedVSCodeNamespaces.window.showQuickPick(anything(), anything())).thenReturn(
2146+
Promise.resolve({ label: 'Jupyter Notebook (.ipynb)', value: 'jupyter' }) as any
2147+
);
2148+
when(mockedVSCodeNamespaces.window.showOpenDialog(anything())).thenReturn(
2149+
Promise.resolve([Uri.file('/output/folder')])
2150+
);
2151+
// User confirms overwrite
2152+
when(mockedVSCodeNamespaces.window.showWarningMessage(anything(), anything(), anything())).thenReturn(
2153+
Promise.resolve('Overwrite') as any
2154+
);
2155+
when(mockedVSCodeNamespaces.window.showInformationMessage(anything())).thenReturn(
2156+
Promise.resolve(undefined)
2157+
);
2158+
2159+
let writeCount = 0;
2160+
when(mockFS.writeFile(anything(), anything())).thenCall(() => {
2161+
writeCount++;
2162+
return Promise.resolve();
2163+
});
2164+
2165+
const treeItem: Partial<DeepnoteTreeItem> = {
2166+
type: DeepnoteTreeItemType.ProjectFile,
2167+
context: {
2168+
filePath: '/test/project.deepnote',
2169+
projectId: 'project-id'
2170+
}
2171+
};
2172+
2173+
await (explorerView as any).exportProject(treeItem);
2174+
2175+
// Verify file was written after user confirmed overwrite
2176+
assert.strictEqual(writeCount, 1);
2177+
});
20722178
});
20732179

20742180
suite('exportNotebook', () => {
@@ -2189,6 +2295,7 @@ suite('DeepnoteExplorerView - Empty State Commands', () => {
21892295

21902296
const mockFS = mock<typeof workspace.fs>();
21912297
when(mockFS.readFile(anything())).thenReturn(Promise.resolve(Buffer.from(yaml.dump(projectData))));
2298+
when(mockFS.stat(anything())).thenReject(new Error('File not found'));
21922299
when(mockedVSCodeNamespaces.workspace.fs).thenReturn(instance(mockFS));
21932300

21942301
when(mockedVSCodeNamespaces.window.showQuickPick(anything(), anything())).thenReturn(
@@ -2287,6 +2394,7 @@ suite('DeepnoteExplorerView - Empty State Commands', () => {
22872394

22882395
const mockFS = mock<typeof workspace.fs>();
22892396
when(mockFS.readFile(anything())).thenReturn(Promise.resolve(Buffer.from(yaml.dump(projectData))));
2397+
when(mockFS.stat(anything())).thenReject(new Error('File not found'));
22902398
when(mockedVSCodeNamespaces.workspace.fs).thenReturn(instance(mockFS));
22912399

22922400
when(mockedVSCodeNamespaces.window.showQuickPick(anything(), anything())).thenReturn(
@@ -2314,5 +2422,106 @@ suite('DeepnoteExplorerView - Empty State Commands', () => {
23142422
// Verify error message was shown
23152423
verify(mockedVSCodeNamespaces.window.showErrorMessage(anything())).once();
23162424
});
2425+
2426+
test('should prompt for overwrite when file already exists and cancel if declined', async () => {
2427+
resetVSCodeMocks();
2428+
2429+
const projectData = {
2430+
version: '1.0.0',
2431+
metadata: { createdAt: '2024-01-01T00:00:00.000Z' },
2432+
project: {
2433+
id: 'project-id',
2434+
name: 'Test Project',
2435+
notebooks: [{ id: 'nb-1', name: 'Notebook 1', blocks: [], executionMode: 'block' }]
2436+
}
2437+
};
2438+
2439+
const mockFS = mock<typeof workspace.fs>();
2440+
when(mockFS.readFile(anything())).thenReturn(Promise.resolve(Buffer.from(yaml.dump(projectData))));
2441+
// File exists - stat returns successfully
2442+
when(mockFS.stat(anything())).thenReturn(Promise.resolve({} as any));
2443+
when(mockedVSCodeNamespaces.workspace.fs).thenReturn(instance(mockFS));
2444+
2445+
when(mockedVSCodeNamespaces.window.showQuickPick(anything(), anything())).thenReturn(
2446+
Promise.resolve({ label: 'Jupyter Notebook (.ipynb)', value: 'jupyter' }) as any
2447+
);
2448+
when(mockedVSCodeNamespaces.window.showOpenDialog(anything())).thenReturn(
2449+
Promise.resolve([Uri.file('/output/folder')])
2450+
);
2451+
// User cancels overwrite
2452+
when(mockedVSCodeNamespaces.window.showWarningMessage(anything(), anything(), anything())).thenReturn(
2453+
Promise.resolve(undefined)
2454+
);
2455+
2456+
const treeItem: Partial<DeepnoteTreeItem> = {
2457+
type: DeepnoteTreeItemType.Notebook,
2458+
context: {
2459+
filePath: '/test/project.deepnote',
2460+
projectId: 'project-id',
2461+
notebookId: 'nb-1'
2462+
}
2463+
};
2464+
2465+
await (explorerView as any).exportNotebook(treeItem);
2466+
2467+
// Verify warning message was shown about file existing
2468+
verify(mockedVSCodeNamespaces.window.showWarningMessage(anything(), anything(), anything())).once();
2469+
// Verify no file was written
2470+
verify(mockFS.writeFile(anything(), anything())).never();
2471+
});
2472+
2473+
test('should overwrite file when user confirms', async () => {
2474+
resetVSCodeMocks();
2475+
2476+
const projectData = {
2477+
version: '1.0.0',
2478+
metadata: { createdAt: '2024-01-01T00:00:00.000Z' },
2479+
project: {
2480+
id: 'project-id',
2481+
name: 'Test Project',
2482+
notebooks: [{ id: 'nb-1', name: 'Notebook 1', blocks: [], executionMode: 'block' }]
2483+
}
2484+
};
2485+
2486+
const mockFS = mock<typeof workspace.fs>();
2487+
when(mockFS.readFile(anything())).thenReturn(Promise.resolve(Buffer.from(yaml.dump(projectData))));
2488+
// File exists - stat returns successfully
2489+
when(mockFS.stat(anything())).thenReturn(Promise.resolve({} as any));
2490+
when(mockedVSCodeNamespaces.workspace.fs).thenReturn(instance(mockFS));
2491+
2492+
when(mockedVSCodeNamespaces.window.showQuickPick(anything(), anything())).thenReturn(
2493+
Promise.resolve({ label: 'Jupyter Notebook (.ipynb)', value: 'jupyter' }) as any
2494+
);
2495+
when(mockedVSCodeNamespaces.window.showOpenDialog(anything())).thenReturn(
2496+
Promise.resolve([Uri.file('/output/folder')])
2497+
);
2498+
// User confirms overwrite
2499+
when(mockedVSCodeNamespaces.window.showWarningMessage(anything(), anything(), anything())).thenReturn(
2500+
Promise.resolve('Overwrite') as any
2501+
);
2502+
when(mockedVSCodeNamespaces.window.showInformationMessage(anything())).thenReturn(
2503+
Promise.resolve(undefined)
2504+
);
2505+
2506+
let writeCount = 0;
2507+
when(mockFS.writeFile(anything(), anything())).thenCall(() => {
2508+
writeCount++;
2509+
return Promise.resolve();
2510+
});
2511+
2512+
const treeItem: Partial<DeepnoteTreeItem> = {
2513+
type: DeepnoteTreeItemType.Notebook,
2514+
context: {
2515+
filePath: '/test/project.deepnote',
2516+
projectId: 'project-id',
2517+
notebookId: 'nb-1'
2518+
}
2519+
};
2520+
2521+
await (explorerView as any).exportNotebook(treeItem);
2522+
2523+
// Verify file was written after user confirmed overwrite
2524+
assert.strictEqual(writeCount, 1);
2525+
});
23172526
});
23182527
});

0 commit comments

Comments
 (0)