Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add rename action #138

Merged
merged 3 commits into from
Apr 24, 2022
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
70 changes: 67 additions & 3 deletions js/src/treefinder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,29 +9,32 @@
import { ILayoutRestorer, IRouter, JupyterFrontEnd } from "@jupyterlab/application";
import {
IWindowResolver,
showErrorMessage,
Toolbar,
ToolbarButton,
WidgetTracker, /*Clipboard, Dialog, IWindowResolver, showDialog, showErrorMessage*/
WidgetTracker, /*Clipboard, Dialog, IWindowResolver, showDialog*/
} from "@jupyterlab/apputils";
// import { PathExt, URLExt } from "@jupyterlab/coreutils";
import { IDocumentManager /*isValidFileName, renameFile*/ } from "@jupyterlab/docmanager";
import { IDocumentManager, isValidFileName /*renameFile*/ } from "@jupyterlab/docmanager";
// import { DocumentRegistry } from "@jupyterlab/docregistry";
import { Contents, ContentsManager } from "@jupyterlab/services";
import {
closeIcon,
copyIcon,
cutIcon,
editIcon,
pasteIcon,
refreshIcon,
} from "@jupyterlab/ui-components";
// import JSZip from "jszip";
import { DisposableSet, IDisposable } from "@lumino/disposable";
import { PanelLayout, Widget } from "@lumino/widgets";
import { Format, IContentRow, Path, TreeFinderPanelElement } from "tree-finder";
import { Content, Format, IContentRow, Path, TreeFinderPanelElement } from "tree-finder";

import { JupyterClipboard } from "./clipboard";
import { IFSResource } from "./filesystem";
import { fileTreeIcon } from "./icons";
import { doRename } from "./utils";
// import { Uploader } from "./upload";

export class JupyterContents {
Expand All @@ -45,6 +48,12 @@ export class JupyterContents {
return JupyterContents.toJupyterContentRow(await this.cm.get(path), this.cm, this.drive);
}

async rename(path: string, newPath: string) {
path = JupyterContents.toFullPath(path, this.drive);
newPath = JupyterContents.toFullPath(newPath, this.drive);
return JupyterContents.toJupyterContentRow(await this.cm.rename(path, newPath), this.cm, this.drive);
}

readonly cm: ContentsManager;
readonly drive: string;
}
Expand Down Expand Up @@ -405,6 +414,18 @@ export namespace TreeFinderSidebar {
icon: pasteIcon,
label: "Paste",
}),
app.commands.addCommand(widget.commandIDs.rename, {
execute: args => {
const oldContent = widget.treefinder.selection[0];
void _doRename(widget, oldContent).then(newContent => {
widget.treefinder.model.renamerSub.next( { name: newContent.name, target: oldContent } );
// TODO: Model state of TreeFinderWidget should be updated by renamerSub process.
oldContent.row = newContent;
});
},
icon: editIcon,
label: "Rename",
}),
app.commands.addCommand(widget.commandIDs.refresh, {
execute: args => args["selection"] ? clipboard.refreshSelection(widget.treefinder.model) : clipboard.refresh(widget.treefinder.model),
icon: refreshIcon,
Expand Down Expand Up @@ -438,6 +459,11 @@ export namespace TreeFinderSidebar {
selector,
rank: 5,
}),
app.contextMenu.addItem({
command: widget.commandIDs.rename,
selector,
rank: 6,
}),

app.contextMenu.addItem({
args: { selection: true },
Expand All @@ -452,4 +478,42 @@ export namespace TreeFinderSidebar {
set.add(d); return set;
}, new DisposableSet());
}

export function _doRename(widget: TreeFinderSidebar, oldContent: Content<JupyterContents.IJupyterContentRow>): Promise<JupyterContents.IJupyterContentRow> {
const textNode = document.querySelector(".tf-mod-select .rt-tree-container .rt-group-name").firstChild as HTMLElement;
const original = textNode.textContent;
const editNode = document.createElement("input");
editNode.value = original;
return doRename(textNode, editNode, original).then(
newName => {
if (!newName || newName === oldContent.name) {
return oldContent.row;
}
if (!isValidFileName(newName)) {
void showErrorMessage(
"Rename Error",
Error(newName +' is not a valid name for a file. Names must have nonzero length, and cannot include "/", "\\", or ":"')
);
return oldContent.row;
}
const oldPath = oldContent.getPathAtDepth(1).join("/");
const newPath = oldPath.slice(0, -1 * original.length) + newName;
const promise = widget.treefinder.cm.rename(oldPath, newPath);
return promise
.catch(error => {
if (error !== "File not renamed") {
void showErrorMessage(
"Rename Error",
error
);
}
return oldContent.row;
})
.then(newContent => {
textNode.textContent = newName;
return newContent;
});
}
);
}
}
13 changes: 9 additions & 4 deletions js/src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,17 +66,21 @@ export function createOpenNode(): HTMLElement {
return body;
}

export function doRename(text: HTMLElement, edit: HTMLInputElement) {
const parent = text.parentElement;
export function doRename(
text: HTMLElement,
edit: HTMLInputElement,
original: string
): Promise<string> {
const parent = text.parentElement as HTMLElement;
parent.replaceChild(edit, text);
edit.focus();
const index = edit.value.lastIndexOf(".");
const index = edit.value.lastIndexOf('.');
if (index === -1) {
edit.setSelectionRange(0, edit.value.length);
} else {
edit.setSelectionRange(0, index);
}
// handle enter

return new Promise<string>((resolve, reject) => {
edit.onblur = () => {
parent.replaceChild(text, edit);
Expand All @@ -92,6 +96,7 @@ export function doRename(text: HTMLElement, edit: HTMLInputElement) {
case 27: // Escape
event.stopPropagation();
event.preventDefault();
edit.value = original;
edit.blur();
break;
case 38: // Up arrow
Expand Down