Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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