Skip to content

Commit

Permalink
feat: adds copy file support to file explorer and fixes rename bug (d…
Browse files Browse the repository at this point in the history
…eephaven#1491)

Fixes deephaven#185, Fixes deephaven#1375, Fixes deephaven#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 <[email protected]>
  • Loading branch information
dsmmcken and mofojed authored Sep 8, 2023
1 parent f626876 commit d35aa49
Show file tree
Hide file tree
Showing 9 changed files with 109 additions and 13 deletions.
10 changes: 10 additions & 0 deletions packages/code-studio/src/storage/grpc/GrpcFileStorage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,16 @@ export class GrpcFileStorage implements FileStorage {
};
}

async copyFile(name: string, newName: string): Promise<void> {
const fileContents = await this.storageService.loadFile(this.addRoot(name));
await this.storageService.saveFile(
this.addRoot(newName),
fileContents,
false
);
this.refreshTables();
}

async deleteFile(name: string): Promise<void> {
await this.storageService.deleteItem(this.addRoot(name));
this.refreshTables();
Expand Down
2 changes: 2 additions & 0 deletions packages/components/src/ItemList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -371,6 +371,8 @@ export class ItemList<T> extends PureComponent<
itemIndex: number,
e: React.MouseEvent<HTMLDivElement>
): void {
this.setState({ focusIndex: itemIndex });

// Update the selection, but don't consume the mouse event - it will trigger the context menu
const { selectedRanges } = this.state;
const isSelected = RangeUtils.isSelected(selectedRanges, itemIndex);
Expand Down
30 changes: 29 additions & 1 deletion packages/dashboard-core-plugins/src/panels/FileExplorerPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import FileExplorer, {
FileStorageItem,
FileUtils,
NewItemModal,
isDirectory,
} from '@deephaven/file-explorer';
import React, { ReactNode } from 'react';
import { connect, ConnectedProps } from 'react-redux';
Expand Down Expand Up @@ -94,6 +95,7 @@ export class FileExplorerPanel extends React.Component<
super(props);

this.handleFileSelect = this.handleFileSelect.bind(this);
this.handleCopyFile = this.handleCopyFile.bind(this);
this.handleCreateFile = this.handleCreateFile.bind(this);
this.handleCreateDirectory = this.handleCreateDirectory.bind(this);
this.handleCreateDirectoryCancel =
Expand Down Expand Up @@ -139,7 +141,7 @@ export class FileExplorerPanel extends React.Component<
);
}

handleCreateDirectory(path?: string): void {
handleCreateDirectory(): void {
this.setState({ showCreateFolder: true });
}

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

async handleCopyFile(file: FileStorageItem): Promise<void> {
const { fileStorage } = this.props;
if (isDirectory(file)) {
log.error('Invalid item in handleCopyItem', file);
return;
}
let newName = FileUtils.getCopyFileName(file.filename);
const checkNewName = async (): Promise<boolean> => {
try {
await fileStorage.info(newName);
return true;
} catch (error) {
return false;
}
};
// await in loop is fine here, this isn't a parallel task
// eslint-disable-next-line no-await-in-loop
while (await checkNewName()) {
newName = FileUtils.getCopyFileName(newName);
}
await fileStorage.copyFile(file.filename, newName);
}

handleDelete(files: FileStorageItem[]): void {
const { glEventHub } = this.props;
files.forEach(file => {
Expand Down Expand Up @@ -257,6 +282,9 @@ export class FileExplorerPanel extends React.Component<
<FileExplorer
isMultiSelect
storage={fileStorage}
onCopy={this.handleCopyFile}
onCreateFile={this.handleCreateFile}
onCreateFolder={this.handleCreateDirectory}
onDelete={this.handleDelete}
onRename={this.handleRename}
onSelect={this.handleFileSelect}
Expand Down
4 changes: 4 additions & 0 deletions packages/dashboard-core-plugins/src/panels/MockFileStorage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,10 @@ export class MockFileStorage implements FileStorage {
throw new Error('Method not implemented.');
}

async copyFile(name: string, newName: string): Promise<void> {
throw new Error('Method not implemented.');
}

async deleteFile(name: string): Promise<void> {
this.items = this.items.filter(value => value.filename !== name);
}
Expand Down
27 changes: 27 additions & 0 deletions packages/file-explorer/src/FileExplorer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,9 @@ export interface FileExplorerProps {
isMultiSelect?: boolean;
focusedPath?: string;

onCopy?: (file: FileStorageItem) => void;
onCreateFile?: () => void;
onCreateFolder?: () => void;
onDelete?: (files: FileStorageItem[]) => void;
onRename?: (oldName: string, newName: string) => void;
onSelect: (file: FileStorageItem, event: React.SyntheticEvent) => void;
Expand All @@ -39,8 +42,11 @@ export function FileExplorer(props: FileExplorerProps): JSX.Element {
storage,
isMultiSelect = false,
focusedPath,
onCopy = () => undefined,
onDelete = () => undefined,
onRename = () => undefined,
onCreateFile = () => undefined,
onCreateFolder = () => undefined,
onSelect,
onSelectionChange,
rowHeight = DEFAULT_ROW_HEIGHT,
Expand Down Expand Up @@ -80,6 +86,24 @@ export function FileExplorer(props: FileExplorerProps): JSX.Element {
}
}, []);

const handleCreateFile = useCallback(() => {
log.debug('handleCreateFile');
onCreateFile();
}, [onCreateFile]);

const handleCreateFolder = useCallback(() => {
log.debug('handleCreateFolder');
onCreateFolder();
}, [onCreateFolder]);

const handleCopyFile = useCallback(
(file: FileStorageItem) => {
log.debug('handleCopyFile', file.filename);
onCopy(file);
},
[onCopy]
);

const handleDelete = useCallback((files: FileStorageItem[]) => {
log.debug('handleDelete, pending confirmation', files);
setItemsToDelete(files);
Expand Down Expand Up @@ -183,6 +207,9 @@ export function FileExplorer(props: FileExplorerProps): JSX.Element {
isMultiSelect={isMultiSelect}
focusedPath={focusedPath}
showContextMenu
onCopy={handleCopyFile}
onCreateFolder={handleCreateFolder}
onCreateFile={handleCreateFile}
onMove={handleMove}
onDelete={handleDelete}
onRename={handleRename}
Expand Down
24 changes: 22 additions & 2 deletions packages/file-explorer/src/FileList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,8 @@ export function FileList(props: FileListProps): JSX.Element {
const [dragPlaceholder, setDragPlaceholder] = useState<HTMLDivElement>();
const [selectedRanges, setSelectedRanges] = useState([] as Range[]);

const focusedIndex = useRef<number | null>();

const itemList = useRef<ItemList<FileStorageItem>>(null);
const fileList = useRef<HTMLDivElement>(null);

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

const handleSelectionChange = useCallback(
newSelectedRanges => {
(newSelectedRanges, force = false) => {
log.debug2('handleSelectionChange', newSelectedRanges);
if (newSelectedRanges !== selectedRanges) {
if (force === true || newSelectedRanges !== selectedRanges) {
setSelectedRanges(newSelectedRanges);
const selectedItems = getItems(newSelectedRanges);
onSelectionChange(selectedItems);
Expand All @@ -299,6 +301,7 @@ export function FileList(props: FileListProps): JSX.Element {
} else {
onFocusChange();
}
focusedIndex.current = focusIndex;
},
[getItems, onFocusChange]
);
Expand Down Expand Up @@ -371,6 +374,23 @@ export function FileList(props: FileListProps): JSX.Element {
[table]
);

// if the loadedViewport changes, re-fire the focused
// item and the selected range items as they could have
// been updated
useEffect(
function updateFocusAndSelection() {
if (focusedIndex.current != null) {
handleFocusChange(focusedIndex.current);
}
if (selectedRanges.length > 0) {
// force the update, as the selected range may be the same
// but the selected items may now be different
handleSelectionChange(selectedRanges, true);
}
},
[loadedViewport, handleFocusChange, handleSelectionChange, selectedRanges]
);

// Expand a folder if hovering over it
useEffect(
function expandFolderOnHover() {
Expand Down
4 changes: 2 additions & 2 deletions packages/file-explorer/src/FileListContainer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -134,8 +134,8 @@ export function FileListContainer(props: FileListContainerProps): JSX.Element {
}
if (onCopy) {
result.push({
title: 'Copy',
description: 'Copy',
title: 'Copy File',
description: 'Copy the selected file',
action: handleCopyAction,
group: ContextActions.groups.low,
disabled: focusedItem == null || isDirectory(focusedItem),
Expand Down
14 changes: 6 additions & 8 deletions packages/file-explorer/src/FileListItem.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -105,14 +105,12 @@ export function FileListItem(props: FileListRenderItemProps): JSX.Element {
{depthLines}{' '}
<FontAwesomeIcon icon={icon} className="item-icon" fixedWidth />{' '}
<span className="truncation-wrapper">
{children ?? item.basename}
<Tooltip
options={{
placement: 'left',
}}
>
{children ?? item.basename}
</Tooltip>
{children ?? (
<>
{item.basename}
<Tooltip>{item.basename}</Tooltip>
</>
)}
</span>
</div>
);
Expand Down
7 changes: 7 additions & 0 deletions packages/file-explorer/src/FileStorage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,13 @@ export interface FileStorage {
*/
moveFile(name: string, newName: string): Promise<void>;

/**
* Copy a file to a new location
* @param name The name of the file to copy
* @param newName The new file name, including path
*/
copyFile(name: string, newName: string): Promise<void>;

/**
* Get the info for the file at the specified path.
* If the file does not exists, rejects with a FileNotFoundError
Expand Down

0 comments on commit d35aa49

Please sign in to comment.