Skip to content
Snippets Groups Projects

Compare revisions

Changes are shown as if the source revision was being merged into the target revision. Learn more about comparing revisions.

Source

Select target project
No results found

Target

Select target project
  • maspect/amiv-admintool
  • emustafa/amiv-admintool
  • dvruette/amiv-admintool
  • amiv/amiv-admintool
4 results
Show changes
Showing
with 555 additions and 284 deletions
import m from 'mithril';
import {
Switch,
Toolbar,
ToolbarTitle,
Card,
TextField,
Button,
} from 'polythene-mithril';
import { Button } from 'polythene-mithril';
import Stream from 'mithril/stream';
import { styler } from 'polythene-core-css';
import { DropdownCard, DatalistController } from 'amiv-web-ui-components';
import { DropdownCard, Chip } from 'amiv-web-ui-components';
// eslint-disable-next-line import/extensions
import { apiUrl } from 'networkConfig';
import ItemView from '../views/itemView';
import { eventsignups as signupConfig } from '../resourceConfig.json';
import TableView from '../views/tableView';
import RelationlistController from '../relationlistcontroller';
import { ParticipantsController, ParticipantsTable } from './participants';
import { dateFormatter } from '../utils';
import { icons, Property, chip } from '../views/elements';
import { ResourceHandler } from '../auth';
import { Property, FilterChip, icons } from '../views/elements';
import { colors } from '../style';
const viewLayout = [
......@@ -70,95 +60,104 @@ class DuoLangProperty {
}
}
// Helper class to either display the signed up participants or those on the
// waiting list.
class ParticipantsTable {
constructor({ attrs: { where } }) {
this.ctrl = new RelationlistController('eventsignups', 'users', { where }, ['email']);
class ParticipantsSummary {
constructor() {
this.onlyAccepted = true;
}
getItemData(data) {
// TODO list should not have hardcoded size outside of stylesheet
return [
m('div', { style: { width: '9em' } }, dateFormatter(data._created)),
m(
'div',
{ style: { width: '18em' } },
data.user ? `${data.user.firstname} ${data.user.lastname}` : '',
),
m('div', { style: { width: '9em' } }, data.email),
];
}
view({ attrs: { participants, additionalFields = "{'properties': {}}" } }) {
// Parse the JSON from additional fields into an object
const parsedParticipants = participants.map(signup => ({
...signup,
additional_fields: signup.additional_fields
? JSON.parse(signup.additional_fields) : {},
}));
// Filter if only accepted participants should be shown
const filteredParticipants = parsedParticipants.filter(
participant => (this.onlyAccepted ? participant.accepted : true),
);
view({ attrs: { title } }) {
return m(Card, {
style: { height: '400px', 'margin-bottom': '10px' },
content: m('div', [
m(Toolbar, { compact: true }, [
m(ToolbarTitle, { text: title }),
]),
m(TableView, {
tableHeight: '275px',
controller: this.ctrl,
keys: signupConfig.tableKeys,
tileContent: this.getItemData,
titles: [
{ text: 'Date of Signup', width: '9em' },
{ text: 'Name', width: '18em' },
{ text: 'Email', width: '9em' },
],
}),
]),
});
}
}
// check which additional fields should get summarized
let hasSBB = false;
let hasFood = false;
if (additionalFields) {
hasSBB = 'sbb_abo' in JSON.parse(additionalFields).properties;
hasFood = 'food' in JSON.parse(additionalFields).properties;
}
class EmailList {
view({ attrs: { list } }) {
const emails = list.toString().replace(/,/g, '; ');
return m(Card, {
content: m(TextField, { value: emails, label: '', multiLine: true }, ''),
});
return m('div', [
m('div', {
style: {
height: '50px',
'overflow-x': 'auto',
'overflow-y': 'hidden',
'white-space': 'nowrap',
padding: '0px 5px',
},
}, [].concat(['Filters: '], ...[
m(FilterChip, {
selected: this.onlyAccepted,
onclick: () => { this.onlyAccepted = !this.onlyAccepted; },
}, 'accepted users'),
])),
hasSBB ? m('div', { style: { display: 'flex' } }, [
m(Property, { title: 'No SBB', leftAlign: false }, filteredParticipants.filter(
signup => signup.additional_fields.sbb_abo === 'None',
).length),
m(Property, { title: 'GA', leftAlign: false }, filteredParticipants.filter(
signup => signup.additional_fields.sbb_abo === 'GA',
).length),
m(Property, { title: 'Halbtax', leftAlign: false }, filteredParticipants.filter(
signup => signup.additional_fields.sbb_abo === 'Halbtax',
).length),
m(Property, { title: 'Gleis 7', leftAlign: false }, filteredParticipants.filter(
signup => signup.additional_fields.sbb_abo === 'Gleis 7',
).length),
]) : '',
hasFood ? m('div', { style: { display: 'flex' } }, [
m(Property, { title: 'Omnivors', leftAlign: false }, filteredParticipants.filter(
signup => signup.additional_fields.food === 'Omnivor',
).length),
m(Property, { title: 'Vegis', leftAlign: false }, filteredParticipants.filter(
signup => signup.additional_fields.food === 'Vegi',
).length),
m(Property, { title: 'Vegans', leftAlign: false }, filteredParticipants.filter(
signup => signup.additional_fields.food === 'Vegan',
).length),
]) : '',
m('textarea', {
style: { opacity: '0', width: '0px' },
id: 'participantsemails',
}, filteredParticipants.map(signup => signup.email).toString().replace(/,/g, '; ')),
m(Button, {
label: 'Copy Emails',
events: {
onclick: () => {
document.getElementById('participantsemails').select();
document.execCommand('copy');
},
},
}),
]);
}
}
export default class viewEvent extends ItemView {
constructor(vnode) {
super(vnode);
this.signupHandler = new ResourceHandler('eventsignups');
this.signupCtrl = new DatalistController((query, search) => this.signupHandler.get({
search, ...query,
}));
this.participantsCtrl = new ParticipantsController();
this.description = false;
this.advertisement = false;
this.registration = false;
this.emailAdresses = false;
this.emaillist = [''];
this.showAllEmails = false;
this.modalDisplay = Stream('none');
}
oninit() {
this.setUpEmailList(this.showAllEmails);
}
setUpEmailList(showAll) {
// setup where query
const where = { event: this.data._id };
if (!showAll) {
// only show accepted
where.accepted = true;
}
this.signupCtrl.getFullList().then((list) => {
this.emaillist = (list.map(item => item.email));
m.redraw();
});
this.participantsCtrl.setEventId(this.data._id);
}
cloneEvent() {
const event = Object.assign({}, this.data);
console.log(event);
const eventInfoToDelete = [
'_id',
......@@ -167,17 +166,17 @@ export default class viewEvent extends ItemView {
'_links',
'_updated',
'signup_count',
'unaccepted_count',
'__proto__',
];
const now = new Date();
console.log(`${now.toISOString().slice(0, -5)}Z`);
if (event.time_end < `${now.toISOString().slice(0, -5)}Z`) {
eventInfoToDelete.push(...[
'time_advertising_end',
'time_advertising_start',
'time_end',
'time_register_end',
'time_register_end',
'time_deregister_end',
'time_register_start',
'time_start']);
}
......@@ -185,7 +184,6 @@ export default class viewEvent extends ItemView {
delete event[key];
});
console.log(event);
this.controller.changeModus('new');
this.controller.data = event;
}
......@@ -215,10 +213,16 @@ export default class viewEvent extends ItemView {
]),
// below the title, most important details are listed
m('div.maincontainer', { style: { display: 'flex' } }, [
('signup_count' in this.data && this.data.signup_count !== null) && m(Property, {
this.data.type && m(Property, {
style: stdMargin,
title: 'Signups',
}, `${this.data.signup_count} / ${displaySpots}`),
title: 'Type',
}, `${this.data.type.charAt(0).toUpperCase() + this.data.type.slice(1)}`),
(this.data.spots !== null && 'signup_count' in this.data
&& this.data.signup_count !== null)
? m(Property, {
style: stdMargin,
title: 'Signups',
}, `${this.data.signup_count} / ${displaySpots}`) : '',
this.data.location && m(Property, {
style: stdMargin,
title: 'Location',
......@@ -227,6 +231,11 @@ export default class viewEvent extends ItemView {
title: 'Time',
style: stdMargin,
}, `${dateFormatter(this.data.time_start)} - ${dateFormatter(this.data.time_end)}`),
this.data.moderator && m(Property, {
title: 'Moderator',
style: stdMargin,
}, m.trust(`${this.data.moderator.firstname} ${this.data.moderator.lastname}
(<a href='mailto:${this.data.moderator.email}'>${this.data.moderator.email}</a>)`)),
]),
// everything else is not listed in DropdownCards, which open only on request
m('div.viewcontainer', [
......@@ -246,16 +255,16 @@ export default class viewEvent extends ItemView {
m(DropdownCard, { title: 'advertisement', style: { margin: '10px 0' } }, [
[
m(chip, {
svg: this.data.show_annonce ? icons.checked : icons.clear,
m(Chip, {
svg: this.data.show_announce ? icons.checked : icons.clear,
border: '1px #aaaaaa solid',
}, 'announce'),
m(chip, {
m(Chip, {
svg: this.data.show_infoscreen ? icons.checked : icons.clear,
border: '1px #aaaaaa solid',
margin: '4px',
}, 'infoscreen'),
m(chip, {
m(Chip, {
svg: this.data.show_website ? icons.checked : icons.clear,
border: '1px #aaaaaa solid',
}, 'website'),
......@@ -263,8 +272,8 @@ export default class viewEvent extends ItemView {
this.data.time_advertising_start ? m(
Property,
{ title: 'Advertising Time' },
`${dateFormatter(this.data.time_advertising_start)} - ` +
`${dateFormatter(this.data.time_advertising_end)}`,
`${dateFormatter(this.data.time_advertising_start)} - `
+ `${dateFormatter(this.data.time_advertising_end)}`,
) : '',
this.data.priority ? m(
Property,
......@@ -278,8 +287,13 @@ export default class viewEvent extends ItemView {
this.data.time_register_start ? m(
Property,
{ title: 'Registration Time' },
`${dateFormatter(this.data.time_register_start)} - ` +
`${dateFormatter(this.data.time_register_end)}`,
`${dateFormatter(this.data.time_register_start)} - `
+ `${dateFormatter(this.data.time_register_end)}`,
) : '',
this.data.time_deregister_end ? m(
Property,
{ title: 'Deregistration Time' },
`${dateFormatter(this.data.time_deregister_end)}`,
) : '',
this.data.selection_strategy ? m(
Property,
......@@ -292,20 +306,22 @@ export default class viewEvent extends ItemView {
{ title: 'Registration Form' },
this.data.additional_fields,
),
this.data.external_registration && m(
Property,
{ title: 'External Registration' },
m('a', { href: this.data.external_registration, target: '_blank' },
this.data.external_registration),
),
]),
// a list of email adresses of all participants, easy to copy-paste
m(DropdownCard, { title: 'Email Adresses', style: { margin: '10px 0' } }, [
m(Switch, {
defaultChecked: false,
label: 'show unaccepted',
onChange: () => {
this.showAllEmails = !this.showAllEmails;
this.setUpEmailList(this.showAllEmails);
},
}),
m(EmailList, { list: this.emaillist }),
]),
this.data.spots !== null ? m(DropdownCard, {
title: 'Participants Summary',
style: { margin: '10px 0' },
}, m(ParticipantsSummary, {
participants: this.participantsCtrl.allParticipants,
additionalFields: this.data.additional_fields,
})) : '',
m(DropdownCard, { title: 'Images' }, [
m('div', {
......@@ -346,29 +362,26 @@ export default class viewEvent extends ItemView {
},
}),
]),
m('div', [
this.data.img_banner && m('div', 'Banner'),
this.data.img_banner && m('img', {
src: `${apiUrl}${this.data.img_banner.file}`,
width: '100%',
onclick: () => {
this.modalDisplay('block');
modalImg.src = `${apiUrl}${this.data.img_banner.file}`;
},
}),
]),
]),
]),
]),
]),
m('div.viewcontainercolumn', [
m('div.viewcontainercolumn', { style: { width: '50em' } }, [
this.data.time_register_start ? m(ParticipantsTable, {
where: { accepted: true, event: this.data._id },
title: 'Accepted Participants',
filePrefix: 'accepted',
event: this.data,
waitingList: false,
additional_fields_schema: this.data.additional_fields,
participantsCtrl: this.participantsCtrl,
}) : '',
this.data.time_register_start ? m(ParticipantsTable, {
where: { accepted: false, event: this.data._id },
title: 'Participants on Waiting List',
filePrefix: 'waitinglist',
event: this.data,
waitingList: true,
additional_fields_schema: this.data.additional_fields,
participantsCtrl: this.participantsCtrl,
}) : '',
]),
]),
......
import m from 'mithril';
import { TextField } from 'polythene-mithril';
import { ListSelect, DatalistController } from 'amiv-web-ui-components';
import { ListSelect, DatalistController, Select } from 'amiv-web-ui-components';
// eslint-disable-next-line import/extensions
import { apiUrl } from 'networkConfig';
import { ResourceHandler } from '../auth';
import { MDCSelect } from '../views/selectOption';
import EditView from '../views/editView';
......@@ -48,27 +47,22 @@ class PermissionEditor {
}, m('div', {
style: { display: 'flex', width: '100%', 'flex-flow': 'row wrap' },
}, this.apiEndpoints.map(apiEndpoint => m('div', {
style: { display: 'flex', width: '330px', 'padding-right': '20px' },
style: { display: 'flex', width: '220px', 'padding-right': '20px' },
}, [
m(TextField, {
m(Select, {
label: apiEndpoint.title,
disabled: true,
style: { width: '60%' },
}),
m('div', { style: { width: '40%' } }, m(MDCSelect, {
name: apiEndpoint.href,
options: ['no permission', 'read', 'readwrite'],
onchange: (newVal) => {
if (newVal === 'no permission') {
// the api equivalent to no permission if to delete the key out of the dict
default: 'no permission',
style: { width: '200px' },
onChange({ value }) {
if (value === 'no permission') {
// the api equivalent to no permission is to delete the key out of the dict
if (internalPerm[apiEndpoint.href]) delete internalPerm[apiEndpoint.href];
} else {
internalPerm[apiEndpoint.href] = newVal;
}
} else internalPerm[apiEndpoint.href] = value;
onChange(internalPerm);
},
value: internalPerm[apiEndpoint.href],
})),
}),
])))),
]);
}
......@@ -79,30 +73,21 @@ export default class NewGroup extends EditView {
constructor(vnode) {
super(vnode);
this.userHandler = new ResourceHandler('users', ['firstname', 'lastname', 'email', 'nethz']);
this.userController = new DatalistController((query, search) =>
this.userHandler.get({ search, ...query }));
this.userController = new DatalistController((query, search) => this.userHandler.get(
{ search, ...query },
));
}
beforeSubmit() {
// exchange moderator object with string of id
const { moderator } = this.form.data;
if (moderator) { this.form.data.moderator = `${moderator._id}`; }
this.submit();
const data = Object.assign({}, this.form.data);
// exchange moderator object with string of id, will return null if moderator is null
data.moderator = data.moderator ? data.moderator._id : null;
this.submit(data).then(() => this.controller.changeModus('view'));
}
view() {
return this.layout([
...this.form.renderPage({
name: { type: 'text', label: 'Group Name' },
allow_self_enrollment: {
type: 'checkbox',
label: 'the group can be seen by all users and they can subscribe themselves',
},
requires_storage: {
type: 'checkbox',
label: "the group shares a folder with it's members in the AMIV Cloud",
},
}),
...this.form.renderSchema(['name', 'allow_self_enrollment', 'requires_storage']),
m('div', { style: { display: 'flex' } }, [
m(TextField, { label: 'Group Moderator: ', disabled: true, style: { width: '160px' } }),
m('div', { style: { 'flex-grow': 1 } }, m(ListSelect, {
......@@ -110,7 +95,7 @@ export default class NewGroup extends EditView {
selection: this.form.data.moderator,
listTileAttrs: user => Object.assign({}, { title: `${user.firstname} ${user.lastname}` }),
selectedText: user => `${user.firstname} ${user.lastname}`,
onSelect: (data) => { console.log('data'); this.form.data.moderator = data; },
onSelect: (data) => { this.form.data.moderator = data; },
})),
]),
m(PermissionEditor, {
......
......@@ -10,7 +10,9 @@ export default class GroupItem {
}
view() {
if (!this.controller || !this.controller.data) return m(loadingScreen);
if (!this.controller || (!this.controller.data && this.controller.modus !== 'new')) {
return m(loadingScreen);
}
if (this.controller.modus !== 'view') return m(editGroup, { controller: this.controller });
return m(viewGroup, { controller: this.controller });
}
......
import m from 'mithril';
import { Card } from 'polythene-mithril';
import { Card, Button, ListTile } from 'polythene-mithril';
import { DatalistController } from 'amiv-web-ui-components';
import { loadingScreen } from '../layout';
import { ResourceHandler, getCurrentUser } from '../auth';
class GroupListItem {
view({ attrs: { name, _id, color = '#ffffff' } }) {
return m('div', {
style: { 'max-width': '500px', margin: '5px' },
onclick: () => {
m.route.set(`/groups/${_id}`);
view({ attrs: { name, _id } }) {
return m(ListTile, {
title: name,
hoverable: true,
rounded: true,
style: { width: '250px' },
url: {
href: `/groups/${_id}`,
oncreate: m.route.link,
},
}, m(Card, {
content: [{ primary: { title: name } }],
style: { backgroundColor: color },
}));
});
}
}
class GroupListCard {
view({ attrs: { title, groups, onAdd = false } }) {
return m('div.maincontainer', { style: { 'margin-top': '5px' } }, m(Card, {
content: m('div', [
m('div', { style: { display: 'flex', 'align-items': 'center' } }, [
m('div.pe-card__title', title),
onAdd && m(Button, {
style: { 'margin-right': '20px' },
className: 'blue-button',
extraWide: true,
label: 'add',
events: { onclick: () => onAdd() },
}),
]),
m('div', {
style: { display: 'flex', 'flex-wrap': 'wrap', margin: '0px 5px 5px 5px' },
}, groups.map(item => m(GroupListItem, { name: item.name, _id: item._id }))),
]),
}));
}
}
export default class GroupList {
constructor() {
......@@ -42,43 +65,15 @@ export default class GroupList {
if (!this.groups) return m(loadingScreen);
return m('div', [
this.moderatedGroups.length > 0 && m('div.maincontainer', {
style: {
'margin-top': '5px',
'border-bottom': '1px solid #aaaaaa',
'padding-bottom': '20px',
},
}, [
m('div', {
style: {
'font-size': '20px',
margin: '10px 5px',
},
}, 'moderated by you'),
m('div', {
style: { display: 'flex', 'flex-wrap': 'wrap' },
}, this.moderatedGroups.map(item =>
m(GroupListItem, { ...item }))),
]),
m('div.maincontainer', {
style: { display: 'flex', 'flex-wrap': 'wrap', 'margin-top': '5px' },
}, [
this.moderatedGroups.length > 0 && m('div', {
style: {
'font-size': '20px',
margin: '10px 5px',
},
}, 'all groups'),
m('div', {
style: { display: 'flex', 'flex-wrap': 'wrap' },
}, [
this.groups.map(item => m(GroupListItem, item)),
m('div', {
style: { 'max-width': '500px', margin: '5px' },
onclick: () => { m.route.set('/newgroup'); },
}, m(Card, { content: [{ primary: { title: '+ add' } }] })),
]),
]),
// groups moderated by the current user
this.moderatedGroups.length > 0
&& m(GroupListCard, { title: 'moderated by you', groups: this.moderatedGroups }),
// all groups
m(GroupListCard, {
title: 'all groups',
groups: this.groups,
onAdd: () => { m.route.set('/newgroup'); },
}),
]);
}
}
......@@ -7,8 +7,8 @@ import {
TextField,
Icon,
} from 'polythene-mithril';
import { DatalistController, ListSelect, DropdownCard } from 'amiv-web-ui-components';
import { icons, Property, chip } from '../views/elements';
import { DatalistController, ListSelect, DropdownCard, Chip } from 'amiv-web-ui-components';
import { icons, Property } from '../views/elements';
import { colors } from '../style';
import ItemView from '../views/itemView';
import TableView from '../views/tableView';
......@@ -20,14 +20,18 @@ import { ResourceHandler } from '../auth';
// Helper class to either display the signed up participants or those on the
// waiting list.
class MembersTable {
constructor({ attrs: { group } }) {
constructor({ attrs: { group, hasPatchRights } }) {
this.group_id = group;
this.ctrl = new RelationlistController('groupmemberships', 'users', { where: { group } });
this.hasPatchRights = hasPatchRights;
this.ctrl = new RelationlistController({
primary: 'groupmemberships', secondary: 'users', query: { where: { group } },
});
// true while in the modus of adding a member
this.addmode = false;
this.userHandler = new ResourceHandler('users');
this.userController = new DatalistController((query, search) =>
this.userHandler.get({ search, ...query }));
this.userController = new DatalistController((query, search) => this.userHandler.get(
{ search, ...query },
));
}
itemRow(data) {
......@@ -36,7 +40,7 @@ class MembersTable {
m('div', { style: { width: '18em' } }, `${data.user.firstname} ${data.user.lastname}`),
m('div', { style: { width: '9em' } }, data.user.email),
m('div', { style: { 'flex-grow': '100' } }),
m('div', m(Button, {
this.hasPatchRights && m('div', m(Button, {
// Button to remove this groupmembership
className: 'red-row-button',
borders: false,
......@@ -75,7 +79,7 @@ class MembersTable {
}) : '',
m(Toolbar, { compact: true }, [
m(ToolbarTitle, { text: 'Members' }),
m(Button, {
this.hasPatchRights && m(Button, {
className: 'blue-button',
borders: true,
label: 'add',
......@@ -100,7 +104,7 @@ class MembersTable {
// Table for list of email adresses, both forward_to and receive
class EmailTable {
constructor({ attrs: { onRemove = () => {} } }) {
constructor({ attrs: { onRemove = false } }) {
this.addmode = false;
this.dirty = false;
this.newvalue = '';
......@@ -117,7 +121,7 @@ class EmailTable {
},
}, [
data,
m(Icon, {
this.onRemove && m(Icon, {
style: { 'margin-left': '3px' },
svg: { content: m.trust(icons.clear) },
size: 'small',
......@@ -128,7 +132,7 @@ class EmailTable {
]);
}
view({ attrs: { list, title, style = {}, onSubmit = () => {} } }) {
view({ attrs: { list, title, style = {}, onSubmit = false } }) {
return m(Card, {
style: { height: '200px', ...style },
content: m('div', [
......@@ -163,7 +167,7 @@ class EmailTable {
]) : '',
m(Toolbar, { compact: true }, [
m(ToolbarTitle, { text: title }),
m(Button, {
onSubmit && m(Button, {
className: 'blue-button',
borders: true,
label: 'add',
......@@ -191,21 +195,21 @@ export default class viewGroup extends ItemView {
view() {
// update the reference to the controller data, as this may be refreshed in between
this.data = this.controller.data;
const hasPatchRights = this.data._links.self.methods.indexOf('PATCH') > -1;
const stdMargin = { margin: '5px' };
return this.layout([
// this div is the title line
m('div.maincontainer', [
m('h1', this.data.name),
this.data.requires_storage && m(chip, {
this.data.requires_storage && m(Chip, {
svg: icons.cloud,
svgColor: '#ffffff',
svgBackground: colors.orange,
...stdMargin,
}, 'has a folder on the AMIV Cloud'),
m('div', { style: { display: 'flex' } }, [
this.numMembers && m(Property, { title: 'Members', style: stdMargin }, this.numMembers),
('numMembers' in this)
&& m(Property, { title: 'Members', style: stdMargin }, this.numMembers),
this.data.moderator && m(Property, {
title: 'Moderator',
onclick: () => { m.route.set(`/users/${this.data.moderator._id}`); },
......@@ -222,22 +226,22 @@ export default class viewGroup extends ItemView {
Object.keys(this.data.permissions)
.map(key => m(Property, { title: key }, this.data.permissions[key])),
) : '',
m(MembersTable, { group: this.data._id }),
m(MembersTable, { group: this.data._id, hasPatchRights }),
]),
// the second column contains receive_from and forward_to emails
m('div.viewcontainercolumn', [
m(EmailTable, {
list: this.data.receive_from || [],
title: 'Receiving Email Adresses',
onSubmit: (newItem) => {
onSubmit: hasPatchRights ? (newItem) => {
const oldList = this.data.receive_from || [];
this.controller.patch({
_id: this.data._id,
_etag: this.data._etag,
receive_from: [...oldList, newItem],
});
},
onRemove: (item) => {
} : undefined,
onRemove: hasPatchRights ? (item) => {
const oldList = this.data.receive_from;
// remove the first occurence of the given item-string
const index = oldList.indexOf(item);
......@@ -249,21 +253,21 @@ export default class viewGroup extends ItemView {
receive_from: oldList,
});
}
},
} : undefined,
}),
m(EmailTable, {
list: this.data.forward_to || [],
title: 'Forwards to Email Adresses',
style: { 'margin-top': '10px' },
onSubmit: (newItem) => {
onSubmit: hasPatchRights ? (newItem) => {
const oldList = this.data.forward_to || [];
this.controller.patch({
_id: this.data._id,
_etag: this.data._etag,
forward_to: [...oldList, newItem],
});
},
onRemove: (item) => {
} : undefined,
onRemove: hasPatchRights ? (item) => {
const oldList = this.data.forward_to;
// remove the first occurence of the given item-string
const index = oldList.indexOf(item);
......@@ -275,7 +279,7 @@ export default class viewGroup extends ItemView {
forward_to: oldList,
});
}
},
} : undefined,
}),
]),
]),
......
......@@ -2,6 +2,8 @@ import m from 'mithril';
import { OauthRedirect } from './auth';
import GroupList from './groups/list';
import GroupItem from './groups/item';
import BlacklistTable from './blacklist/viewBlacklist';
import NewBlacklist from './blacklist/editBlacklist';
import { UserItem, UserTable } from './users/userTool';
import MembershipView from './membershipTool';
import EventTable from './events/table';
......@@ -10,7 +12,8 @@ import JobTable from './jobs/table';
import JobItem from './jobs/item';
import StudydocTable from './studydocs/list';
import studydocItem from './studydocs/item';
import { Layout } from './layout';
import InfoscreenTable from './infoscreen/table';
import { Layout, Error404 } from './layout';
import './style';
const root = document.body;
......@@ -33,9 +36,12 @@ m.route(root, '/events', {
'/events/:id': layoutWith(EventItem),
'/newevent': layoutWith(EventItem),
'/proposeevent': layoutWith(EventItem),
'/infoscreen': layoutWith(InfoscreenTable),
'/groups': layoutWith(GroupList),
'/groups/:id': layoutWith(GroupItem),
'/newgroup': layoutWith(GroupItem),
'/blacklist': layoutWith(BlacklistTable),
'/newblacklistentry': layoutWith(NewBlacklist),
'/oauthcallback': OauthRedirect,
'/joboffers': layoutWith(JobTable),
'/newjoboffer': layoutWith(JobItem),
......@@ -43,4 +49,5 @@ m.route(root, '/events', {
'/studydocuments': layoutWith(StudydocTable),
'/studydocuments/:id': layoutWith(studydocItem),
'/newstudydocument': layoutWith(studydocItem),
'/404': layoutWith(Error404),
});
import m from 'mithril';
import { Snackbar } from 'polythene-mithril';
import { apiUrl } from 'networkConfig';
import { DatalistController } from 'amiv-web-ui-components';
import { ResourceHandler } from '../auth';
import { dateFormatter } from '../utils';
import TableView from '../views/tableView';
const getImgUrl = img => `${apiUrl}${img.file}`;
const exportCSV = (ctrl) => {
ctrl.getFullList().then(
(list) => {
let csv = '';
csv += [
'id',
'title_en',
'time_advertising_start',
'time_advertising_end',
'img_infoscreen_url',
].join(';');
csv += '\n';
list.forEach((event) => {
const fields = [
event._id,
event.title_en.replace(';', ''),
event.time_advertising_start,
event.time_advertising_end,
event.img_infoscreen ? getImgUrl(event.img_infoscreen) : '',
];
csv += fields.join(';');
csv += '\n';
});
const blob = new Blob([csv], { type: 'text/csv' });
const dl = window.document.createElement('a');
const now = new Date();
const pain = [
now.getFullYear().toString(),
now.getMonth().toString().padStart(2, '0'),
now.getDay().toString().padStart(2, '0')].join('-');
dl.href = window.URL.createObjectURL(blob);
dl.download = `infoscreen-export_${pain}.csv`;
dl.style.display = 'none';
document.body.appendChild(dl);
dl.click();
document.body.removeChild(dl);
},
() => {
Snackbar.show({
title: 'Export failed',
style: { color: 'red' },
});
},
);
};
export default class InfoscreenTable {
constructor() {
this.handler = new ResourceHandler('events');
this.ctrl = new DatalistController((query, search) => this.handler.get({ search, ...query }));
}
getItemData(data) {
return [
m('div', { style: { width: 'calc(100% - 36em)' } }, data.title_de || data.title_en),
m('div', { style: { width: '9em' } }, dateFormatter(data.time_start)),
m('div', { style: { width: '9em' } }, dateFormatter(data.time_advertising_start)),
m('div', { style: { width: '9em' } }, dateFormatter(data.time_advertising_end)),
m('div',
{ style: { width: '9em' } },
data.img_infoscreen
? m('a', { href: getImgUrl(data.img_infoscreen) }, data.img_infoscreen.name)
: 'no image'),
];
}
view() {
const now = new Date();
return m(TableView, {
controller: this.ctrl,
keys: [
'titel_en',
'time_start',
'time_advertising_start',
'time_advertising_start',
'img_infoscreen'],
tileContent: this.getItemData,
titles: [
{ text: 'Title', width: 'calc(100% - 36em)' },
{ text: 'Start', width: '9em' },
{ text: 'Advertising Start', width: '9em' },
{ text: 'Advertising End', width: '9em' },
{ text: 'Infoscreen Image', width: '9em' },
],
filters: [[{
name: 'upcoming',
query: { time_start: { $gte: `${now.toISOString().slice(0, -5)}Z` } },
},
{
name: 'advertising upcoming',
query: { time_advertising_start: { $gte: `${now.toISOString().slice(0, -5)}Z` } },
},
{
name: 'advertising in progress',
query: {
time_advertising_start: { $lte: `${now.toISOString().slice(0, -5)}Z` },
time_advertising_end: { $gte: `${now.toISOString().slice(0, -5)}Z` },
},
},
],
[{
name: 'has image',
query: { img_infoscreen: { $ne: null } },
}]],
buttons: [
{ text: 'Export CSV', onclick: () => exportCSV(this.ctrl) },
],
// per default, enable the 'upcoming' filter
initFilterIdxs: [[0, 0], [1, 0]],
});
}
}
......@@ -10,9 +10,9 @@ export default class ItemController {
this.modus = 'view';
} else {
this.modus = 'new';
this.data = {};
this.data = undefined;
}
this.handler = new ResourceHandler(resource, false);
this.handler = new ResourceHandler(resource);
this.embedded = embedded || {};
if (this.id) {
this.handler.getItem(this.id, this.embedded).then((item) => {
......@@ -26,14 +26,16 @@ export default class ItemController {
return new Promise((resolve, reject) => {
this.handler.post(data).then((response) => {
this.id = response._id;
this.changeModus('view');
resolve(response);
}).catch(reject);
});
}
patch(data, formData = false) {
patch(data) {
return new Promise((resolve, reject) => {
this.handler.patch(data, formData).then(() => { this.changeModus('view'); }).catch(reject);
this.handler.patch(data).then((response) => {
resolve(response);
}).catch(reject);
});
}
......
import m from 'mithril';
import { FileInput } from 'amiv-web-ui-components';
import EditView from '../views/editView';
export default class newJob extends EditView {
beforeSubmit() {
// remove all unchanged files
if (this.form.data.pdf !== undefined
&& (this.form.data.pdf === null || 'upload_date' in this.form.data.pdf)) {
delete this.form.data.pdf;
}
if (this.form.data.logo !== undefined
&& (this.form.data.logo === null || 'upload_date' in this.form.data.logo)) {
delete this.form.data.logo;
}
// post everyhing together as FormData
const submitData = new FormData();
Object.keys(this.form.data).forEach((key) => {
submitData.append(key, this.form.data[key]);
});
this.submit(submitData).then(() => this.controller.changeModus('view'));
}
view() {
return this.layout([
m('h3', 'Add a New Job Offer'),
...this.form.renderPage({
title_de: { type: 'text', label: 'German Title' },
...this.form.renderSchema(['company']),
m(FileInput, this.form.bind({
name: 'logo',
label: 'Company Logo',
accept: 'image/png, image/jpeg',
})),
...this.form.renderSchema(['show_website', 'time_end', 'title_en']),
this.form._renderField('description_en', {
multiLine: true,
rows: 5,
...this.form.schema.properties.description_en,
}),
...this.form.renderSchema(['title_de']),
this.form._renderField('description_de', {
multiLine: true,
rows: 5,
...this.form.schema.properties.description_de,
}),
m(FileInput, this.form.bind({
name: 'pdf',
label: 'PDF',
accept: 'application/pdf',
})),
]);
}
}
......@@ -10,7 +10,9 @@ export default class jobModal {
}
view() {
if (!this.controller || !this.controller.data) return m(loadingScreen);
if (!this.controller || (!this.controller.data && this.controller.modus !== 'new')) {
return m(loadingScreen);
}
if (this.controller.modus !== 'view') return m(editJob, { controller: this.controller });
return m(viewJob, { controller: this.controller });
}
......
import m from 'mithril';
import { DatalistController } from 'amiv-web-ui-components';
import { joboffers as config } from '../resourceConfig.json';
import TableView from '../views/tableView';
import { dateFormatter } from '../utils';
import { ResourceHandler } from '../auth';
......@@ -14,7 +13,7 @@ import { ResourceHandler } from '../auth';
export default class JobTable {
constructor() {
this.handler = new ResourceHandler('joboffers', config.tableKeys);
this.handler = new ResourceHandler('joboffers');
this.ctrl = new DatalistController((query, search) => this.handler.get({ search, ...query }));
}
......@@ -26,13 +25,13 @@ export default class JobTable {
];
}
view() {
view(data) {
return m(TableView, {
controller: this.ctrl,
keys: config.tableKeys,
keys: [(data.title_de) ? 'title_de' : 'title_en', 'company', 'time_end'],
tileContent: this.getItemData,
titles: [
{ text: 'Titel', width: 'calc(100% - 30em)' },
{ text: 'Title', width: 'calc(100% - 30em)' },
{ text: 'Company', width: '21em' },
{ text: 'End', width: '9em' },
],
......
import m from 'mithril';
import { Converter } from 'showdown';
import { Card } from 'polythene-mithril';
import { Chip } from 'amiv-web-ui-components';
// eslint-disable-next-line import/extensions
import { apiUrl } from 'networkConfig';
import ItemView from '../views/itemView';
import { dateFormatter } from '../utils';
import { Property } from '../views/elements';
import { icons, Property } from '../views/elements';
export default class viewJob extends ItemView {
constructor(vnode) {
super(vnode);
this.markdown = new Converter();
}
view() {
const stdMargin = { margin: '5px' };
return this.layout([
m('div', [
m('div', { style: { height: '50px' } }, [
// company logo if existing
this.data.img_thumbnail ? m('img', {
this.data.logo ? m('img', {
src: `${apiUrl}/${this.data.logo.file}`,
height: '50px',
style: { float: 'left' },
}) : '',
m('h3', {
style: { 'margin-top': '0px', 'margin-bottom': '0px' },
}, [this.data.title_de || this.data.title_en]),
style: { 'line-height': '50px', 'margin-top': '0px' },
}, this.data.company),
]),
m('div.maincontainer', [
m(Chip, { svg: this.data.show_website ? icons.checked : icons.clear }, 'website'),
]),
// below the title, most important details are listed
this.data.time_end ? m(Property, {
title: 'Offer Ends',
}, `${dateFormatter(this.data.time_end)}`) : '',
m('div', { style: { display: 'flex', margin: '5px 0px 0px 5px' } }, [
this.data.time_end ? m(Property, {
title: 'Offer Ends',
style: stdMargin,
}, `${dateFormatter(this.data.time_end)}`) : '',
m(Property, {
title: 'PDF',
style: stdMargin,
}, this.data.pdf
? m('a', { href: `${apiUrl}${this.data.pdf.file}`, target: '_blank' }, this.data.pdf.name)
: 'not available'),
]),
m('div.viewcontainer', [
m('div.viewcontainercolumn', m(Card, {
content: m('div.maincontainer', [
m('div.pe-card__title', this.data.title_de),
m('div', m.trust(this.markdown.makeHtml(this.data.description_de))),
]),
})),
m('div.viewcontainercolumn', m(Card, {
content: m('div.maincontainer', [
m('div.pe-card__title', this.data.title_en),
m('div', m.trust(this.markdown.makeHtml(this.data.description_en))),
]),
})),
]),
]);
}
}
......@@ -4,7 +4,6 @@ import {
ListTile,
Icon,
Toolbar,
ToolbarTitle,
Dialog,
SVG,
Button,
......@@ -13,7 +12,7 @@ import {
} from 'polythene-mithril';
import { styler } from 'polythene-core-css';
import { icons } from './views/elements';
import { deleteSession } from './auth';
import { deleteSession, getUserRights, getSchema } from './auth';
import { colors } from './style';
const layoutStyle = [
......@@ -94,8 +93,41 @@ class Menupoint {
}
}
export class loadingScreen {
view() {
return m('div', {
style: {
height: '100%',
width: '100%',
display: 'flex',
'flex-direction': 'column',
'justify-content': 'center',
'align-items': 'center',
'animation-name': 'popup',
'animation-duration': '2000ms',
},
}, m('div', { style: { height: '5vh', 'font-size': '4em' } }, 'Loading...'), m('div', {
style: {
height: '20vh',
width: '20vh',
'animation-name': 'spin',
'animation-duration': '2500ms',
'animation-iteration-count': 'infinite',
'animation-timing-function': 'linear',
},
}, m('div', {
style: { height: '20vh', width: '20vh', display: 'inline-block' },
}, m(SVG, {
style: { width: 'inherit', height: 'inherit' },
content: m.trust(icons.amivWheel),
}))));
}
}
export class Layout {
view({ children }) {
if (!getSchema()) return m(loadingScreen);
const userRights = getUserRights();
return m('div', [
m('div.wrapper-main.smooth', [
m(Toolbar, {
......@@ -109,7 +141,18 @@ export class Layout {
events: { onclick: () => { toggleDrawer(); } },
style: { color: '#ffffff' },
})),
m(ToolbarTitle, { text: 'AMIV Admintools' }),
m('div', { style: { 'font-size': '18px', 'margin-left': '20px' } }, 'AMIV Admintools'),
m('a', {
href: 'https://gitlab.ethz.ch/amiv/amiv-admintool/issues/new?issuable_template=Bug',
target: '_blank',
style: {
color: '#888888',
'text-decoration': 'none',
'text-align': 'right',
'margin-right': '20px',
'margin-left': 'auto',
},
}, 'Is something not working? Report a bug!'),
m(Button, {
label: 'logout',
events: { onclick: deleteSession },
......@@ -122,7 +165,7 @@ export class Layout {
header: { title: 'Menu' },
hoverable: true,
tiles: [
m(Menupoint, {
userRights.users.indexOf('POST') > -1 && m(Menupoint, {
href: '/users',
icon: icons.iconUsersSVG,
title: 'Users',
......@@ -137,7 +180,7 @@ export class Layout {
icon: icons.group,
title: 'Groups',
}),
m(Menupoint, {
userRights.joboffers.indexOf('POST') > -1 && m(Menupoint, {
href: '/joboffers',
icon: icons.iconJobsSVG,
title: 'Job offers',
......@@ -147,6 +190,16 @@ export class Layout {
icon: icons.studydoc,
title: 'Studydocs',
}),
m(Menupoint, {
href: '/blacklist',
icon: icons.blacklist,
title: 'Blacklist',
}),
m(Menupoint, {
href: '/infoscreen',
icon: icons.iconEventSVG,
title: 'Infoscreen',
}),
],
}),
),
......@@ -161,7 +214,7 @@ export class Layout {
}
}
export class loadingScreen {
export class Error404 {
view() {
return m('div', {
style: {
......@@ -171,23 +224,9 @@ export class loadingScreen {
'flex-direction': 'column',
'justify-content': 'center',
'align-items': 'center',
'animation-name': 'popup',
'animation-duration': '2000ms',
},
}, m('div', { style: { height: '5vh', 'font-size': '4em' } }, 'Loading...'), m('div', {
style: {
height: '20vh',
width: '20vh',
'animation-name': 'spin',
'animation-duration': '2500ms',
'animation-iteration-count': 'infinite',
'animation-timing-function': 'linear',
},
}, m('div', {
style: { height: '20vh', width: '20vh', display: 'inline-block' },
}, m(SVG, {
style: { width: 'inherit', height: 'inherit' },
content: m.trust(icons.amivWheel),
}))));
}, [
m('div', { style: { height: '5vh', 'font-size': '4em' } }, 'Error 404: Item Not Found!'),
]);
}
}
......@@ -2,9 +2,20 @@ import m from 'mithril';
import Stream from 'mithril/stream';
import { ResourceHandler } from './auth';
import { debounce } from './utils';
// IS NOT IN USE
// IS NOT IN USE
// IS NOT IN USE
// IS NOT IN USE
// IS NOT IN USE
// IS NOT IN USE
// IS NOT IN USE
// IS NOT IN USE
// IS NOT IN USE
// IS NOT IN USE
export default class DatalistController {
constructor(resource, query = {}, searchKeys = false) {
constructor(resource, query = {}, searchKeys = []) {
this.handler = new ResourceHandler(resource, searchKeys);
this.query = query || {};
this.filter = null;
......@@ -46,7 +57,7 @@ export default class DatalistController {
return new Promise((resolve) => {
this.handler.get(query).then((data) => {
// update total number of pages
this.totalPages = Math.ceil(data._meta.total / 10);
this.totalPages = Math.ceil(data._meta.total / 50);
resolve(data._items);
});
});
......@@ -63,8 +74,6 @@ export default class DatalistController {
// save totalPages as a constant to avoid race condition with pages added during this
// process
const { totalPages } = this;
console.log(totalPages);
if (totalPages === 1) {
resolve(firstPage);
}
......@@ -74,8 +83,10 @@ export default class DatalistController {
this.getPageData(pageNum).then((newPage) => {
pages[pageNum] = newPage;
// look if all pages were collected
const missingPages = Array.from(new Array(totalPages), (x, i) => i + 1).filter(i =>
!(i in pages));
const missingPages = Array.from(new Array(totalPages), (x, i) => i + 1).filter(
i => !(i in pages),
);
// eslint-disable-next-line no-console
console.log('missingPages', missingPages);
if (missingPages.length === 0) {
// collect all the so-far loaded pages in order (sorted keys)
......@@ -102,4 +113,3 @@ export default class DatalistController {
this.refresh();
}
}
......@@ -34,4 +34,3 @@ export function set(key, value, shortSession = false) {
window.localStorage.setItem(`glob-${key}`, value);
}
}
{
"apiUrl": "https://api-dev.amiv.ethz.ch",
"ownUrl": "https://admin-dev.amiv.ethz.ch",
"oAuthID": "AMIV Admintool"
"hookUrl": "https://webhooks.amiv.ethz.ch/hook/websitepipeline",
"oAuthID": "AMIV Admintool Dev"
}
{
"apiUrl": "https://api-dev.amiv.ethz.ch",
"ownUrl": "http://localhost:9000",
"hookUrl": "http://0.0.0.0:5000/hook/websitepipeline",
"oAuthID": "Local Tool"
}
{
"apiUrl": "http://127.0.0.1:5000",
"ownUrl": "http://localhost:9000",
"hookUrl": "http://0.0.0.0:6000/hook/websitepipeline",
"oAuthID": "Local Tool"
}
{
"apiUrl": "https://api.amiv.ethz.ch",
"ownUrl": "https://admin.amiv.ethz.ch",
"oAuthID": "Admintools"
"hookUrl": "https://webhooks.amiv.ethz.ch/hook/websitepipeline",
"oAuthID": "AMIV Admintool"
}
{
"apiUrl": "https://api-staging.amiv.ethz.ch",
"ownUrl": "https://admin-staging.amiv.ethz.ch",
"hookUrl": "https://webhooks.amiv.ethz.ch/hook/websitepipeline",
"oAuthID": "AMIV Admintool Staging"
}