Skip to content

Commit

Permalink
Implements file & directory creation on Files app
Browse files Browse the repository at this point in the history
  • Loading branch information
juanlao7 committed Jan 21, 2021
1 parent 7da2232 commit 0c88f48
Show file tree
Hide file tree
Showing 8 changed files with 184 additions and 35 deletions.
26 changes: 24 additions & 2 deletions client/apps/remolacha.Files/src/ContextMenu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,12 @@ export class ContextMenu extends React.Component<ContextMenuProps, ContextMenuSt

private onNewFileMenuItemClick() {
this.props.onClose();
this.props.files.createNewFile();
}

private onNewDirectoryMenuItemClick() {
this.props.onClose();
this.props.files.createNewDirectory();
}

private onOpenFileMenuItemClick() {
Expand All @@ -46,6 +52,22 @@ export class ContextMenu extends React.Component<ContextMenuProps, ContextMenuSt
remolacha.Environment.getInstance().openApp('remolacha.Files', new Map([['cwd', path]]));
}

private onOpenAllSelectedMenuItemClick() {
this.props.onClose();

for (const name of this.props.selected.keys()) {
const element = this.props.files.getElementByName(name);
const path = this.props.files.resolvePath(this.props.selected.keys().next().value);

if (element.type == 'd') {
remolacha.Environment.getInstance().openApp('remolacha.Files', new Map([['cwd', path]]));
}
else {
// TODO: open with text editor.
}
}
}

private onDeleteMenuItemClick() {
this.props.onClose();
this.props.files.openDeleteDialog();
Expand Down Expand Up @@ -79,7 +101,7 @@ export class ContextMenu extends React.Component<ContextMenuProps, ContextMenuSt
</MenuItem>}

{this.props.selected.size == 0 &&
<MenuItem>
<MenuItem onClick={() => this.onNewDirectoryMenuItemClick()}>
New directory
</MenuItem>}

Expand All @@ -94,7 +116,7 @@ export class ContextMenu extends React.Component<ContextMenuProps, ContextMenuSt
</MenuItem>}

{this.props.selected.size > 1 &&
<MenuItem>
<MenuItem onClick={() => this.onOpenAllSelectedMenuItemClick()}>
Open all selected
</MenuItem>}

Expand Down
40 changes: 40 additions & 0 deletions client/apps/remolacha.Files/src/Files.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -27,6 +28,7 @@ interface FilesState {
nextPaths? : Array<string>;
error? : any;
dialogError? : string;
newElementName?: string;
deleteNames? : Array<string>;
renameName? : string;
overwriteName? : string;
Expand All @@ -50,6 +52,9 @@ export class Files extends React.Component<FilesProps, FilesState> {
previousPaths: [],
nextPaths: [],
error: null,
dialogError: null,
newElementName: null,
deleteNames: null,
renameName: null,
overwriteName: null,
dialogLoading: false,
Expand Down Expand Up @@ -196,6 +201,14 @@ export class Files extends React.Component<FilesProps, FilesState> {
this.setState({renameName: this.state.selected.keys().next().value});
}

createNewFile() {
this.setState({newElementName: 'file'});
}

createNewDirectory() {
this.setState({newElementName: 'directory'});
}

private onLocationInputChange(e : React.ChangeEvent<HTMLTextAreaElement | HTMLInputElement>) {
this.setState({locationInputValue: e.target.value});
}
Expand All @@ -204,6 +217,27 @@ export class Files extends React.Component<FilesProps, FilesState> {
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});
Expand Down Expand Up @@ -306,6 +340,12 @@ export class Files extends React.Component<FilesProps, FilesState> {
error={this.state.error}
/>

<NewElementDialog
elementName={this.state.newElementName}
loading={this.state.dialogLoading}
onClose={name => this.onNewElementDialogClose(name)}
/>

<DeleteDialog
names={this.state.deleteNames}
loading={this.state.dialogLoading}
Expand Down
92 changes: 92 additions & 0 deletions client/apps/remolacha.Files/src/NewElementDialog.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
import React from 'react';
import { Dialog, DialogContent, DialogContentText, DialogActions, Button, TextField, LinearProgress } from '@material-ui/core';

interface NewElementDialogProps {
elementName : string;
loading : boolean;
onClose : (name : string) => 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<NewElementDialogProps, NewElementDialogState> {
constructor(props : NewElementDialogProps) {
super(props);

this.state = {
name: '',
lastKnownElementName: this.props.elementName
};
}

private onTextFieldChange(e : React.ChangeEvent<HTMLTextAreaElement | HTMLInputElement>) {
this.setState({name: e.target.value});
}

private onTextFieldKeyPress(e : React.KeyboardEvent<HTMLDivElement>) {
if (e.key == 'Enter') {
this.props.onClose(this.state.name);
}
}

componentDidUpdate(prevProps: Readonly<NewElementDialogProps>) {
if (prevProps.elementName == null && this.props.elementName != null) {
this.setState({
name: '',
lastKnownElementName: this.props.elementName
});
}
}

render() {
return (
<Dialog
open={this.props.elementName != null}
disableBackdropClick={this.props.loading}
disableEscapeKeyDown={this.props.loading}
onClose={() => this.props.onClose(null)}
>
<DialogContent>
<DialogContentText>Create {this.state.lastKnownElementName}?</DialogContentText>

<TextField
autoFocus
variant="filled"
label="Name"
value={this.state.name}
fullWidth
disabled={this.props.loading}
onChange={e => this.onTextFieldChange(e)}
onFocus={e => e.target.select()}
onKeyPress={e => this.onTextFieldKeyPress(e)}
/>
</DialogContent>

<DialogActions>
<Button
color="primary"
disabled={this.props.loading}
onClick={() => this.props.onClose(null)}
>
Cancel
</Button>

<Button
color="primary"
variant="contained"
disabled={this.props.loading}
onClick={() => this.props.onClose(this.state.name)}
>
Create
</Button>
</DialogActions>

{this.props.loading &&
<LinearProgress className="remolacha_app_Files_progressBar" />}
</Dialog>
);
}
}
2 changes: 1 addition & 1 deletion client/apps/remolacha.Files/src/OverwriteDialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ export class OverwriteDialog extends React.Component<OverwriteDialogProps, Overw
onClose={() => this.props.onClose(false)}
>
<DialogContent>
<DialogContentText>File <strong>{this.props.name || this.state.lastKnownName}</strong> already exists. Overwrite?</DialogContentText>
<DialogContentText>File <strong>{this.state.lastKnownName}</strong> already exists. Overwrite?</DialogContentText>
</DialogContent>

<DialogActions>
Expand Down
2 changes: 1 addition & 1 deletion client/apps/remolacha.Files/src/RenameDialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ export class RenameDialog extends React.Component<RenameDialogProps, RenameDialo
onClose={() => this.props.onClose(null)}
>
<DialogContent>
<DialogContentText>Rename <strong>{this.props.originalName || this.state.lastKnownOriginalName}</strong>?</DialogContentText>
<DialogContentText>Rename <strong>{this.state.lastKnownOriginalName}</strong>?</DialogContentText>

<TextField
autoFocus
Expand Down
9 changes: 0 additions & 9 deletions server/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 0 additions & 1 deletion server/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@
"@types/express": "^4.17.9",
"@types/fs-extra": "^9.0.6",
"@types/node": "^14.14.20",
"@types/util.promisify": "^1.0.4",
"@types/ws": "^7.4.0",
"nodemon": "^2.0.7",
"typescript": "^4.1.3"
Expand Down
47 changes: 26 additions & 21 deletions server/src/apps/remolacha.Files.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,10 @@
import fs from 'fs-extra';
import os from 'os';
import path from 'path';
import { promisify } from 'util';
import { TypeTools } from 'remolacha-commons';
import { App } from '../App';
import { Connection } from '../Connection';

const readdir = promisify(fs.readdir);
const lstat = promisify(fs.lstat);
const stat = promisify(fs.stat);
const remove = promisify(fs.remove);

enum DirectoryElementType {
FILE = 'f',
DIRECTORY = 'd',
Expand Down Expand Up @@ -38,7 +32,7 @@ const DIRECTORY_ELEMENT_TYPE_FUNCTIONS : Array<[keyof fs.Stats, DirectoryElement
];

async function getDirectoryElements(directoryPath : string) : Promise<Array<DirectoryElement>> {
const names = await readdir(directoryPath) as Array<string>;
const names = await fs.readdir(directoryPath);
const elements : Array<DirectoryElement> = [];
const promises : Array<Promise<unknown>> = [];

Expand All @@ -48,8 +42,8 @@ async function getDirectoryElements(directoryPath : string) : Promise<Array<Dire
promises.push(new Promise(async (resolve) => {
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) {
Expand Down Expand Up @@ -84,6 +78,19 @@ async function readDirectoryImpl(directoryPath : string, connection : Connection
});
}

async function createElementImpl(params : any, connection : Connection, createElement : (path : string) => Promise<void>) {
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))) {
Expand Down Expand Up @@ -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();
},

Expand All @@ -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();
}
};
Expand Down

0 comments on commit 0c88f48

Please sign in to comment.