Add support for renaming files on the fly in the file manager
This commit is contained in:
parent
52115b5c77
commit
ff820f30ad
10 changed files with 230 additions and 37 deletions
|
@ -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
|
// This token is set in the bootstrap.js file at the beginning of the request
|
||||||
// and is carried through from there.
|
// and is carried through from there.
|
||||||
|
@ -25,3 +26,15 @@ if (typeof window.phpdebugbar !== 'undefined') {
|
||||||
}
|
}
|
||||||
|
|
||||||
export default http;
|
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;
|
||||||
|
}
|
||||||
|
|
|
@ -1,27 +1,13 @@
|
||||||
import {ServerApplicationCredentials} from "@/store/types";
|
import {ServerApplicationCredentials} from "@/store/types";
|
||||||
import http from "@/api/http";
|
import {withCredentials} from "@/api/http";
|
||||||
import {AxiosError, AxiosRequestConfig} from "axios";
|
|
||||||
import {ServerData} from "@/models/server";
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Connects to the remote daemon and creates a new folder on the server.
|
* Connects to the remote daemon and creates a new folder on the server.
|
||||||
*/
|
*/
|
||||||
export function createFolder(server: ServerData, credentials: ServerApplicationCredentials, path: string): Promise<void> {
|
export function createFolder(server: string, credentials: ServerApplicationCredentials, path: string): Promise<void> {
|
||||||
const config: AxiosRequestConfig = {
|
|
||||||
baseURL: credentials.node,
|
|
||||||
headers: {
|
|
||||||
'X-Access-Server': server.uuid,
|
|
||||||
'X-Access-Token': credentials.key,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
http.post('/v1/server/file/folder', { path }, config)
|
withCredentials(server, credentials).post('/v1/server/file/folder', { path })
|
||||||
.then(() => {
|
.then(() => resolve())
|
||||||
resolve();
|
.catch(reject);
|
||||||
})
|
|
||||||
.catch((error: AxiosError) => {
|
|
||||||
reject(error);
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
23
resources/assets/scripts/api/server/files/renameElement.ts
Normal file
23
resources/assets/scripts/api/server/files/renameElement.ts
Normal 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);
|
||||||
|
});
|
||||||
|
}
|
|
@ -2,7 +2,7 @@ import http from '../http';
|
||||||
import {filter, isObject} from 'lodash';
|
import {filter, isObject} from 'lodash';
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
import route from '../../../../../vendor/tightenco/ziggy/src/js/route';
|
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.
|
* 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}))
|
http.get(route('server.files', {server, directory}))
|
||||||
.then((response) => {
|
.then((response) => {
|
||||||
return resolve({
|
return resolve({
|
||||||
files: filter(response.data.contents, function (o) {
|
files: filter(response.data.contents, function (o: DirectoryContentObject) {
|
||||||
return o.file;
|
return o.file;
|
||||||
}),
|
}),
|
||||||
directories: filter(response.data.contents, function (o) {
|
directories: filter(response.data.contents, function (o: DirectoryContentObject) {
|
||||||
return o.directory;
|
return o.directory;
|
||||||
}),
|
}),
|
||||||
editable: response.data.editable,
|
editable: response.data.editable,
|
||||||
|
|
|
@ -1,9 +1,21 @@
|
||||||
export type DirectoryContents = {
|
export type DirectoryContents = {
|
||||||
files: Array<string>,
|
files: Array<DirectoryContentObject>,
|
||||||
directories: Array<string>,
|
directories: Array<DirectoryContentObject>,
|
||||||
editable: Array<string>
|
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 = {
|
export type ServerDatabase = {
|
||||||
id: string,
|
id: string,
|
||||||
name: string,
|
name: string,
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="context-menu">
|
<div class="context-menu">
|
||||||
<div>
|
<div>
|
||||||
<div class="context-row">
|
<div class="context-row" v-on:click="openRenameModal">
|
||||||
<div class="icon">
|
<div class="icon">
|
||||||
<Icon name="edit-3"/>
|
<Icon name="edit-3"/>
|
||||||
</div>
|
</div>
|
||||||
|
@ -54,16 +54,29 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import Vue from 'vue';
|
import Vue from 'vue';
|
||||||
import Icon from "../../../core/Icon.vue";
|
import Icon from "../../../core/Icon.vue";
|
||||||
|
import {DirectoryContentObject} from "@/api/server/types";
|
||||||
|
|
||||||
export default Vue.extend({
|
export default Vue.extend({
|
||||||
name: 'FileContextMenu',
|
name: 'FileContextMenu',
|
||||||
components: {Icon},
|
components: {Icon},
|
||||||
|
|
||||||
|
props: {
|
||||||
|
object: {
|
||||||
|
type: Object as () => DirectoryContentObject,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
methods: {
|
methods: {
|
||||||
openFolderModal: function () {
|
openFolderModal: function () {
|
||||||
window.events.$emit('server:files:open-directory-modal');
|
window.events.$emit('server:files:open-directory-modal');
|
||||||
this.$emit('close');
|
this.$emit('close');
|
||||||
}
|
},
|
||||||
|
|
||||||
|
openRenameModal: function () {
|
||||||
|
window.events.$emit('server:files:rename', this.object);
|
||||||
|
this.$emit('close');
|
||||||
|
},
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -12,6 +12,7 @@
|
||||||
</div>
|
</div>
|
||||||
<FileContextMenu
|
<FileContextMenu
|
||||||
class="context-menu"
|
class="context-menu"
|
||||||
|
v-bind:object="file"
|
||||||
v-show="contextMenuVisible"
|
v-show="contextMenuVisible"
|
||||||
v-on:close="contextMenuVisible = false"
|
v-on:close="contextMenuVisible = false"
|
||||||
ref="contextMenu"
|
ref="contextMenu"
|
||||||
|
@ -25,17 +26,21 @@
|
||||||
import {Vue as VueType} from "vue/types/vue";
|
import {Vue as VueType} from "vue/types/vue";
|
||||||
import {formatDate, readableSize} from '../../../../helpers'
|
import {formatDate, readableSize} from '../../../../helpers'
|
||||||
import FileContextMenu from "./FileContextMenu.vue";
|
import FileContextMenu from "./FileContextMenu.vue";
|
||||||
|
import {DirectoryContentObject} from "@/api/server/types";
|
||||||
|
|
||||||
export default Vue.extend({
|
export default Vue.extend({
|
||||||
name: 'FileRow',
|
name: 'FileRow',
|
||||||
components: {
|
components: {Icon, FileContextMenu},
|
||||||
Icon,
|
|
||||||
FileContextMenu,
|
|
||||||
},
|
|
||||||
|
|
||||||
props: {
|
props: {
|
||||||
file: {type: Object, required: true},
|
file: {
|
||||||
editable: {type: Array, required: true}
|
type: Object as () => DirectoryContentObject,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
editable: {
|
||||||
|
type: Array,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
data: function () {
|
data: function () {
|
||||||
|
|
|
@ -83,7 +83,7 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
this.isLoading = true;
|
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(() => {
|
.then(() => {
|
||||||
this.$emit('close');
|
this.$emit('close');
|
||||||
this.onModalClose();
|
this.onModalClose();
|
||||||
|
|
|
@ -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 }"> </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>
|
|
@ -49,6 +49,7 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<CreateFolderModal v-on:close="listDirectory"/>
|
<CreateFolderModal v-on:close="listDirectory"/>
|
||||||
|
<RenameModal v-on:close="listDirectory"/>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
@ -60,19 +61,21 @@
|
||||||
import FileRow from "@/components/server/components/filemanager/FileRow.vue";
|
import FileRow from "@/components/server/components/filemanager/FileRow.vue";
|
||||||
import FolderRow from "@/components/server/components/filemanager/FolderRow.vue";
|
import FolderRow from "@/components/server/components/filemanager/FolderRow.vue";
|
||||||
import CreateFolderModal from '../components/filemanager/modals/CreateFolderModal.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 = {
|
type DataStructure = {
|
||||||
loading: boolean,
|
loading: boolean,
|
||||||
errorMessage: string | null,
|
errorMessage: string | null,
|
||||||
currentDirectory: string,
|
currentDirectory: string,
|
||||||
files: Array<any>,
|
files: Array<DirectoryContentObject>,
|
||||||
directories: Array<any>,
|
directories: Array<DirectoryContentObject>,
|
||||||
editableFiles: Array<string>,
|
editableFiles: Array<string>,
|
||||||
}
|
}
|
||||||
|
|
||||||
export default Vue.extend({
|
export default Vue.extend({
|
||||||
name: 'FileManager',
|
name: 'FileManager',
|
||||||
components: {CreateFolderModal, FileRow, FolderRow},
|
components: {CreateFolderModal, FileRow, FolderRow, RenameModal},
|
||||||
computed: {
|
computed: {
|
||||||
...mapState('server', ['server', 'credentials']),
|
...mapState('server', ['server', 'credentials']),
|
||||||
...mapState('socket', ['connected']),
|
...mapState('socket', ['connected']),
|
||||||
|
|
Loading…
Reference in a new issue