To receive notifications about scheduled maintenance, please subscribe to the mailing-list gitlab-operations@sympa.ethz.ch. You can subscribe to the mailing-list at https://sympa.ethz.ch

Verified Commit a2c034b3 authored by Sandro Lutz's avatar Sandro Lutz
Browse files

Add edit/delete for studydocuments

parent fe1298dd
......@@ -9,10 +9,10 @@ import { i18n } from '../models/language';
import './SelectTextField.less';
export default class SelectTextField {
constructor({ attrs: { options, onChange = () => {} } }) {
constructor({ attrs: { options, value = null, onChange = () => {} } }) {
this.addNew = false;
this.value = '';
this.selected = null;
this.selected = value;
this.options = options;
this.filteredOptions = options;
this.onChange = onChange;
......
......@@ -23,4 +23,4 @@
overflow-y: auto;
}
}
}
\ No newline at end of file
}
......@@ -44,7 +44,12 @@ Raven.context(() => {
{
url: '/:language/studydocuments/new',
reason: 'studydocs.accessDenied',
viewAsync: './views/studydocs/studydocNew',
viewAsync: './views/studydocs/studydocForm',
},
{
url: '/:language/studydocuments/:documentId/edit',
reason: 'studydocs.accessDenied',
viewAsync: './views/studydocs/studydocForm',
},
{
url: '/:language/studydocuments/:documentId',
......@@ -194,8 +199,16 @@ Raven.context(() => {
};
m.route.setOrig = m.route.set;
m.route.set = (path, data, options) => {
// Allow change of route without scrolling to the top of the page.
m.route.setNoScroll = (path, data, options) => {
m.route.previousPath = m.route.get();
m.route.setOrig(path, data, options);
};
// Scroll to the top of the page when changing route (default behavior).
m.route.set = (path, data, options) => {
m.route.setNoScroll(path, data, options);
// Delay scroll to top due to rendering latency.
setTimeout(() => window.scrollTo(0, 0), 10);
};
......
......@@ -254,8 +254,12 @@ export default {
noSemester: 'Kein Semester',
courseYear: 'Kursjahr',
files: 'Dateien',
noFiles: 'Keine Dateien ausgewählt.',
upload: 'Dokument(e) hochladen',
uploadTitle: 'Ein neues Dokument hochladen',
uploadTitle: {
new: 'Ein neues Dokument hochladen',
edit: 'Dokument bearbeiten',
},
uploadFileHint: 'Du kannst mehrere Dateien auswählen!',
uploadLoadingError: 'Das Upload-Formular konnte nicht geladen werden.',
uploadError: 'Während dem Hochladen ist ein Fehler aufgetreten.',
......
......@@ -253,8 +253,12 @@ export default {
noSemester: 'No semester',
courseYear: 'Course Year',
files: 'Files',
noFiles: 'No files selected.',
upload: 'Upload study document(s)',
uploadTitle: 'Upload a new study document',
uploadTitle: {
new: 'Upload a new study document',
edit: 'Edit a study document',
},
uploadFileHint: 'You can select multiple files!',
uploadLoadingError: 'Could not load the upload form.',
uploadError: 'There was an error while uploading the documents.',
......
......@@ -102,7 +102,7 @@ export default class StudydocsController extends PaginationController {
* @return {Promise}
* @static
*/
static addNew(doc) {
static post(doc) {
if (typeof doc !== 'object') {
return new Promise(() => {}); // empty promise
}
......@@ -112,7 +112,7 @@ export default class StudydocsController extends PaginationController {
for (let i = 0; i < doc.files.length; i += 1) {
form.append('files', doc.files[i]);
}
} else if (doc[key] !== '') {
} else if (doc[key] && doc[key] !== '') {
form.append(key, doc[key]);
}
});
......@@ -127,6 +127,70 @@ export default class StudydocsController extends PaginationController {
});
}
/**
* Patches an existing studydocument in the AMIV API.
*
* @param {Object} doc studydocument object to be patched containing the new values.
* @param {boolean} updateDocuments Specify whether to upload new documents or not.
* @return {Promise}
* @static
*/
static async patch(doc, updateDocuments) {
if (typeof doc !== 'object') {
return new Promise(() => {}); // empty promise
}
let etag = doc._etag;
// Upload documents
if (updateDocuments && doc.files.length > 0) {
const form = new FormData();
for (let i = 0; i < doc.files.length; i += 1) {
form.append('files', doc.files[i]);
}
const response = await m.request({
method: 'PATCH',
url: `${apiUrl}/studydocuments/${doc._id}`,
data: form,
headers: {
Authorization: getToken(),
'If-Match': etag,
},
});
etag = response._etag;
}
const data = {};
const ignoredFields = ['files', 'uploader'];
Object.keys(doc).forEach(key => {
if (!key.startsWith('_') && !ignoredFields.includes(key)) {
data[key] = doc[key];
}
});
// Update meta information
return m.request({
method: 'PATCH',
url: `${apiUrl}/studydocuments/${doc._id}`,
data,
headers: {
Authorization: getToken(),
'If-Match': etag,
},
});
}
/**
* Deletes an existing studydocument from the AMIV API.
*
* @param {string} id studydocument id
* @param {string} etag etag value for the given studydocument
* @return {Promise}
* @static
*/
static delete(id, etag) {
return m.request({
method: 'DELETE',
......
......@@ -18,7 +18,7 @@
@import './profile.less';
@import './studydocList.less';
@import './studydocQuickFilter.less';
@import './studydocNew.less';
@import './studydocForm.less';
@import './fileUpload.less';
@import './filterView.less';
@import './jobofferList.less';
......
......@@ -60,9 +60,37 @@
}
}
.files {
width: 100%;
margin: 2em 0;
.no-files {
display: block;
width: 100%;
text-align: center;
color: @color-dark-grey;
}
.file {
display: flex;
justify-content: space-between;
align-items: baseline;
span.name {
margin-left: .25em;
}
span.size {
display: inline-block;
color: @color-dark-grey;
margin-left: 1em;
}
}
}
.file-input {
margin: 2em 0;
color: rgba(0, 0, 0, .54);
color: @color-dark-grey;
span {
@media @mobile {
......
......@@ -364,7 +364,7 @@ export class FilteredListPage {
this.changeTimeout = setTimeout(() => {
const basePath = `/${currentLanguage()}/${this.name}`;
const path = this.itemId ? `${basePath}/${id}` : basePath;
m.route.setOrig(path);
m.route.setNoScroll(path);
}, delay);
}
......
import m from 'mithril';
import marked from 'marked';
import filesize from 'filesize';
import { apiUrl } from 'config';
import animateScrollTo from 'animated-scroll-to';
import { Icon } from 'polythene-mithril-icon';
import Spinner from 'amiv-web-ui-components/src/spinner';
......@@ -9,6 +11,7 @@ import Button from '../../components/Button';
import TextField from '../../components/TextField';
import SelectTextField from '../../components/SelectTextField';
import FileInput from '../../components/FileInput';
import mimeTypeToIcon from '../../images/mimeTypeToIcon';
import { i18n, currentLanguage } from '../../models/language';
import { Infobox } from '../errors';
import icons from '../../images/icons';
......@@ -38,15 +41,24 @@ const departments = [
'gess',
];
export default class StudydocNew {
oninit() {
export default class StudydocForm {
async oninit({ attrs: { documentId = null } }) {
this.invalid = new Set([]);
this.doc = { course_year: new Date().getFullYear() };
this.files = [];
this.filesChanged = false;
this.isValid = false;
this.isBusy = false;
this.state = STATE_LOADING;
this.uploadError = false;
if (documentId) {
this.doc = await controller.loadDocument(documentId);
this.files = this.doc.files.map(item => ({ info: item, file: null }));
delete this.doc.files;
}
this._loadAvailableValues();
}
......@@ -73,18 +85,38 @@ export default class StudydocNew {
validate() {
this.isValid =
this.invalid.size === 0 &&
this.doc.files !== undefined &&
this.doc.files.length > 0 &&
this.files.length > 0 &&
this.doc.title !== undefined &&
this.doc.title !== '';
}
static async _prepareFileForUpload({ info = null, file = null }) {
if (file) return Promise.resolve(file);
return m.request({
method: 'GET',
url: `${apiUrl}${info.file}`,
responseType: 'blob',
extract: xhr =>
new File([xhr.response], info.name, { type: xhr.responseType, lastModified: Date.now() }),
});
}
async submit() {
if (this.isValid && !this.isBusy) {
this.isBusy = true;
this.uploadError = false;
try {
const response = await StudydocsController.addNew(this.doc);
if (this.filesChanged) {
this.doc.files = await Promise.all(
this.files.map(item => this.constructor._prepareFileForUpload(item))
);
}
const response = this.doc._id
? await StudydocsController.patch(this.doc, this.filesChanged)
: await StudydocsController.post(this.doc);
this.filesChanged = false;
this.isBusy = false;
m.route.set(`/${currentLanguage()}/studydocuments/${response._id}`);
} catch (Exception) {
......@@ -97,7 +129,13 @@ export default class StudydocNew {
view() {
return m('div.studydocs-upload', [
m('div.title', m('h2', i18n('studydocs.uploadTitle'))),
m(
'div.title',
m(
'h2',
this.doc._id ? i18n('studydocs.uploadTitle.edit') : i18n('studydocs.uploadTitle.new')
)
),
m('div.studydocs-upload-form', [
this._renderForm(),
m('div.separator'),
......@@ -145,6 +183,7 @@ export default class StudydocNew {
m(SelectTextField, {
name: 'author',
label: i18n('studydocs.author'),
value: this.doc.author,
floatingLabel: true,
options: controller.availableFilterValues.author
? Object.keys(controller.availableFilterValues.author).sort()
......@@ -161,6 +200,7 @@ export default class StudydocNew {
}),
m(SelectTextField, {
name: 'lecture',
value: this.doc.lecture,
label: i18n('studydocs.lecture'),
floatingLabel: true,
options: controller.availableFilterValues.lecture
......@@ -178,6 +218,7 @@ export default class StudydocNew {
}),
m(SelectTextField, {
name: 'professor',
value: this.doc.professor,
label: i18n('studydocs.professor'),
floatingLabel: true,
options: controller.availableFilterValues.professor
......@@ -196,6 +237,7 @@ export default class StudydocNew {
m('div.select-row', [
m(Select, {
name: 'type',
value: this.doc.type,
label: i18n('studydocs.type'),
onChange: ({ value }) => {
if (value === '') {
......@@ -215,6 +257,7 @@ export default class StudydocNew {
}),
m(Select, {
name: 'department',
value: this.doc.department,
label: i18n('studydocs.department'),
onChange: ({ value }) => {
if (value === '') {
......@@ -245,6 +288,7 @@ export default class StudydocNew {
}),
m(Select, {
name: 'semester',
value: this.doc.semeter,
label: i18n('studydocs.semester'),
onChange: ({ value }) => {
if (value === '') {
......@@ -263,11 +307,45 @@ export default class StudydocNew {
],
}),
]),
m(
'div.files',
this.files.length > 0
? this.files.map((item, index) => {
const filename = item.info ? item.info.name : item.file.name;
const length = item.info ? item.info.length : item.file.size;
const content_type = item.info ? item.info.content_type : item.file.type;
const file_index = index;
return m('div.file', [
m('div', [
m(Icon, { svg: { content: m.trust(mimeTypeToIcon(content_type)) } }),
m('span.name', filename),
m('span.size', filesize(length)),
]),
m(Button, {
className: 'red-flat-button',
label: i18n('studydocs.actions.delete'),
events: {
onclick: () => {
this.files.splice(file_index, 1);
this.filesChanged = true;
this.validate();
},
},
}),
]);
})
: m('span.no-files', i18n('studydocs.noFiles'))
),
m('div.file-input', [
m(FileInput, {
multiple: 1,
value: [],
onchange: e => {
this.doc.files = e.target.files;
for (let i = 0; i < e.target.files.length; i += 1) {
this.files.push({ info: null, file: e.target.files[i] });
this.filesChanged = true;
}
this.validate();
},
}),
......
......@@ -32,6 +32,16 @@ export default class StudydocList extends FilteredListPage {
oninit(vnode) {
super.oninit(vnode, vnode.attrs.documentId);
if (
vnode.attrs.documentId &&
m.route.previousPath &&
m.route.previousPath.includes('studydocuments') &&
m.route.previousPath.includes('edit')
) {
this.dataStore.shouldScroll = true;
this.reload();
}
}
// eslint-disable-next-line class-methods-use-this
......@@ -295,6 +305,21 @@ export default class StudydocList extends FilteredListPage {
? studydocument.title
: i18n('studydocs.name.default');
if (studydocument._links.self.methods.includes('PATCH')) {
actionButtons.push(
m(Button, {
name: `edit-${studydocument._id}`,
className: 'blue-flat-button',
label: i18n('studydocs.actions.edit'),
events: {
onclick: () => {
m.route.set(`/${currentLanguage()}/studydocuments/${studydocument._id}/edit`);
},
},
})
);
}
if (studydocument._links.self.methods.includes('DELETE')) {
// User is allowed to delete this document
if (this.deleteDocument.id === studydocument._id && !this.deleteDocument.busy) {
......
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment