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