diff --git a/client/apps/remolacha.Files/src/ContextMenu.tsx b/client/apps/remolacha.Files/src/ContextMenu.tsx index 4f8a944..afe2b32 100644 --- a/client/apps/remolacha.Files/src/ContextMenu.tsx +++ b/client/apps/remolacha.Files/src/ContextMenu.tsx @@ -25,6 +25,12 @@ export class ContextMenu extends React.Component} {this.props.selected.size == 0 && - + this.onNewDirectoryMenuItemClick()}> New directory } @@ -94,7 +116,7 @@ export class ContextMenu extends React.Component} {this.props.selected.size > 1 && - + this.onOpenAllSelectedMenuItemClick()}> Open all selected } diff --git a/client/apps/remolacha.Files/src/Files.tsx b/client/apps/remolacha.Files/src/Files.tsx index ad364c1..05f6efe 100644 --- a/client/apps/remolacha.Files/src/Files.tsx +++ b/client/apps/remolacha.Files/src/Files.tsx @@ -8,6 +8,7 @@ import { RenameDialog } from './RenameDialog'; import { OverwriteDialog } from './OverwriteDialog'; import { ErrorDialog } from './ErrorDialog'; import { DeleteDialog } from './DeleteDialog'; +import { NewElementDialog } from './NewElementDialog'; declare var remolacha : any; // TODO: https://github.com/juanlao7/remolacha/issues/1 @@ -27,6 +28,7 @@ interface FilesState { nextPaths? : Array; error? : any; dialogError? : string; + newElementName?: string; deleteNames? : Array; renameName? : string; overwriteName? : string; @@ -50,6 +52,9 @@ export class Files extends React.Component { previousPaths: [], nextPaths: [], error: null, + dialogError: null, + newElementName: null, + deleteNames: null, renameName: null, overwriteName: null, dialogLoading: false, @@ -196,6 +201,14 @@ export class Files extends React.Component { this.setState({renameName: this.state.selected.keys().next().value}); } + createNewFile() { + this.setState({newElementName: 'file'}); + } + + createNewDirectory() { + this.setState({newElementName: 'directory'}); + } + private onLocationInputChange(e : React.ChangeEvent) { this.setState({locationInputValue: e.target.value}); } @@ -204,6 +217,27 @@ export class Files extends React.Component { this.setState({locationInputValue: this.state.currentPath}); } + private async onNewElementDialogClose(name : string) { + if (name == null) { + this.setState({newElementName: null}); + return; + } + + this.setState({dialogLoading: true}); + + try { + await this.props.appInstance.callBackend((this.state.newElementName == 'file') ? 'createFile' : 'createDirectory', {path: this.resolvePath(name)}); + } + catch (e) { + this.setState({dialogError: e.message}); + } + + this.setState({ + newElementName: null, + dialogLoading: false + }); + } + private async onDeleteDialogClose(deleteConfirmed : boolean) { if (!deleteConfirmed) { this.setState({deleteNames: null}); @@ -306,6 +340,12 @@ export class Files extends React.Component { error={this.state.error} /> + this.onNewElementDialogClose(name)} + /> + void; +} + +interface NewElementDialogState { + name? : string; + lastKnownElementName? : string; // To avoid a weird glitch where the text instantly disappears while the dialog is closing. +} + +export class NewElementDialog extends React.Component { + constructor(props : NewElementDialogProps) { + super(props); + + this.state = { + name: '', + lastKnownElementName: this.props.elementName + }; + } + + private onTextFieldChange(e : React.ChangeEvent) { + this.setState({name: e.target.value}); + } + + private onTextFieldKeyPress(e : React.KeyboardEvent) { + if (e.key == 'Enter') { + this.props.onClose(this.state.name); + } + } + + componentDidUpdate(prevProps: Readonly) { + if (prevProps.elementName == null && this.props.elementName != null) { + this.setState({ + name: '', + lastKnownElementName: this.props.elementName + }); + } + } + + render() { + return ( + this.props.onClose(null)} + > + + Create {this.state.lastKnownElementName}? + + this.onTextFieldChange(e)} + onFocus={e => e.target.select()} + onKeyPress={e => this.onTextFieldKeyPress(e)} + /> + + + + + + + + + {this.props.loading && + } + + ); + } +} diff --git a/client/apps/remolacha.Files/src/OverwriteDialog.tsx b/client/apps/remolacha.Files/src/OverwriteDialog.tsx index 308f267..c492d89 100644 --- a/client/apps/remolacha.Files/src/OverwriteDialog.tsx +++ b/client/apps/remolacha.Files/src/OverwriteDialog.tsx @@ -32,7 +32,7 @@ export class OverwriteDialog extends React.Component this.props.onClose(false)} > - File {this.props.name || this.state.lastKnownName} already exists. Overwrite? + File {this.state.lastKnownName} already exists. Overwrite? diff --git a/client/apps/remolacha.Files/src/RenameDialog.tsx b/client/apps/remolacha.Files/src/RenameDialog.tsx index f6b1f00..3456c14 100644 --- a/client/apps/remolacha.Files/src/RenameDialog.tsx +++ b/client/apps/remolacha.Files/src/RenameDialog.tsx @@ -50,7 +50,7 @@ export class RenameDialog extends React.Component this.props.onClose(null)} > - Rename {this.props.originalName || this.state.lastKnownOriginalName}? + Rename {this.state.lastKnownOriginalName}? > { - const names = await readdir(directoryPath) as Array; + const names = await fs.readdir(directoryPath); const elements : Array = []; const promises : Array> = []; @@ -48,8 +42,8 @@ async function getDirectoryElements(directoryPath : string) : Promise { try { const elementPath = path.join(directoryPath, name); - const lstatResult = await lstat(elementPath) as fs.Stats; - const typeStatResult = (lstatResult.isSymbolicLink()) ? await stat(elementPath) as fs.Stats : lstatResult; + const lstatResult = await fs.lstat(elementPath); + const typeStatResult = (lstatResult.isSymbolicLink()) ? await fs.stat(elementPath) as fs.Stats : lstatResult; element.type = DirectoryElementType.FILE; for (const [func, elementType] of DIRECTORY_ELEMENT_TYPE_FUNCTIONS) { @@ -84,6 +78,19 @@ async function readDirectoryImpl(directoryPath : string, connection : Connection }); } +async function createElementImpl(params : any, connection : Connection, createElement : (path : string) => Promise) { + if (params == null || typeof params != 'object' || !TypeTools.isString(params.path)) { + throw new Error('Unexpected params.'); + } + + if (await fs.pathExists(params.path)) { + throw new Error(`Path already exists: ${params.path}`); + } + + await createElement(params.path); + connection.close(); +} + const app : App = { readDirectory: async (params : any, connection : Connection) => { if (params == null || typeof params != 'object' || !('goHome' in params || TypeTools.isString(params.path))) { @@ -133,22 +140,20 @@ const app : App = { } }, + createFile: async (params : any, connection : Connection) => { + await createElementImpl(params, connection, path => fs.ensureFile(path)); + }, + + createDirectory: async (params : any, connection : Connection) => { + await createElementImpl(params, connection, path => fs.ensureDir(path)) + }, + move: async (params : any, connection : Connection) => { if (params == null || typeof params != 'object' || !TypeTools.isString(params.from) || !TypeTools.isString(params.to)) { throw new Error('Unexpected params.'); } - // promisify(fs.move) is not compatible with passing a fs.MoveOptions parameter. - - await new Promise((resolve, reject) => fs.move(params.from, params.to, {overwrite: true}, e => { - if (e) { - reject(e); - } - else { - resolve(null); - } - })); - + await fs.move(params.from, params.to, {overwrite: true}); connection.close(); }, @@ -157,7 +162,7 @@ const app : App = { throw new Error('Unexpected params.'); } - await Promise.all(params.paths.map((x : string) => remove(x))); + await Promise.all(params.paths.map((x : string) => fs.remove(x))); connection.close(); } };