Changed file list

- Grid style more suited for large file listings
- Multi-select allows individual files to be deleted
This commit is contained in:
Mark van Renswoude 2018-12-09 17:26:23 +01:00
parent fbf6e6a96f
commit 10322bc26f
10 changed files with 2934 additions and 2234 deletions

View File

@ -1,6 +1,8 @@
const asyncHandler = require('express-async-handler'); const asyncHandler = require('express-async-handler');
const AuthTokens = require('../../authtokens'); const AuthTokens = require('../../authtokens');
const resolvePath = require('resolve-path'); const resolvePath = require('resolve-path');
const groupBy = require('lodash/groupBy');
const map = require('lodash/map');
module.exports = (repository, router) => 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) => router.delete('/codeuploads/:code', asyncHandler(async (req, res) =>
{ {
var uploads = await repository.uploads.listForCode(req.params.code); var uploads = await repository.uploads.listForCode(req.params.code);

View File

@ -1,5 +1,7 @@
const map = require('lodash/map'); const map = require('lodash/map');
const filter = require('lodash/filter'); const filter = require('lodash/filter');
const intersectionWith = require('lodash/intersectionWith');
const pullAllWith = require('lodash/pullAllWith');
const resolvePath = require('resolve-path'); const resolvePath = require('resolve-path');
const fs = require('mz/fs'); const fs = require('mz/fs');
const async = require('async'); const async = require('async');
@ -167,6 +169,8 @@ class UploadRepository
$set: { $set: {
userId: userId userId: userId
} }
}, {
multi: true
}, },
(err, numAffected) => (err, numAffected) =>
{ {
@ -217,23 +221,7 @@ class UploadRepository
return; return;
} }
await Promise.all(upload.files.map(async (file) => await self.deleteOrphanedFiles(upload.files);
{
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);
}
}
}));
resolve(); 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) fileExists(fileId)
{ {
var self = this; var self = this;

4336
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -16,40 +16,41 @@
"author": "Mark van Renswoude <mark@x2software.net>", "author": "Mark van Renswoude <mark@x2software.net>",
"license": "Unlicense", "license": "Unlicense",
"dependencies": { "dependencies": {
"@fortawesome/fontawesome": "^1.1.5", "@fortawesome/fontawesome": "^1.1.8",
"@fortawesome/fontawesome-free-solid": "^5.0.10", "@fortawesome/fontawesome-free-regular": "^5.0.13",
"@fortawesome/fontawesome-free-solid": "^5.0.13",
"@fortawesome/vue-fontawesome": "0.0.22", "@fortawesome/vue-fontawesome": "0.0.22",
"async": "^2.6.0", "async": "^2.6.1",
"async-retry": "^1.2.1", "async-retry": "^1.2.3",
"babel-polyfill": "^6.26.0", "babel-polyfill": "^6.26.0",
"bcrypt": "^1.0.3", "bcrypt": "^1.0.3",
"body-parser": "^1.18.2", "body-parser": "^1.18.3",
"cookie-parser": "^1.4.3", "cookie-parser": "^1.4.3",
"debug": "^3.1.0", "debug": "^3.2.5",
"diskusage": "^0.2.4", "diskusage": "^0.2.4",
"ejs": "^2.5.9", "ejs": "^2.6.1",
"email-templates": "^3.6.0", "email-templates": "^3.6.1",
"es6-promise": "^4.2.4", "es6-promise": "^4.2.5",
"express": "^4.16.3", "express": "^4.16.3",
"express-async-handler": "^1.1.3", "express-async-handler": "^1.1.4",
"js-cookie": "^2.2.0", "js-cookie": "^2.2.0",
"jsonwebtoken": "^8.2.1", "jsonwebtoken": "^8.3.0",
"lodash": "^4.17.10", "lodash": "^4.17.11",
"markdown": "^0.5.0", "markdown": "^0.5.0",
"mkdirp": "^0.5.1", "mkdirp": "^0.5.1",
"moment": "^2.22.1", "moment": "^2.22.2",
"mz": "^2.7.0", "mz": "^2.7.0",
"nanoid": "^1.0.2", "nanoid": "^1.2.3",
"nedb": "^1.8.0", "nedb": "^1.8.0",
"nodemailer": "^4.6.4", "nodemailer": "^4.6.8",
"npm": "^5.8.0", "npm": "^5.10.0",
"pug": "^2.0.3", "pug": "^2.0.3",
"resolve-path": "^1.4.0", "resolve-path": "^1.4.0",
"tus-node-server": "^0.2.11" "tus-node-server": "^0.2.11"
}, },
"devDependencies": { "devDependencies": {
"axios": "^0.18.0", "axios": "^0.18.0",
"babel-loader": "^7.1.4", "babel-loader": "^7.1.5",
"babel-plugin-syntax-dynamic-import": "^6.18.0", "babel-plugin-syntax-dynamic-import": "^6.18.0",
"babel-plugin-transform-es2015-arrow-functions": "^6.22.0", "babel-plugin-transform-es2015-arrow-functions": "^6.22.0",
"babel-plugin-transform-es2015-block-scoping": "^6.26.0", "babel-plugin-transform-es2015-block-scoping": "^6.26.0",
@ -57,22 +58,22 @@
"css-loader": "^0.28.11", "css-loader": "^0.28.11",
"file-loader": "^1.1.11", "file-loader": "^1.1.11",
"html-webpack-plugin": "^3.2.0", "html-webpack-plugin": "^3.2.0",
"node-sass": "^4.9.0", "node-sass": "^4.9.3",
"purecss": "^1.0.0", "purecss": "^1.0.0",
"sass-loader": "^6.0.7", "sass-loader": "^6.0.7",
"style-loader": "^0.20.3", "style-loader": "^0.20.3",
"tus-js-client": "^1.5.1", "tus-js-client": "^1.5.1",
"uppy": "^0.23.3", "uppy": "^0.23.3",
"vue": "^2.5.16", "vue": "^2.5.17",
"vue-i18n": "^7.6.0", "vue-i18n": "^7.8.1",
"vue-loader": "^14.2.2", "vue-loader": "^14.2.3",
"vue-router": "^3.0.1", "vue-router": "^3.0.1",
"vue-style-loader": "^4.1.0", "vue-style-loader": "^4.1.2",
"vue-template-compiler": "^2.5.16", "vue-template-compiler": "^2.5.17",
"webpack": "^4.6.0", "webpack": "^4.19.1",
"webpack-bundle-analyzer": "^2.11.1", "webpack-bundle-analyzer": "^2.13.1",
"webpack-cli": "^2.1.2", "webpack-cli": "^2.1.5",
"webpack-dev-middleware": "^3.1.3", "webpack-dev-middleware": "^3.3.0",
"webpack-hot-middleware": "^2.22.1" "webpack-hot-middleware": "^2.24.0"
} }
} }

View File

@ -193,4 +193,10 @@ a
{ {
color: red !important; color: red !important;
} }
.pure-button-confirm-delete
{
background: rgb(223, 117, 20);
color: white;
}
</style> </style>

View File

@ -17,6 +17,8 @@ import faSpinner from '@fortawesome/fontawesome-free-solid/faSpinner';
import faBan from '@fortawesome/fontawesome-free-solid/faBan'; import faBan from '@fortawesome/fontawesome-free-solid/faBan';
import faTrashAlt from '@fortawesome/fontawesome-free-solid/faTrashAlt'; import faTrashAlt from '@fortawesome/fontawesome-free-solid/faTrashAlt';
import faUser from '@fortawesome/fontawesome-free-solid/faUser'; 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') 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.component('fa', FontAwesomeIcon);
Vue.use(VueI18n); Vue.use(VueI18n);

View File

@ -48,6 +48,7 @@ export default {
cancel: 'Cancel', cancel: 'Cancel',
save: 'Save', save: 'Save',
delete: 'Delete', delete: 'Delete',
deleteApply: 'Delete selection',
diskspace: '{available} disk space available of {total} total', diskspace: '{available} disk space available of {total} total',
@ -59,7 +60,7 @@ export default {
}, },
menu: { menu: {
uploads: 'Uploads', uploads: 'Received',
codes: 'Codes', codes: 'Codes',
users: 'Users' users: 'Users'
}, },
@ -68,6 +69,7 @@ export default {
created: 'Date', created: 'Date',
code: 'Code', code: 'Code',
owner: 'Owner', owner: 'Owner',
expiration: 'Expires:',
userDeleted: 'deleted', userDeleted: 'deleted',
assign: 'Change owner', assign: 'Change owner',

View File

@ -48,6 +48,7 @@ export default {
cancel: 'Annuleren', cancel: 'Annuleren',
save: 'Opslaan', save: 'Opslaan',
delete: 'Verwijderen', delete: 'Verwijderen',
deleteApply: 'Selectie verwijderen',
diskspace: '{available} schijfruimte beschikbaar van {total} totaal', diskspace: '{available} schijfruimte beschikbaar van {total} totaal',
@ -59,7 +60,7 @@ export default {
}, },
menu: { menu: {
uploads: 'Uploads', uploads: 'Ontvangen',
codes: 'Codes', codes: 'Codes',
users: 'Gebruikers' users: 'Gebruikers'
}, },
@ -68,11 +69,12 @@ export default {
created: 'Datum', created: 'Datum',
code: 'Code', code: 'Code',
owner: 'Eigenaar', owner: 'Eigenaar',
expiration: 'Verloopt:',
userDeleted: 'verwijderd', userDeleted: 'verwijderd',
assign: 'Verander eigenaar', assign: 'Verander eigenaar',
assignApply: 'Toepassen' assignApply: 'Toepassen'
}, },
codes: { codes: {
add: 'Genereer code', add: 'Genereer code',

View File

@ -2,45 +2,22 @@
<div id="uploads"> <div id="uploads">
<div v-if="uploads !== null" class="list"> <div v-if="uploads !== null" class="list">
<div v-for="group in groupedUploads"> <div v-for="group in groupedUploads">
<template v-for="(upload, index) in group.uploads"> <div class="properties">
<div class="properties" v-if="index == 0"> <div class="pure-g">
<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-1"><span class="text codedescription">{{ upload.codedescription || upload.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">{{ upload.codeId }}</span></div> <div class="pure-u-1-3">
<div class="pure-u-1-3"> <span class="text" v-if="hasAuth('viewAllUploads')" :class="{ userDeleted: !group.lastUpload.username }">
<span class="text" v-if="hasAuth('viewAllUploads')" :class="{ userDeleted: !upload.username }"> {{ group.lastUpload.username || $t('admin.uploads.userDeleted') }}
{{ upload.username || $t('admin.uploads.userDeleted') }} </span>
</span> </div>
</div> <div class="pure-u-1-3 right">
<div class="pure-u-1-3 right"><span class="text">{{ upload.created | formatDateTime }}</span></div> <span class="text" v-if="group.lastUpload.expirationDate">{{ $t('admin.uploads.expiration') }} {{ group.lastUpload.expirationDate | formatDateTime }}</span>
</div> </div>
</div> </div>
</div>
<div class="pure-menu pure-menu-horizontal" v-if="index == 0"> <UploadFiles :group="group" v-bind:assignUsers="assignUsers" @files-deleted="onFilesDeleted" @code-assigned="onCodeAssigned"></UploadFiles>
<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>
</div> </div>
<div v-if="uploads.length == 0" class="nodata"> <div v-if="uploads.length == 0" class="nodata">
@ -55,6 +32,7 @@
</template> </template>
<script> <script>
import UploadFiles from './components/UploadFiles.vue';
import orderBy from 'lodash/orderBy'; import orderBy from 'lodash/orderBy';
import findIndex from 'lodash/findIndex'; import findIndex from 'lodash/findIndex';
import forEach from 'lodash/forEach'; import forEach from 'lodash/forEach';
@ -62,13 +40,14 @@ import axios from 'axios';
import shared from '../../shared'; import shared from '../../shared';
export default { export default {
components: {
UploadFiles
},
data() data()
{ {
return { return {
uploads: null, uploads: null,
confirmDelete: null,
showAssign: false,
assignUser: null,
assignUsers: [] assignUsers: []
}; };
}, },
@ -122,7 +101,8 @@ export default {
{ {
codes[upload.codeId] = { codes[upload.codeId] = {
maxCreated: upload.created, maxCreated: upload.created,
uploads: [upload] uploads: [upload],
lastUpload: upload
}; };
} }
else else
@ -142,95 +122,43 @@ export default {
}, },
getFileIconUrl(filename) onFilesDeleted(files)
{
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)
{ {
var self = this; var self = this;
for (let i = self.uploads.length - 1; i >= 0; i--)
if (self.confirmDelete == codeId)
{ {
self.confirmDelete = null; let upload = self.uploads[i];
axios.delete('/admin/codeuploads/' + encodeURIComponent(codeId), { for (let f = upload.files.length - 1; f >= 0; f--)
headers: { {
Authorization: 'Bearer ' + shared.adminToken if (findIndex(files, { uploadId: upload.id, fileId: upload.files[f].id }) > -1)
}})
.then((response) =>
{ {
}) upload.files.splice(f, 1);
.catch((error) => { shared.$emit('apiError', error, this.$router) }); }
}
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 else
{ self.removeCodeUploads(codeId);
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) })
}, },
@ -264,53 +192,9 @@ export default {
</script> </script>
<style lang="scss"> <style lang="scss">
.file .right
{ {
display: inline-block; text-align: right;
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;
}
} }
@ -319,16 +203,4 @@ export default {
background-color: #f4f4f4; background-color: #f4f4f4;
padding: .2rem; padding: .2rem;
} }
.codedescription
{
font-weight: bold;
}
.assign
{
margin-top: .5rem;
}
</style> </style>

View File

@ -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>