Skip to content

Commit d35aa49

Browse files
dsmmckenmofojed
andauthored
feat: adds copy file support to file explorer and fixes rename bug (#1491)
Fixes #185, Fixes #1375, Fixes #1488 and other issues with File Explorer Fixes issue where you couldn't rename a renamed item, and then can't use context menu at all on file explorer. Fixes issue where triggering a tooltip while renaming would re-render and exit edit mode. Wires up copy, new file, new folder in context menu. Renames copy -> copy file --------- Co-authored-by: Mike Bender <mikebender@deephaven.io>
1 parent f626876 commit d35aa49

9 files changed

Lines changed: 109 additions & 13 deletions

File tree

packages/code-studio/src/storage/grpc/GrpcFileStorage.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,16 @@ export class GrpcFileStorage implements FileStorage {
8080
};
8181
}
8282

83+
async copyFile(name: string, newName: string): Promise<void> {
84+
const fileContents = await this.storageService.loadFile(this.addRoot(name));
85+
await this.storageService.saveFile(
86+
this.addRoot(newName),
87+
fileContents,
88+
false
89+
);
90+
this.refreshTables();
91+
}
92+
8393
async deleteFile(name: string): Promise<void> {
8494
await this.storageService.deleteItem(this.addRoot(name));
8595
this.refreshTables();

packages/components/src/ItemList.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -371,6 +371,8 @@ export class ItemList<T> extends PureComponent<
371371
itemIndex: number,
372372
e: React.MouseEvent<HTMLDivElement>
373373
): void {
374+
this.setState({ focusIndex: itemIndex });
375+
374376
// Update the selection, but don't consume the mouse event - it will trigger the context menu
375377
const { selectedRanges } = this.state;
376378
const isSelected = RangeUtils.isSelected(selectedRanges, itemIndex);

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

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import FileExplorer, {
77
FileStorageItem,
88
FileUtils,
99
NewItemModal,
10+
isDirectory,
1011
} from '@deephaven/file-explorer';
1112
import React, { ReactNode } from 'react';
1213
import { connect, ConnectedProps } from 'react-redux';
@@ -94,6 +95,7 @@ export class FileExplorerPanel extends React.Component<
9495
super(props);
9596

9697
this.handleFileSelect = this.handleFileSelect.bind(this);
98+
this.handleCopyFile = this.handleCopyFile.bind(this);
9799
this.handleCreateFile = this.handleCreateFile.bind(this);
98100
this.handleCreateDirectory = this.handleCreateDirectory.bind(this);
99101
this.handleCreateDirectoryCancel =
@@ -139,7 +141,7 @@ export class FileExplorerPanel extends React.Component<
139141
);
140142
}
141143

142-
handleCreateDirectory(path?: string): void {
144+
handleCreateDirectory(): void {
143145
this.setState({ showCreateFolder: true });
144146
}
145147

@@ -157,6 +159,29 @@ export class FileExplorerPanel extends React.Component<
157159
fileStorage.createDirectory(path).catch(FileExplorerPanel.handleError);
158160
}
159161

162+
async handleCopyFile(file: FileStorageItem): Promise<void> {
163+
const { fileStorage } = this.props;
164+
if (isDirectory(file)) {
165+
log.error('Invalid item in handleCopyItem', file);
166+
return;
167+
}
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+
}
182+
await fileStorage.copyFile(file.filename, newName);
183+
}
184+
160185
handleDelete(files: FileStorageItem[]): void {
161186
const { glEventHub } = this.props;
162187
files.forEach(file => {
@@ -257,6 +282,9 @@ export class FileExplorerPanel extends React.Component<
257282
<FileExplorer
258283
isMultiSelect
259284
storage={fileStorage}
285+
onCopy={this.handleCopyFile}
286+
onCreateFile={this.handleCreateFile}
287+
onCreateFolder={this.handleCreateDirectory}
260288
onDelete={this.handleDelete}
261289
onRename={this.handleRename}
262290
onSelect={this.handleFileSelect}

packages/dashboard-core-plugins/src/panels/MockFileStorage.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,10 @@ export class MockFileStorage implements FileStorage {
2828
throw new Error('Method not implemented.');
2929
}
3030

31+
async copyFile(name: string, newName: string): Promise<void> {
32+
throw new Error('Method not implemented.');
33+
}
34+
3135
async deleteFile(name: string): Promise<void> {
3236
this.items = this.items.filter(value => value.filename !== name);
3337
}

packages/file-explorer/src/FileExplorer.tsx

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,9 @@ export interface FileExplorerProps {
2222
isMultiSelect?: boolean;
2323
focusedPath?: string;
2424

25+
onCopy?: (file: FileStorageItem) => void;
26+
onCreateFile?: () => void;
27+
onCreateFolder?: () => void;
2528
onDelete?: (files: FileStorageItem[]) => void;
2629
onRename?: (oldName: string, newName: string) => void;
2730
onSelect: (file: FileStorageItem, event: React.SyntheticEvent) => void;
@@ -39,8 +42,11 @@ export function FileExplorer(props: FileExplorerProps): JSX.Element {
3942
storage,
4043
isMultiSelect = false,
4144
focusedPath,
45+
onCopy = () => undefined,
4246
onDelete = () => undefined,
4347
onRename = () => undefined,
48+
onCreateFile = () => undefined,
49+
onCreateFolder = () => undefined,
4450
onSelect,
4551
onSelectionChange,
4652
rowHeight = DEFAULT_ROW_HEIGHT,
@@ -80,6 +86,24 @@ export function FileExplorer(props: FileExplorerProps): JSX.Element {
8086
}
8187
}, []);
8288

89+
const handleCreateFile = useCallback(() => {
90+
log.debug('handleCreateFile');
91+
onCreateFile();
92+
}, [onCreateFile]);
93+
94+
const handleCreateFolder = useCallback(() => {
95+
log.debug('handleCreateFolder');
96+
onCreateFolder();
97+
}, [onCreateFolder]);
98+
99+
const handleCopyFile = useCallback(
100+
(file: FileStorageItem) => {
101+
log.debug('handleCopyFile', file.filename);
102+
onCopy(file);
103+
},
104+
[onCopy]
105+
);
106+
83107
const handleDelete = useCallback((files: FileStorageItem[]) => {
84108
log.debug('handleDelete, pending confirmation', files);
85109
setItemsToDelete(files);
@@ -183,6 +207,9 @@ export function FileExplorer(props: FileExplorerProps): JSX.Element {
183207
isMultiSelect={isMultiSelect}
184208
focusedPath={focusedPath}
185209
showContextMenu
210+
onCopy={handleCopyFile}
211+
onCreateFolder={handleCreateFolder}
212+
onCreateFile={handleCreateFile}
186213
onMove={handleMove}
187214
onDelete={handleDelete}
188215
onRename={handleRename}

packages/file-explorer/src/FileList.tsx

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,8 @@ export function FileList(props: FileListProps): JSX.Element {
8181
const [dragPlaceholder, setDragPlaceholder] = useState<HTMLDivElement>();
8282
const [selectedRanges, setSelectedRanges] = useState([] as Range[]);
8383

84+
const focusedIndex = useRef<number | null>();
85+
8486
const itemList = useRef<ItemList<FileStorageItem>>(null);
8587
const fileList = useRef<HTMLDivElement>(null);
8688

@@ -279,9 +281,9 @@ export function FileList(props: FileListProps): JSX.Element {
279281
);
280282

281283
const handleSelectionChange = useCallback(
282-
newSelectedRanges => {
284+
(newSelectedRanges, force = false) => {
283285
log.debug2('handleSelectionChange', newSelectedRanges);
284-
if (newSelectedRanges !== selectedRanges) {
286+
if (force === true || newSelectedRanges !== selectedRanges) {
285287
setSelectedRanges(newSelectedRanges);
286288
const selectedItems = getItems(newSelectedRanges);
287289
onSelectionChange(selectedItems);
@@ -299,6 +301,7 @@ export function FileList(props: FileListProps): JSX.Element {
299301
} else {
300302
onFocusChange();
301303
}
304+
focusedIndex.current = focusIndex;
302305
},
303306
[getItems, onFocusChange]
304307
);
@@ -371,6 +374,23 @@ export function FileList(props: FileListProps): JSX.Element {
371374
[table]
372375
);
373376

377+
// if the loadedViewport changes, re-fire the focused
378+
// item and the selected range items as they could have
379+
// been updated
380+
useEffect(
381+
function updateFocusAndSelection() {
382+
if (focusedIndex.current != null) {
383+
handleFocusChange(focusedIndex.current);
384+
}
385+
if (selectedRanges.length > 0) {
386+
// force the update, as the selected range may be the same
387+
// but the selected items may now be different
388+
handleSelectionChange(selectedRanges, true);
389+
}
390+
},
391+
[loadedViewport, handleFocusChange, handleSelectionChange, selectedRanges]
392+
);
393+
374394
// Expand a folder if hovering over it
375395
useEffect(
376396
function expandFolderOnHover() {

packages/file-explorer/src/FileListContainer.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -134,8 +134,8 @@ export function FileListContainer(props: FileListContainerProps): JSX.Element {
134134
}
135135
if (onCopy) {
136136
result.push({
137-
title: 'Copy',
138-
description: 'Copy',
137+
title: 'Copy File',
138+
description: 'Copy the selected file',
139139
action: handleCopyAction,
140140
group: ContextActions.groups.low,
141141
disabled: focusedItem == null || isDirectory(focusedItem),

packages/file-explorer/src/FileListItem.tsx

Lines changed: 6 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -105,14 +105,12 @@ export function FileListItem(props: FileListRenderItemProps): JSX.Element {
105105
{depthLines}{' '}
106106
<FontAwesomeIcon icon={icon} className="item-icon" fixedWidth />{' '}
107107
<span className="truncation-wrapper">
108-
{children ?? item.basename}
109-
<Tooltip
110-
options={{
111-
placement: 'left',
112-
}}
113-
>
114-
{children ?? item.basename}
115-
</Tooltip>
108+
{children ?? (
109+
<>
110+
{item.basename}
111+
<Tooltip>{item.basename}</Tooltip>
112+
</>
113+
)}
116114
</span>
117115
</div>
118116
);

packages/file-explorer/src/FileStorage.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,13 @@ export interface FileStorage {
8484
*/
8585
moveFile(name: string, newName: string): Promise<void>;
8686

87+
/**
88+
* Copy a file to a new location
89+
* @param name The name of the file to copy
90+
* @param newName The new file name, including path
91+
*/
92+
copyFile(name: string, newName: string): Promise<void>;
93+
8794
/**
8895
* Get the info for the file at the specified path.
8996
* If the file does not exists, rejects with a FileNotFoundError

0 commit comments

Comments
 (0)