mirror of
https://github.com/filegator/filegator.git
synced 2025-08-19 04:21:30 +02:00
initial commit
This commit is contained in:
568
frontend/views/Browser.vue
Normal file
568
frontend/views/Browser.vue
Normal file
@@ -0,0 +1,568 @@
|
||||
<template>
|
||||
<div id="dropzone" class="container"
|
||||
@dragover="dropZone = can('upload') && ! isLoading ? true : false"
|
||||
@dragleave="dropZone = false"
|
||||
@drop="dropZone = false">
|
||||
|
||||
<div id="loading" v-if="isLoading"></div>
|
||||
|
||||
<Upload v-if="can('upload')" v-show="dropZone == false" :files="files" :dropZone="dropZone"></Upload>
|
||||
|
||||
<b-upload v-if="dropZone && ! isLoading" @input="files = $event" multiple drag-drop>
|
||||
<b class="drop-info">{{ lang('Drop files to upload') }}</b>
|
||||
</b-upload>
|
||||
|
||||
<div class="container" v-if="!dropZone">
|
||||
|
||||
<Menu></Menu>
|
||||
|
||||
<div id="browser">
|
||||
|
||||
<div v-if="can('read')" class="is-flex is-justify-between">
|
||||
<div class="breadcrumb" aria-label="breadcrumbs">
|
||||
<ul>
|
||||
<li v-for="(item, key) in breadcrumbs" v-if="item.name">
|
||||
<a @click="goTo(item.path)">{{ item.name }}</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div>
|
||||
<a class="is-paddingless" @click="selectDir">
|
||||
<b-icon icon="sitemap" class="is-marginless" size="is-small"></b-icon>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<section class="actions is-flex is-justify-between">
|
||||
<div>
|
||||
<b-field v-if="can('upload') && ! checked.length" class="file is-inline-block">
|
||||
<b-upload @input="files = $event" multiple native>
|
||||
<a v-if="! checked.length" class="is-inline-block">
|
||||
<b-icon icon="upload" size="is-small"></b-icon> {{ lang('Add files') }}
|
||||
</a>
|
||||
</a>
|
||||
</b-upload>
|
||||
</b-field>
|
||||
<a v-if="can(['read', 'write']) && ! checked.length" class="is-inline-block">
|
||||
<b-dropdown aria-role="list" :disabled="checked.length > 0">
|
||||
<span slot="trigger">
|
||||
<b-icon icon="plus" size="is-small"></b-icon> {{ lang('New') }}
|
||||
</span>
|
||||
|
||||
<b-dropdown-item @click="create('dir')" aria-role="listitem">
|
||||
<b-icon icon="folder" size="is-small"></b-icon> {{ lang('Folder') }}
|
||||
</b-dropdown-item>
|
||||
<b-dropdown-item @click="create('file')" aria-role="listitem">
|
||||
<b-icon icon="file" size="is-small"></b-icon> {{ lang('File') }}
|
||||
</b-dropdown-item>
|
||||
|
||||
</b-dropdown>
|
||||
</a>
|
||||
<a v-if="can('batchdownload') && checked.length" @click="batchDownload" class="is-inline-block">
|
||||
<b-icon icon="download" size="is-small"></b-icon> {{ lang('Download') }}
|
||||
</a>
|
||||
<a v-if="can('write') && checked.length" @click="copy" class="is-inline-block">
|
||||
<b-icon icon="copy" size="is-small"></b-icon> {{ lang('Copy') }}
|
||||
</a>
|
||||
<a v-if="can('write') && checked.length" @click="move" class="is-inline-block">
|
||||
<b-icon icon="external-link-square-alt" size="is-small"></b-icon> {{ lang('Move') }}
|
||||
</a>
|
||||
<a v-if="can(['write', 'zip']) && checked.length" @click="zip" class="is-inline-block">
|
||||
<b-icon icon="file-archive" size="is-small"></b-icon> {{ lang('Zip') }}
|
||||
</a>
|
||||
<a v-if="can('write') && checked.length" @click="remove" class="is-inline-block">
|
||||
<b-icon icon="trash-alt" size="is-small"></b-icon> {{ lang('Delete') }}
|
||||
</a>
|
||||
</div>
|
||||
<div v-if="can('read')">
|
||||
<Pagination :perpage="perPage" @selected="perPage = $event"></Pagination>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<b-table v-if="can('read')"
|
||||
:data="content"
|
||||
:default-sort="defaultSort"
|
||||
:paginated="perPage > 0"
|
||||
:per-page="perPage"
|
||||
:current-page.sync="currentPage"
|
||||
:hoverable="true"
|
||||
:is-row-checkable="(row) => row.type != 'back'"
|
||||
:row-class="(row) => 'file-row type-'+row.type"
|
||||
:checked-rows.sync="checked"
|
||||
:loading="isLoading"
|
||||
checkable>
|
||||
<template slot-scope="props">
|
||||
|
||||
<b-table-column field="data.name" :label="lang('Name')" :custom-sort="sortByName" sortable>
|
||||
<a @click="itemClick(props.row)" class="is-block name">
|
||||
{{ props.row.name }}
|
||||
</a>
|
||||
</b-table-column>
|
||||
|
||||
<b-table-column field="data.size" :label="lang('Size')" :custom-sort="sortBySize" sortable numeric width="150">
|
||||
{{ props.row.type == 'back' || props.row.type == 'dir' ? lang('Folder') : formatBytes(props.row.size) }}
|
||||
</b-table-column>
|
||||
|
||||
<b-table-column field="data.time" :label="lang('Time')" :custom-sort="sortByTime" sortable numeric width="200">
|
||||
{{ props.row.time ? formatDate(props.row.time) : '' }}
|
||||
</b-table-column>
|
||||
|
||||
<b-table-column class="action-padding" width="51">
|
||||
<b-dropdown v-if="props.row.type != 'back'" aria-role="list" position="is-bottom-left" :disabled="checked.length > 0">
|
||||
<button class="button is-small" slot="trigger">
|
||||
<b-icon icon="ellipsis-h" size="is-small"></b-icon>
|
||||
</button>
|
||||
|
||||
<b-dropdown-item v-if="props.row.type == 'file' && can('download')" @click="download(props.row)" aria-role="listitem">
|
||||
<b-icon icon="download" size="is-small"></b-icon> {{ lang('Download') }}
|
||||
</b-dropdown-item>
|
||||
<b-dropdown-item v-if="can('write')" @click="copy($event, props.row)" aria-role="listitem">
|
||||
<b-icon icon="copy" size="is-small"></b-icon> {{ lang('Copy') }}
|
||||
</b-dropdown-item>
|
||||
<b-dropdown-item v-if="can('write')" @click="move($event, props.row)" aria-role="listitem">
|
||||
<b-icon icon="external-link-square-alt" size="is-small"></b-icon> {{ lang('Move') }}
|
||||
</b-dropdown-item>
|
||||
<b-dropdown-item v-if="can('write')" @click="rename($event, props.row)" aria-role="listitem">
|
||||
<b-icon icon="file-signature" size="is-small"></b-icon> {{ lang('Rename') }}
|
||||
</b-dropdown-item>
|
||||
<b-dropdown-item v-if="can(['write', 'zip']) && isArchive(props.row)" @click="unzip($event, props.row)" aria-role="listitem">
|
||||
<b-icon icon="file-archive" size="is-small"></b-icon> {{ lang('Unzip') }}
|
||||
</b-dropdown-item>
|
||||
<b-dropdown-item v-if="can(['write', 'zip']) && ! isArchive(props.row)" @click="zip($event, props.row)" aria-role="listitem">
|
||||
<b-icon icon="file-archive" size="is-small"></b-icon> {{ lang('Zip') }}
|
||||
</b-dropdown-item>
|
||||
<b-dropdown-item v-if="can('write')" @click="remove($event, props.row)" aria-role="listitem">
|
||||
<b-icon icon="trash-alt" size="is-small"></b-icon> {{ lang('Delete') }}
|
||||
</b-dropdown-item>
|
||||
<b-dropdown-item v-if="props.row.type == 'file' && can('download')" v-clipboard:copy="getDownloadLink(props.row)" aria-role="listitem">
|
||||
<b-icon icon="clipboard" size="is-small"></b-icon> {{ lang('Copy link') }}
|
||||
</b-dropdown-item>
|
||||
|
||||
</b-dropdown>
|
||||
</b-table-column>
|
||||
|
||||
</template>
|
||||
|
||||
<template slot="bottom-left">
|
||||
<span>{{ lang('Selected', checked.length, totalCount) }}</span>
|
||||
</template>
|
||||
|
||||
</b-table>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Vue from 'vue'
|
||||
import Menu from './partials/Menu'
|
||||
import Tree from './partials/Tree'
|
||||
import Pagination from './partials/Pagination'
|
||||
import Upload from './partials/Upload'
|
||||
import api from '../api/api'
|
||||
import VueClipboard from 'vue-clipboard2'
|
||||
|
||||
Vue.use(VueClipboard)
|
||||
|
||||
export default {
|
||||
name: 'Browser',
|
||||
components: { Menu, Tree, Pagination, Upload },
|
||||
data() {
|
||||
return {
|
||||
dropZone: false,
|
||||
perPage: "",
|
||||
currentPage: 1,
|
||||
checked: [],
|
||||
isLoading: false,
|
||||
defaultSort: ['data.name', 'desc'],
|
||||
files: [],
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
if (this.can('read')) {
|
||||
this.loadFiles()
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
'$route' (to, from) {
|
||||
this.isLoading = true
|
||||
this.checked = []
|
||||
this.currentPage = 1
|
||||
api.changeDir({
|
||||
to: to.query.cd
|
||||
})
|
||||
.then(ret => {
|
||||
this.$store.commit('setCwd', {
|
||||
content: ret.files,
|
||||
location: ret.location,
|
||||
})
|
||||
this.isLoading = false
|
||||
})
|
||||
.catch(error => {
|
||||
this.isLoading = false
|
||||
this.handleError(error)
|
||||
})
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
breadcrumbs() {
|
||||
let path = ''
|
||||
let breadcrumbs = [{name: this.lang('Home'), path: '/'}]
|
||||
|
||||
_.forEach(_.split(this.$store.state.cwd.location, '/'), (dir) => {
|
||||
path += dir + '/'
|
||||
breadcrumbs.push({
|
||||
name: dir,
|
||||
path: path,
|
||||
})
|
||||
})
|
||||
|
||||
return breadcrumbs
|
||||
},
|
||||
content() {
|
||||
return this.$store.state.cwd.content
|
||||
},
|
||||
totalCount() {
|
||||
return _.sumBy(this.$store.state.cwd.content, (o) => {
|
||||
return o.type == 'file' || o.type == 'dir'
|
||||
}) || 0
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
loadFiles() {
|
||||
api.getDir({
|
||||
to: '',
|
||||
})
|
||||
.then(ret => {
|
||||
this.$store.commit('setCwd', {
|
||||
content: ret.files,
|
||||
location: ret.location,
|
||||
})
|
||||
})
|
||||
.catch(error => this.handleError(error))
|
||||
},
|
||||
goTo(path) {
|
||||
this.$router.push({ name: 'browser', query: { 'cd': path }})
|
||||
},
|
||||
getSelected() {
|
||||
return _.reduce(this.checked, function(result, value, key) {
|
||||
result.push(value)
|
||||
return result
|
||||
}, [])
|
||||
},
|
||||
itemClick(item) {
|
||||
if (item.type == 'dir' || item.type == 'back') {
|
||||
this.goTo(item.path)
|
||||
} else {
|
||||
this.download(item)
|
||||
}
|
||||
},
|
||||
selectDir() {
|
||||
this.$modal.open({
|
||||
parent: this,
|
||||
hasModalCard: true,
|
||||
component: Tree,
|
||||
events: {
|
||||
selected: dir => {
|
||||
this.goTo(dir.path)
|
||||
}
|
||||
},
|
||||
})
|
||||
},
|
||||
copy(event, item) {
|
||||
this.$modal.open({
|
||||
parent: this,
|
||||
hasModalCard: true,
|
||||
component: Tree,
|
||||
events: {
|
||||
selected: dir => {
|
||||
this.isLoading = true
|
||||
api.copyItems({
|
||||
destination: dir.path,
|
||||
items: item ? [item] : this.getSelected(),
|
||||
})
|
||||
.then(res => {
|
||||
this.isLoading = false
|
||||
})
|
||||
.catch(error => {
|
||||
this.isLoading = false
|
||||
this.handleError(error)
|
||||
})
|
||||
this.checked = []
|
||||
}
|
||||
},
|
||||
})
|
||||
},
|
||||
move(event, item) {
|
||||
this.$modal.open({
|
||||
parent: this,
|
||||
hasModalCard: true,
|
||||
component: Tree,
|
||||
events: {
|
||||
selected: dir => {
|
||||
this.isLoading = true
|
||||
api.moveItems({
|
||||
destination: dir.path,
|
||||
items: item ? [item] : this.getSelected(),
|
||||
})
|
||||
.then(res => {
|
||||
this.isLoading = false
|
||||
})
|
||||
.catch(error => {
|
||||
this.isLoading = false
|
||||
this.handleError(error)
|
||||
})
|
||||
this.checked = []
|
||||
}
|
||||
},
|
||||
})
|
||||
},
|
||||
batchDownload() {
|
||||
let items = this.getSelected()
|
||||
|
||||
this.isLoading = true
|
||||
api.batchDownload({
|
||||
items: items,
|
||||
})
|
||||
.then(ret => {
|
||||
this.isLoading = false
|
||||
this.$dialog.alert({
|
||||
message: this.lang('Your file is ready'),
|
||||
confirmText: this.lang('Download'),
|
||||
onConfirm: () => {
|
||||
window.open(Vue.config.baseURL+'/batchdownload&uniqid='+ret.uniqid, '_blank')
|
||||
}
|
||||
})
|
||||
})
|
||||
.catch(error => {
|
||||
this.isLoading = false
|
||||
this.handleError(error)
|
||||
})
|
||||
},
|
||||
getDownloadLink(item) {
|
||||
return Vue.config.baseURL+'/download/'+btoa(item.path);
|
||||
},
|
||||
download(item) {
|
||||
window.open(this.getDownloadLink(item), '_blank')
|
||||
},
|
||||
search() {
|
||||
// TODO: create search logic
|
||||
},
|
||||
edit() {
|
||||
// TODO: create edit file logic
|
||||
},
|
||||
isArchive(item) {
|
||||
return item.type == 'file' && item.name.split('.').pop() == 'zip'
|
||||
},
|
||||
unzip(event, item) {
|
||||
this.$dialog.confirm({
|
||||
message: this.lang('Are you sure you want to do this?'),
|
||||
type: 'is-danger',
|
||||
cancelText: this.lang('Cancel'),
|
||||
confirmText: this.lang('Unzip'),
|
||||
onConfirm: () => {
|
||||
this.isLoading = true
|
||||
api.unzipItem({
|
||||
item: item.path,
|
||||
destination: this.$store.state.cwd.location,
|
||||
})
|
||||
.then(res => {
|
||||
this.isLoading = false
|
||||
this.loadFiles()
|
||||
})
|
||||
.catch(error => {
|
||||
this.isLoading = false
|
||||
this.handleError(error)
|
||||
})
|
||||
this.checked = []
|
||||
}
|
||||
})
|
||||
},
|
||||
zip(event, item) {
|
||||
this.$dialog.prompt({
|
||||
message: this.lang('Name'),
|
||||
cancelText: this.lang('Cancel'),
|
||||
confirmText: this.lang('Create'),
|
||||
inputAttrs: {
|
||||
value: this.$store.state.config.default_archive_name,
|
||||
placeholder: this.$store.state.config.default_archive_name,
|
||||
maxlength: 100,
|
||||
required: false,
|
||||
},
|
||||
onConfirm: (value) => {
|
||||
if (! value) {
|
||||
return;
|
||||
}
|
||||
this.isLoading = true
|
||||
api.zipItems({
|
||||
name: value,
|
||||
items: item ? [item] : this.getSelected(),
|
||||
destination: this.$store.state.cwd.location,
|
||||
})
|
||||
.then(ret => {
|
||||
this.isLoading = false
|
||||
this.loadFiles()
|
||||
})
|
||||
.catch(error => {
|
||||
this.isLoading = false
|
||||
this.handleError(error)
|
||||
})
|
||||
this.checked = []
|
||||
}
|
||||
})
|
||||
},
|
||||
rename(event, item) {
|
||||
this.$dialog.prompt({
|
||||
message: this.lang('New name'),
|
||||
cancelText: this.lang('Cancel'),
|
||||
confirmText: this.lang('Rename'),
|
||||
inputAttrs: {
|
||||
value: item ? item.name : this.getSelected()[0].name,
|
||||
maxlength: 100,
|
||||
required: false,
|
||||
},
|
||||
onConfirm: (value) => {
|
||||
this.isLoading = true
|
||||
api.renameItem({
|
||||
from: item.name,
|
||||
to: value,
|
||||
destination: this.$store.state.cwd.location,
|
||||
})
|
||||
.then(res => {
|
||||
this.isLoading = false
|
||||
this.loadFiles()
|
||||
})
|
||||
.catch(error => {
|
||||
this.isLoading = false
|
||||
this.handleError(error)
|
||||
})
|
||||
this.checked = []
|
||||
}
|
||||
})
|
||||
},
|
||||
create(type) {
|
||||
this.$dialog.prompt({
|
||||
cancelText: this.lang('Cancel'),
|
||||
confirmText: this.lang('Create'),
|
||||
inputAttrs: {
|
||||
placeholder: type == 'dir' ? 'MyFolder' : 'file.txt',
|
||||
maxlength: 100,
|
||||
required: false,
|
||||
},
|
||||
onConfirm: (value) => {
|
||||
this.isLoading = true
|
||||
api.createNew({
|
||||
type: type,
|
||||
name: value,
|
||||
destination: this.$store.state.cwd.location,
|
||||
})
|
||||
// TODO: cors is triggering this too early?
|
||||
.then(ret => {
|
||||
this.isLoading = false
|
||||
this.loadFiles()
|
||||
})
|
||||
.catch(error => {
|
||||
this.isLoading = false
|
||||
this.handleError(error)
|
||||
})
|
||||
this.checked = []
|
||||
}
|
||||
})
|
||||
},
|
||||
remove(event, item) {
|
||||
this.$dialog.confirm({
|
||||
message: this.lang('Are you sure you want to do this?'),
|
||||
type: 'is-danger',
|
||||
cancelText: this.lang('Cancel'),
|
||||
confirmText: this.lang('Delete'),
|
||||
onConfirm: () => {
|
||||
this.isLoading = true
|
||||
api.removeItems({
|
||||
items: item ? [item] : this.getSelected(),
|
||||
})
|
||||
.then(ret => {
|
||||
this.isLoading = false
|
||||
this.loadFiles()
|
||||
})
|
||||
.catch(error => {
|
||||
this.isLoading = false
|
||||
this.handleError(error)
|
||||
})
|
||||
this.checked = []
|
||||
}
|
||||
})
|
||||
},
|
||||
sortByName(a, b, isAsc) {
|
||||
return this.customSort(a, b, isAsc, 'name')
|
||||
},
|
||||
sortBySize(a, b, isAsc) {
|
||||
return this.customSort(a, b, isAsc, 'size')
|
||||
},
|
||||
sortByTime(a, b, isAsc) {
|
||||
return this.customSort(a, b, isAsc, 'time')
|
||||
},
|
||||
customSort(a, b, isAsc, param) {
|
||||
// TODO: firefox is broken
|
||||
if (b.type == 'back') return 1
|
||||
if (a.type == 'back') return -1
|
||||
if (b.type == 'dir' && a.type == 'dir') {
|
||||
return (a[param] < b[param]) || isAsc ? -1 : 1
|
||||
} else if (b.type == 'dir') {
|
||||
return 1
|
||||
} else if (a.type == 'dir') {
|
||||
return -1
|
||||
}
|
||||
if (_.isString(a[param])) {
|
||||
return (_.lowerCase(a[param]) < _.lowerCase(b[param])) || isAsc ? -1 : 1
|
||||
} else {
|
||||
return (a[param] < b[param]) || isAsc ? -1 : 1
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
#loading {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
position: fixed;
|
||||
z-index: 1000;
|
||||
top: 0;
|
||||
left: 0;
|
||||
user-drag: none;
|
||||
user-select: none;
|
||||
-moz-user-select: none;
|
||||
-webkit-user-drag: none;
|
||||
-webkit-user-select: none;
|
||||
-ms-user-select: none;
|
||||
}
|
||||
#dropzone {
|
||||
padding: 0;
|
||||
}
|
||||
#browser {
|
||||
margin: 50px auto 100px auto;
|
||||
}
|
||||
.breadcrumb a {
|
||||
font-weight: bold;
|
||||
}
|
||||
.actions {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
.actions a {
|
||||
margin-right: 15px;
|
||||
}
|
||||
.file-row a {
|
||||
color: #373737;
|
||||
}
|
||||
.file-row.type-dir a.name {
|
||||
font-weight: bold
|
||||
}
|
||||
.action-padding {
|
||||
padding: 6px 12px;
|
||||
}
|
||||
.drop-info {
|
||||
margin: 20% auto;
|
||||
}
|
||||
</style>
|
95
frontend/views/Login.vue
Normal file
95
frontend/views/Login.vue
Normal file
@@ -0,0 +1,95 @@
|
||||
<template>
|
||||
<div>
|
||||
<a v-if="can('read')" @click="$router.push('/')" id="back-arrow">
|
||||
<b-icon icon="times"></b-icon>
|
||||
</a>
|
||||
|
||||
</button>
|
||||
<div id="login" class="columns is-centered">
|
||||
<div class="column is-narrow">
|
||||
<form @submit.prevent="login">
|
||||
<div class="box">
|
||||
<div class="has-text-centered">
|
||||
<img class="logo" :src="$store.state.config.logo">
|
||||
</div>
|
||||
<br>
|
||||
<b-field :label="lang('Username')">
|
||||
<b-input name="username" v-model="username" @input="error = ''" required></b-input>
|
||||
</b-field>
|
||||
<b-field :label="lang('Password')">
|
||||
<b-input type="password" name="password" v-model="password" @input="error = ''" required></b-input>
|
||||
</b-field>
|
||||
|
||||
<div class="is-flex is-justify-end">
|
||||
<button class="button is-primary">
|
||||
{{ lang('Login') }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div v-if="error">
|
||||
<code>{{ error }}</code>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import api from '../api/api'
|
||||
|
||||
export default {
|
||||
name: 'Login',
|
||||
data() {
|
||||
return {
|
||||
username: '',
|
||||
password: '',
|
||||
error: '',
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
login() {
|
||||
api.login({
|
||||
username: this.username,
|
||||
password: this.password,
|
||||
})
|
||||
.then(user => {
|
||||
this.$store.commit('setUser', user)
|
||||
this.$router.push('/')
|
||||
})
|
||||
.catch(error => {
|
||||
if (error.response && error.response.data) {
|
||||
this.error = this.lang(error.response.data.data)
|
||||
} else {
|
||||
this.handleError(error)
|
||||
}
|
||||
this.password = ''
|
||||
})
|
||||
},
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.logo {
|
||||
width: 300px;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.box {
|
||||
padding: 30px;
|
||||
}
|
||||
|
||||
#login {
|
||||
padding: 120px 20px;
|
||||
}
|
||||
#back-arrow {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
right: 0;
|
||||
margin: 20px;
|
||||
}
|
||||
</style>
|
146
frontend/views/Users.vue
Normal file
146
frontend/views/Users.vue
Normal file
@@ -0,0 +1,146 @@
|
||||
<template>
|
||||
<div class="container">
|
||||
<Menu></Menu>
|
||||
|
||||
<section class="actions is-flex is-justify-between">
|
||||
<div>
|
||||
<a @click="addUser">
|
||||
<b-icon icon="plus" size="is-small"></b-icon> {{ lang('New') }}
|
||||
</a>
|
||||
</div>
|
||||
<div>
|
||||
<Pagination :perpage="perPage" @selected="perPage = $event"></Pagination>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<b-table
|
||||
:data="users"
|
||||
:default-sort="defaultSort"
|
||||
:paginated="perPage > 0"
|
||||
:per-page="perPage"
|
||||
:current-page.sync="currentPage"
|
||||
:hoverable="true"
|
||||
:loading="isLoading">
|
||||
<template slot-scope="props">
|
||||
|
||||
<b-table-column field="name" :label="lang('Name')" sortable>
|
||||
<a @click="editUser(props.row)">
|
||||
{{ props.row.name }}
|
||||
</a>
|
||||
</b-table-column>
|
||||
|
||||
<b-table-column field="username" :label="lang('Username')" sortable>
|
||||
<a @click="editUser(props.row)">
|
||||
{{ props.row.username }}
|
||||
</a>
|
||||
</b-table-column>
|
||||
|
||||
<b-table-column field="role" :label="lang('Permissions')">
|
||||
{{ permissions(props.row.permissions) }}
|
||||
</b-table-column>
|
||||
|
||||
<b-table-column field="role" :label="lang('Role')" sortable>
|
||||
{{ props.row.role }}
|
||||
</b-table-column>
|
||||
|
||||
<b-table-column>
|
||||
<a v-if="props.row.role != 'guest'" @click="remove(props.row)">
|
||||
<b-icon icon="trash-alt" size="is-small"></b-icon>
|
||||
</a>
|
||||
</b-table-column>
|
||||
|
||||
</template>
|
||||
</b-table>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import UserEdit from './partials/UserEdit'
|
||||
import Menu from './partials/Menu'
|
||||
import Pagination from './partials/Pagination'
|
||||
import api from '../api/api'
|
||||
|
||||
export default {
|
||||
name: 'Users',
|
||||
components: { Menu, Pagination },
|
||||
data() {
|
||||
return {
|
||||
perPage: "",
|
||||
currentPage: 1,
|
||||
isLoading: false,
|
||||
defaultSort: ['name', 'desc'],
|
||||
users: [],
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
api.listUsers()
|
||||
.then(ret => {
|
||||
this.users = ret
|
||||
})
|
||||
.catch(error => this.handleError(error))
|
||||
},
|
||||
methods: {
|
||||
remove(user) {
|
||||
this.$dialog.confirm({
|
||||
message: this.lang('Are you sure you want to do this?'),
|
||||
type: 'is-danger',
|
||||
cancelText: this.lang('Cancel'),
|
||||
confirmText: this.lang('Confirm'),
|
||||
onConfirm: () => {
|
||||
api.deleteUser({
|
||||
username: user.username
|
||||
})
|
||||
.then(res => {
|
||||
this.users = _.reject(this.users, u => u.username == user.username)
|
||||
this.$toast.open({
|
||||
message: this.lang('Deleted'),
|
||||
type: 'is-success',
|
||||
})
|
||||
})
|
||||
.catch(error => this.handleError(error))
|
||||
this.checked = []
|
||||
}
|
||||
})
|
||||
},
|
||||
permissions(array) {
|
||||
return _.join(array, ', ')
|
||||
},
|
||||
addUser() {
|
||||
this.$modal.open({
|
||||
parent: this,
|
||||
props: { user: { role: 'user'}, action: 'add' },
|
||||
hasModalCard: true,
|
||||
component: UserEdit,
|
||||
events: {
|
||||
updated: ret => {
|
||||
this.users.push(ret)
|
||||
}
|
||||
},
|
||||
})
|
||||
},
|
||||
editUser(user) {
|
||||
if (! user.username) {
|
||||
this.handleError('Missing username')
|
||||
return
|
||||
}
|
||||
this.$modal.open({
|
||||
parent: this,
|
||||
props: { user: user, action: 'edit' },
|
||||
hasModalCard: true,
|
||||
component: UserEdit,
|
||||
events: {
|
||||
updated: ret => {
|
||||
this.users.splice(_.findIndex(this.users, {username: ret.username}), 1, ret)
|
||||
}
|
||||
},
|
||||
})
|
||||
},
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.actions {
|
||||
margin: 50px 0 30px 0;
|
||||
}
|
||||
</style>
|
111
frontend/views/partials/Menu.vue
Normal file
111
frontend/views/partials/Menu.vue
Normal file
@@ -0,0 +1,111 @@
|
||||
<template>
|
||||
<nav class="navbar" role="navigation" aria-label="main navigation">
|
||||
<div class="navbar-brand">
|
||||
<a class="navbar-item logo" @click="$router.push('/')">
|
||||
<img :src="this.$store.state.config.logo">
|
||||
</a>
|
||||
|
||||
<a @click="navbarActive = !navbarActive" role="button" :class="[navbarActive ? 'is-active' : '', 'navbar-burger burger']" aria-label="menu" aria-expanded="false">
|
||||
<span aria-hidden="true"></span>
|
||||
<span aria-hidden="true"></span>
|
||||
<span aria-hidden="true"></span>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div :class="[navbarActive ? 'is-active' : '', 'navbar-menu']">
|
||||
<div class="navbar-end">
|
||||
<a @click="$router.push('/')" v-if="is('admin')" class="navbar-item">
|
||||
{{ lang('Files') }}
|
||||
</a>
|
||||
<a @click="$router.push('/users')" v-if="is('admin')" class="navbar-item">
|
||||
{{ lang('Users') }}
|
||||
</a>
|
||||
<a @click="login" v-if="is('guest')" class="navbar-item">
|
||||
{{ lang('Login') }}
|
||||
</a>
|
||||
<a @click="profile" v-if="!is('guest')" class="navbar-item">
|
||||
{{ lang('Profile') }}
|
||||
</a>
|
||||
<a @click="logout" v-if="!is('guest')" class="navbar-item">
|
||||
{{ lang('Logout') }}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</nav>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Profile from './Profile'
|
||||
import api from '../../api/api'
|
||||
|
||||
export default {
|
||||
name: 'Menu',
|
||||
components: { Profile },
|
||||
data() {
|
||||
return {
|
||||
navbarActive: false,
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
if (this.$store.state.user.firstlogin) {
|
||||
this.profile()
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
logout() {
|
||||
api.logout()
|
||||
.then(() => {
|
||||
this.$store.commit('initialize')
|
||||
api.getUser()
|
||||
.then(user => {
|
||||
this.$store.commit('setUser', user)
|
||||
this.$router.push('/login')
|
||||
})
|
||||
.catch(() => {
|
||||
this.$store.commit('initialize')
|
||||
})
|
||||
})
|
||||
.catch(error => {
|
||||
this.$store.commit('initialize')
|
||||
this.handleError(error)
|
||||
})
|
||||
},
|
||||
login() {
|
||||
this.$router.push('/login')
|
||||
},
|
||||
profile() {
|
||||
this.$modal.open({
|
||||
parent: this,
|
||||
hasModalCard: true,
|
||||
component: Profile,
|
||||
})
|
||||
},
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.navbar {
|
||||
z-index: 10;
|
||||
}
|
||||
@media all and (max-width: 1088px) {
|
||||
.logo {
|
||||
padding: 0;
|
||||
}
|
||||
.logo img {
|
||||
max-height: 3rem;
|
||||
}
|
||||
}
|
||||
@media all and (min-width: 1088px) {
|
||||
.navbar {
|
||||
padding: 1rem 0;
|
||||
}
|
||||
.logo {
|
||||
padding: 0 0 0 12px;
|
||||
}
|
||||
.logo img {
|
||||
max-height: 2.5rem;
|
||||
}
|
||||
}
|
||||
</style>
|
20
frontend/views/partials/Pagination.vue
Normal file
20
frontend/views/partials/Pagination.vue
Normal file
@@ -0,0 +1,20 @@
|
||||
<template>
|
||||
<div>
|
||||
<b-select :value="perpage" @input="$emit('selected', $event)" size="is-small">
|
||||
<option value="">{{ lang('No pagination') }}</option>
|
||||
<option value="3">{{ lang('Per page', 3) }}</option>
|
||||
<option value="5">{{ lang('Per page', 5) }}</option>
|
||||
<option value="10">{{ lang('Per page', 10) }}</option>
|
||||
<option value="15">{{ lang('Per page', 15) }}</option>
|
||||
</b-select>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
||||
export default {
|
||||
name: 'Pagination',
|
||||
props: [ 'perpage' ]
|
||||
}
|
||||
</script>
|
||||
|
63
frontend/views/partials/Profile.vue
Normal file
63
frontend/views/partials/Profile.vue
Normal file
@@ -0,0 +1,63 @@
|
||||
<template>
|
||||
<div class="modal-card">
|
||||
<header class="modal-card-head">
|
||||
<p class="modal-card-title">{{ $store.state.user.name }}</p>
|
||||
</header>
|
||||
<section class="modal-card-body">
|
||||
<form @submit="save">
|
||||
<b-field :label="lang('Old password')" :type="formErrors.oldpassword ? 'is-danger' : ''" :message="formErrors.oldpassword">
|
||||
<b-input v-model="oldpassword" @keydown.native="formErrors.oldpassword = ''" required></b-input>
|
||||
</b-field>
|
||||
|
||||
<b-field :label="lang('New password')" :type="formErrors.newpassword ? 'is-danger' : ''" :message="formErrors.newpassword">
|
||||
<b-input v-model="newpassword" @keydown.native="formErrors.newpassword = ''" password-reveal required></b-input>
|
||||
</b-field>
|
||||
</form>
|
||||
</section>
|
||||
<footer class="modal-card-foot">
|
||||
<button class="button" type="button" @click="$parent.close()">{{ lang('Close') }}</button>
|
||||
<button class="button is-primary" type="button" @click="save">{{ lang('Save') }}</button>
|
||||
</footer>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import api from '../../api/api'
|
||||
|
||||
export default {
|
||||
name: 'Profile',
|
||||
data() {
|
||||
return {
|
||||
oldpassword: '',
|
||||
newpassword: '',
|
||||
formErrors: {},
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
save() {
|
||||
api.changePassword({
|
||||
oldpassword: this.oldpassword,
|
||||
newpassword: this.newpassword,
|
||||
})
|
||||
.then(res => {
|
||||
this.$toast.open({
|
||||
message: this.lang('Updated'),
|
||||
type: 'is-success',
|
||||
})
|
||||
this.$parent.close()
|
||||
})
|
||||
.catch(errors => {
|
||||
if (typeof errors.response.data.data != 'object') {
|
||||
this.handleError(errors)
|
||||
}
|
||||
_.forEach(errors.response.data, err => {
|
||||
_.forEach(err, (val, key) => {
|
||||
this.formErrors[key] = this.lang(val)
|
||||
this.$forceUpdate()
|
||||
})
|
||||
})
|
||||
})
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
37
frontend/views/partials/Tree.vue
Normal file
37
frontend/views/partials/Tree.vue
Normal file
@@ -0,0 +1,37 @@
|
||||
<template>
|
||||
<div class="modal-card">
|
||||
<header class="modal-card-head">
|
||||
<p class="modal-card-title">{{ lang('Select Folder') }}</p>
|
||||
</header>
|
||||
<section class="modal-card-body">
|
||||
<div class="tree">
|
||||
<ul class="tree-list">
|
||||
<TreeNode @selected="$emit('selected', $event) && $parent.close()" :node="$store.state.tree"></TreeNode>
|
||||
</ul>
|
||||
</div>
|
||||
</section>
|
||||
<footer class="modal-card-foot">
|
||||
<button class="button" type="button" @click="$parent.close()">{{ lang('Close') }}</button>
|
||||
</footer>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import TreeNode from './TreeNode'
|
||||
|
||||
export default {
|
||||
name: 'Tree',
|
||||
components: { TreeNode },
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.tree {
|
||||
min-height: 450px
|
||||
}
|
||||
|
||||
.tree-list ul li {
|
||||
padding-left: 20px;
|
||||
margin: 6px 0;
|
||||
}
|
||||
</style>
|
78
frontend/views/partials/TreeNode.vue
Normal file
78
frontend/views/partials/TreeNode.vue
Normal file
@@ -0,0 +1,78 @@
|
||||
<template>
|
||||
<li class="node-tree">
|
||||
<b-button :type="button_type" size="is-small" @click="toggleButton(node)">
|
||||
<span class="icon"><i :class="icon"></i></span>
|
||||
</b-button>
|
||||
<a @click="$emit('selected', node)">{{ node.name }}</a>
|
||||
|
||||
<ul v-if="node.children && node.children.length">
|
||||
<TreeNode v-for="child in node.children" :node="child" @selected="$emit('selected', $event)"></TreeNode>
|
||||
</ul>
|
||||
</li>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import api from '../../api/api'
|
||||
|
||||
export default {
|
||||
name: "TreeNode",
|
||||
props: {
|
||||
node: Object
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
active: false,
|
||||
button_type: 'is-primary'
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
if (this.node.path == '/') {
|
||||
this.$store.commit('resetTree')
|
||||
this.toggleButton(this.node)
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
icon() {
|
||||
return {
|
||||
'fas': true,
|
||||
'mdi-24px': true,
|
||||
'fa-plus': ! this.active,
|
||||
'fa-minus': this.active,
|
||||
}
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
toggleButton(node) {
|
||||
if (! this.active) {
|
||||
this.active = true
|
||||
this.button_type = 'is-primary is-loading'
|
||||
api.getDir({
|
||||
dir: node.path
|
||||
})
|
||||
.then(ret => {
|
||||
this.$store.commit('updateTreeNode', {
|
||||
children: _.filter(ret.files, ['type', 'dir']),
|
||||
path: node.path,
|
||||
})
|
||||
this.$forceUpdate()
|
||||
this.button_type = 'is-primary'
|
||||
})
|
||||
.catch(error => this.handleError(error))
|
||||
} else {
|
||||
this.active = false
|
||||
this.$store.commit('updateTreeNode', {
|
||||
children: [],
|
||||
path: node.path,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
a {
|
||||
color: #373737;
|
||||
font-weight: bold;
|
||||
}
|
||||
</style>
|
192
frontend/views/partials/Upload.vue
Normal file
192
frontend/views/partials/Upload.vue
Normal file
@@ -0,0 +1,192 @@
|
||||
<template>
|
||||
<div>
|
||||
<div v-if="visible && dropZone == false" class="progress-box">
|
||||
<div class="box">
|
||||
<div>
|
||||
<div class="is-flex is-justify-between">
|
||||
<div class="is-flex">
|
||||
<a @click="toggleWindow">
|
||||
<b-icon :icon="progressVisible ? 'angle-down' : 'angle-up'"></b-icon>
|
||||
</a>
|
||||
<span v-if="activeUploads">
|
||||
{{ lang('Uploading files', Math.round(resumable.progress()*100), formatBytes(resumable.getSize())) }}
|
||||
</span>
|
||||
<span v-if="activeUploads && paused">
|
||||
({{ lang('Paused') }})
|
||||
</span>
|
||||
<span v-if="! activeUploads">
|
||||
{{ lang('Done') }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="is-flex">
|
||||
<a v-if="activeUploads" @click="togglePause()">
|
||||
<b-icon :icon="paused ? 'play-circle' : 'pause-circle'"></b-icon>
|
||||
</a>
|
||||
<a @click="closeWindow()" class="progress-icon">
|
||||
<b-icon icon="times"></b-icon>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<hr>
|
||||
</div>
|
||||
<div v-if="progressVisible" class="progress-items">
|
||||
<div v-for="file in resumable.files.slice().reverse()">
|
||||
<div>
|
||||
<div>{{ file.relativePath != '/' ? file.relativePath : '' }}/{{ file.fileName }}</div>
|
||||
<div class="is-flex is-justify-between">
|
||||
<progress :class="[file.file.uploadingError ? 'is-danger' : 'is-primary', 'progress is-large']" :value="file.progress()*100" max="100"></progress>
|
||||
<a v-if="! file.isUploading() && file.file.uploadingError" @click="file.retry()" class="progress-icon">
|
||||
<b-icon icon="redo" type="is-danger"></b-icon>
|
||||
</a>
|
||||
<a v-else @click="file.cancel()" class="progress-icon">
|
||||
<b-icon :icon="file.isComplete() ? 'check' : 'times'"></b-icon>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Resumable from 'resumablejs'
|
||||
import Vue from 'vue'
|
||||
import api from '../../api/api'
|
||||
import axios from 'axios'
|
||||
|
||||
export default {
|
||||
name: 'Upload',
|
||||
props: [ 'files', 'dropZone' ],
|
||||
data() {
|
||||
return {
|
||||
resumable: {},
|
||||
visible: false,
|
||||
paused: false,
|
||||
progressVisible: false,
|
||||
progress: 0,
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
'files' (files) {
|
||||
this.visible = true
|
||||
this.progressVisible = true
|
||||
_.forEach(files, file => {
|
||||
file.relativePath = this.$store.state.cwd.location
|
||||
this.resumable.addFile(file)
|
||||
})
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
this.resumable = new Resumable({
|
||||
target: Vue.config.baseURL+'/upload',
|
||||
headers: {
|
||||
'x-csrf-token': axios.defaults.headers.common['x-csrf-token']
|
||||
},
|
||||
withCredentials: true,
|
||||
simultaneousUploads: this.$store.state.config.upload_simultaneous,
|
||||
chunkSize: this.$store.state.config.upload_chunk_size,
|
||||
maxFileSize: this.$store.state.config.upload_max_size,
|
||||
maxFileSizeErrorCallback: (file, errorCount) => {
|
||||
this.$notification.open({
|
||||
message: this.lang('File size error', file.name, this.formatBytes(this.$store.state.config.upload_max_size)),
|
||||
type: 'is-danger',
|
||||
queue: false,
|
||||
indefinite: true,
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
if (! this.resumable.support) {
|
||||
this.$dialog.alert({
|
||||
type: 'is-danger',
|
||||
message: this.lang('Browser not supported.'),
|
||||
})
|
||||
return;
|
||||
}
|
||||
|
||||
this.resumable.on('fileAdded', (file) => {
|
||||
if (! this.paused) {
|
||||
this.resumable.upload()
|
||||
}
|
||||
})
|
||||
this.resumable.on('fileSuccess', (file) => {
|
||||
file.file.uploadingError = false
|
||||
this.$forceUpdate()
|
||||
if (this.can('read')) {
|
||||
api.getDir({
|
||||
to: '',
|
||||
})
|
||||
.then(ret => {
|
||||
this.$store.commit('setCwd', {
|
||||
content: ret.files,
|
||||
location: ret.location,
|
||||
})
|
||||
})
|
||||
.catch(error => this.handleError(error))
|
||||
}
|
||||
})
|
||||
this.resumable.on('fileError', (file) => {
|
||||
file.file.uploadingError = true
|
||||
})
|
||||
},
|
||||
computed: {
|
||||
activeUploads() {
|
||||
return this.resumable.files.length && this.resumable.progress() < 1
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
closeWindow() {
|
||||
if (this.activeUploads) {
|
||||
this.$dialog.confirm({
|
||||
message: this.lang('Are you sure you want to stop all uploads?'),
|
||||
type: 'is-danger',
|
||||
cancelText: this.lang('Cancel'),
|
||||
confirmText: this.lang('Confirm'),
|
||||
onConfirm: () => {
|
||||
this.resumable.cancel()
|
||||
this.visible = false
|
||||
}
|
||||
})
|
||||
} else {
|
||||
this.visible = false
|
||||
this.resumable.cancel()
|
||||
}
|
||||
},
|
||||
toggleWindow() {
|
||||
this.progressVisible = ! this.progressVisible
|
||||
},
|
||||
togglePause() {
|
||||
if (this.paused) {
|
||||
this.resumable.upload()
|
||||
this.paused = false
|
||||
} else {
|
||||
this.resumable.pause()
|
||||
this.paused = true
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.progress-icon {
|
||||
margin-left: 15px;
|
||||
}
|
||||
.progress-box {
|
||||
position: fixed;
|
||||
width: 100%;
|
||||
bottom: -30px;
|
||||
left: 0;
|
||||
padding: 25px;
|
||||
max-height: 50%;
|
||||
z-index: 1;
|
||||
}
|
||||
.progress-items {
|
||||
overflow-y: scroll;
|
||||
margin-right: -100px;
|
||||
padding-right: 100px;
|
||||
max-height: 300px; /* fix this */
|
||||
}
|
||||
</style>
|
206
frontend/views/partials/UserEdit.vue
Normal file
206
frontend/views/partials/UserEdit.vue
Normal file
@@ -0,0 +1,206 @@
|
||||
<template>
|
||||
<div class="modal-card">
|
||||
<header class="modal-card-head">
|
||||
<p class="modal-card-title">{{ user.name }}</p>
|
||||
</header>
|
||||
<section class="modal-card-body">
|
||||
<form @submit.prevent="save">
|
||||
|
||||
<div v-if="user.role == 'user' || user.role == 'admin'" class="field">
|
||||
|
||||
<b-field :label="lang('Role')">
|
||||
<b-select v-model="formFields.role" :placeholder="lang('Role')" expanded required>
|
||||
<option value="user" key="user">{{ lang('User') }}</option>
|
||||
<option value="admin" key="admin">{{ lang('Admin') }}</option>
|
||||
</b-select>
|
||||
</b-field>
|
||||
|
||||
<b-field :label="lang('Username')" :type="formErrors.username ? 'is-danger' : ''" :message="formErrors.username">
|
||||
<b-input v-model="formFields.username" @keydown.native="formErrors.username = ''"></b-input>
|
||||
</b-field>
|
||||
|
||||
<b-field :label="lang('Name')" :type="formErrors.name ? 'is-danger' : ''" :message="formErrors.name">
|
||||
<b-input v-model="formFields.name" @keydown.native="formErrors.name = ''"></b-input>
|
||||
</b-field>
|
||||
|
||||
<b-field :label="lang('Password')" :type="formErrors.password ? 'is-danger' : ''" :message="formErrors.password">
|
||||
<b-input v-model="formFields.password" @keydown.native="formErrors.password = ''" :placeholder="action == 'edit' ? lang('Leave blank for no change') : ''" password-reveal></b-input>
|
||||
</b-field>
|
||||
|
||||
</div>
|
||||
|
||||
<b-field :label="lang('Homedir')" :type="formErrors.homedir ? 'is-danger' : ''" :message="formErrors.homedir">
|
||||
<b-input v-model="formFields.homedir" @focus="selectDir"></b-input>
|
||||
</b-field>
|
||||
|
||||
<b-field :label="lang('Permissions')">
|
||||
<div class="block">
|
||||
<b-checkbox v-model="permissions.read">
|
||||
{{ lang('Read') }}
|
||||
</b-checkbox>
|
||||
<b-checkbox v-model="permissions.write">
|
||||
{{ lang('Write') }}
|
||||
</b-checkbox>
|
||||
<b-checkbox v-model="permissions.upload">
|
||||
{{ lang('Upload') }}
|
||||
</b-checkbox>
|
||||
<b-checkbox v-model="permissions.download">
|
||||
{{ lang('Download') }}
|
||||
</b-checkbox>
|
||||
<b-checkbox v-model="permissions.batchdownload">
|
||||
{{ lang('Batch Download') }}
|
||||
</b-checkbox>
|
||||
<b-checkbox v-model="permissions.zip">
|
||||
{{ lang('Zip') }}
|
||||
</b-checkbox>
|
||||
</div>
|
||||
</b-field>
|
||||
|
||||
</form>
|
||||
</section>
|
||||
<footer class="modal-card-foot">
|
||||
<button class="button" type="button" @click="$parent.close()">{{ lang('Close') }}</button>
|
||||
<button class="button is-primary" type="button" @click="confirmSave">{{ lang('Save') }}</button>
|
||||
</footer>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Tree from './Tree'
|
||||
import api from '../../api/api'
|
||||
|
||||
export default {
|
||||
name: 'UserEdit',
|
||||
components: { Tree },
|
||||
props: [ 'user', 'action' ],
|
||||
computed: {
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
formFields: {
|
||||
role: this.user.role,
|
||||
name: this.user.name,
|
||||
username: this.user.username,
|
||||
homedir: this.user.homedir,
|
||||
password: '',
|
||||
},
|
||||
formErrors: {},
|
||||
permissions: {
|
||||
read: _.find(this.user.permissions, p => p == 'read') ? true : false,
|
||||
write: _.find(this.user.permissions, p => p == 'write') ? true : false,
|
||||
upload: _.find(this.user.permissions, p => p == 'upload') ? true : false,
|
||||
download: _.find(this.user.permissions, p => p == 'download') ? true : false,
|
||||
batchdownload: _.find(this.user.permissions, p => p == 'batchdownload') ? true : false,
|
||||
zip: _.find(this.user.permissions, p => p == 'zip') ? true : false,
|
||||
}
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
'permissions.read' (val) {
|
||||
if (!val) {
|
||||
this.permissions.write = false
|
||||
this.permissions.batchdownload = false
|
||||
this.permissions.zip = false
|
||||
}
|
||||
},
|
||||
'permissions.write' (val) {
|
||||
if (val) {
|
||||
this.permissions.read = true
|
||||
} else {
|
||||
this.permissions.zip = false
|
||||
}
|
||||
},
|
||||
'permissions.download' (val) {
|
||||
if (!val) {
|
||||
this.permissions.batchdownload = false
|
||||
}
|
||||
},
|
||||
'permissions.batchdownload' (val) {
|
||||
if (val) {
|
||||
this.permissions.read = true
|
||||
this.permissions.download = true
|
||||
}
|
||||
},
|
||||
'permissions.zip' (val) {
|
||||
if (val) {
|
||||
this.permissions.read = true
|
||||
this.permissions.write = true
|
||||
}
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
selectDir() {
|
||||
this.formErrors.homedir = ''
|
||||
|
||||
this.$modal.open({
|
||||
parent: this,
|
||||
hasModalCard: true,
|
||||
component: Tree,
|
||||
events: {
|
||||
selected: dir => {
|
||||
this.formFields.homedir = dir.path
|
||||
}
|
||||
},
|
||||
})
|
||||
},
|
||||
getPermissionsArray() {
|
||||
return _.reduce(this.permissions, (result, value, key) => {
|
||||
if (value == true) {
|
||||
result.push(key)
|
||||
}
|
||||
return result
|
||||
}, [])
|
||||
},
|
||||
confirmSave() {
|
||||
|
||||
if (this.formFields.role == 'guest' && this.getPermissionsArray().length) {
|
||||
this.$dialog.confirm({
|
||||
message: this.lang('Are you sure you want to allow access to everyone?'),
|
||||
type: 'is-danger',
|
||||
cancelText: this.lang('Cancel'),
|
||||
confirmText: this.lang('Confirm'),
|
||||
onConfirm: () => {
|
||||
this.save()
|
||||
}
|
||||
})
|
||||
} else {
|
||||
this.save()
|
||||
}
|
||||
},
|
||||
save() {
|
||||
|
||||
let method = this.action == 'add' ? api.storeUser : api.updateUser
|
||||
|
||||
method({
|
||||
key: this.user.username,
|
||||
role: this.formFields.role,
|
||||
name: this.formFields.name,
|
||||
username: this.formFields.username,
|
||||
homedir: this.formFields.homedir,
|
||||
password: this.formFields.password,
|
||||
permissions: this.getPermissionsArray(),
|
||||
})
|
||||
.then(res => {
|
||||
this.$toast.open({
|
||||
message: this.lang('Updated'),
|
||||
type: 'is-success',
|
||||
})
|
||||
this.$emit('updated', res)
|
||||
this.$parent.close()
|
||||
})
|
||||
.catch(errors => {
|
||||
if (typeof errors.response.data.data != 'object') {
|
||||
this.handleError(errors)
|
||||
}
|
||||
_.forEach(errors.response.data, err => {
|
||||
_.forEach(err, (val, key) => {
|
||||
this.formErrors[key] = this.lang(val)
|
||||
this.$forceUpdate()
|
||||
})
|
||||
})
|
||||
})
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
Reference in New Issue
Block a user