Add support for renaming files on the fly in the file manager

This commit is contained in:
Dane Everitt 2019-02-18 20:41:58 -08:00
parent 52115b5c77
commit ff820f30ad
No known key found for this signature in database
GPG key ID: EEA66103B3D71F53
10 changed files with 230 additions and 37 deletions

View file

@ -1,4 +1,5 @@
import axios, {AxiosInstance} from 'axios';
import axios, {AxiosInstance, AxiosRequestConfig} from 'axios';
import {ServerApplicationCredentials} from "@/store/types";
// This token is set in the bootstrap.js file at the beginning of the request
// and is carried through from there.
@ -25,3 +26,15 @@ if (typeof window.phpdebugbar !== 'undefined') {
}
export default http;
/**
* Creates a request object for the node that uses the server UUID and connection
* credentials. Basically just a tiny wrapper to set this quickly.
*/
export function withCredentials(server: string, credentials: ServerApplicationCredentials): AxiosInstance {
http.defaults.baseURL = credentials.node;
http.defaults.headers['X-Access-Server'] = server;
http.defaults.headers['X-Access-Token'] = credentials.key;
return http;
}

View file

@ -1,27 +1,13 @@
import {ServerApplicationCredentials} from "@/store/types";
import http from "@/api/http";
import {AxiosError, AxiosRequestConfig} from "axios";
import {ServerData} from "@/models/server";
import {withCredentials} from "@/api/http";
/**
* Connects to the remote daemon and creates a new folder on the server.
*/
export function createFolder(server: ServerData, credentials: ServerApplicationCredentials, path: string): Promise<void> {
const config: AxiosRequestConfig = {
baseURL: credentials.node,
headers: {
'X-Access-Server': server.uuid,
'X-Access-Token': credentials.key,
},
};
export function createFolder(server: string, credentials: ServerApplicationCredentials, path: string): Promise<void> {
return new Promise((resolve, reject) => {
http.post('/v1/server/file/folder', { path }, config)
.then(() => {
resolve();
})
.catch((error: AxiosError) => {
reject(error);
});
withCredentials(server, credentials).post('/v1/server/file/folder', { path })
.then(() => resolve())
.catch(reject);
});
}

View file

@ -0,0 +1,23 @@
import {withCredentials} from "@/api/http";
import {ServerApplicationCredentials} from "@/store/types";
import { join } from 'path';
type RenameObject = {
path: string,
fromName: string,
toName: string,
}
/**
* Renames a file or folder on the server using the node.
*/
export function renameElement(server: string, credentials: ServerApplicationCredentials, data: RenameObject): Promise<void> {
return new Promise((resolve, reject) => {
withCredentials(server, credentials).post('/v1/server/file/rename', {
from: join(data.path, data.fromName),
to: join(data.path, data.toName),
})
.then(() => resolve())
.catch(reject);
});
}

View file

@ -2,7 +2,7 @@ import http from '../http';
import {filter, isObject} from 'lodash';
// @ts-ignore
import route from '../../../../../vendor/tightenco/ziggy/src/js/route';
import {DirectoryContents} from "./types";
import {DirectoryContentObject, DirectoryContents} from "./types";
/**
* Get the contents of a specific directory for a given server.
@ -12,10 +12,10 @@ export function getDirectoryContents(server: string, directory: string): Promise
http.get(route('server.files', {server, directory}))
.then((response) => {
return resolve({
files: filter(response.data.contents, function (o) {
files: filter(response.data.contents, function (o: DirectoryContentObject) {
return o.file;
}),
directories: filter(response.data.contents, function (o) {
directories: filter(response.data.contents, function (o: DirectoryContentObject) {
return o.directory;
}),
editable: response.data.editable,

View file

@ -1,9 +1,21 @@
export type DirectoryContents = {
files: Array<string>,
directories: Array<string>,
files: Array<DirectoryContentObject>,
directories: Array<DirectoryContentObject>,
editable: Array<string>
}
export type DirectoryContentObject = {
name: string,
created: string,
modified: string,
mode: number,
size: number,
directory: boolean,
file: boolean,
symlink: boolean,
mime: string,
}
export type ServerDatabase = {
id: string,
name: string,

View file

@ -1,7 +1,7 @@
<template>
<div class="context-menu">
<div>
<div class="context-row">
<div class="context-row" v-on:click="openRenameModal">
<div class="icon">
<Icon name="edit-3"/>
</div>
@ -54,16 +54,29 @@
<script lang="ts">
import Vue from 'vue';
import Icon from "../../../core/Icon.vue";
import {DirectoryContentObject} from "@/api/server/types";
export default Vue.extend({
name: 'FileContextMenu',
components: {Icon},
props: {
object: {
type: Object as () => DirectoryContentObject,
required: true,
},
},
methods: {
openFolderModal: function () {
window.events.$emit('server:files:open-directory-modal');
this.$emit('close');
}
},
openRenameModal: function () {
window.events.$emit('server:files:rename', this.object);
this.$emit('close');
},
}
});
</script>

View file

@ -12,6 +12,7 @@
</div>
<FileContextMenu
class="context-menu"
v-bind:object="file"
v-show="contextMenuVisible"
v-on:close="contextMenuVisible = false"
ref="contextMenu"
@ -25,17 +26,21 @@
import {Vue as VueType} from "vue/types/vue";
import {formatDate, readableSize} from '../../../../helpers'
import FileContextMenu from "./FileContextMenu.vue";
import {DirectoryContentObject} from "@/api/server/types";
export default Vue.extend({
name: 'FileRow',
components: {
Icon,
FileContextMenu,
},
components: {Icon, FileContextMenu},
props: {
file: {type: Object, required: true},
editable: {type: Array, required: true}
file: {
type: Object as () => DirectoryContentObject,
required: true,
},
editable: {
type: Array,
required: true,
},
},
data: function () {

View file

@ -83,7 +83,7 @@
}
this.isLoading = true;
createFolder(this.server, this.credentials, `${this.fm.currentDirectory}/${this.folderName.replace(/^\//, '')}`)
createFolder(this.server.uuid, this.credentials, `${this.fm.currentDirectory}/${this.folderName.replace(/^\//, '')}`)
.then(() => {
this.$emit('close');
this.onModalClose();

View file

@ -0,0 +1,138 @@
<template>
<Modal
:show="visible"
v-on:close="closeModal"
:showCloseIcon="false"
:dismissable="!isLoading"
>
<MessageBox
class="alert error mb-8"
title="Error"
:message="error"
v-if="error"
/>
<div class="flex items-end" v-if="object">
<div class="flex-1">
<label class="input-label">
Rename {{ object.file ? 'File' : 'Folder' }}
</label>
<input
type="text" class="input" name="element_name"
ref="elementNameField"
v-model="newName"
v-validate.disabled="'required'"
v-validate="'alpha_dash'"
v-on:keyup.enter="submit"
/>
</div>
<div class="ml-4">
<button type="submit"
class="btn btn-primary btn-sm"
v-on:click.prevent="submit"
:disabled="errors.any() || isLoading"
>
<span class="spinner white" v-bind:class="{ hidden: !isLoading }">&nbsp;</span>
<span :class="{ hidden: isLoading }">
Edit
</span>
</button>
</div>
</div>
<p class="input-help error">
{{ errors.first('folder_name') }}
</p>
</Modal>
</template>
<script lang="ts">
import Vue from 'vue';
import Flash from '@/components/Flash.vue';
import Modal from '@/components/core/Modal.vue';
import MessageBox from '@/components/MessageBox.vue';
import {DirectoryContentObject} from "@/api/server/types";
import {mapState} from "vuex";
import {renameElement} from "@/api/server/files/renameElement";
import {AxiosError} from 'axios';
type DataStructure = {
object: null | DirectoryContentObject,
error: null | string,
newName: string,
visible: boolean,
isLoading: boolean,
};
export default Vue.extend({
name: 'RenameModal',
components: { Flash, Modal, MessageBox },
computed: {
...mapState('server', ['fm', 'server', 'credentials']),
},
data: function (): DataStructure {
return {
object: null,
newName: '',
error: null,
visible: false,
isLoading: false,
};
},
mounted: function () {
window.events.$on('server:files:rename', (data: DirectoryContentObject): void => {
this.visible = true;
this.object = data;
this.newName = data.name;
this.$nextTick(() => {
if (this.$refs.elementNameField) {
(this.$refs.elementNameField as HTMLInputElement).focus();
}
})
});
},
beforeDestroy: function () {
window.events.$off('server:files:rename');
},
methods: {
submit: function () {
if (!this.object) {
return;
}
this.isLoading = true;
this.error = null;
renameElement(this.server.uuid, this.credentials, {
path: this.fm.currentDirectory,
toName: this.newName,
fromName: this.object.name
})
.then(() => {
if (this.object) {
this.object.name = this.newName;
}
this.closeModal();
})
.catch((error: AxiosError) => {
const t = this.object ? (this.object.file ? 'file' : 'folder') : 'item';
this.error = `There was an error while renaming the requested ${t}. Response: ${error.message}`;
console.error('Error at Server::Files::Rename', { error });
})
.then(() => this.isLoading = false);
},
closeModal: function () {
this.object = null;
this.newName = '';
this.visible = false;
this.error = null;
},
},
});
</script>

View file

@ -49,6 +49,7 @@
</div>
</div>
<CreateFolderModal v-on:close="listDirectory"/>
<RenameModal v-on:close="listDirectory"/>
</div>
</template>
@ -60,19 +61,21 @@
import FileRow from "@/components/server/components/filemanager/FileRow.vue";
import FolderRow from "@/components/server/components/filemanager/FolderRow.vue";
import CreateFolderModal from '../components/filemanager/modals/CreateFolderModal.vue';
import RenameModal from '../components/filemanager/modals/RenameModal.vue';
import {DirectoryContentObject} from "@/api/server/types";
type DataStructure = {
loading: boolean,
errorMessage: string | null,
currentDirectory: string,
files: Array<any>,
directories: Array<any>,
files: Array<DirectoryContentObject>,
directories: Array<DirectoryContentObject>,
editableFiles: Array<string>,
}
export default Vue.extend({
name: 'FileManager',
components: {CreateFolderModal, FileRow, FolderRow},
components: {CreateFolderModal, FileRow, FolderRow, RenameModal},
computed: {
...mapState('server', ['server', 'credentials']),
...mapState('socket', ['connected']),