Skip to content

Commit

Permalink
feat(jupyter): #60 support file upload
Browse files Browse the repository at this point in the history
  • Loading branch information
BroKun committed Jan 31, 2024
1 parent be7190e commit f7c6839
Show file tree
Hide file tree
Showing 3 changed files with 167 additions and 1 deletion.
32 changes: 32 additions & 0 deletions .changeset/blue-pugs-roll.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
---
'@difizen/libro-cofine-editor-contribution': patch
'@difizen/libro-cofine-editor-core': patch
'@difizen/libro-search-code-cell': patch
'@difizen/libro-cofine-textmate': patch
'@difizen/libro-cofine-editor': patch
'@difizen/libro-markdown-cell': patch
'@difizen/libro-shared-model': patch
'@difizen/libro-code-editor': patch
'@difizen/libro-prompt-cell': patch
'@difizen/libro-virtualized': patch
'@difizen/libro-codemirror': patch
'@difizen/libro-rendermime': patch
'@difizen/libro-code-cell': patch
'@difizen/libro-markdown': patch
'@difizen/libro-raw-cell': patch
'@difizen/libro-terminal': patch
'@difizen/libro-jupyter': patch
'@difizen/libro-common': patch
'@difizen/libro-kernel': patch
'@difizen/libro-output': patch
'@difizen/libro-search': patch
'@difizen/libro-widget': patch
'@difizen/libro-core': patch
'@difizen/libro-l10n': patch
'@difizen/libro-lab': patch
'@difizen/libro-lsp': patch
'@difizen/libro-toc': patch
'@difizen/libro-docs': patch
---

File uploads can now be triggered by right-clicking on the file tree.
46 changes: 45 additions & 1 deletion packages/libro-jupyter/src/file/file-command.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,10 @@ export const FileCommands = {
id: 'fileTree.command.download',
label: '下载',
},
UPLOAD: {
id: 'fileTree.command.upload',
label: '上传',
},
};
export const FileTreeContextMenuPath: MenuPath = ['file-tree-context-menu'];
export const FileTreeContextMenuGroups: Record<string, MenuPath> = {
Expand All @@ -105,6 +109,7 @@ export class FileCommandContribution
lastAction: 'COPY' | 'CUT';
lastActionNode: FileStatNode;
allowDownload = false;
allowUpload = false;

constructor(@inject(ViewManager) viewManager: ViewManager) {
this.viewManager = viewManager;
Expand Down Expand Up @@ -180,6 +185,11 @@ export class FileCommandContribution
command: FileCommands.DOWNLOAD.id,
order: 'h',
});
menu.registerMenuAction(FileTreeContextMenuGroups['extra'], {
id: FileCommands.UPLOAD.id,
command: FileCommands.UPLOAD.id,
order: 'i',
});
}
registerCommands(command: CommandRegistry): void {
command.registerCommand(FileCommands.OPEN_FILE, {
Expand Down Expand Up @@ -356,6 +366,11 @@ export class FileCommandContribution
this.contentsManager
.getDownloadUrl(path)
.then((url) => {
const urlObj = new URL(url);
if (urlObj.origin !== location.origin) {
// not same origin
return;
}
const element = document.createElement('a');
element.href = url;
element.download = '';
Expand All @@ -368,7 +383,36 @@ export class FileCommandContribution
.catch(console.error);
},
isVisible: (data) => {
return this.allowDownload && FileStatNode.is(data);
return this.allowDownload && FileStatNode.is(data) && data.fileStat.isFile;
},
});

command.registerCommand(FileCommands.UPLOAD, {
execute: (data, view) => {
if (!this.allowUpload) {
return;
}
if (!view || !(view instanceof FileView)) {
return;
}
if (!data || data instanceof FileView) {
return view.uploadSubmit();
}
if (FileStatNode.is(data) && data.fileStat.isDirectory) {
return view.uploadSubmit(data.uri.path.toString());
}
},
isVisible: (data, view) => {
if (!this.allowUpload) {
return false;
}
if (!view || !(view instanceof FileView)) {
return false;
}
if (!data || data instanceof FileView) {
return true;
}
return FileStatNode.is(data) && data.fileStat.isDirectory;
},
});
}
Expand Down
90 changes: 90 additions & 0 deletions packages/libro-jupyter/src/file/file-view/index.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import { ExclamationCircleFilled, FolderFilled } from '@ant-design/icons';
import { ContentsManager } from '@difizen/libro-kernel';
import type { IContentsModel } from '@difizen/libro-kernel';
import type { TreeNode, ViewOpenHandler } from '@difizen/mana-app';
import { FileTreeViewFactory } from '@difizen/mana-app';
import {
Expand Down Expand Up @@ -39,7 +41,9 @@ const noVerifyFileType = ['.ipynb', '.py'];
@view(FileTreeViewFactory, FileTreeModule)
export class FileView extends FileTreeView {
@inject(OpenerService) protected openService: OpenerService;
@inject(ContentsManager) protected contentsManager: ContentsManager;
@inject(CommandRegistry) protected command: CommandRegistry;
uploadInput?: HTMLInputElement;
override id = FileTreeViewFactory;
override className = 'libro-jupyter-file-tree';

Expand All @@ -64,6 +68,92 @@ export class FileView extends FileTreeView {
this.toDispose.push(this.model.onOpenNode(this.openNode));
}

override onViewMount(): void {
super.onViewMount?.();
if (!this.container?.current) {
return;
}
const container = this.container.current;
const input = document.createElement('input');
input.type = 'file';
input.multiple = true;
input.onclick = this.onInputClicked;
input.onchange = this.onInputChanged;
input.style.display = 'none';
container.appendChild(input);
this.uploadInput = input;
}

uploadSubmit = (basePath?: string) => {
if (this.uploadInput) {
this.uploadInput.setAttribute('data-path', basePath || '');
this.uploadInput.click();
}
};
/**
* Perform the actual upload.
*/
protected async doUpload(file: File, basePath: string): Promise<IContentsModel> {
// Gather the file model parameters.
let path = basePath;
path = path ? path + '/' + file.name : file.name;
const name = file.name;
const type = 'file';
const format = 'base64';

const uploadInner = async (blob: Blob, chunk?: number): Promise<IContentsModel> => {
const reader = new FileReader();
reader.readAsDataURL(blob);
await new Promise((resolve, reject) => {
reader.onload = resolve;
reader.onerror = (event) => reject(`Failed to upload "${file.name}":` + event);
});

// remove header https://stackoverflow.com/a/24289420/907060
const content = (reader.result as string).split(',')[1];

const model: Partial<IContentsModel> = {
type,
format,
name,
chunk,
content,
};
return await this.contentsManager.save(path, model);
};

return await uploadInner(file);
}

onInputChanged = () => {
if (!this.uploadInput) {
return;
}
let path = this.uploadInput.getAttribute('data-path') || '';
if (!path) {
path = this.model.location?.path.toString() || '';
}
if (!path) {
return;
}
const files = Array.prototype.slice.call(this.uploadInput.files) as File[];
const pending = files.map((file) => this.doUpload(file, path));
Promise.all(pending)
.then(() => {
this.model.refresh();
return;
})
.catch((error) => {
console.error('Upload Error:', error);
});
};

onInputClicked = () => {
if (this.uploadInput) {
this.uploadInput.value = '';
}
};

openNode = async (treeNode: TreeNode) => {
if (FileStatNode.is(treeNode) && !treeNode.fileStat.isDirectory) {
if (
Expand Down

0 comments on commit f7c6839

Please sign in to comment.