Skip to content

Commit ff820f3

Browse files
committed
Add support for renaming files on the fly in the file manager
1 parent 52115b5 commit ff820f3

File tree

10 files changed

+230
-37
lines changed

10 files changed

+230
-37
lines changed

resources/assets/scripts/api/http.ts

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
import axios, {AxiosInstance} from 'axios';
1+
import axios, {AxiosInstance, AxiosRequestConfig} from 'axios';
2+
import {ServerApplicationCredentials} from "@/store/types";
23

34
// This token is set in the bootstrap.js file at the beginning of the request
45
// and is carried through from there.
@@ -25,3 +26,15 @@ if (typeof window.phpdebugbar !== 'undefined') {
2526
}
2627

2728
export default http;
29+
30+
/**
31+
* Creates a request object for the node that uses the server UUID and connection
32+
* credentials. Basically just a tiny wrapper to set this quickly.
33+
*/
34+
export function withCredentials(server: string, credentials: ServerApplicationCredentials): AxiosInstance {
35+
http.defaults.baseURL = credentials.node;
36+
http.defaults.headers['X-Access-Server'] = server;
37+
http.defaults.headers['X-Access-Token'] = credentials.key;
38+
39+
return http;
40+
}
Lines changed: 5 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,13 @@
11
import {ServerApplicationCredentials} from "@/store/types";
2-
import http from "@/api/http";
3-
import {AxiosError, AxiosRequestConfig} from "axios";
4-
import {ServerData} from "@/models/server";
2+
import {withCredentials} from "@/api/http";
53

64
/**
75
* Connects to the remote daemon and creates a new folder on the server.
86
*/
9-
export function createFolder(server: ServerData, credentials: ServerApplicationCredentials, path: string): Promise<void> {
10-
const config: AxiosRequestConfig = {
11-
baseURL: credentials.node,
12-
headers: {
13-
'X-Access-Server': server.uuid,
14-
'X-Access-Token': credentials.key,
15-
},
16-
};
17-
7+
export function createFolder(server: string, credentials: ServerApplicationCredentials, path: string): Promise<void> {
188
return new Promise((resolve, reject) => {
19-
http.post('/v1/server/file/folder', { path }, config)
20-
.then(() => {
21-
resolve();
22-
})
23-
.catch((error: AxiosError) => {
24-
reject(error);
25-
});
9+
withCredentials(server, credentials).post('/v1/server/file/folder', { path })
10+
.then(() => resolve())
11+
.catch(reject);
2612
});
2713
}
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import {withCredentials} from "@/api/http";
2+
import {ServerApplicationCredentials} from "@/store/types";
3+
import { join } from 'path';
4+
5+
type RenameObject = {
6+
path: string,
7+
fromName: string,
8+
toName: string,
9+
}
10+
11+
/**
12+
* Renames a file or folder on the server using the node.
13+
*/
14+
export function renameElement(server: string, credentials: ServerApplicationCredentials, data: RenameObject): Promise<void> {
15+
return new Promise((resolve, reject) => {
16+
withCredentials(server, credentials).post('/v1/server/file/rename', {
17+
from: join(data.path, data.fromName),
18+
to: join(data.path, data.toName),
19+
})
20+
.then(() => resolve())
21+
.catch(reject);
22+
});
23+
}

resources/assets/scripts/api/server/getDirectoryContents.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import http from '../http';
22
import {filter, isObject} from 'lodash';
33
// @ts-ignore
44
import route from '../../../../../vendor/tightenco/ziggy/src/js/route';
5-
import {DirectoryContents} from "./types";
5+
import {DirectoryContentObject, DirectoryContents} from "./types";
66

77
/**
88
* Get the contents of a specific directory for a given server.
@@ -12,10 +12,10 @@ export function getDirectoryContents(server: string, directory: string): Promise
1212
http.get(route('server.files', {server, directory}))
1313
.then((response) => {
1414
return resolve({
15-
files: filter(response.data.contents, function (o) {
15+
files: filter(response.data.contents, function (o: DirectoryContentObject) {
1616
return o.file;
1717
}),
18-
directories: filter(response.data.contents, function (o) {
18+
directories: filter(response.data.contents, function (o: DirectoryContentObject) {
1919
return o.directory;
2020
}),
2121
editable: response.data.editable,

resources/assets/scripts/api/server/types.ts

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,21 @@
11
export type DirectoryContents = {
2-
files: Array<string>,
3-
directories: Array<string>,
2+
files: Array<DirectoryContentObject>,
3+
directories: Array<DirectoryContentObject>,
44
editable: Array<string>
55
}
66

7+
export type DirectoryContentObject = {
8+
name: string,
9+
created: string,
10+
modified: string,
11+
mode: number,
12+
size: number,
13+
directory: boolean,
14+
file: boolean,
15+
symlink: boolean,
16+
mime: string,
17+
}
18+
719
export type ServerDatabase = {
820
id: string,
921
name: string,

resources/assets/scripts/components/server/components/filemanager/FileContextMenu.vue

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
<template>
22
<div class="context-menu">
33
<div>
4-
<div class="context-row">
4+
<div class="context-row" v-on:click="openRenameModal">
55
<div class="icon">
66
<Icon name="edit-3"/>
77
</div>
@@ -54,16 +54,29 @@
5454
<script lang="ts">
5555
import Vue from 'vue';
5656
import Icon from "../../../core/Icon.vue";
57+
import {DirectoryContentObject} from "@/api/server/types";
5758
5859
export default Vue.extend({
5960
name: 'FileContextMenu',
6061
components: {Icon},
6162
63+
props: {
64+
object: {
65+
type: Object as () => DirectoryContentObject,
66+
required: true,
67+
},
68+
},
69+
6270
methods: {
6371
openFolderModal: function () {
6472
window.events.$emit('server:files:open-directory-modal');
6573
this.$emit('close');
66-
}
74+
},
75+
76+
openRenameModal: function () {
77+
window.events.$emit('server:files:rename', this.object);
78+
this.$emit('close');
79+
},
6780
}
6881
});
6982
</script>

resources/assets/scripts/components/server/components/filemanager/FileRow.vue

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
</div>
1313
<FileContextMenu
1414
class="context-menu"
15+
v-bind:object="file"
1516
v-show="contextMenuVisible"
1617
v-on:close="contextMenuVisible = false"
1718
ref="contextMenu"
@@ -25,17 +26,21 @@
2526
import {Vue as VueType} from "vue/types/vue";
2627
import {formatDate, readableSize} from '../../../../helpers'
2728
import FileContextMenu from "./FileContextMenu.vue";
29+
import {DirectoryContentObject} from "@/api/server/types";
2830
2931
export default Vue.extend({
3032
name: 'FileRow',
31-
components: {
32-
Icon,
33-
FileContextMenu,
34-
},
33+
components: {Icon, FileContextMenu},
3534
3635
props: {
37-
file: {type: Object, required: true},
38-
editable: {type: Array, required: true}
36+
file: {
37+
type: Object as () => DirectoryContentObject,
38+
required: true,
39+
},
40+
editable: {
41+
type: Array,
42+
required: true,
43+
},
3944
},
4045
4146
data: function () {

resources/assets/scripts/components/server/components/filemanager/modals/CreateFolderModal.vue

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,7 @@
8383
}
8484
8585
this.isLoading = true;
86-
createFolder(this.server, this.credentials, `${this.fm.currentDirectory}/${this.folderName.replace(/^\//, '')}`)
86+
createFolder(this.server.uuid, this.credentials, `${this.fm.currentDirectory}/${this.folderName.replace(/^\//, '')}`)
8787
.then(() => {
8888
this.$emit('close');
8989
this.onModalClose();
Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,138 @@
1+
<template>
2+
<Modal
3+
:show="visible"
4+
v-on:close="closeModal"
5+
:showCloseIcon="false"
6+
:dismissable="!isLoading"
7+
>
8+
<MessageBox
9+
class="alert error mb-8"
10+
title="Error"
11+
:message="error"
12+
v-if="error"
13+
/>
14+
<div class="flex items-end" v-if="object">
15+
<div class="flex-1">
16+
<label class="input-label">
17+
Rename {{ object.file ? 'File' : 'Folder' }}
18+
</label>
19+
<input
20+
type="text" class="input" name="element_name"
21+
ref="elementNameField"
22+
v-model="newName"
23+
v-validate.disabled="'required'"
24+
v-validate="'alpha_dash'"
25+
v-on:keyup.enter="submit"
26+
/>
27+
</div>
28+
<div class="ml-4">
29+
<button type="submit"
30+
class="btn btn-primary btn-sm"
31+
v-on:click.prevent="submit"
32+
:disabled="errors.any() || isLoading"
33+
>
34+
<span class="spinner white" v-bind:class="{ hidden: !isLoading }">&nbsp;</span>
35+
<span :class="{ hidden: isLoading }">
36+
Edit
37+
</span>
38+
</button>
39+
</div>
40+
</div>
41+
<p class="input-help error">
42+
{{ errors.first('folder_name') }}
43+
</p>
44+
</Modal>
45+
</template>
46+
47+
<script lang="ts">
48+
import Vue from 'vue';
49+
import Flash from '@/components/Flash.vue';
50+
import Modal from '@/components/core/Modal.vue';
51+
import MessageBox from '@/components/MessageBox.vue';
52+
import {DirectoryContentObject} from "@/api/server/types";
53+
import {mapState} from "vuex";
54+
import {renameElement} from "@/api/server/files/renameElement";
55+
import {AxiosError} from 'axios';
56+
57+
type DataStructure = {
58+
object: null | DirectoryContentObject,
59+
error: null | string,
60+
newName: string,
61+
visible: boolean,
62+
isLoading: boolean,
63+
};
64+
65+
export default Vue.extend({
66+
name: 'RenameModal',
67+
components: { Flash, Modal, MessageBox },
68+
69+
computed: {
70+
...mapState('server', ['fm', 'server', 'credentials']),
71+
},
72+
73+
data: function (): DataStructure {
74+
return {
75+
object: null,
76+
newName: '',
77+
error: null,
78+
visible: false,
79+
isLoading: false,
80+
};
81+
},
82+
83+
mounted: function () {
84+
window.events.$on('server:files:rename', (data: DirectoryContentObject): void => {
85+
this.visible = true;
86+
this.object = data;
87+
this.newName = data.name;
88+
89+
this.$nextTick(() => {
90+
if (this.$refs.elementNameField) {
91+
(this.$refs.elementNameField as HTMLInputElement).focus();
92+
}
93+
})
94+
});
95+
},
96+
97+
beforeDestroy: function () {
98+
window.events.$off('server:files:rename');
99+
},
100+
101+
methods: {
102+
submit: function () {
103+
if (!this.object) {
104+
return;
105+
}
106+
107+
this.isLoading = true;
108+
this.error = null;
109+
renameElement(this.server.uuid, this.credentials, {
110+
path: this.fm.currentDirectory,
111+
toName: this.newName,
112+
fromName: this.object.name
113+
})
114+
.then(() => {
115+
if (this.object) {
116+
this.object.name = this.newName;
117+
}
118+
119+
this.closeModal();
120+
})
121+
.catch((error: AxiosError) => {
122+
const t = this.object ? (this.object.file ? 'file' : 'folder') : 'item';
123+
124+
this.error = `There was an error while renaming the requested ${t}. Response: ${error.message}`;
125+
console.error('Error at Server::Files::Rename', { error });
126+
})
127+
.then(() => this.isLoading = false);
128+
},
129+
130+
closeModal: function () {
131+
this.object = null;
132+
this.newName = '';
133+
this.visible = false;
134+
this.error = null;
135+
},
136+
},
137+
});
138+
</script>

resources/assets/scripts/components/server/subpages/FileManager.vue

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@
4949
</div>
5050
</div>
5151
<CreateFolderModal v-on:close="listDirectory"/>
52+
<RenameModal v-on:close="listDirectory"/>
5253
</div>
5354
</template>
5455

@@ -60,19 +61,21 @@
6061
import FileRow from "@/components/server/components/filemanager/FileRow.vue";
6162
import FolderRow from "@/components/server/components/filemanager/FolderRow.vue";
6263
import CreateFolderModal from '../components/filemanager/modals/CreateFolderModal.vue';
64+
import RenameModal from '../components/filemanager/modals/RenameModal.vue';
65+
import {DirectoryContentObject} from "@/api/server/types";
6366
6467
type DataStructure = {
6568
loading: boolean,
6669
errorMessage: string | null,
6770
currentDirectory: string,
68-
files: Array<any>,
69-
directories: Array<any>,
71+
files: Array<DirectoryContentObject>,
72+
directories: Array<DirectoryContentObject>,
7073
editableFiles: Array<string>,
7174
}
7275
7376
export default Vue.extend({
7477
name: 'FileManager',
75-
components: {CreateFolderModal, FileRow, FolderRow},
78+
components: {CreateFolderModal, FileRow, FolderRow, RenameModal},
7679
computed: {
7780
...mapState('server', ['server', 'credentials']),
7881
...mapState('socket', ['connected']),

0 commit comments

Comments
 (0)