Changed file list
- Grid style more suited for large file listings - Multi-select allows individual files to be deleted
This commit is contained in:
parent
fbf6e6a96f
commit
10322bc26f
|
@ -1,6 +1,8 @@
|
|||
const asyncHandler = require('express-async-handler');
|
||||
const AuthTokens = require('../../authtokens');
|
||||
const resolvePath = require('resolve-path');
|
||||
const groupBy = require('lodash/groupBy');
|
||||
const map = require('lodash/map');
|
||||
|
||||
|
||||
module.exports = (repository, router) =>
|
||||
|
@ -35,6 +37,20 @@ module.exports = (repository, router) =>
|
|||
}));
|
||||
|
||||
|
||||
router.delete('/fileuploads/', asyncHandler(async (req, res) =>
|
||||
{
|
||||
var groupedFiles = groupBy(req.body, (value) => value.uploadId);
|
||||
|
||||
for (var uploadId in groupedFiles)
|
||||
{
|
||||
await repository.uploads.deleteFiles(uploadId,
|
||||
map(groupedFiles[uploadId], (file) => { return file.fileId }));
|
||||
}
|
||||
|
||||
res.sendStatus(200);
|
||||
}));
|
||||
|
||||
|
||||
router.delete('/codeuploads/:code', asyncHandler(async (req, res) =>
|
||||
{
|
||||
var uploads = await repository.uploads.listForCode(req.params.code);
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
const map = require('lodash/map');
|
||||
const filter = require('lodash/filter');
|
||||
const intersectionWith = require('lodash/intersectionWith');
|
||||
const pullAllWith = require('lodash/pullAllWith');
|
||||
const resolvePath = require('resolve-path');
|
||||
const fs = require('mz/fs');
|
||||
const async = require('async');
|
||||
|
@ -167,6 +169,8 @@ class UploadRepository
|
|||
$set: {
|
||||
userId: userId
|
||||
}
|
||||
}, {
|
||||
multi: true
|
||||
},
|
||||
(err, numAffected) =>
|
||||
{
|
||||
|
@ -217,23 +221,7 @@ class UploadRepository
|
|||
return;
|
||||
}
|
||||
|
||||
await Promise.all(upload.files.map(async (file) =>
|
||||
{
|
||||
if (!file.id) return;
|
||||
if (!(await self.fileExists(file.id)))
|
||||
{
|
||||
var fullpath = resolvePath(config.fileUpload.path, file.id);
|
||||
try
|
||||
{
|
||||
await fs.unlink(fullpath);
|
||||
}
|
||||
catch (err)
|
||||
{
|
||||
console.log('Failed to delete ' + fullpath);
|
||||
}
|
||||
}
|
||||
}));
|
||||
|
||||
await self.deleteOrphanedFiles(upload.files);
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
|
@ -241,6 +229,102 @@ class UploadRepository
|
|||
}
|
||||
|
||||
|
||||
async deleteOrphanedFiles(files)
|
||||
{
|
||||
var self = this;
|
||||
|
||||
await Promise.all(files.map(async (file) =>
|
||||
{
|
||||
if (!file.id) return;
|
||||
if (!(await self.fileExists(file.id)))
|
||||
{
|
||||
var fullpath = resolvePath(config.fileUpload.path, file.id);
|
||||
try
|
||||
{
|
||||
await fs.unlink(fullpath);
|
||||
}
|
||||
catch (err)
|
||||
{
|
||||
console.log('Failed to delete ' + fullpath);
|
||||
}
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
|
||||
deleteFiles(uploadId, fileIds)
|
||||
{
|
||||
var self = this;
|
||||
|
||||
return new Promise((resolve, reject) =>
|
||||
{
|
||||
self.store.findOne({ _id: uploadId }, (err, doc) =>
|
||||
{
|
||||
if (err)
|
||||
{
|
||||
reject(err);
|
||||
return;
|
||||
}
|
||||
|
||||
if (doc === null)
|
||||
{
|
||||
resolve();
|
||||
return;
|
||||
}
|
||||
|
||||
let upload = new Upload(doc);
|
||||
let deletedFiles = intersectionWith(upload.files, fileIds, (arrVal, othVal) => { return arrVal.id == othVal; });
|
||||
|
||||
if (deletedFiles.length == 0)
|
||||
{
|
||||
resolve();
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
pullAllWith(upload.files, fileIds, (arrVal, othVal) => { return arrVal.id == othVal; });
|
||||
if (upload.files.length == 0)
|
||||
{
|
||||
// Remove entire upload
|
||||
self.store.remove({ _id: uploadId }, async (err, numRemoved) =>
|
||||
{
|
||||
if (err)
|
||||
{
|
||||
reject(err);
|
||||
return;
|
||||
}
|
||||
|
||||
await self.deleteOrphanedFiles(deletedFiles);
|
||||
resolve();
|
||||
});
|
||||
}
|
||||
else
|
||||
{
|
||||
// Update file list
|
||||
self.store.update({ _id: uploadId }, {
|
||||
$set: {
|
||||
files: map(filter(upload.files,
|
||||
(file) => file.hasOwnProperty('id') && file.hasOwnProperty('name')),
|
||||
(file) => { return { id: file.id, name: file.name, size: file.size } })
|
||||
},
|
||||
},
|
||||
async (err, numRemoved) =>
|
||||
{
|
||||
if (err)
|
||||
{
|
||||
reject(err);
|
||||
return;
|
||||
}
|
||||
|
||||
await self.deleteOrphanedFiles(deletedFiles);
|
||||
resolve();
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
fileExists(fileId)
|
||||
{
|
||||
var self = this;
|
||||
|
|
File diff suppressed because it is too large
Load Diff
57
package.json
57
package.json
|
@ -16,40 +16,41 @@
|
|||
"author": "Mark van Renswoude <mark@x2software.net>",
|
||||
"license": "Unlicense",
|
||||
"dependencies": {
|
||||
"@fortawesome/fontawesome": "^1.1.5",
|
||||
"@fortawesome/fontawesome-free-solid": "^5.0.10",
|
||||
"@fortawesome/fontawesome": "^1.1.8",
|
||||
"@fortawesome/fontawesome-free-regular": "^5.0.13",
|
||||
"@fortawesome/fontawesome-free-solid": "^5.0.13",
|
||||
"@fortawesome/vue-fontawesome": "0.0.22",
|
||||
"async": "^2.6.0",
|
||||
"async-retry": "^1.2.1",
|
||||
"async": "^2.6.1",
|
||||
"async-retry": "^1.2.3",
|
||||
"babel-polyfill": "^6.26.0",
|
||||
"bcrypt": "^1.0.3",
|
||||
"body-parser": "^1.18.2",
|
||||
"body-parser": "^1.18.3",
|
||||
"cookie-parser": "^1.4.3",
|
||||
"debug": "^3.1.0",
|
||||
"debug": "^3.2.5",
|
||||
"diskusage": "^0.2.4",
|
||||
"ejs": "^2.5.9",
|
||||
"email-templates": "^3.6.0",
|
||||
"es6-promise": "^4.2.4",
|
||||
"ejs": "^2.6.1",
|
||||
"email-templates": "^3.6.1",
|
||||
"es6-promise": "^4.2.5",
|
||||
"express": "^4.16.3",
|
||||
"express-async-handler": "^1.1.3",
|
||||
"express-async-handler": "^1.1.4",
|
||||
"js-cookie": "^2.2.0",
|
||||
"jsonwebtoken": "^8.2.1",
|
||||
"lodash": "^4.17.10",
|
||||
"jsonwebtoken": "^8.3.0",
|
||||
"lodash": "^4.17.11",
|
||||
"markdown": "^0.5.0",
|
||||
"mkdirp": "^0.5.1",
|
||||
"moment": "^2.22.1",
|
||||
"moment": "^2.22.2",
|
||||
"mz": "^2.7.0",
|
||||
"nanoid": "^1.0.2",
|
||||
"nanoid": "^1.2.3",
|
||||
"nedb": "^1.8.0",
|
||||
"nodemailer": "^4.6.4",
|
||||
"npm": "^5.8.0",
|
||||
"nodemailer": "^4.6.8",
|
||||
"npm": "^5.10.0",
|
||||
"pug": "^2.0.3",
|
||||
"resolve-path": "^1.4.0",
|
||||
"tus-node-server": "^0.2.11"
|
||||
},
|
||||
"devDependencies": {
|
||||
"axios": "^0.18.0",
|
||||
"babel-loader": "^7.1.4",
|
||||
"babel-loader": "^7.1.5",
|
||||
"babel-plugin-syntax-dynamic-import": "^6.18.0",
|
||||
"babel-plugin-transform-es2015-arrow-functions": "^6.22.0",
|
||||
"babel-plugin-transform-es2015-block-scoping": "^6.26.0",
|
||||
|
@ -57,22 +58,22 @@
|
|||
"css-loader": "^0.28.11",
|
||||
"file-loader": "^1.1.11",
|
||||
"html-webpack-plugin": "^3.2.0",
|
||||
"node-sass": "^4.9.0",
|
||||
"node-sass": "^4.9.3",
|
||||
"purecss": "^1.0.0",
|
||||
"sass-loader": "^6.0.7",
|
||||
"style-loader": "^0.20.3",
|
||||
"tus-js-client": "^1.5.1",
|
||||
"uppy": "^0.23.3",
|
||||
"vue": "^2.5.16",
|
||||
"vue-i18n": "^7.6.0",
|
||||
"vue-loader": "^14.2.2",
|
||||
"vue": "^2.5.17",
|
||||
"vue-i18n": "^7.8.1",
|
||||
"vue-loader": "^14.2.3",
|
||||
"vue-router": "^3.0.1",
|
||||
"vue-style-loader": "^4.1.0",
|
||||
"vue-template-compiler": "^2.5.16",
|
||||
"webpack": "^4.6.0",
|
||||
"webpack-bundle-analyzer": "^2.11.1",
|
||||
"webpack-cli": "^2.1.2",
|
||||
"webpack-dev-middleware": "^3.1.3",
|
||||
"webpack-hot-middleware": "^2.22.1"
|
||||
"vue-style-loader": "^4.1.2",
|
||||
"vue-template-compiler": "^2.5.17",
|
||||
"webpack": "^4.19.1",
|
||||
"webpack-bundle-analyzer": "^2.13.1",
|
||||
"webpack-cli": "^2.1.5",
|
||||
"webpack-dev-middleware": "^3.3.0",
|
||||
"webpack-hot-middleware": "^2.24.0"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -193,4 +193,10 @@ a
|
|||
{
|
||||
color: red !important;
|
||||
}
|
||||
|
||||
.pure-button-confirm-delete
|
||||
{
|
||||
background: rgb(223, 117, 20);
|
||||
color: white;
|
||||
}
|
||||
</style>
|
|
@ -17,6 +17,8 @@ import faSpinner from '@fortawesome/fontawesome-free-solid/faSpinner';
|
|||
import faBan from '@fortawesome/fontawesome-free-solid/faBan';
|
||||
import faTrashAlt from '@fortawesome/fontawesome-free-solid/faTrashAlt';
|
||||
import faUser from '@fortawesome/fontawesome-free-solid/faUser';
|
||||
import faSquare from '@fortawesome/fontawesome-free-regular/faSquare';
|
||||
import faCheckSquare from '@fortawesome/fontawesome-free-regular/faCheckSquare';
|
||||
|
||||
|
||||
if (typeof customMessages !== 'undefined')
|
||||
|
@ -34,7 +36,7 @@ if (typeof customMessages !== 'undefined')
|
|||
}
|
||||
|
||||
|
||||
fontawesome.library.add(faSpinner, faBan, faTrashAlt, faUser);
|
||||
fontawesome.library.add(faSpinner, faBan, faTrashAlt, faUser, faSquare, faCheckSquare);
|
||||
Vue.component('fa', FontAwesomeIcon);
|
||||
|
||||
Vue.use(VueI18n);
|
||||
|
|
|
@ -48,6 +48,7 @@ export default {
|
|||
cancel: 'Cancel',
|
||||
save: 'Save',
|
||||
delete: 'Delete',
|
||||
deleteApply: 'Delete selection',
|
||||
|
||||
diskspace: '{available} disk space available of {total} total',
|
||||
|
||||
|
@ -59,7 +60,7 @@ export default {
|
|||
},
|
||||
|
||||
menu: {
|
||||
uploads: 'Uploads',
|
||||
uploads: 'Received',
|
||||
codes: 'Codes',
|
||||
users: 'Users'
|
||||
},
|
||||
|
@ -68,6 +69,7 @@ export default {
|
|||
created: 'Date',
|
||||
code: 'Code',
|
||||
owner: 'Owner',
|
||||
expiration: 'Expires:',
|
||||
userDeleted: 'deleted',
|
||||
|
||||
assign: 'Change owner',
|
||||
|
|
|
@ -48,6 +48,7 @@ export default {
|
|||
cancel: 'Annuleren',
|
||||
save: 'Opslaan',
|
||||
delete: 'Verwijderen',
|
||||
deleteApply: 'Selectie verwijderen',
|
||||
|
||||
diskspace: '{available} schijfruimte beschikbaar van {total} totaal',
|
||||
|
||||
|
@ -59,7 +60,7 @@ export default {
|
|||
},
|
||||
|
||||
menu: {
|
||||
uploads: 'Uploads',
|
||||
uploads: 'Ontvangen',
|
||||
codes: 'Codes',
|
||||
users: 'Gebruikers'
|
||||
},
|
||||
|
@ -68,11 +69,12 @@ export default {
|
|||
created: 'Datum',
|
||||
code: 'Code',
|
||||
owner: 'Eigenaar',
|
||||
expiration: 'Verloopt:',
|
||||
userDeleted: 'verwijderd',
|
||||
|
||||
assign: 'Verander eigenaar',
|
||||
assignApply: 'Toepassen'
|
||||
},
|
||||
},
|
||||
|
||||
codes: {
|
||||
add: 'Genereer code',
|
||||
|
|
|
@ -2,45 +2,22 @@
|
|||
<div id="uploads">
|
||||
<div v-if="uploads !== null" class="list">
|
||||
<div v-for="group in groupedUploads">
|
||||
<template v-for="(upload, index) in group.uploads">
|
||||
<div class="properties" v-if="index == 0">
|
||||
<div class="pure-g">
|
||||
<div class="pure-u-1-1"><span class="text codedescription">{{ upload.codedescription || upload.codeId }}</span></div>
|
||||
<div class="pure-u-1-3"><span class="text">{{ upload.codeId }}</span></div>
|
||||
<div class="pure-u-1-3">
|
||||
<span class="text" v-if="hasAuth('viewAllUploads')" :class="{ userDeleted: !upload.username }">
|
||||
{{ upload.username || $t('admin.uploads.userDeleted') }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="pure-u-1-3 right"><span class="text">{{ upload.created | formatDateTime }}</span></div>
|
||||
<div class="properties">
|
||||
<div class="pure-g">
|
||||
<div class="pure-u-1-1"><span class="text codedescription">{{ group.lastUpload.codedescription || group.lastUpload.codeId }}</span></div>
|
||||
<div class="pure-u-1-3"><span class="text">{{ group.lastUpload.codeId }}</span></div>
|
||||
<div class="pure-u-1-3">
|
||||
<span class="text" v-if="hasAuth('viewAllUploads')" :class="{ userDeleted: !group.lastUpload.username }">
|
||||
{{ group.lastUpload.username || $t('admin.uploads.userDeleted') }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="pure-u-1-3 right">
|
||||
<span class="text" v-if="group.lastUpload.expirationDate">{{ $t('admin.uploads.expiration') }} {{ group.lastUpload.expirationDate | formatDateTime }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="pure-menu pure-menu-horizontal" v-if="index == 0">
|
||||
<ul class="pure-menu-list">
|
||||
<li class="pure-menu-item"><a href="#" class="pure-menu-link" @click.prevent="showAssign = !showAssign"><fa icon="user"></fa> {{ $t('admin.uploads.assign') }}</a></li>
|
||||
<li class="pure-menu-item" v-if="confirmDelete == upload.codeId"><a href="#" class="pure-menu-link" @click.prevent="cancelDelete"><fa icon="ban"></fa> {{ $t('admin.cancel') }}</a></li>
|
||||
<li class="pure-menu-item"><a href="#" class="pure-menu-link" :class="{ confirmDelete: confirmDelete == upload.codeId }" @click.prevent="deleteClick(upload.codeId)"><fa icon="trash-alt"></fa> {{ $t('admin.delete') }}</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div v-if="showAssign" class="assign pure-form pure-form-horizontal">
|
||||
<select v-model="assignUser">
|
||||
<option v-for="user in assignUsers" v-if="user.id !== upload.userId" :value="user.id">{{ user.name }}</option>
|
||||
</select>
|
||||
|
||||
<button class="pure-button pure-button-primary" @click.prevent="assignCode(upload.codeId)">{{ $t('admin.uploads.assignApply') }}</button>
|
||||
<button class="pure-button" @click.prevent="showAssign = false">{{ $t('admin.cancel') }}</button>
|
||||
</div>
|
||||
|
||||
<div class="file" v-for="file in upload.files" :title="file.name">
|
||||
<a :href="getDownloadUrl(file)">
|
||||
<img :src="getFileIconUrl(file.name)" class="icon">
|
||||
<span class="filename">{{ file.name }}</span>
|
||||
<span class="size">{{ file.size | formatSizeSI }}</span>
|
||||
</a>
|
||||
</div>
|
||||
</template>
|
||||
<UploadFiles :group="group" v-bind:assignUsers="assignUsers" @files-deleted="onFilesDeleted" @code-assigned="onCodeAssigned"></UploadFiles>
|
||||
</div>
|
||||
|
||||
<div v-if="uploads.length == 0" class="nodata">
|
||||
|
@ -55,6 +32,7 @@
|
|||
</template>
|
||||
|
||||
<script>
|
||||
import UploadFiles from './components/UploadFiles.vue';
|
||||
import orderBy from 'lodash/orderBy';
|
||||
import findIndex from 'lodash/findIndex';
|
||||
import forEach from 'lodash/forEach';
|
||||
|
@ -62,13 +40,14 @@ import axios from 'axios';
|
|||
import shared from '../../shared';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
UploadFiles
|
||||
},
|
||||
|
||||
data()
|
||||
{
|
||||
return {
|
||||
uploads: null,
|
||||
confirmDelete: null,
|
||||
showAssign: false,
|
||||
assignUser: null,
|
||||
assignUsers: []
|
||||
};
|
||||
},
|
||||
|
@ -122,7 +101,8 @@ export default {
|
|||
{
|
||||
codes[upload.codeId] = {
|
||||
maxCreated: upload.created,
|
||||
uploads: [upload]
|
||||
uploads: [upload],
|
||||
lastUpload: upload
|
||||
};
|
||||
}
|
||||
else
|
||||
|
@ -142,95 +122,43 @@ export default {
|
|||
},
|
||||
|
||||
|
||||
getFileIconUrl(filename)
|
||||
{
|
||||
var ext = this.getExtension(filename);
|
||||
if (ext == '')
|
||||
ext = '_blank';
|
||||
|
||||
return '/images/fileicons/32px/' + ext + '.png';
|
||||
},
|
||||
|
||||
|
||||
getExtension(filename)
|
||||
{
|
||||
var parts = filename.split('.');
|
||||
return parts.length > 0 ? parts.pop() : '';
|
||||
},
|
||||
|
||||
|
||||
getDownloadUrl(file)
|
||||
{
|
||||
return '/admin/download/' + encodeURIComponent(file.id) + '/' + encodeURIComponent(file.name);
|
||||
},
|
||||
|
||||
|
||||
deleteClick(codeId)
|
||||
onFilesDeleted(files)
|
||||
{
|
||||
var self = this;
|
||||
|
||||
if (self.confirmDelete == codeId)
|
||||
for (let i = self.uploads.length - 1; i >= 0; i--)
|
||||
{
|
||||
self.confirmDelete = null;
|
||||
let upload = self.uploads[i];
|
||||
|
||||
axios.delete('/admin/codeuploads/' + encodeURIComponent(codeId), {
|
||||
headers: {
|
||||
Authorization: 'Bearer ' + shared.adminToken
|
||||
}})
|
||||
.then((response) =>
|
||||
for (let f = upload.files.length - 1; f >= 0; f--)
|
||||
{
|
||||
if (findIndex(files, { uploadId: upload.id, fileId: upload.files[f].id }) > -1)
|
||||
{
|
||||
})
|
||||
.catch((error) => { shared.$emit('apiError', error, this.$router) });
|
||||
upload.files.splice(f, 1);
|
||||
}
|
||||
}
|
||||
|
||||
if (upload.files.length == 0)
|
||||
self.uploads.splice(i, 1);
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
onCodeAssigned(codeId, userId)
|
||||
{
|
||||
var self = this;
|
||||
if (self.hasAuth('viewAllUploads'))
|
||||
{
|
||||
let username = null;
|
||||
forEach(self.assignUsers, (user) =>
|
||||
{
|
||||
if (user.id == userId)
|
||||
username = user.name;
|
||||
})
|
||||
|
||||
self.updateCodeUser(codeId, userId, username);
|
||||
}
|
||||
else
|
||||
{
|
||||
self.confirmDelete = codeId;
|
||||
}
|
||||
},
|
||||
|
||||
cancelDelete()
|
||||
{
|
||||
var self = this;
|
||||
self.confirmDelete = null;
|
||||
},
|
||||
|
||||
|
||||
assignCode(codeId)
|
||||
{
|
||||
var self = this;
|
||||
|
||||
let userId = self.assignUser;
|
||||
if (!userId)
|
||||
return;
|
||||
|
||||
self.assignUser = null;
|
||||
self.showAssign = false;
|
||||
|
||||
axios.post('/admin/assign/code', {
|
||||
id: codeId,
|
||||
userId: userId
|
||||
}, {
|
||||
headers: {
|
||||
Authorization: 'Bearer ' + shared.adminToken
|
||||
}
|
||||
})
|
||||
.then((response) =>
|
||||
{
|
||||
if (self.hasAuth('viewAllUploads'))
|
||||
{
|
||||
let username = null;
|
||||
forEach(self.assignUsers, (user) =>
|
||||
{
|
||||
if (user.id == userId)
|
||||
username = user.name;
|
||||
})
|
||||
|
||||
self.updateCodeUser(codeId, userId, username);
|
||||
}
|
||||
else
|
||||
self.removeCodeUploads(codeId);
|
||||
})
|
||||
.catch((error) => { shared.$emit('apiError', error, this.$router) })
|
||||
self.removeCodeUploads(codeId);
|
||||
},
|
||||
|
||||
|
||||
|
@ -264,53 +192,9 @@ export default {
|
|||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.file
|
||||
.right
|
||||
{
|
||||
display: inline-block;
|
||||
width: 10rem;
|
||||
padding: .5rem;
|
||||
margin-top: .5rem;
|
||||
border: solid 1px transparent;
|
||||
|
||||
font-size: 75%;
|
||||
|
||||
text-align: center;
|
||||
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
|
||||
a
|
||||
{
|
||||
color: black;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.icon
|
||||
{
|
||||
display: block;
|
||||
margin-bottom: .5rem;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
}
|
||||
|
||||
|
||||
.filename, .size
|
||||
{
|
||||
display: block;
|
||||
}
|
||||
|
||||
.size
|
||||
{
|
||||
color: #808080;
|
||||
}
|
||||
|
||||
|
||||
&:hover
|
||||
{
|
||||
background-color: #d3e9f8;
|
||||
border: solid 1px #a7d3f1;
|
||||
}
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
|
||||
|
@ -319,16 +203,4 @@ export default {
|
|||
background-color: #f4f4f4;
|
||||
padding: .2rem;
|
||||
}
|
||||
|
||||
|
||||
.codedescription
|
||||
{
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
|
||||
.assign
|
||||
{
|
||||
margin-top: .5rem;
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,389 @@
|
|||
<template>
|
||||
<div class="uploadFiles">
|
||||
<div class="pure-menu pure-menu-horizontal toolbar">
|
||||
<ul class="pure-menu-list">
|
||||
<li class="pure-menu-item"><a href="#" class="pure-menu-link" @click.prevent="startAssign()"><fa icon="user"></fa> {{ $t('admin.uploads.assign') }}</a></li>
|
||||
<li class="pure-menu-item"><a href="#" class="pure-menu-link" @click.prevent="startDelete()"><fa icon="trash-alt"></fa> {{ $t('admin.delete') }}</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div v-if="showAssign" class="confirmToolbar pure-form pure-form-horizontal">
|
||||
<select v-model="assignUser">
|
||||
<option v-for="user in assignUsers" v-if="user.id !== group.lastUpload.userId" :value="user.id">{{ user.name }}</option>
|
||||
</select>
|
||||
|
||||
<button class="pure-button pure-button-primary" @click.prevent="confirmAssign()" :disabled="!hasAssignUser"><fa icon="spinner" spin v-if="assigning"></fa> {{ $t('admin.uploads.assignApply') }}</button>
|
||||
<button class="pure-button" @click.prevent="cancelAssign()">{{ $t('admin.cancel') }}</button>
|
||||
</div>
|
||||
|
||||
<div v-if="showDelete" class="confirmToolbar pure-form pure-form-horizontal">
|
||||
<button class="pure-button pure-button-confirm-delete" @click.prevent="confirmDelete()" :disabled="!isAnySelected"><fa icon="spinner" spin v-if="deleting"></fa> {{ $t('admin.deleteApply') }}</button>
|
||||
<button class="pure-button" @click.prevent="cancelDelete()">{{ $t('admin.cancel') }}</button>
|
||||
|
||||
<button class="pure-button separator" @click.prevent="selectAll()"><fa :icon="['far', 'check-square']"></fa></button>
|
||||
<button class="pure-button" @click.prevent="selectNone()"><fa :icon="['far', 'square']"></fa></button>
|
||||
</div>
|
||||
|
||||
<template v-for="upload in group.uploads">
|
||||
<template v-for="file in orderedFiles(upload.files)">
|
||||
<div class="pure-g file" @click.prevent="fileClick(upload, file)" :title="file.name">
|
||||
<div class="pure-u-1-2 filename">
|
||||
<input type="checkbox" v-if="showDelete" v-bind:checked="getSelected(upload, file)" @input="(v) => setSelected(upload, file, v)" />
|
||||
<a :href="getDownloadUrl(file)"><img :src="getFileIconUrl(file.name)" class="icon"> {{ file.name }}</a>
|
||||
</div>
|
||||
<div class="pure-u-1-4 date">
|
||||
{{ upload.created | formatDateTime }}
|
||||
</div>
|
||||
<div class="pure-u-1-4 size">
|
||||
{{ file.size | formatSizeSI }}
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import axios from 'axios';
|
||||
import shared from '../../../shared';
|
||||
import orderBy from 'lodash/orderBy';
|
||||
import forEach from 'lodash/forEach';
|
||||
|
||||
export default {
|
||||
props: ['group', 'assignUsers'],
|
||||
|
||||
data()
|
||||
{
|
||||
return {
|
||||
showDelete: false,
|
||||
deleting: false,
|
||||
showAssign: false,
|
||||
assigning: false,
|
||||
assignUser: null,
|
||||
selection: {}
|
||||
}
|
||||
},
|
||||
|
||||
computed: {
|
||||
hasAssignUser()
|
||||
{
|
||||
var self = this;
|
||||
return self.assignUser !== null;
|
||||
},
|
||||
|
||||
|
||||
isAnySelected()
|
||||
{
|
||||
var self = this;
|
||||
|
||||
for (var fileId in self.selection)
|
||||
{
|
||||
if (self.selection.hasOwnProperty(fileId) && self.selection[fileId])
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
orderedFiles(files)
|
||||
{
|
||||
return orderBy(files, ['name'], ['asc']);
|
||||
},
|
||||
|
||||
|
||||
getFileIconUrl(filename)
|
||||
{
|
||||
var ext = this.getExtension(filename);
|
||||
if (ext == '')
|
||||
ext = '_blank';
|
||||
|
||||
return '/images/fileicons/16px/' + ext + '.png';
|
||||
},
|
||||
|
||||
|
||||
getExtension(filename)
|
||||
{
|
||||
var parts = filename.split('.');
|
||||
return parts.length > 0 ? parts.pop() : '';
|
||||
},
|
||||
|
||||
|
||||
getDownloadUrl(file)
|
||||
{
|
||||
return '/admin/download/' + encodeURIComponent(file.id) + '/' + encodeURIComponent(file.name);
|
||||
},
|
||||
|
||||
|
||||
getSelected(upload, file)
|
||||
{
|
||||
var self = this;
|
||||
|
||||
let uniqueId = upload.id + ';' + file.id;
|
||||
|
||||
if (typeof self.selection[uniqueId] === 'undefined')
|
||||
self.$set(self.selection, uniqueId, true);
|
||||
|
||||
return self.selection[uniqueId];
|
||||
},
|
||||
|
||||
|
||||
setSelected(upload, file, value)
|
||||
{
|
||||
var self = this;
|
||||
|
||||
let uniqueId = upload.id + ';' + file.id;
|
||||
|
||||
if (typeof self.selection[uniqueId] === 'undefined')
|
||||
self.$set(self.selection, uniqueId, value);
|
||||
else
|
||||
self.selection[uniqueId] = value;
|
||||
},
|
||||
|
||||
|
||||
fileClick(upload, file)
|
||||
{
|
||||
var self = this;
|
||||
|
||||
if (self.showDelete)
|
||||
self.setSelected(upload, file, !self.getSelected(upload, file));
|
||||
else
|
||||
location.href = self.getDownloadUrl(file);
|
||||
},
|
||||
|
||||
|
||||
startDelete()
|
||||
{
|
||||
var self = this;
|
||||
|
||||
if (self.showDelete)
|
||||
self.cancelDelete();
|
||||
else
|
||||
{
|
||||
self.showDelete = !self.showDelete;
|
||||
self.showAssign = false;
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
confirmDelete()
|
||||
{
|
||||
var self = this;
|
||||
|
||||
if (self.deleting)
|
||||
return;
|
||||
|
||||
let deleteFiles = [];
|
||||
|
||||
forEach(self.group.uploads, (upload) =>
|
||||
{
|
||||
forEach(upload.files, (file) =>
|
||||
{
|
||||
if (self.getSelected(upload, file))
|
||||
{
|
||||
deleteFiles.push({
|
||||
uploadId: upload.id,
|
||||
fileId: file.id
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
self.deleting = true;
|
||||
|
||||
axios.delete('/admin/fileuploads/', {
|
||||
data: deleteFiles,
|
||||
headers: {
|
||||
Authorization: 'Bearer ' + shared.adminToken
|
||||
}})
|
||||
.then((response) =>
|
||||
{
|
||||
self.$emit('files-deleted', deleteFiles);
|
||||
})
|
||||
.catch((error) => { shared.$emit('apiError', error, this.$router) })
|
||||
.then(() =>
|
||||
{
|
||||
self.deleting = false;
|
||||
self.showDelete = false;
|
||||
self.selection = {};
|
||||
});
|
||||
},
|
||||
|
||||
|
||||
setAllSelected(value)
|
||||
{
|
||||
var self = this;
|
||||
|
||||
for (var fileId in self.selection)
|
||||
{
|
||||
if (self.selection.hasOwnProperty(fileId))
|
||||
self.selection[fileId] = value;
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
selectAll()
|
||||
{
|
||||
var self = this;
|
||||
self.setAllSelected(true);
|
||||
},
|
||||
|
||||
|
||||
selectNone()
|
||||
{
|
||||
var self = this;
|
||||
self.setAllSelected(false);
|
||||
},
|
||||
|
||||
|
||||
cancelDelete()
|
||||
{
|
||||
var self = this;
|
||||
|
||||
self.showDelete = false
|
||||
self.selection = {};
|
||||
},
|
||||
|
||||
|
||||
startAssign()
|
||||
{
|
||||
var self = this;
|
||||
|
||||
if (self.showAssign)
|
||||
self.cancelAssign();
|
||||
else
|
||||
{
|
||||
self.showAssign = !self.showAssign;
|
||||
self.showDelete = false;
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
confirmAssign()
|
||||
{
|
||||
var self = this;
|
||||
|
||||
if (self.assigning)
|
||||
return;
|
||||
|
||||
let userId = self.assignUser;
|
||||
if (!userId)
|
||||
return;
|
||||
|
||||
let codeId = self.group.lastUpload.codeId;
|
||||
|
||||
self.assignUser = null;
|
||||
self.assigning = false;
|
||||
|
||||
axios.post('/admin/assign/code', {
|
||||
id: codeId,
|
||||
userId: userId
|
||||
}, {
|
||||
headers: {
|
||||
Authorization: 'Bearer ' + shared.adminToken
|
||||
}
|
||||
})
|
||||
.then((response) =>
|
||||
{
|
||||
self.$emit('code-assigned', codeId, userId);
|
||||
})
|
||||
.catch((error) => { shared.$emit('apiError', error, this.$router) })
|
||||
.then(() =>
|
||||
{
|
||||
self.assigning = false;
|
||||
self.showAssign = false;
|
||||
})
|
||||
},
|
||||
|
||||
|
||||
cancelAssign()
|
||||
{
|
||||
var self = this;
|
||||
|
||||
self.showAssign = false;
|
||||
self.assignUser = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.file
|
||||
{
|
||||
border: solid 1px transparent;
|
||||
font-size: 75%;
|
||||
padding: .5rem;
|
||||
vertical-align: middle;
|
||||
cursor: pointer;
|
||||
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
|
||||
a
|
||||
{
|
||||
color: black;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.icon
|
||||
{
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
|
||||
.date
|
||||
{
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
|
||||
.size
|
||||
{
|
||||
color: #808080;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
|
||||
&:hover
|
||||
{
|
||||
background-color: #d3e9f8;
|
||||
border: solid 1px #a7d3f1;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.upload
|
||||
{
|
||||
background-color: #fafafa;
|
||||
border-bottom: solid 1px #f4f4f4;
|
||||
padding: .2rem;
|
||||
}
|
||||
|
||||
|
||||
.codedescription
|
||||
{
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
|
||||
.toolbar
|
||||
{
|
||||
border-bottom: solid 2px #f0f0f0;
|
||||
}
|
||||
|
||||
|
||||
.confirmToolbar
|
||||
{
|
||||
margin-top: .5rem;
|
||||
margin-bottom: .5rem;
|
||||
font-size: 75%;
|
||||
}
|
||||
|
||||
|
||||
.separator
|
||||
{
|
||||
margin-left: 2rem;
|
||||
}
|
||||
</style>
|
Loading…
Reference in New Issue