Verified Commit 4cc42a53 authored by Sandro Lutz's avatar Sandro Lutz
Browse files

Improve studydocument upload form

parent d7b23fa2
......@@ -8,14 +8,6 @@ import { Toolbar, ToolbarTitle } from 'polythene-mithril-toolbar';
import debounce from 'amiv-web-ui-components/src/debounce';
import icons from '../images/icons';
const BackButton = {
view: ({ attrs }) =>
m(IconButton, {
icon: { svg: m.trust(icons.back) },
ink: false,
events: { onclick: attrs.leave },
}),
};
const ClearButton = {
view: ({ attrs }) =>
m(IconButton, {
......@@ -55,15 +47,10 @@ class SearchField {
defaultValue: attrs.defaultValue,
},
buttons: {
focus: {
before: m(BackButton, { leave: this.leave }),
},
focus_dirty: {
before: m(BackButton, { leave: this.leave }),
after: m(ClearButton, { clear: this.clear }),
},
dirty: {
before: m(BackButton, { leave: this.leave }),
after: m(ClearButton, { clear: this.clear }),
},
},
......
......@@ -222,12 +222,13 @@ export default class SelectComponent {
return m(ListTile, {
title: label,
hoverable: true,
ink: true,
hoverable: !option.disabled && true,
ink: !option.disabled && true,
highlight: selected,
selected: !this.multiple ? selected : null,
events: {
onclick: () => {
if (option.disabled) return;
const value = isObject ? option.value : option;
if (this.multiple) {
const i = this.value.indexOf(value);
......
......@@ -49,7 +49,7 @@
line-height: 1.19em;
align-items: center;
outline: none;
border-bottom: 1px solid rgba(0, 0, 0, .42);
border-bottom: 1px solid rgba(0, 0, 0, .11);
> div {
width: 100%;
......
......@@ -26,6 +26,8 @@ export default {
reset: 'Zurücksetzen',
externalLink: 'Externer Link',
button: {
clear: 'löschen',
create: 'erstellen',
cancel: 'abbrechen',
confirm: 'bestätigen',
enroll: 'einschreiben',
......@@ -221,7 +223,7 @@ export default {
semester2: '2. Semester',
semester3: '3. Semester',
semester4: '4. Semester',
semester5: '5+ Semester',
'semester5+': '5+ Semester',
lecture: 'Vorlesung',
allLectures: 'Alle Vorlesungen',
department: 'Departement',
......@@ -236,18 +238,23 @@ export default {
courseYear: 'Kursjahr',
files: 'Dateien',
upload: 'Dokument(e) hochladen',
uploadTitle: 'Ein neues Dokument hochladen',
uploadFileHint: 'Du kannst mehrere Dateien auswählen!',
uploadLoadingError: 'Das Upload-Formular konnte nicht geladen werden.',
uploadError: 'Während dem Hochladen ist ein Fehler aufgetreten.',
uploading: 'lädt hoch...',
accessDenied: 'Studienunterlagen sind nur für ETH Studenten verfügbar.',
selectTextHelp: 'Kreuze «erstellen» an, um einen neuen Eintrag zu erstellen.',
rules: {
title: 'Regeln',
one:
'Diese Plattform lebt vom geben und nehmen - investiere also auch mal ein paar Minuten und schaue ob du für andere nützliches Material hast und lade es hoch. Es kostet nicht viel Zeit.',
two:
'Die Unterlagenübersicht ist noch im Beta-Stadium. Fehler und Anregungen bitte an unterlagen@amiv.ethz.ch senden.',
'Einige der hier zu findenden Daten unterliegen der [BOT](https://rechtssammlung.sp.ethz.ch/_layouts/15/start.aspx#/default.aspx) sowie dem Urheberrecht und dienen zur internen Dokumentation gemäss [Bundesgesetz SR 231.1, Art. 19.1c](https://www.admin.ch/opc/de/classified-compilation/19920251/index.html#a19) und dürfen nicht an Nicht-ETH-Angehörige weitergegeben werden - daher ist der Login verpflichtend.',
three:
'Einige der hier zu findenden Daten unterliegen der [BOT](https://rechtssammlung.sp.ethz.ch/_layouts/15/start.aspx#/default.aspx) sowie dem Urheberrecht und dienen zur internen Dokumentation gemäss [Bundesgesetz SR 231.1, Art. 19.1c](https://www.admin.ch/opc/de/classified-compilation/19920251/index.html#a19) und dürfen nicht an Nicht-ETH-Angehörige weitergegeben werden - daher ist der LogIn verpflichtend.',
'Plagiate können schwerwiegende Folgen haben. Gib also keine Werke von anderen als dein eigenes aus und lies dir [diese Seite](https://www.ethz.ch/studierende/de/studium/leistungskontrollen/plagiate.html) durch.',
four:
'Plagiate können schwerwiegende Folgen haben. Gib also keine Werke von anderen als dein eigenes aus und lies dir [diese Seite](https://www.ethz.ch/studierende/de/studium/leistungskontrollen/plagiate.html) sowie [dieses Merkblatt der ETH](http://www.lit.ethz.ch/faq/Italienisch/Lehre/box_feeder/PlagioETH_studenti) durch.',
'Hast du Feedback für uns oder ein Problem entdeckt? Sag uns Bescheid unter [info@amiv.ethz.ch](mailto:info@amiv.ethz.ch).',
},
thanks:
'Wir bedanken uns im Namen aller Studenten bei Allen, die ihre Unterlagen, Zusammenfassungen, und und und hier für andere Studenten zugänglich machen. Danke tausend, Gruss und Kuss.',
......
......@@ -26,6 +26,8 @@ export default {
reset: 'Reset',
externalLink: 'External link',
button: {
clear: 'clear',
create: 'create',
cancel: 'cancel',
confirm: 'confirm',
enroll: 'enroll',
......@@ -221,7 +223,7 @@ export default {
semester2: '2nd Semester',
semester3: '3rd Semester',
semester4: '4th Semester',
semester5: '5+ Semester',
'semester5+': '5+ Semester',
lecture: 'Lecture',
allLectures: 'All lectures',
department: 'Department',
......@@ -236,21 +238,26 @@ export default {
courseYear: 'Course Year',
files: 'Files',
upload: 'Upload study document(s)',
uploadTitle: 'Upload a new study document',
uploadFileHint: 'You can select multiple files!',
uploadLoadingError: 'Could not load the upload form.',
uploadError: 'There was an error while uploading the documents.',
uploading: 'Uploading...',
accessDenied: 'Study documents are available only for ETH students.',
selectTextHelp: 'Tick «create» to create a new entry.',
rules: {
title: 'Rules',
one:
'This platform is based on a give-and-take principle, so please consider investing a few minutes to see whether you could contribute anything yourself. It does not take much time.',
two:
'This overview is still in development. Feedback would be appreciated and can be submitted at unterlagen@amiv.ethz.ch.',
three:
'Some of the listed documents are subject to the [BOT](https://rechtssammlung.sp.ethz.ch/_layouts/15/start.aspx#/default.aspx) as well as copyright and serve only as internal documentation according to [Bundesgesetz SR 231.1, Art. 19.1c](https://www.admin.ch/opc/de/classified-compilation/19920251/index.html#a19) and must not be distributed to non-ETH members. For these reasons Login is mandatory.',
three:
'Plagiarism may have dire consequences. Do not promote work of others as your own! Please read [this page](https://www.ethz.ch/students/en/studies/performance-assessments/plagiarism.html).',
four:
'Plagiarism may have dire consequences. Do not promote work of others as your own and read [this page](https://www.ethz.ch/studierende/de/studium/leistungskontrollen/plagiate.html) as well as [this ETH guide](http://www.lit.ethz.ch/faq/Italienisch/Lehre/box_feeder/PlagioETH_studenti)',
'If you have any feedback or spotted some issues, tell us by mail on [info@amiv.ethz.ch](mailto:info@amiv.ethz.ch)',
},
thanks:
'On behalf of all students we would like to thank those who contribute with their own documents and summaries – You da real MVP <3',
'On behalf of all students we would like to thank those who contribute with their own documents and summaries!',
oralExams: 'Oral Exams',
oralExamsExplanation:
'You can order your exam protocols here and get them for a deposit of CHF 20.- at the AMIV Office in CAB E37. You will get your deposit back for providing a protocol yourself. This shall make sure that our system is always up to date. If you need or want to provide an exam protocol, please send an e-mail to pruefungen@amiv.ethz.ch. A protocol includes at least the following content: course, professor/examinator, key words, what was asked and so on…',
......
......@@ -26,9 +26,6 @@ export default class StudydocsController extends PaginationController {
async setQuery(query) {
if (!super.setQuery(query)) return false;
await this.loadPageData(1);
// const data = await this.loadPageData(1);
// TODO: extract available filter values from response.
// this._availableFilterValues = data.<some-filter-field-values>
return true;
}
......@@ -115,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 {
} else if (doc[key] !== '') {
form.append(key, doc[key]);
}
});
......
#studydoc-new {
.file-style {
height: 10em;
.studydocs-upload {
margin: 1em auto 7em;
}
.studydocs-upload-form {
display: grid;
grid-template-columns: 1fr 4px 1fr;
grid-gap: 1em;
@media @tablet, @mobile {
grid-template-columns: 1fr;
grid-template-rows: auto 4px auto;
}
> * {
margin: 1em 0;
}
.form-container-loading,.form-container-error {
position: relative;
width: 100%;
text-align: center;
}
.spinner {
position: absolute;
top: 50%;
left: 50%;
transform: translateX(-50%) translateY(-50%);
}
.separator {
background: @color-grey;
width: 100%;
height: calc(100% - 2em);
@media @tablet, @mobile {
width: calc(100% - 2em);
height: 100%;
margin: 0 1em;
}
}
.title .pe-textfield__error-placeholder {
display: none;
}
.select-row {
display: grid;
grid-template-columns: 1.5fr .8fr .7fr 1fr;
align-items: flex-end;
grid-gap: 1em;
> * {
padding-bottom: 0;
}
}
.file-input {
margin: 2em 0;
color: rgba(0, 0, 0, .54);
}
}
.studydocs-upload-textfield {
height: 96px;
overflow-y: visible;
.textfield {
display: grid;
grid-template-columns: 1fr auto;
grid-gap: 1em;
align-items: end;
.pe-checkbox-control {
position: relative;
bottom: 34px;
}
}
.suggestions {
position: relative;
top: -24px;
z-index: 10000;
.pe-list {
overflow-y: auto;
}
}
}
import m from 'mithril';
import marked from 'marked';
import animateScrollTo from 'animated-scroll-to';
import { List } from 'polythene-mithril-list';
import { ListTile } from 'polythene-mithril-list-tile';
import { Card } from 'polythene-mithril-card';
import { Icon } from 'polythene-mithril-icon';
import debounce from 'amiv-web-ui-components/src/debounce';
import Spinner from 'amiv-web-ui-components/src/spinner';
import StudydocsController from '../../models/studydocs';
import Select from '../../components/Select';
import Button from '../../components/Button';
import Dropdown from '../../components/Dropdown';
import TextField from '../../components/TextField';
import FileInput from '../../components/FileInput';
import { currentLanguage, i18n } from '../../models/language';
import Checkbox from '../../components/Checkbox';
import { i18n, currentLanguage } from '../../models/language';
import { Infobox } from '../errors';
import icons from '../../images/icons';
export default class studydocNew {
const STATE_LOADING = 0;
const STATE_LOADED = 1;
const STATE_LOADING_ERROR = 2;
const controller = new StudydocsController();
const departments = [
'itet',
'mavt',
'arch',
'baug',
'bsse',
'infk',
'matl',
'biol',
'chab',
'math',
'phys',
'erdw',
'usys',
'hest',
'mtec',
'gess',
];
class SelectTextField {
constructor({ attrs: { options, onChange = () => {} } }) {
this.addNew = false;
this.value = '';
this.selected = null;
this.options = options;
this.filteredOptions = options;
this.onChange = onChange;
this.valid = true;
this.showList = false;
this.debouncedSearch = debounce(search => {
this.value = search;
if (search) {
const regex = RegExp(`.*(${search}).*`, 'gi');
this.filteredOptions = this.options.filter(item => regex.test(item));
} else {
this.filteredOptions = this.options;
}
this.notify();
}, 100);
}
validate() {
this.valid = this.value === '' || this.addNew || this.selected;
}
notify() {
this.validate();
let value = '';
if (this.selected) {
value = this.selected;
} else if (this.addNew) {
// eslint-disable-next-line prefer-destructuring
value = this.value;
}
this.onChange({ value, isValid: this.valid });
m.redraw();
}
onupdate({ dom, attrs: { options } }) {
if (this.options.length !== options.length) {
this.options = options;
this.debouncedSearch(this.value);
}
// Turn of browser's autofill functionality
dom.querySelector('input').setAttribute('autocomplete', 'off');
}
view({ attrs: { name, label, help = i18n('studydocs.selectTextHelp'), ...attrs } }) {
return m('div.studydocs-upload-textfield', [
m('div.textfield', [
m(TextField, {
...attrs,
name,
label,
help,
error: help,
floatingLabel: true,
value: this.selected || this.value,
valid: this.valid,
readonly: this.selected !== null,
onChange: ({ focus, value }) => {
if (focus) {
this.showList = true;
} else if (!focus) {
// don't close the list immidiately, as 'out of focus' could
// also mean that the user is clicking on a list item
setTimeout(() => {
this.showList = false;
m.redraw();
}, 500);
}
if (value !== this.value) {
// if we always update the search value, this would also happen
// immidiately in the moment where we click on the listitem.
// Then, the list get's updated before the click is registered.
// So, we make sure this state change is due to value change and
// not due to focus change.
this.value = value;
this.debouncedSearch(value);
}
},
}),
this.selected
? m(Button, {
className: 'flat-button',
label: i18n('button.clear'),
events: {
onclick: () => {
this.value = '';
this.selected = null;
this.debouncedSearch('');
},
},
})
: m(Checkbox, {
label: i18n('button.create'),
onChange: ({ checked }) => {
this.addNew = checked;
this.notify();
},
}),
]),
this.showList && !this.selected && this.filteredOptions.length > 0
? m(Card, {
className: 'suggestions',
content: m(
'div',
m(List, {
style: { height: '400px', 'background-color': 'white' },
tiles: this.filteredOptions.map(option =>
m(ListTile, {
title: option,
hoverable: true,
compactFront: true,
events: {
onclick: () => {
this.selected = option;
this.showList = false;
},
},
})
),
})
),
})
: '',
]);
}
}
export default class StudydocNew {
oninit() {
// We need to set the default values because they get only added to the request
// 'onchange' and thus do not appear in a request if the user does not change them
this.doc = { semester: null, type: null, department: null };
this.doc = { course_year: new Date().getFullYear() };
this.isValid = false;
this.isBusy = false;
this.state = STATE_LOADING;
this.uploadError = false;
this._loadAvailableValues();
}
static _getInputSuggestions(field, input, callback) {
if (input.length > 2) {
StudydocsController.getInputSuggestions(field, input).then(result => {
const suggestions = new Set();
result._items.forEach(item => {
suggestions.add(item[field]);
});
callback(Array.from(suggestions));
});
_loadAvailableValues() {
if (Object.keys(controller.availableFilterValues).length > 0) {
// Reload values in the background while showing the old values in the form.
this.state = STATE_LOADED;
} else {
callback([]);
this.state = STATE_LOADING;
}
controller
.loadPageData(1)
.then(() => {
this.state = STATE_LOADED;
m.redraw();
})
.catch(() => {
this.state = STATE_LOADING_ERROR;
m.redraw();
});
}
validate() {
this.isValid = this.doc.files && this.doc.files.length > 0 && this.doc.type !== null;
this.isValid =
this.doc.files !== undefined &&
this.doc.files.length > 0 &&
this.doc.title !== undefined &&
this.doc.title !== '';
}
async submit() {
if (this.isValid && !this.isBusy) {
this.isBusy = true;
await StudydocsController.addNew(this.doc);
this.isBusy = false;
m.route.set(`/${currentLanguage()}/studydocuments`);
this.uploadError = false;
try {
const response = await StudydocsController.addNew(this.doc);
this.isBusy = false;
m.route.set(`/${currentLanguage()}/studydocuments/${response._id}`);
} catch (Exception) {
this.isBusy = false;
this.uploadError = true;
animateScrollTo(document.body);
}
}
}
view() {
return m('div#fileUpload-container', [
m(
'div#uploader-info',
m('form.new-style', { onsubmit: () => false }, [
m(TextField, {
name: 'title',
label: i18n('studydocs.title'),
floatingLabel: true,
value: this.doc.title,
events: {
oninput: e => {
this.doc.title = e.target.value;
},
},
}),
m(TextField, {
name: 'author',
label: i18n('studydocs.author'),
floatingLabel: true,
value: this.doc.author,
events: {
oninput: e => {
this.doc.author = e.target.value;
},
},
}),
m(TextField, {
name: 'course_year',
label: i18n('studydocs.courseYear'),
floatingLabel: true,
args: {
placeholder: new Date().getFullYear(),
},
oninput: e => {
this.doc.course_year = e.target.value;
},
}),
m(Dropdown, {
name: i18n('studydocs.type'),
onchange: e => {
const { value } = e.target;
if (value === '') {
this.doc.type = null;
} else {
this.doc.type = value;
}
this.validate();
},
selected: '',
data: [
{ value: '', label: `${i18n('studydocs.type')}*`, disabled: true },
{ value: 'exams', label: i18n('studydocs.types.exams') },
{ value: 'cheat sheets', label: i18n('studydocs.types.cheatsheets') },
{ value: 'lecture documents', label: i18n('studydocs.types.lectureDocuments') },
{ value: 'exercises', label: i18n('studydocs.types.exercises') },
],
}),
m(FileInput, {
multiple: 1,
onchange: e => {
this.doc.files = e.target.files;
this.validate();
},
}),
m(Button, {
name: 'submit',
label: this.isBusy ? i18n('studydocs.uploading') : i18n('studydocs.upload'),
active: this.isValid && !this.isBusy,
events: {
onclick: () => this.submit(),
},
}),
])
),
return m('div.studydocs-upload', [
m('div.title', m('h2', i18n('studydocs.uploadTitle'))),
m('div.studydocs-upload-form', [
this._renderForm(),
m('div.separator'),
this.constructor._renderRules(),
]),
]);
}
m('div#document-info', [
m(TextField, {
name: 'lecture',
label: i18n('studydocs.lecture'),
floatingLabel: true,