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

Fixes #4741 allow creation of sub-files and/or sub-folders if name has "/" #4764

Merged
merged 1 commit into from
Apr 2, 2019
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
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,11 @@
## v0.6.0

- [filesystem] added the menu item `Upload Files...` to easily upload files into a workspace
- [workspace] allow creation of files and folders using recursive paths

Breaking changes:

- [dialog] `validate` and `accept` methods are now Promisified [#4764](https://github.com/theia-ide/theia/pull/4764)

## v0.5.0

Expand Down
48 changes: 31 additions & 17 deletions packages/core/src/browser/dialogs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
********************************************************************************/

import { injectable, inject } from 'inversify';
import { Disposable } from '../common';
import { Disposable, MaybePromise, CancellationTokenSource } from '../common';
import { Key } from './keys';
import { Widget, BaseWidget, Message } from './widgets';

Expand Down Expand Up @@ -194,31 +194,45 @@ export abstract class AbstractDialog<T> extends BaseWidget {
this.activeElement = undefined;
super.close();
}

protected onUpdateRequest(msg: Message): void {
super.onUpdateRequest(msg);
this.validate();
}

protected validate(): void {
protected validateCancellationSource = new CancellationTokenSource();
protected async validate(): Promise<void> {
uniibu marked this conversation as resolved.
Show resolved Hide resolved
if (!this.resolve) {
return;
}
this.validateCancellationSource.cancel();
this.validateCancellationSource = new CancellationTokenSource();
const token = this.validateCancellationSource.token;
const value = this.value;
const error = this.isValid(value, 'preview');
const error = await this.isValid(value, 'preview');
if (token.isCancellationRequested) {
return;
}
this.setErrorMessage(error);
}

protected accept(): void {
if (this.resolve) {
const value = this.value;
const error = this.isValid(value, 'open');
if (!DialogError.getResult(error)) {
this.setErrorMessage(error);
} else {
this.resolve(value);
Widget.detach(this);
}
protected acceptCancellationSource = new CancellationTokenSource();
protected async accept(): Promise<void> {
if (!this.resolve) {
return;
}
this.acceptCancellationSource.cancel();
this.acceptCancellationSource = new CancellationTokenSource();
const token = this.acceptCancellationSource.token;
const value = this.value;
const error = await this.isValid(value, 'open');
if (token.isCancellationRequested) {
return;
}
if (!DialogError.getResult(error)) {
this.setErrorMessage(error);
} else {
this.resolve(value);
Widget.detach(this);
}
}

Expand All @@ -227,7 +241,7 @@ export abstract class AbstractDialog<T> extends BaseWidget {
/**
* Return a string of zero-length or true if valid.
*/
protected isValid(value: T, mode: DialogMode): DialogError {
protected isValid(value: T, mode: DialogMode): MaybePromise<DialogError> {
return '';
}

Expand Down Expand Up @@ -299,7 +313,7 @@ export class SingleTextInputDialogProps extends DialogProps {
end: number
direction?: 'forward' | 'backward' | 'none'
};
readonly validate?: (input: string, mode: DialogMode) => DialogError;
readonly validate?: (input: string, mode: DialogMode) => MaybePromise<DialogError>;
}

export class SingleTextInputDialog extends AbstractDialog<string> {
Expand Down Expand Up @@ -333,7 +347,7 @@ export class SingleTextInputDialog extends AbstractDialog<string> {
return this.inputField.value;
}

protected isValid(value: string, mode: DialogMode): DialogError {
protected isValid(value: string, mode: DialogMode): MaybePromise<DialogError> {
if (this.props.validate) {
return this.props.validate(value, mode);
}
Expand Down
2 changes: 1 addition & 1 deletion packages/filesystem/src/browser/file-dialog/file-dialog.ts
Original file line number Diff line number Diff line change
Expand Up @@ -261,7 +261,7 @@ export class OpenFileDialog extends FileDialog<MaybeArray<FileStatNode>> {
}
}

protected accept(): void {
protected async accept(): Promise<void> {
uniibu marked this conversation as resolved.
Show resolved Hide resolved
if (this.props.canSelectFolders === false && !Array.isArray(this.value)) {
this.widget.model.openNode(this.value);
return;
Expand Down
2 changes: 1 addition & 1 deletion packages/filesystem/src/browser/file-resource.ts
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,7 @@ export class FileResource implements Resource {
}

protected async getFileStat(): Promise<FileStat | undefined> {
if (!this.fileSystem.exists(this.uriString)) {
if (!await this.fileSystem.exists(this.uriString)) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

how it used to work! ❤️ for fixing it!

return undefined;
}
try {
Expand Down
46 changes: 33 additions & 13 deletions packages/workspace/src/browser/workspace-commands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -182,11 +182,13 @@ export class WorkspaceCommandContribution implements CommandContribution {
const parentUri = new URI(parent.uri);
const { fileName, fileExtension } = this.getDefaultFileConfig();
const vacantChildUri = FileSystemUtils.generateUniqueResourceURI(parentUri, parent, fileName, fileExtension);

const dialog = new SingleTextInputDialog({
title: 'New File',
initialValue: vacantChildUri.path.base,
validate: name => this.validateFileName(name, parent)
validate: name => this.validateFileName(name, parent, true)
});

dialog.open().then(name => {
if (name) {
const fileUri = parentUri.resolve(name);
Expand All @@ -206,7 +208,7 @@ export class WorkspaceCommandContribution implements CommandContribution {
const dialog = new SingleTextInputDialog({
title: 'New Folder',
initialValue: vacantChildUri.path.base,
validate: name => this.validateFileName(name, parent)
validate: name => this.validateFileName(name, parent, true)
});
dialog.open().then(name => {
if (name) {
Expand Down Expand Up @@ -240,7 +242,7 @@ export class WorkspaceCommandContribution implements CommandContribution {
if (initialValue === name && mode === 'preview') {
return false;
}
return this.validateFileName(name, parent);
return this.validateFileName(name, parent, false);
}
});
dialog.open().then(name => {
Expand Down Expand Up @@ -308,21 +310,40 @@ export class WorkspaceCommandContribution implements CommandContribution {
*
* @param name the simple file name of the file to validate.
* @param parent the parent directory's file stat.
* @param recursive allow file or folder creation using recursive path
*/
protected validateFileName(name: string, parent: FileStat): string {
if (!validFilename(name)) {
return 'Invalid name, try other';
protected async validateFileName(name: string, parent: FileStat, recursive: boolean = false): Promise<string> {
if (!name) {
return '';
}
if (parent.children) {
for (const child of parent.children) {
if (new URI(child.uri).path.base === name) {
return 'A file with this name already exists.';
}
}
// do not allow recursive rename
if (!recursive && !validFilename(name)) {
return 'Invalid file or folder name';
}
if (name.startsWith('/')) {
return 'Absolute paths or names that starts with / are not allowed';
} else if (name.startsWith(' ') || name.endsWith(' ')) {
return 'Names with leading or trailing whitespaces are not allowed';
}
// check and validate each sub-paths
if (name.split(/[\\/]/).some(file => !file || !validFilename(file) || /^\s+$/.test(file))) {
return `The name <strong>${this.trimFileName(name)}</strong> is not a valid file or folder name.`;
uniibu marked this conversation as resolved.
Show resolved Hide resolved
}
const childUri = new URI(parent.uri).resolve(name).toString();
const exists = await this.fileSystem.exists(childUri);
if (exists) {
return `A file or folder <strong>${this.trimFileName(name)}</strong> already exists at this location.`;
}
return '';
}

protected trimFileName(name: string): string {
if (name && name.length > 30) {
return `${name.substr(0, 30)}...`;
}
return name;
}

protected async getDirectory(candidate: URI): Promise<FileStat | undefined> {
const stat = await this.fileSystem.getFileStat(candidate.toString());
if (stat && stat.isDirectory) {
Expand Down Expand Up @@ -400,7 +421,6 @@ export class WorkspaceCommandContribution implements CommandContribution {
}
return false;
}

}

export class WorkspaceRootUriAwareCommandHandler extends UriAwareCommandHandler<URI> {
Expand Down