Skip to content

Commit 4021aac

Browse files
authored
fix: Handle deletion of unsaved copied file in NotebookPanel (#1557)
* Previously, duplicating a notebook file and then trying to delete it without saving it first will cause an error to occur * Added a `fileExists` function to `FileUtils` to handle this check * Added a `getUniqueCopyFileName` function to `FileUtils` * Behaviour of copying a file from `NotebookPanel` changed from opening an unsaved copy of the file to duplicating the file in `FileStorage` first * Related to #1359 #### Testing Instructions: * Open a file in the notebook panel * Copy the opened file by using the 'Copy File' option in the overflow menu * Delete the file by using the overflow menu (No error should occur) * Repeat the same steps but copy the file by right clicking the file in the file explorer * Delete the copied file again, this time it should also disappear from the file explorer --------- Co-authored-by: georgecwan <georgecwan@users.noreply.github.com>
1 parent 327bcb6 commit 4021aac

3 files changed

Lines changed: 61 additions & 20 deletions

File tree

packages/dashboard-core-plugins/src/panels/FileExplorerPanel.tsx

Lines changed: 4 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -165,20 +165,10 @@ export class FileExplorerPanel extends React.Component<
165165
log.error('Invalid item in handleCopyItem', file);
166166
return;
167167
}
168-
let newName = FileUtils.getCopyFileName(file.filename);
169-
const checkNewName = async (): Promise<boolean> => {
170-
try {
171-
await fileStorage.info(newName);
172-
return true;
173-
} catch (error) {
174-
return false;
175-
}
176-
};
177-
// await in loop is fine here, this isn't a parallel task
178-
// eslint-disable-next-line no-await-in-loop
179-
while (await checkNewName()) {
180-
newName = FileUtils.getCopyFileName(newName);
181-
}
168+
const newName = await FileUtils.getUniqueCopyFileName(
169+
fileStorage,
170+
file.filename
171+
);
182172
await fileStorage.copyFile(file.filename, newName);
183173
}
184174

packages/dashboard-core-plugins/src/panels/NotebookPanel.tsx

Lines changed: 26 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -681,23 +681,39 @@ class NotebookPanel extends Component<NotebookPanelProps, NotebookPanelState> {
681681
this.focus();
682682
}
683683

684-
handleCopy(): void {
684+
async handleCopy(): Promise<void> {
685+
const { fileStorage, glEventHub, session } = this.props;
685686
const { fileMetadata, settings } = this.state;
686687
assertNotNull(fileMetadata);
687-
const content = this.getNotebookValue();
688688
const { language } = settings;
689689
const { itemName } = fileMetadata;
690-
const copyName = FileUtils.getCopyFileName(itemName);
690+
const copyName = await FileUtils.getUniqueCopyFileName(
691+
fileStorage,
692+
itemName
693+
);
691694
log.debug('handleCopy', fileMetadata, itemName, copyName);
692-
this.createNotebook(copyName, language, content);
695+
await fileStorage.copyFile(itemName, copyName);
696+
const newFileMetadata = { id: copyName, itemName: copyName };
697+
const notebookSettings = {
698+
value: null,
699+
language,
700+
};
701+
glEventHub.emit(
702+
NotebookEvent.SELECT_NOTEBOOK,
703+
session,
704+
language,
705+
notebookSettings,
706+
newFileMetadata,
707+
true
708+
);
693709
}
694710

695711
handleDelete(): void {
696712
log.debug('handleDelete, pending confirmation');
697713
this.setState({ showDeleteModal: true });
698714
}
699715

700-
handleDeleteConfirm(): void {
716+
async handleDeleteConfirm(): Promise<void> {
701717
const { fileStorage, glContainer, glEventHub } = this.props;
702718
const { fileMetadata } = this.state;
703719

@@ -708,7 +724,11 @@ class NotebookPanel extends Component<NotebookPanelProps, NotebookPanelState> {
708724
return;
709725
}
710726

711-
if (FileUtils.hasPath(fileMetadata.itemName)) {
727+
if (
728+
FileUtils.hasPath(fileMetadata.itemName) &&
729+
// eslint-disable-next-line @typescript-eslint/strict-boolean-expressions
730+
(await FileUtils.fileExists(fileStorage, fileMetadata.itemName))
731+
) {
712732
glEventHub.emit(NotebookEvent.CLOSE_FILE, fileMetadata, { force: true });
713733
fileStorage.deleteFile(fileMetadata.itemName);
714734
} else {

packages/file-explorer/src/FileUtils.ts

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
import { ValidationError } from '@deephaven/utils';
2+
import FileNotFoundError from './FileNotFoundError';
3+
import { FileStorage } from './FileStorage';
24

35
/**
46
* A basic list of some common MIME types.
@@ -16,6 +18,22 @@ export enum MIME_TYPE {
1618
* Collection of utils for operating on file names
1719
*/
1820
export class FileUtils {
21+
static async fileExists(
22+
fileStorage: FileStorage,
23+
name: string
24+
): Promise<boolean> {
25+
try {
26+
await fileStorage.info(name);
27+
return true;
28+
} catch (e) {
29+
// Should probably make sure it's a `FileNotFoundError`, and re-throw the error if it is not
30+
if (!(e instanceof FileNotFoundError)) {
31+
throw e;
32+
}
33+
return false;
34+
}
35+
}
36+
1937
/**
2038
* Format file extension
2139
* @param extension File extension to format, defaults to empty string
@@ -80,6 +98,19 @@ export class FileUtils {
8098
return extension !== null ? `${copyName}.${extension}` : copyName;
8199
}
82100

101+
static async getUniqueCopyFileName(
102+
fileStorage: FileStorage,
103+
originalName: string
104+
): Promise<string> {
105+
let copyName = FileUtils.getCopyFileName(originalName);
106+
// await in loop is fine here, this isn't a parallel task
107+
// eslint-disable-next-line no-await-in-loop
108+
while (await FileUtils.fileExists(fileStorage, copyName)) {
109+
copyName = FileUtils.getCopyFileName(copyName);
110+
}
111+
return copyName;
112+
}
113+
83114
/**
84115
* Return a MIME type for the provided file
85116
* @param name The file name to get the type for

0 commit comments

Comments
 (0)