Skip to content

Commit

Permalink
fix #4088: Add 'Upload Files...' menu entries
Browse files Browse the repository at this point in the history
Signed-off-by: Anton Kosyakov <[email protected]>
  • Loading branch information
akosyakov committed Mar 28, 2019
1 parent 21e318a commit 9f5cd55
Show file tree
Hide file tree
Showing 15 changed files with 386 additions and 39 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
- [cpp] added new clang-tidy and clang-tidy-checks preferences to lint cpp program when clangd v9+ is used.
- [plugin] `workspace.openTextDocument` API now respects the contributed `FileSystemProviders`
- [search-in-workspace] added a new preference `search.lineNumbers` to control whether to show line numbers for search results.
- [workspace] added the menu item `Upload Files...` to easily upload files into a workspace

Breaking changes:

Expand Down
6 changes: 3 additions & 3 deletions packages/core/src/browser/tree/tree-expansion.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,15 +65,15 @@ export interface ExpandableTreeNode extends CompositeTreeNode {
}

export namespace ExpandableTreeNode {
export function is(node: TreeNode | undefined): node is ExpandableTreeNode {
export function is(node: Object | undefined): node is ExpandableTreeNode {
return !!node && CompositeTreeNode.is(node) && 'expanded' in node;
}

export function isExpanded(node: TreeNode | undefined): node is ExpandableTreeNode {
export function isExpanded(node: Object | undefined): node is ExpandableTreeNode {
return ExpandableTreeNode.is(node) && node.expanded;
}

export function isCollapsed(node: TreeNode | undefined): node is ExpandableTreeNode {
export function isCollapsed(node: Object | undefined): node is ExpandableTreeNode {
return ExpandableTreeNode.is(node) && !node.expanded;
}
}
Expand Down
7 changes: 4 additions & 3 deletions packages/core/src/browser/tree/tree-widget.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -272,9 +272,6 @@ export class TreeWidget extends ReactWidget implements StatefulWidget {

protected onActivateRequest(msg: Message): void {
super.onActivateRequest(msg);
if (this.props.globalSelection) {
this.updateGlobalSelection();
}
this.node.focus();
if (this.model.selectedNodes.length === 0) {
const root = this.model.root;
Expand All @@ -287,6 +284,10 @@ export class TreeWidget extends ReactWidget implements StatefulWidget {
}
}
}
// it has to be called after nodes are selected
if (this.props.globalSelection) {
this.updateGlobalSelection();
}
this.forceUpdate();
}

Expand Down
2 changes: 1 addition & 1 deletion packages/core/src/browser/tree/tree.ts
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,7 @@ export interface CompositeTreeNode extends TreeNode {
}

export namespace CompositeTreeNode {
export function is(node: TreeNode | undefined): node is CompositeTreeNode {
export function is(node: Object | undefined): node is CompositeTreeNode {
return !!node && 'children' in node;
}

Expand Down
101 changes: 101 additions & 0 deletions packages/core/src/common/selection-command-handler.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
/********************************************************************************
* Copyright (C) 2019 TypeFox and others.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License v. 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0.
*
* This Source Code may also be made available under the following Secondary
* Licenses when the conditions for such availability set forth in the Eclipse
* Public License v. 2.0 are satisfied: GNU General Public License, version 2
* with the GNU Classpath Exception which is available at
* https://www.gnu.org/software/classpath/license.html.
*
* SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
********************************************************************************/

// tslint:disable:no-any
import { CommandHandler } from './command';
import { SelectionService } from '../common/selection-service';

export class SelectionCommandHandler<S> implements CommandHandler {

constructor(
protected readonly selectionService: SelectionService,
protected readonly toSelection: (arg: any) => S | undefined,
protected readonly options: SelectionCommandHandler.Options<S>
) { }

execute(...args: any[]): Object | undefined {
const selection = this.getSelection(...args);
return selection ? (this.options.execute as any)(selection, ...args) : undefined;
}

isVisible(...args: any[]): boolean {
const selection = this.getSelection(...args);
return !!selection && (!this.options.isVisible || (this.options.isVisible as any)(selection as any, ...args));
}

isEnabled(...args: any[]): boolean {
const selection = this.getSelection(...args);
return !!selection && (!this.options.isEnabled || (this.options.isEnabled as any)(selection as any, ...args));
}

protected isMulti(): boolean {
return this.options && !!this.options.multi;
}

protected getSelection(...args: any[]): S | S[] | undefined {
const givenSelection = args.length && this.toSelection(args[0]);
if (givenSelection) {
return this.isMulti() ? [givenSelection] : givenSelection;
}
const globalSelection = this.getSingleSelection(this.selectionService.selection);
if (this.isMulti()) {
return this.getMulitSelection(globalSelection);
}
return this.getSingleSelection(globalSelection);
}

protected getSingleSelection(arg: Object | undefined): S | undefined {
let selection = this.toSelection(arg);
if (selection) {
return selection;
}
if (Array.isArray(arg)) {
for (const element of arg) {
selection = this.toSelection(element);
if (selection) {
return selection;
}
}
}
return undefined;
}

protected getMulitSelection(arg: Object | undefined): S[] | undefined {
let selection = this.toSelection(arg);
if (selection) {
return [selection];
}
const result = [];
if (Array.isArray(arg)) {
for (const element of arg) {
selection = this.toSelection(element);
if (selection) {
result.push(selection);
}
}
}
return result.length ? result : undefined;
}
}
export namespace SelectionCommandHandler {
export type Options<S> = SelectionOptions<false, S> | SelectionOptions<true, S[]>;
export interface SelectionOptions<Multi extends boolean, T> {
multi: Multi;
execute(selection: T, ...args: any[]): any;
isEnabled?(selection: T, ...args: any[]): boolean;
isVisible?(selection: T, ...args: any[]): boolean;
}
}
2 changes: 2 additions & 0 deletions packages/filesystem/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
"@theia/core": "^0.4.0",
"@types/base64-js": "^1.2.5",
"@types/body-parser": "^1.17.0",
"@types/formidable": "^1.0.31",
"@types/fs-extra": "^4.0.2",
"@types/mime-types": "^2.1.0",
"@types/rimraf": "^2.0.2",
Expand All @@ -15,6 +16,7 @@
"base64-js": "^1.2.1",
"body-parser": "^1.18.3",
"drivelist": "^6.4.3",
"formidable": "^1.2.1",
"fs-extra": "^4.0.2",
"http-status-codes": "^1.3.0",
"mime-types": "^2.1.18",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,12 +16,14 @@

import { inject, injectable } from 'inversify';
import URI from '@theia/core/lib/common/uri';
import { notEmpty } from '@theia/core/lib/common/objects';
import { UriSelection } from '@theia/core/lib/common/selection';
import { SelectionService } from '@theia/core/lib/common/selection-service';
import { Command, CommandContribution, CommandRegistry } from '@theia/core/lib/common/command';
import { UriAwareCommandHandler, UriCommandHandler } from '@theia/core/lib/common/uri-command-handler';
import { ExpandableTreeNode } from '@theia/core/lib/browser/tree';
import { FileDownloadService } from './file-download-service';
import { FileSelection } from '../file-selection';
import { TreeWidgetSelection } from '@theia/core/lib/browser/tree/tree-widget-selection';
import { isCancelled } from '@theia/core/lib/common/cancellation';

@injectable()
export class FileDownloadCommandContribution implements CommandContribution {
Expand All @@ -35,6 +37,30 @@ export class FileDownloadCommandContribution implements CommandContribution {
registerCommands(registry: CommandRegistry): void {
const handler = new UriAwareCommandHandler<URI[]>(this.selectionService, this.downloadHandler(), { multi: true });
registry.registerCommand(FileDownloadCommands.DOWNLOAD, handler);
registry.registerCommand(FileDownloadCommands.UPLOAD, new FileSelection.CommandHandler(this.selectionService, {
multi: false,
isEnabled: selection => this.canUpload(selection),
isVisible: selection => this.canUpload(selection),
execute: selection => this.upload(selection)
}));
}

protected canUpload({ fileStat }: FileSelection): boolean {
return fileStat.isDirectory;
}

protected async upload(selection: FileSelection): Promise<void> {
try {
const source = TreeWidgetSelection.getSource(this.selectionService.selection);
await this.downloadService.upload(selection.fileStat.uri);
if (ExpandableTreeNode.is(selection) && source) {
await source.model.expandNode(selection);
}
} catch (e) {
if (!isCancelled(e)) {
console.error(e);
}
}
}

protected downloadHandler(): UriCommandHandler<URI[]> {
Expand All @@ -57,29 +83,20 @@ export class FileDownloadCommandContribution implements CommandContribution {
return this.isDownloadEnabled(uris);
}

protected getUris(uri: Object | undefined): URI[] {
if (uri === undefined) {
return [];
}
return (Array.isArray(uri) ? uri : [uri]).map(u => this.getUri(u)).filter(notEmpty);
}

protected getUri(uri: Object | undefined): URI | undefined {
if (uri instanceof URI) {
return uri;
}
if (UriSelection.is(uri)) {
return uri.uri;
}
return undefined;
}

}

export namespace FileDownloadCommands {

export const DOWNLOAD: Command = {
id: 'file.download'
id: 'file.download',
category: 'File',
label: 'Download'
};

export const UPLOAD: Command = {
id: 'file.upload',
category: 'File',
label: 'Upload Files...'
};

}
110 changes: 108 additions & 2 deletions packages/filesystem/src/browser/download/file-download-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,16 @@
* SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
********************************************************************************/

import { inject, injectable } from 'inversify';
import { inject, injectable, postConstruct } from 'inversify';
import URI from '@theia/core/lib/common/uri';
import { cancelled } from '@theia/core/lib/common/cancellation';
import { ILogger } from '@theia/core/lib/common/logger';
import { Endpoint } from '@theia/core/lib/browser/endpoint';
import { StatusBar, StatusBarAlignment } from '@theia/core/lib/browser/status-bar';
import { FileSystem } from '../../common/filesystem';
import { FileDownloadData } from '../../common/download/file-download-data';
import { Deferred } from '@theia/core/lib/common/promise-util';
import { MessageService } from '@theia/core/lib/common/message-service';

@injectable()
export class FileDownloadService {
Expand All @@ -40,6 +43,105 @@ export class FileDownloadService {
@inject(StatusBar)
protected readonly statusBar: StatusBar;

@inject(MessageService)
protected readonly messageService: MessageService;

protected uploadForm: {
target: HTMLInputElement
file: HTMLInputElement
};

@postConstruct()
protected init(): void {
this.uploadForm = this.createUploadForm();
}

protected createUploadForm(): {
target: HTMLInputElement
file: HTMLInputElement
} {
const target = document.createElement('input');
target.type = 'text';
target.name = 'target';

const file = document.createElement('input');
file.type = 'file';
file.name = 'upload';
file.multiple = true;

const form = document.createElement('form');
form.style.display = 'none';
form.enctype = 'multipart/form-data';
form.append(target);
form.append(file);

document.body.appendChild(form);

file.addEventListener('change', async () => {
if (file.value) {
const body = new FormData(form);
// clean up to allow upload to the same folder twice
file.value = '';
const filesUrl = this.filesUrl();
const deferredUpload = this.deferredUpload;
try {
const request = new XMLHttpRequest();

const cb = () => {
if (request.status === 200) {
deferredUpload.resolve();
} else {
let message: string = 'Upload Failed';
if (request.status === 413) {
message += ': Payload Too Large';
} else if (request.status) {
message += ': ' + String(request.status);
}
deferredUpload.reject(new Error(message));
this.messageService.error(message);
}
};
request.addEventListener('load', cb);
request.addEventListener('error', cb);
request.addEventListener('abort', () => deferredUpload.reject(cancelled()));

const progress = await this.messageService.showProgress({
text: 'Uploading Files...', options: { cancelable: true }
}, () => {
request.upload.removeEventListener('progress', progressListener);
request.abort();
});
deferredUpload.promise.then(() => progress.cancel(), () => progress.cancel());
const progressListener = (event: ProgressEvent) => {
if (event.lengthComputable) {
progress.report({
work: {
done: event.loaded,
total: event.total
}
});
}
};
request.upload.addEventListener('progress', progressListener);

request.open('POST', filesUrl);
request.send(body);
} catch (e) {
deferredUpload.reject(e);
}
}
});
return { target, file };
}

protected deferredUpload = new Deferred<void>();
upload(targetUri: string | URI): Promise<void> {
this.deferredUpload = new Deferred<void>();
this.uploadForm.target.value = String(targetUri);
this.uploadForm.file.click();
return this.deferredUpload.promise;
}

async download(uris: URI[]): Promise<void> {
if (uris.length === 0) {
return;
Expand Down Expand Up @@ -155,8 +257,12 @@ export class FileDownloadService {
}

protected endpoint(): string {
const url = new Endpoint({ path: 'files' }).getRestUrl().toString();
const url = this.filesUrl();
return url.endsWith('/') ? url.slice(0, -1) : url;
}

protected filesUrl(): string {
return new Endpoint({ path: 'files' }).getRestUrl().toString();
}

}
Loading

0 comments on commit 9f5cd55

Please sign in to comment.