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 1028 additions and 375 deletions
import m from 'mithril';
import {
TextField,
Button,
Card
} from 'polythene-mithril';
import EditView from '../views/editView';
import {styler} from 'polythene-core-css';
const draftStyle = [
{
'.footer': {
position: 'fixed',
left: 0,
bottom: 0,
width: '100%',
'background-color': '#E8462B',
'text-align': 'right',
}
}
]
styler.add('eventDraft', draftStyle);
export default class eventWithExport extends EditView {
constructor(vnode) {
super(vnode, 'events');
this.performedEdits = 0;
}
view() {
// Editable by event creator.
const fieldTitleEn = m(TextField, {
label: 'Event Title [EN]',
required: true,
floatingLabel: true,
dense: true,
onChange : (newState) => {this.title_en = newState.value; console.log(this.title_en);},
value: this.title_en,
});
const fieldDescriptionEn = m(TextField, {
label: 'Description [EN]',
required: true,
floatingLabel: true,
dense: true,
multiLine: true,
rows: 6,
onChange : (newState) => {this.fieldDescriptionEn = newState.value; console.log(this.fieldDescriptionEn);},
value: this.fieldDescriptionEn,
});
// Needs administrator (Kulturi).
const fieldLocation = m(TextField, {
label: 'Location:',
floatingLabel: true,
required: true,
onChange : (newState) => {this.fieldLocation = newState.value; console.log(this.fieldLocation);},
value: this.fieldLocation,
});
// Bottom.
const buttonMaker = m(Button, {
// console.log(JSON.stringify(this.keyDescriptors)),
label: "Submit Request!",
onClick: () => alert("You did not finish the editing of the fields.")
});
// Return!
return m('div', {
style: { height: '100%', 'overflow-y': 'scroll'}
}, [
m('h1', 'For the event creator:', fieldTitleEn , fieldDescriptionEn, 'For the AMIV administrator:', fieldLocation),
m('div.footer', buttonMaker),
]);
}
}
\ No newline at end of file
...@@ -6,11 +6,13 @@ import { loadingScreen } from '../layout'; ...@@ -6,11 +6,13 @@ import { loadingScreen } from '../layout';
export default class EventItem { export default class EventItem {
constructor() { constructor() {
this.controller = new ItemController('events'); this.controller = new ItemController('events', { moderator: 1 });
} }
view() { 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(editEvent, { controller: this.controller }); if (this.controller.modus !== 'view') return m(editEvent, { controller: this.controller });
return m(viewEvent, { controller: this.controller }); return m(viewEvent, { controller: this.controller });
} }
......
import m from 'mithril';
import { ListSelect, DatalistController, Form } from 'amiv-web-ui-components';
import { Toolbar, ToolbarTitle, Dialog, Card, Button } from 'polythene-mithril';
import { ResourceHandler } from '../auth';
import RelationlistController from '../relationlistcontroller';
import TableView from '../views/tableView';
import { dateFormatter } from '../utils';
export class ParticipantsController {
constructor() {
this.signupHandler = new ResourceHandler('eventsignups');
this.signupCtrl = new DatalistController((query, search) => this.signupHandler.get({
search, ...query,
}));
this.userHandler = new ResourceHandler('users');
this.acceptedUserController = new RelationlistController({
primary: 'eventsignups',
secondary: 'users',
query: { where: { accepted: true } },
searchKeys: ['email'],
includeWithoutRelation: true,
});
this.waitingUserController = new RelationlistController({
primary: 'eventsignups',
secondary: 'users',
query: { where: { accepted: false } },
searchKeys: ['email'],
includeWithoutRelation: true,
});
this.allParticipants = [];
}
setEventId(eventId) {
this.signupCtrl.setQuery({ where: { event: eventId } });
this.acceptedUserController.setQuery({ where: { event: eventId, accepted: true } });
this.waitingUserController.setQuery({ where: { event: eventId, accepted: false } });
}
refresh() {
this.signupCtrl.getFullList().then((list) => {
this.allParticipants = list;
});
this.acceptedUserController.refresh();
this.waitingUserController.refresh();
}
}
// Helper class to either display the signed up participants or those on the
// waiting list.
export class ParticipantsTable {
constructor({
attrs: {
participantsCtrl,
waitingList,
additional_fields_schema: additionalFieldsSchema,
},
}) {
this.participantsCtrl = participantsCtrl;
if (waitingList) {
this.ctrl = this.participantsCtrl.waitingUserController;
} else {
this.ctrl = this.participantsCtrl.acceptedUserController;
}
this.add_fields_schema = additionalFieldsSchema
? JSON.parse(additionalFieldsSchema).properties : null;
// true while in the modus of adding a signup
this.addmode = false;
this.userHandler = new ResourceHandler('users');
this.userController = new DatalistController(
(query, search) => this.userHandler.get({ search, ...query }).then(data => ({
...data,
_items: data._items.map(user => ({
hasSignup: this.participantsCtrl.allParticipants.some(
signupUser => user._id === signupUser.user,
),
...user,
})),
})),
);
}
exportAsCSV(filePrefix) {
this.ctrl.getFullList().then((list) => {
const csvData = (list.map((item) => {
const additionalFields = item.additional_fields && JSON.parse(item.additional_fields);
const line = [
item.position,
item._created,
item.user ? item.user.firstname : '',
item.user ? item.user.lastname : '',
item.user ? item.user.membership : 'none',
item.email,
item.accepted,
item.confirmed,
...Object.keys(this.add_fields_schema || {}).map(key => (
additionalFields && additionalFields[key] ? additionalFields[key] : '')),
].join('","');
return `"${line}"`;
})).join('\n');
const headercontent = [
'Position', 'Date', 'Firstname', 'Lastname',
'Membership', 'Email', 'Accepted', 'Confirmed',
...Object.keys(this.add_fields_schema || {}).map(key => this.add_fields_schema[key].title),
].join('","');
const header = `"${headercontent}"`;
const filename = `${filePrefix}_participants_export.csv`;
const fileContent = `data:text/csv;charset=utf-8,${header}\n${csvData}`;
const link = document.createElement('a');
link.setAttribute('href', encodeURI(fileContent));
link.setAttribute('download', filename);
link.click();
});
}
itemRow(data) {
// TODO list should not have hardcoded size outside of stylesheet
const hasPatchRights = data._links.self.methods.indexOf('PATCH') > -1;
const additionalFields = data.additional_fields && JSON.parse(data.additional_fields);
const canBeAccepted = !data.accepted;
return [
m('div', { style: { width: '9em' } }, dateFormatter(data._created)),
m('div', { style: { width: '16em' } }, [
...data.user ? [`${data.user.firstname} ${data.user.lastname}`, m('br')] : '',
data.email,
]),
m(
'div', { style: { width: '14em' } },
m('div', ...data.user ? `Membership: ${data.user.membership}` : ''),
(additionalFields && this.add_fields_schema) ? Object.keys(additionalFields).map(
key => m('div', `${this.add_fields_schema[key].title}: ${additionalFields[key]}`),
) : '',
),
m('div', { style: { 'flex-grow': '100' } }),
canBeAccepted ? m('div', m(Button, {
// Button to accept this eventsignup
className: 'blue-row-button',
style: {
margin: '0px 4px',
},
borders: false,
label: 'accept',
events: {
onclick: () => {
// preapare data for patch request
const patch = (({ _id, _etag }) => ({ _id, _etag }))(data);
patch.accepted = true;
this.ctrl.handler.patch(patch).then(() => {
this.participantsCtrl.refresh();
m.redraw();
});
},
},
})) : '',
hasPatchRights ? m('div', m(Button, {
// Button to remove this eventsignup
className: 'red-row-button',
borders: false,
label: 'remove',
events: {
onclick: () => {
this.ctrl.handler.delete(data).then(() => {
this.participantsCtrl.refresh();
m.redraw();
});
},
},
})) : '',
];
}
editEventSignup(user, event) {
const form = new Form();
const schema = JSON.parse(event.additional_fields);
if (schema && schema.$schema) {
// ajv fails to verify the v4 schema of some resources
schema.$schema = 'http://json-schema.org/draft-06/schema#';
form.setSchema(schema);
}
const elements = form.renderSchema();
Dialog.show({
body: m('form', { onsubmit: () => false }, elements),
backdrop: true,
footerButtons: [
m(Button, {
label: 'Cancel',
events: { onclick: () => Dialog.hide() },
}),
m(Button, {
label: 'Submit',
events: {
onclick: () => {
const additionalFieldsString = JSON.stringify(form.getData());
const data = {
event: event._id,
additional_fields: additionalFieldsString,
};
data.user = user._id;
this.ctrl.handler.post(data).then(() => {
Dialog.hide();
this.participantsCtrl.refresh();
m.redraw();
});
},
},
})],
});
}
view({ attrs: { title, filePrefix, event, waitingList } }) {
return m(Card, {
style: { height: '400px', 'margin-bottom': '10px' },
content: m('div', [
this.addmode ? m(ListSelect, {
controller: this.userController,
listTileAttrs: user => Object.assign({}, {
title: `${user.firstname} ${user.lastname}`,
style: (user.hasSignup ? { color: 'rgba(0, 0, 0, 0.2)' } : {}),
hoverable: !user.hasSignup,
}),
selectedText: user => `${user.firstname} ${user.lastname}`,
onSubmit: (user) => {
this.addmode = false;
if (event.additional_fields) {
this.editEventSignup(user, event);
} else {
this.ctrl.handler.post({
user: user._id,
event: event._id,
accepted: !waitingList,
}).then(() => {
this.participantsCtrl.refresh();
m.redraw();
});
}
},
onCancel: () => { this.addmode = false; m.redraw(); },
}) : '',
m(Toolbar, { compact: true }, [
m(ToolbarTitle, { text: title }),
(!waitingList
|| event.selection_strategy === 'manual'
|| event.signup_count >= event.spots) && m(Button, {
style: { margin: '0px 4px' },
className: 'blue-button',
borders: true,
label: 'add',
events: { onclick: () => { this.addmode = true; } },
}),
m(Button, {
className: 'blue-button',
borders: true,
label: 'export CSV',
events: { onclick: () => this.exportAsCSV(filePrefix) },
}),
]),
m(TableView, {
tableHeight: '275px',
controller: this.ctrl,
tileContent: data => this.itemRow(data),
clickOnRows: false,
titles: [
{ text: 'Date of Signup', width: '9em' },
{ text: 'Participant', width: '16em' },
{ text: 'Additional Info', width: '16em' },
],
}),
]),
});
}
}
import m from 'mithril'; import m from 'mithril';
import { events as config } from '../resourceConfig.json'; import { Snackbar } from 'polythene-mithril';
import { DatalistController } from 'amiv-web-ui-components';
import axios from 'axios';
import { hookUrl } from 'networkConfig';
import TableView from '../views/tableView'; import TableView from '../views/tableView';
import DatalistController from '../listcontroller';
import { dateFormatter } from '../utils'; import { dateFormatter } from '../utils';
import { ResourceHandler } from '../auth';
import { get } from '../localStorage';
/* Table of all Events /* Table of all Events
...@@ -10,10 +14,23 @@ import { dateFormatter } from '../utils'; ...@@ -10,10 +14,23 @@ import { dateFormatter } from '../utils';
* Makes use of the standard TableView * Makes use of the standard TableView
*/ */
const triggerHook = () => {
axios.post(hookUrl, { token: get('token') }).then(() => {
Snackbar.show({ title: 'Successful', style: { color: 'green' } });
}).catch((e) => {
// eslint-disable-next-line no-console
console.log(e);
Snackbar.show({
title: 'Network Error, please contact administrator',
style: { color: 'red' },
});
});
};
export default class EventTable { export default class EventTable {
constructor() { constructor() {
this.ctrl = new DatalistController('events', {}, config.tableKeys); this.handler = new ResourceHandler('events');
this.ctrl = new DatalistController((query, search) => this.handler.get({ search, ...query }));
} }
getItemData(data) { getItemData(data) {
...@@ -28,7 +45,7 @@ export default class EventTable { ...@@ -28,7 +45,7 @@ export default class EventTable {
const now = new Date(); const now = new Date();
return m(TableView, { return m(TableView, {
controller: this.ctrl, controller: this.ctrl,
keys: config.tableKeys, keys: ['titel_en', 'time_start', 'time_end'],
tileContent: this.getItemData, tileContent: this.getItemData,
titles: [ titles: [
{ text: 'Titel', width: 'calc(100% - 18em)' }, { text: 'Titel', width: 'calc(100% - 18em)' },
...@@ -42,9 +59,19 @@ export default class EventTable { ...@@ -42,9 +59,19 @@ export default class EventTable {
name: 'past', name: 'past',
query: { time_start: { $lt: `${now.toISOString().slice(0, -5)}Z` } }, query: { time_start: { $lt: `${now.toISOString().slice(0, -5)}Z` } },
}]], }]],
buttons: this.handler.rights.includes('POST') ? [
{ text: 'Rerender website', onclick: triggerHook },
] : [],
// per default, enable the 'upcoming' filter // per default, enable the 'upcoming' filter
initFilterIdxs: [[0, 0]], initFilterIdxs: [[0, 0]],
onAdd: () => { m.route.set('/newevent'); }, onAdd: (this.handler.rights.length > 0)
? () => {
if (this.handler.rights.includes('POST')) {
m.route.set('/newevent');
} else {
m.route.set('/proposeevent');
}
} : false,
}); });
} }
} }
import m from 'mithril'; import m from 'mithril';
import { import { Button } from 'polythene-mithril';
Switch, import Stream from 'mithril/stream';
Toolbar,
ToolbarTitle,
Card,
TextField,
} from 'polythene-mithril';
import { styler } from 'polythene-core-css'; import { styler } from 'polythene-core-css';
import { DropdownCard, Chip } from 'amiv-web-ui-components';
// eslint-disable-next-line import/extensions // eslint-disable-next-line import/extensions
import { apiUrl } from 'networkConfig'; import { apiUrl } from 'networkConfig';
import ItemView from '../views/itemView'; import ItemView from '../views/itemView';
import { eventsignups as signupConfig } from '../resourceConfig.json'; import { ParticipantsController, ParticipantsTable } from './participants';
import TableView from '../views/tableView';
import RelationlistController from '../relationlistcontroller';
import { dateFormatter } from '../utils'; import { dateFormatter } from '../utils';
import { icons, DropdownCard, Property, chip } from '../views/elements'; import { Property, FilterChip, icons } from '../views/elements';
import { ResourceHandler } from '../auth'; import { colors } from '../style';
const viewLayout = [ const viewLayout = [
{ {
...@@ -66,91 +60,142 @@ class DuoLangProperty { ...@@ -66,91 +60,142 @@ class DuoLangProperty {
} }
} }
// Helper class to either display the signed up participants or those on the class ParticipantsSummary {
// waiting list. constructor() {
class ParticipantsTable { this.onlyAccepted = true;
constructor({ attrs: { where } }) {
this.ctrl = new RelationlistController('eventsignups', 'users', { where }, ['email']);
} }
getItemData(data) { view({ attrs: { participants, additionalFields = "{'properties': {}}" } }) {
// TODO list should not have hardcoded size outside of stylesheet // Parse the JSON from additional fields into an object
return [ const parsedParticipants = participants.map(signup => ({
m('div', { style: { width: '9em' } }, dateFormatter(data._created)), ...signup,
m( additional_fields: signup.additional_fields
'div', ? JSON.parse(signup.additional_fields) : {},
{ style: { width: '18em' } }, }));
data.user ? `${data.user.firstname} ${data.user.lastname}` : '', // Filter if only accepted participants should be shown
), const filteredParticipants = parsedParticipants.filter(
m('div', { style: { width: '9em' } }, data.email), participant => (this.onlyAccepted ? participant.accepted : true),
]; );
}
view({ attrs: { title } }) { // check which additional fields should get summarized
return m(Card, { let hasSBB = false;
style: { height: '400px', 'margin-bottom': '10px' }, let hasFood = false;
content: m('div', [ if (additionalFields) {
m(Toolbar, { compact: true }, [ hasSBB = 'sbb_abo' in JSON.parse(additionalFields).properties;
m(ToolbarTitle, { text: title }), hasFood = 'food' in JSON.parse(additionalFields).properties;
]), }
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' },
],
}),
]),
});
}
}
class EmailList { return m('div', [
view({ attrs: { list } }) { m('div', {
const emails = list.toString().replace(/,/g, '; '); style: {
return m(Card, { height: '50px',
content: m(TextField, { value: emails, label: '', multiLine: true }, ''), '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 { export default class viewEvent extends ItemView {
constructor(vnode) { constructor(vnode) {
super(vnode); super(vnode);
this.signupHandler = new ResourceHandler('eventsignups'); this.participantsCtrl = new ParticipantsController();
this.description = false; this.description = false;
this.advertisement = false; this.advertisement = false;
this.registration = false; this.registration = false;
this.emailAdresses = false; this.modalDisplay = Stream('none');
this.emaillist = [''];
this.showAllEmails = false;
} }
oninit() { oninit() {
this.setUpEmailList(this.showAllEmails); this.participantsCtrl.setEventId(this.data._id);
} }
setUpEmailList(showAll) { cloneEvent() {
// setup where query const event = Object.assign({}, this.data);
const where = { event: this.data._id };
if (!showAll) { const eventInfoToDelete = [
// only show accepted '_id',
where.accepted = true; '_created',
'_etag',
'_links',
'_updated',
'signup_count',
'unaccepted_count',
'__proto__',
];
const now = new Date();
if (event.time_end < `${now.toISOString().slice(0, -5)}Z`) {
eventInfoToDelete.push(...[
'time_advertising_end',
'time_advertising_start',
'time_end',
'time_register_end',
'time_deregister_end',
'time_register_start',
'time_start']);
} }
this.signupHandler.get({ where }).then((data) => { eventInfoToDelete.forEach((key) => {
this.emaillist = (data._items.map(item => item.email)); delete event[key];
m.redraw();
}); });
this.controller.changeModus('new');
this.controller.data = event;
} }
view() { view() {
let displaySpots = '-'; let displaySpots = '-';
const stdMargin = { margin: '5px' }; const stdMargin = { margin: '5px' };
// Get the image and insert it inside the modal -
// use its "alt" text as a caption
const modalImg = document.getElementById('modalImg');
if (this.data.spots !== 0) { if (this.data.spots !== 0) {
displaySpots = this.data.spots; displaySpots = this.data.spots;
} }
...@@ -168,10 +213,16 @@ export default class viewEvent extends ItemView { ...@@ -168,10 +213,16 @@ export default class viewEvent extends ItemView {
]), ]),
// below the title, most important details are listed // below the title, most important details are listed
m('div.maincontainer', { style: { display: 'flex' } }, [ 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, style: stdMargin,
title: 'Signups', title: 'Type',
}, `${this.data.signup_count} / ${displaySpots}`), }, `${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, { this.data.location && m(Property, {
style: stdMargin, style: stdMargin,
title: 'Location', title: 'Location',
...@@ -180,6 +231,11 @@ export default class viewEvent extends ItemView { ...@@ -180,6 +231,11 @@ export default class viewEvent extends ItemView {
title: 'Time', title: 'Time',
style: stdMargin, style: stdMargin,
}, `${dateFormatter(this.data.time_start)} - ${dateFormatter(this.data.time_end)}`), }, `${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 // everything else is not listed in DropdownCards, which open only on request
m('div.viewcontainer', [ m('div.viewcontainer', [
...@@ -199,16 +255,16 @@ export default class viewEvent extends ItemView { ...@@ -199,16 +255,16 @@ export default class viewEvent extends ItemView {
m(DropdownCard, { title: 'advertisement', style: { margin: '10px 0' } }, [ m(DropdownCard, { title: 'advertisement', style: { margin: '10px 0' } }, [
[ [
m(chip, { m(Chip, {
svg: this.data.show_annonce ? icons.checked : icons.clear, svg: this.data.show_announce ? icons.checked : icons.clear,
border: '1px #aaaaaa solid', border: '1px #aaaaaa solid',
}, 'announce'), }, 'announce'),
m(chip, { m(Chip, {
svg: this.data.show_infoscreen ? icons.checked : icons.clear, svg: this.data.show_infoscreen ? icons.checked : icons.clear,
border: '1px #aaaaaa solid', border: '1px #aaaaaa solid',
margin: '4px', margin: '4px',
}, 'infoscreen'), }, 'infoscreen'),
m(chip, { m(Chip, {
svg: this.data.show_website ? icons.checked : icons.clear, svg: this.data.show_website ? icons.checked : icons.clear,
border: '1px #aaaaaa solid', border: '1px #aaaaaa solid',
}, 'website'), }, 'website'),
...@@ -216,8 +272,8 @@ export default class viewEvent extends ItemView { ...@@ -216,8 +272,8 @@ export default class viewEvent extends ItemView {
this.data.time_advertising_start ? m( this.data.time_advertising_start ? m(
Property, Property,
{ title: 'Advertising Time' }, { title: 'Advertising Time' },
`${dateFormatter(this.data.time_advertising_start)} - ` + `${dateFormatter(this.data.time_advertising_start)} - `
`${dateFormatter(this.data.time_advertising_end)}`, + `${dateFormatter(this.data.time_advertising_end)}`,
) : '', ) : '',
this.data.priority ? m( this.data.priority ? m(
Property, Property,
...@@ -231,8 +287,13 @@ export default class viewEvent extends ItemView { ...@@ -231,8 +287,13 @@ export default class viewEvent extends ItemView {
this.data.time_register_start ? m( this.data.time_register_start ? m(
Property, Property,
{ title: 'Registration Time' }, { title: 'Registration Time' },
`${dateFormatter(this.data.time_register_start)} - ` + `${dateFormatter(this.data.time_register_start)} - `
`${dateFormatter(this.data.time_register_end)}`, + `${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( this.data.selection_strategy ? m(
Property, Property,
...@@ -245,34 +306,139 @@ export default class viewEvent extends ItemView { ...@@ -245,34 +306,139 @@ export default class viewEvent extends ItemView {
{ title: 'Registration Form' }, { title: 'Registration Form' },
this.data.additional_fields, 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 // a list of email adresses of all participants, easy to copy-paste
m(DropdownCard, { title: 'Email Adresses' }, [ this.data.spots !== null ? m(DropdownCard, {
m(Switch, { title: 'Participants Summary',
defaultChecked: false, style: { margin: '10px 0' },
label: 'show unaccepted', }, m(ParticipantsSummary, {
onChange: () => { participants: this.participantsCtrl.allParticipants,
this.showAllEmails = !this.showAllEmails; additionalFields: this.data.additional_fields,
this.setUpEmailList(this.showAllEmails); })) : '',
m(DropdownCard, { title: 'Images' }, [
m('div', {
style: {
display: 'flex',
}, },
}), }, [
m(EmailList, { list: this.emaillist }), m('div', {
style: {
width: '40%',
padding: '5px',
},
}, [
this.data.img_poster && m('div', 'Poster'),
this.data.img_poster && m('img', {
src: `${apiUrl}${this.data.img_poster.file}`,
width: '100%',
onclick: () => {
this.modalDisplay('block');
modalImg.src = `${apiUrl}${this.data.img_poster.file}`;
},
}),
]),
m('div', {
style: {
width: '52%',
padding: '5px',
},
}, [
m('div', [
this.data.img_infoscreen && m('div', 'Infoscreen'),
this.data.img_infoscreen && m('img', {
src: `${apiUrl}${this.data.img_infoscreen.file}`,
width: '100%',
onclick: () => {
this.modalDisplay('block');
modalImg.src = `${apiUrl}${this.data.img_infoscreen.file}`;
},
}),
]),
]),
]),
]), ]),
]), ]),
m('div.viewcontainercolumn', { style: { width: '50em' } }, [
m('div.viewcontainercolumn', [
this.data.time_register_start ? m(ParticipantsTable, { this.data.time_register_start ? m(ParticipantsTable, {
where: { accepted: true, event: this.data._id },
title: 'Accepted Participants', 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, { this.data.time_register_start ? m(ParticipantsTable, {
where: { accepted: false, event: this.data._id },
title: 'Participants on Waiting List', title: 'Participants on Waiting List',
filePrefix: 'waitinglist',
event: this.data,
waitingList: true,
additional_fields_schema: this.data.additional_fields,
participantsCtrl: this.participantsCtrl,
}) : '', }) : '',
]), ]),
]), ]),
m('div', {
id: 'imgModal',
style: {
display: this.modalDisplay(),
position: 'fixed',
'z-index': '100',
'padding-top': '100px',
left: 0,
top: 0,
width: '100vw',
height: '100vh',
overflow: 'auto',
'background-color': 'rgba(0, 0, 0, 0.9)',
},
}, [
m('img', {
id: 'modalImg',
style: {
margin: 'auto',
display: 'block',
'max-width': '80vw',
'max-heigth': '80vh',
},
}),
m('div', {
onclick: () => {
this.modalDisplay('none');
},
style: {
top: '15px',
right: '35px',
color: '#f1f1f1',
transition: '0.3s',
'z-index': 10,
position: 'absolute',
'font-size': '40px',
'font-weight': 'bold',
},
}, 'x'),
]),
], [
m(Button, {
label: 'Clone Event',
border: true,
style: {
color: colors.light_blue,
'border-color': colors.light_blue,
},
events: {
// opens 'new event' ,
// coping All information but the 'event_id', past dates and API generated properties
onclick: () => this.cloneEvent(),
},
}),
]); ]);
} }
} }
import m from 'mithril'; import m from 'mithril';
import { TextField } from 'polythene-mithril'; import { TextField } from 'polythene-mithril';
import { ListSelect, DatalistController, Select } from 'amiv-web-ui-components';
// eslint-disable-next-line import/extensions // eslint-disable-next-line import/extensions
import { apiUrl } from 'networkConfig'; import { apiUrl } from 'networkConfig';
import SelectList from '../views/selectList'; import { ResourceHandler } from '../auth';
import { MDCSelect } from '../views/selectOption';
import DatalistController from '../listcontroller';
import EditView from '../views/editView'; import EditView from '../views/editView';
...@@ -48,27 +47,22 @@ class PermissionEditor { ...@@ -48,27 +47,22 @@ class PermissionEditor {
}, m('div', { }, m('div', {
style: { display: 'flex', width: '100%', 'flex-flow': 'row wrap' }, style: { display: 'flex', width: '100%', 'flex-flow': 'row wrap' },
}, this.apiEndpoints.map(apiEndpoint => m('div', { }, 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, label: apiEndpoint.title,
disabled: true,
style: { width: '60%' },
}),
m('div', { style: { width: '40%' } }, m(MDCSelect, {
name: apiEndpoint.href,
options: ['no permission', 'read', 'readwrite'], options: ['no permission', 'read', 'readwrite'],
onchange: (newVal) => { default: 'no permission',
if (newVal === 'no permission') { style: { width: '200px' },
// the api equivalent to no permission if to delete the key out of the dict 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]; if (internalPerm[apiEndpoint.href]) delete internalPerm[apiEndpoint.href];
} else { } else internalPerm[apiEndpoint.href] = value;
internalPerm[apiEndpoint.href] = newVal;
}
onChange(internalPerm); onChange(internalPerm);
}, },
value: internalPerm[apiEndpoint.href], value: internalPerm[apiEndpoint.href],
})), }),
])))), ])))),
]); ]);
} }
...@@ -78,46 +72,35 @@ class PermissionEditor { ...@@ -78,46 +72,35 @@ class PermissionEditor {
export default class NewGroup extends EditView { export default class NewGroup extends EditView {
constructor(vnode) { constructor(vnode) {
super(vnode); super(vnode);
this.userController = new DatalistController( this.userHandler = new ResourceHandler('users', ['firstname', 'lastname', 'email', 'nethz']);
'users', {}, this.userController = new DatalistController((query, search) => this.userHandler.get(
['firstname', 'lastname', 'email', 'nethz'], { search, ...query },
); ));
console.log(this.data);
} }
beforeSubmit() { beforeSubmit() {
// exchange moderator object with string of id const data = Object.assign({}, this.form.data);
const { moderator } = this.data; // exchange moderator object with string of id, will return null if moderator is null
if (moderator) { this.data.moderator = `${moderator._id}`; } data.moderator = data.moderator ? data.moderator._id : null;
this.submit(); this.submit(data).then(() => this.controller.changeModus('view'));
} }
view() { view() {
return this.layout([ return this.layout([
...this.renderPage({ ...this.form.renderSchema(['name', 'allow_self_enrollment', 'requires_storage']),
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",
},
}),
m('div', { style: { display: 'flex' } }, [ m('div', { style: { display: 'flex' } }, [
m(TextField, { label: 'Group Moderator: ', disabled: true, style: { width: '160px' } }), m(TextField, { label: 'Group Moderator: ', disabled: true, style: { width: '160px' } }),
m('div', { style: { 'flex-grow': 1 } }, m(SelectList, { m('div', { style: { 'flex-grow': 1 } }, m(ListSelect, {
controller: this.userController, controller: this.userController,
selection: this.data.moderator, selection: this.form.data.moderator,
listTileAttrs: user => Object.assign({}, { title: `${user.firstname} ${user.lastname}` }), listTileAttrs: user => Object.assign({}, { title: `${user.firstname} ${user.lastname}` }),
selectedText: user => `${user.firstname} ${user.lastname}`, selectedText: user => `${user.firstname} ${user.lastname}`,
onSelect: (data) => { this.data.moderator = data; }, onSelect: (data) => { this.form.data.moderator = data; },
})), })),
]), ]),
m(PermissionEditor, { m(PermissionEditor, {
permissions: this.data.permissions, permissions: this.form.data.permissions,
onChange: (newPermissions) => { this.data.permissions = newPermissions; }, onChange: (newPermissions) => { this.form.data.permissions = newPermissions; },
}), }),
]); ]);
} }
......
...@@ -10,7 +10,9 @@ export default class GroupItem { ...@@ -10,7 +10,9 @@ export default class GroupItem {
} }
view() { 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 }); if (this.controller.modus !== 'view') return m(editGroup, { controller: this.controller });
return m(viewGroup, { controller: this.controller }); return m(viewGroup, { controller: this.controller });
} }
......
import m from 'mithril'; import m from 'mithril';
import { Card } from 'polythene-mithril'; import { Card, Button, ListTile } from 'polythene-mithril';
import DatalistController from '../listcontroller'; import { DatalistController } from 'amiv-web-ui-components';
import { loadingScreen } from '../layout'; import { loadingScreen } from '../layout';
import { ResourceHandler, getCurrentUser } from '../auth';
class GroupListItem { class GroupListItem {
view({ attrs: { name, _id } }) { view({ attrs: { name, _id } }) {
return m('div', { return m(ListTile, {
style: { 'max-width': '500px', margin: '5px' }, title: name,
onclick: () => { hoverable: true,
m.route.set(`/groups/${_id}`); rounded: true,
style: { width: '250px' },
url: {
href: `/groups/${_id}`,
oncreate: m.route.link,
}, },
}, m(Card, { content: [{ primary: { title: name } }] })); });
} }
} }
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 { export default class GroupList {
constructor() { constructor() {
this.ctrl = new DatalistController('groups', { sort: [['name', 1]] }, ['name']); this.handler = new ResourceHandler('groups', ['name']);
this.data = []; this.ctrl = new DatalistController(
this.ctrl.getFullList().then((list) => { this.data = list; m.redraw(); }); (query, search) => this.handler.get({ search, ...query }),
{ sort: [['name', 1]] },
);
this.groups = [];
this.moderatedGroups = [];
this.ctrl.getFullList().then((list) => {
this.groups = list;
this.ctrl.setQuery({ where: { moderator: getCurrentUser() } });
this.ctrl.getFullList().then((moderatedList) => {
this.moderatedGroups = moderatedList;
m.redraw();
});
});
} }
view() { view() {
if (!this.data) return m(loadingScreen); if (!this.groups) return m(loadingScreen);
return m( return m('div', [
'div.maincontainer', { style: { display: 'flex', 'flex-wrap': 'wrap', 'margin-top': '5px' } }, // groups moderated by the current user
this.data.map(item => m(GroupListItem, item)), this.moderatedGroups.length > 0
m('div', { && m(GroupListCard, { title: 'moderated by you', groups: this.moderatedGroups }),
style: { 'max-width': '500px', margin: '5px' }, // all groups
onclick: () => { m.route.set('/newgroup'); }, m(GroupListCard, {
}, m(Card, { content: [{ primary: { title: '+ add' } }] })), title: 'all groups',
); groups: this.groups,
onAdd: () => { m.route.set('/newgroup'); },
}),
]);
} }
} }
...@@ -7,29 +7,31 @@ import { ...@@ -7,29 +7,31 @@ import {
TextField, TextField,
Icon, Icon,
} from 'polythene-mithril'; } from 'polythene-mithril';
import { icons, Property, DropdownCard, 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 { colors } from '../style';
import ItemView from '../views/itemView'; import ItemView from '../views/itemView';
import TableView from '../views/tableView'; import TableView from '../views/tableView';
import DatalistController from '../listcontroller';
import RelationlistController from '../relationlistcontroller'; import RelationlistController from '../relationlistcontroller';
import SelectList from '../views/selectList';
import { ResourceHandler } from '../auth'; import { ResourceHandler } from '../auth';
// Helper class to either display the signed up participants or those on the // Helper class to either display the signed up participants or those on the
// waiting list. // waiting list.
class MembersTable { class MembersTable {
constructor({ attrs: { group } }) { constructor({ attrs: { group, hasPatchRights } }) {
this.group_id = group; 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 // true while in the modus of adding a member
this.addmode = false; this.addmode = false;
this.userController = new DatalistController( this.userHandler = new ResourceHandler('users');
'users', {}, this.userController = new DatalistController((query, search) => this.userHandler.get(
['firstname', 'lastname', 'email', 'nethz'], { search, ...query },
); ));
} }
itemRow(data) { itemRow(data) {
...@@ -38,7 +40,7 @@ class MembersTable { ...@@ -38,7 +40,7 @@ class MembersTable {
m('div', { style: { width: '18em' } }, `${data.user.firstname} ${data.user.lastname}`), m('div', { style: { width: '18em' } }, `${data.user.firstname} ${data.user.lastname}`),
m('div', { style: { width: '9em' } }, data.user.email), m('div', { style: { width: '9em' } }, data.user.email),
m('div', { style: { 'flex-grow': '100' } }), m('div', { style: { 'flex-grow': '100' } }),
m('div', m(Button, { this.hasPatchRights && m('div', m(Button, {
// Button to remove this groupmembership // Button to remove this groupmembership
className: 'red-row-button', className: 'red-row-button',
borders: false, borders: false,
...@@ -59,7 +61,7 @@ class MembersTable { ...@@ -59,7 +61,7 @@ class MembersTable {
return m(Card, { return m(Card, {
style: { height: '500px' }, style: { height: '500px' },
content: m('div', [ content: m('div', [
this.addmode ? m(SelectList, { this.addmode ? m(ListSelect, {
controller: this.userController, controller: this.userController,
listTileAttrs: user => Object.assign({}, { title: `${user.firstname} ${user.lastname}` }), listTileAttrs: user => Object.assign({}, { title: `${user.firstname} ${user.lastname}` }),
selectedText: user => `${user.firstname} ${user.lastname}`, selectedText: user => `${user.firstname} ${user.lastname}`,
...@@ -77,7 +79,7 @@ class MembersTable { ...@@ -77,7 +79,7 @@ class MembersTable {
}) : '', }) : '',
m(Toolbar, { compact: true }, [ m(Toolbar, { compact: true }, [
m(ToolbarTitle, { text: 'Members' }), m(ToolbarTitle, { text: 'Members' }),
m(Button, { this.hasPatchRights && m(Button, {
className: 'blue-button', className: 'blue-button',
borders: true, borders: true,
label: 'add', label: 'add',
...@@ -102,7 +104,7 @@ class MembersTable { ...@@ -102,7 +104,7 @@ class MembersTable {
// Table for list of email adresses, both forward_to and receive // Table for list of email adresses, both forward_to and receive
class EmailTable { class EmailTable {
constructor({ attrs: { onRemove = () => {} } }) { constructor({ attrs: { onRemove = false } }) {
this.addmode = false; this.addmode = false;
this.dirty = false; this.dirty = false;
this.newvalue = ''; this.newvalue = '';
...@@ -119,7 +121,7 @@ class EmailTable { ...@@ -119,7 +121,7 @@ class EmailTable {
}, },
}, [ }, [
data, data,
m(Icon, { this.onRemove && m(Icon, {
style: { 'margin-left': '3px' }, style: { 'margin-left': '3px' },
svg: { content: m.trust(icons.clear) }, svg: { content: m.trust(icons.clear) },
size: 'small', size: 'small',
...@@ -130,7 +132,7 @@ class EmailTable { ...@@ -130,7 +132,7 @@ class EmailTable {
]); ]);
} }
view({ attrs: { list, title, style = {}, onSubmit = () => {} } }) { view({ attrs: { list, title, style = {}, onSubmit = false } }) {
return m(Card, { return m(Card, {
style: { height: '200px', ...style }, style: { height: '200px', ...style },
content: m('div', [ content: m('div', [
...@@ -165,7 +167,7 @@ class EmailTable { ...@@ -165,7 +167,7 @@ class EmailTable {
]) : '', ]) : '',
m(Toolbar, { compact: true }, [ m(Toolbar, { compact: true }, [
m(ToolbarTitle, { text: title }), m(ToolbarTitle, { text: title }),
m(Button, { onSubmit && m(Button, {
className: 'blue-button', className: 'blue-button',
borders: true, borders: true,
label: 'add', label: 'add',
...@@ -181,7 +183,6 @@ class EmailTable { ...@@ -181,7 +183,6 @@ class EmailTable {
} }
export default class viewGroup extends ItemView { export default class viewGroup extends ItemView {
oninit() { oninit() {
// load the number of members in this group // load the number of members in this group
const handler = new ResourceHandler('groupmemberships'); const handler = new ResourceHandler('groupmemberships');
...@@ -194,21 +195,21 @@ export default class viewGroup extends ItemView { ...@@ -194,21 +195,21 @@ export default class viewGroup extends ItemView {
view() { view() {
// update the reference to the controller data, as this may be refreshed in between // update the reference to the controller data, as this may be refreshed in between
this.data = this.controller.data; this.data = this.controller.data;
const hasPatchRights = this.data._links.self.methods.indexOf('PATCH') > -1;
const stdMargin = { margin: '5px' }; const stdMargin = { margin: '5px' };
return this.layout([ return this.layout([
// this div is the title line // this div is the title line
m('div.maincontainer', [ m('div.maincontainer', [
m('h1', this.data.name), m('h1', this.data.name),
this.data.requires_storage && m(chip, { this.data.requires_storage && m(Chip, {
svg: icons.cloud, svg: icons.cloud,
svgColor: '#ffffff', svgColor: '#ffffff',
svgBackground: colors.orange, svgBackground: colors.orange,
...stdMargin, ...stdMargin,
}, 'has a folder on the AMIV Cloud'), }, 'has a folder on the AMIV Cloud'),
m('div', { style: { display: 'flex' } }, [ 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, { this.data.moderator && m(Property, {
title: 'Moderator', title: 'Moderator',
onclick: () => { m.route.set(`/users/${this.data.moderator._id}`); }, onclick: () => { m.route.set(`/users/${this.data.moderator._id}`); },
...@@ -225,22 +226,22 @@ export default class viewGroup extends ItemView { ...@@ -225,22 +226,22 @@ export default class viewGroup extends ItemView {
Object.keys(this.data.permissions) Object.keys(this.data.permissions)
.map(key => m(Property, { title: key }, this.data.permissions[key])), .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 // the second column contains receive_from and forward_to emails
m('div.viewcontainercolumn', [ m('div.viewcontainercolumn', [
m(EmailTable, { m(EmailTable, {
list: this.data.receive_from || [], list: this.data.receive_from || [],
title: 'Receiving Email Adresses', title: 'Receiving Email Adresses',
onSubmit: (newItem) => { onSubmit: hasPatchRights ? (newItem) => {
const oldList = this.data.receive_from || []; const oldList = this.data.receive_from || [];
this.controller.patch({ this.controller.patch({
_id: this.data._id, _id: this.data._id,
_etag: this.data._etag, _etag: this.data._etag,
receive_from: [...oldList, newItem], receive_from: [...oldList, newItem],
}); });
}, } : undefined,
onRemove: (item) => { onRemove: hasPatchRights ? (item) => {
const oldList = this.data.receive_from; const oldList = this.data.receive_from;
// remove the first occurence of the given item-string // remove the first occurence of the given item-string
const index = oldList.indexOf(item); const index = oldList.indexOf(item);
...@@ -252,21 +253,21 @@ export default class viewGroup extends ItemView { ...@@ -252,21 +253,21 @@ export default class viewGroup extends ItemView {
receive_from: oldList, receive_from: oldList,
}); });
} }
}, } : undefined,
}), }),
m(EmailTable, { m(EmailTable, {
list: this.data.forward_to || [], list: this.data.forward_to || [],
title: 'Forwards to Email Adresses', title: 'Forwards to Email Adresses',
style: { 'margin-top': '10px' }, style: { 'margin-top': '10px' },
onSubmit: (newItem) => { onSubmit: hasPatchRights ? (newItem) => {
const oldList = this.data.forward_to || []; const oldList = this.data.forward_to || [];
this.controller.patch({ this.controller.patch({
_id: this.data._id, _id: this.data._id,
_etag: this.data._etag, _etag: this.data._etag,
forward_to: [...oldList, newItem], forward_to: [...oldList, newItem],
}); });
}, } : undefined,
onRemove: (item) => { onRemove: hasPatchRights ? (item) => {
const oldList = this.data.forward_to; const oldList = this.data.forward_to;
// remove the first occurence of the given item-string // remove the first occurence of the given item-string
const index = oldList.indexOf(item); const index = oldList.indexOf(item);
...@@ -278,7 +279,7 @@ export default class viewGroup extends ItemView { ...@@ -278,7 +279,7 @@ export default class viewGroup extends ItemView {
forward_to: oldList, forward_to: oldList,
}); });
} }
}, } : undefined,
}), }),
]), ]),
]), ]),
......
...@@ -2,15 +2,18 @@ import m from 'mithril'; ...@@ -2,15 +2,18 @@ import m from 'mithril';
import { OauthRedirect } from './auth'; import { OauthRedirect } from './auth';
import GroupList from './groups/list'; import GroupList from './groups/list';
import GroupItem from './groups/item'; import GroupItem from './groups/item';
import BlacklistTable from './blacklist/viewBlacklist';
import NewBlacklist from './blacklist/editBlacklist';
import { UserItem, UserTable } from './users/userTool'; import { UserItem, UserTable } from './users/userTool';
import { MembershipView } from './membershipTool'; import MembershipView from './membershipTool';
import EventTable from './events/table'; import EventTable from './events/table';
import EventItem from './events/item'; import EventItem from './events/item';
import eventDraft from './events/eventDraft';
import eventWithExport from './events/eventWithExport';
import JobTable from './jobs/table'; import JobTable from './jobs/table';
import JobItem from './jobs/item'; import JobItem from './jobs/item';
import { Layout } from './layout'; import StudydocTable from './studydocs/list';
import studydocItem from './studydocs/item';
import InfoscreenTable from './infoscreen/table';
import { Layout, Error404 } from './layout';
import './style'; import './style';
const root = document.body; const root = document.body;
...@@ -32,13 +35,19 @@ m.route(root, '/events', { ...@@ -32,13 +35,19 @@ m.route(root, '/events', {
'/events': layoutWith(EventTable), '/events': layoutWith(EventTable),
'/events/:id': layoutWith(EventItem), '/events/:id': layoutWith(EventItem),
'/newevent': layoutWith(EventItem), '/newevent': layoutWith(EventItem),
'/draftevent': layoutWith(eventDraft), '/proposeevent': layoutWith(EventItem),
'/eventwithexport': layoutWith(eventWithExport), '/infoscreen': layoutWith(InfoscreenTable),
'/groups': layoutWith(GroupList), '/groups': layoutWith(GroupList),
'/groups/:id': layoutWith(GroupItem), '/groups/:id': layoutWith(GroupItem),
'/newgroup': layoutWith(GroupItem), '/newgroup': layoutWith(GroupItem),
'/blacklist': layoutWith(BlacklistTable),
'/newblacklistentry': layoutWith(NewBlacklist),
'/oauthcallback': OauthRedirect, '/oauthcallback': OauthRedirect,
'/joboffers': layoutWith(JobTable), '/joboffers': layoutWith(JobTable),
'/newjoboffer': layoutWith(JobItem), '/newjoboffer': layoutWith(JobItem),
'/joboffers/:id': layoutWith(JobItem), '/joboffers/:id': layoutWith(JobItem),
'/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 { ...@@ -10,9 +10,9 @@ export default class ItemController {
this.modus = 'view'; this.modus = 'view';
} else { } else {
this.modus = 'new'; this.modus = 'new';
this.data = {}; this.data = undefined;
} }
this.handler = new ResourceHandler(resource, false); this.handler = new ResourceHandler(resource);
this.embedded = embedded || {}; this.embedded = embedded || {};
if (this.id) { if (this.id) {
this.handler.getItem(this.id, this.embedded).then((item) => { this.handler.getItem(this.id, this.embedded).then((item) => {
...@@ -26,20 +26,22 @@ export default class ItemController { ...@@ -26,20 +26,22 @@ export default class ItemController {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
this.handler.post(data).then((response) => { this.handler.post(data).then((response) => {
this.id = response._id; this.id = response._id;
this.changeModus('view'); resolve(response);
}).catch(reject); }).catch(reject);
}); });
} }
patch(data, formData = false) { patch(data) {
return new Promise((resolve, reject) => { 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);
}); });
} }
cancel() { cancel() {
if (this.modus === 'edit') this.changeModus('view'); if (this.modus === 'edit') this.changeModus('view');
if (this.modus === 'new') m.route.set(`/${this.resource}`); else m.route.set(`/${this.resource}`);
} }
changeModus(newModus) { changeModus(newModus) {
......
import m from 'mithril'; import m from 'mithril';
import { FileInput } from 'amiv-web-ui-components';
import EditView from '../views/editView'; import EditView from '../views/editView';
export default class newJob extends 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() { view() {
return this.layout([ return this.layout([
m('h3', 'Add a New Job Offer'), ...this.form.renderSchema(['company']),
...this.renderPage({ m(FileInput, this.form.bind({
title_de: { type: 'text', label: 'German Title' }, 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 { ...@@ -10,7 +10,9 @@ export default class jobModal {
} }
view() { 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 }); if (this.controller.modus !== 'view') return m(editJob, { controller: this.controller });
return m(viewJob, { controller: this.controller }); return m(viewJob, { controller: this.controller });
} }
......
import m from 'mithril'; import m from 'mithril';
import { joboffers as config } from '../resourceConfig.json'; import { DatalistController } from 'amiv-web-ui-components';
import TableView from '../views/tableView'; import TableView from '../views/tableView';
import DatalistController from '../listcontroller';
import { dateFormatter } from '../utils'; import { dateFormatter } from '../utils';
import { ResourceHandler } from '../auth';
/* Table of all current Jobs /* Table of all current Jobs
...@@ -13,7 +13,8 @@ import { dateFormatter } from '../utils'; ...@@ -13,7 +13,8 @@ import { dateFormatter } from '../utils';
export default class JobTable { export default class JobTable {
constructor() { constructor() {
this.ctrl = new DatalistController('joboffers', {}, config.tableKeys); this.handler = new ResourceHandler('joboffers');
this.ctrl = new DatalistController((query, search) => this.handler.get({ search, ...query }));
} }
getItemData(data) { getItemData(data) {
...@@ -24,13 +25,13 @@ export default class JobTable { ...@@ -24,13 +25,13 @@ export default class JobTable {
]; ];
} }
view() { view(data) {
return m(TableView, { return m(TableView, {
controller: this.ctrl, controller: this.ctrl,
keys: config.tableKeys, keys: [(data.title_de) ? 'title_de' : 'title_en', 'company', 'time_end'],
tileContent: this.getItemData, tileContent: this.getItemData,
titles: [ titles: [
{ text: 'Titel', width: 'calc(100% - 30em)' }, { text: 'Title', width: 'calc(100% - 30em)' },
{ text: 'Company', width: '21em' }, { text: 'Company', width: '21em' },
{ text: 'End', width: '9em' }, { text: 'End', width: '9em' },
], ],
......
import m from 'mithril'; 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 // eslint-disable-next-line import/extensions
import { apiUrl } from 'networkConfig'; import { apiUrl } from 'networkConfig';
import ItemView from '../views/itemView'; import ItemView from '../views/itemView';
import { dateFormatter } from '../utils'; import { dateFormatter } from '../utils';
import { Property } from '../views/elements'; import { icons, Property } from '../views/elements';
// small helper class to display both German and English content together, dependent export default class viewJob extends ItemView {
// on which content is available. constructor(vnode) {
class DuoLangProperty { super(vnode);
view({ attrs: { title, de, en } }) { this.markdown = new Converter();
// TODO Lang indicators should be smaller and there should be less margin
// between languages
return m(
Property,
{ title },
de ? m('div', [
m('div', { className: 'propertyLangIndicator' }, 'DE'),
m('p', de),
]) : '',
en ? m('div', [
m('div', { className: 'propertyLangIndicator' }, 'EN'),
m('p', en),
]) : '',
);
} }
}
export default class viewJob extends ItemView {
view() { view() {
const stdMargin = { margin: '5px' };
return this.layout([ return this.layout([
m('div', [ m('div', { style: { height: '50px' } }, [
// company logo if existing // company logo if existing
this.data.img_thumbnail ? m('img', { this.data.logo ? m('img', {
src: `${apiUrl}/${this.data.logo.file}`, src: `${apiUrl}/${this.data.logo.file}`,
height: '50px', height: '50px',
style: { float: 'left' }, style: { float: 'left' },
}) : '', }) : '',
m('h3', { m('h3', {
style: { 'margin-top': '0px', 'margin-bottom': '0px' }, style: { 'line-height': '50px', 'margin-top': '0px' },
}, [this.data.title_de || this.data.title_en]), }, 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 // below the title, most important details are listed
this.data.time_end ? m(Property, { m('div', { style: { display: 'flex', margin: '5px 0px 0px 5px' } }, [
title: 'Offer Ends', this.data.time_end ? m(Property, {
}, `${dateFormatter(this.data.time_end)}`) : '', 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))),
]),
})),
]),
]); ]);
} }
} }
import m from 'mithril'; import m from 'mithril';
//import * as mdc from 'material-components-web';
//import "@material/drawer";
import { import {
List, List,
ListTile, ListTile,
Icon, Icon,
Toolbar, Toolbar,
ToolbarTitle,
Dialog, Dialog,
SVG, SVG,
Button, Button,
IconButton, IconButton,
Snackbar,
} from 'polythene-mithril'; } from 'polythene-mithril';
import { styler } from 'polythene-core-css'; import { styler } from 'polythene-core-css';
import { icons } from './views/elements'; import { icons } from './views/elements';
import { deleteSession } from './auth'; import { deleteSession, getUserRights, getSchema } from './auth';
import { colors } from './style'; import { colors } from './style';
const layoutStyle = [ const layoutStyle = [
...@@ -66,7 +64,7 @@ const layoutStyle = [ ...@@ -66,7 +64,7 @@ const layoutStyle = [
width: '100%', width: '100%',
height: '100%', height: '100%',
background: '#000000aa', background: '#000000aa',
'z-index': 100000000 'z-index': 100000000,
}, },
}, },
]; ];
...@@ -95,8 +93,41 @@ class Menupoint { ...@@ -95,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 { export class Layout {
view({ children }) { view({ children }) {
if (!getSchema()) return m(loadingScreen);
const userRights = getUserRights();
return m('div', [ return m('div', [
m('div.wrapper-main.smooth', [ m('div.wrapper-main.smooth', [
m(Toolbar, { m(Toolbar, {
...@@ -110,7 +141,18 @@ export class Layout { ...@@ -110,7 +141,18 @@ export class Layout {
events: { onclick: () => { toggleDrawer(); } }, events: { onclick: () => { toggleDrawer(); } },
style: { color: '#ffffff' }, 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, { m(Button, {
label: 'logout', label: 'logout',
events: { onclick: deleteSession }, events: { onclick: deleteSession },
...@@ -123,7 +165,7 @@ export class Layout { ...@@ -123,7 +165,7 @@ export class Layout {
header: { title: 'Menu' }, header: { title: 'Menu' },
hoverable: true, hoverable: true,
tiles: [ tiles: [
m(Menupoint, { userRights.users.indexOf('POST') > -1 && m(Menupoint, {
href: '/users', href: '/users',
icon: icons.iconUsersSVG, icon: icons.iconUsersSVG,
title: 'Users', title: 'Users',
...@@ -138,14 +180,25 @@ export class Layout { ...@@ -138,14 +180,25 @@ export class Layout {
icon: icons.group, icon: icons.group,
title: 'Groups', title: 'Groups',
}), }),
m(Menupoint, { userRights.joboffers.indexOf('POST') > -1 && m(Menupoint, {
href: '/joboffers', href: '/joboffers',
icon: icons.iconJobsSVG, icon: icons.iconJobsSVG,
title: 'Job offers', title: 'Job offers',
}), }),
m(Menupoint, { m(Menupoint, {
href: '/announce', href: '/studydocuments',
title: 'Announce', icon: icons.studydoc,
title: 'Studydocs',
}),
m(Menupoint, {
href: '/blacklist',
icon: icons.blacklist,
title: 'Blacklist',
}),
m(Menupoint, {
href: '/infoscreen',
icon: icons.iconEventSVG,
title: 'Infoscreen',
}), }),
], ],
}), }),
...@@ -154,13 +207,14 @@ export class Layout { ...@@ -154,13 +207,14 @@ export class Layout {
// shadow over content in case drawer is out // shadow over content in case drawer is out
m('div.content-hider'), m('div.content-hider'),
]), ]),
m(Snackbar),
// dialog element will show when Dialog.show() is called, this is only a placeholder // dialog element will show when Dialog.show() is called, this is only a placeholder
m(Dialog), m(Dialog),
]); ]);
} }
} }
export class loadingScreen { export class Error404 {
view() { view() {
return m('div', { return m('div', {
style: { style: {
...@@ -170,23 +224,9 @@ export class loadingScreen { ...@@ -170,23 +224,9 @@ export class loadingScreen {
'flex-direction': 'column', 'flex-direction': 'column',
'justify-content': 'center', 'justify-content': 'center',
'align-items': '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('div', { style: { height: '5vh', 'font-size': '4em' } }, 'Error 404: Item Not Found!'),
}, m(SVG, { ]);
style: { width: 'inherit', height: 'inherit' },
content: m.trust(icons.amivWheel),
}))));
} }
} }
...@@ -2,9 +2,20 @@ import m from 'mithril'; ...@@ -2,9 +2,20 @@ import m from 'mithril';
import Stream from 'mithril/stream'; import Stream from 'mithril/stream';
import { ResourceHandler } from './auth'; import { ResourceHandler } from './auth';
import { debounce } from './utils'; 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 { export default class DatalistController {
constructor(resource, query = {}, searchKeys = false) { constructor(resource, query = {}, searchKeys = []) {
this.handler = new ResourceHandler(resource, searchKeys); this.handler = new ResourceHandler(resource, searchKeys);
this.query = query || {}; this.query = query || {};
this.filter = null; this.filter = null;
...@@ -46,7 +57,7 @@ export default class DatalistController { ...@@ -46,7 +57,7 @@ export default class DatalistController {
return new Promise((resolve) => { return new Promise((resolve) => {
this.handler.get(query).then((data) => { this.handler.get(query).then((data) => {
// update total number of pages // update total number of pages
this.totalPages = Math.ceil(data._meta.total / 10); this.totalPages = Math.ceil(data._meta.total / 50);
resolve(data._items); resolve(data._items);
}); });
}); });
...@@ -63,8 +74,6 @@ export default class DatalistController { ...@@ -63,8 +74,6 @@ export default class DatalistController {
// save totalPages as a constant to avoid race condition with pages added during this // save totalPages as a constant to avoid race condition with pages added during this
// process // process
const { totalPages } = this; const { totalPages } = this;
console.log(totalPages);
if (totalPages === 1) { if (totalPages === 1) {
resolve(firstPage); resolve(firstPage);
} }
...@@ -74,8 +83,10 @@ export default class DatalistController { ...@@ -74,8 +83,10 @@ export default class DatalistController {
this.getPageData(pageNum).then((newPage) => { this.getPageData(pageNum).then((newPage) => {
pages[pageNum] = newPage; pages[pageNum] = newPage;
// look if all pages were collected // look if all pages were collected
const missingPages = Array.from(new Array(totalPages), (x, i) => i + 1).filter(i => const missingPages = Array.from(new Array(totalPages), (x, i) => i + 1).filter(
!(i in pages)); i => !(i in pages),
);
// eslint-disable-next-line no-console
console.log('missingPages', missingPages); console.log('missingPages', missingPages);
if (missingPages.length === 0) { if (missingPages.length === 0) {
// collect all the so-far loaded pages in order (sorted keys) // collect all the so-far loaded pages in order (sorted keys)
...@@ -102,4 +113,3 @@ export default class DatalistController { ...@@ -102,4 +113,3 @@ export default class DatalistController {
this.refresh(); this.refresh();
} }
} }
...@@ -34,4 +34,3 @@ export function set(key, value, shortSession = false) { ...@@ -34,4 +34,3 @@ export function set(key, value, shortSession = false) {
window.localStorage.setItem(`glob-${key}`, value); window.localStorage.setItem(`glob-${key}`, value);
} }
} }
import m from 'mithril';
import EditView from './views/editView'; import EditView from './views/editView';
import SelectList from './views/selectList';
const m = require('mithril'); export default class MembershipView extends EditView {
export class MembershipView extends EditView {
constructor(vnode) { constructor(vnode) {
super(vnode, 'groupmemberships', { user: 1, group: 1 }); super(vnode, 'groupmemberships', { user: 1, group: 1 });
} }
...@@ -23,17 +21,3 @@ export class MembershipView extends EditView { ...@@ -23,17 +21,3 @@ export class MembershipView extends EditView {
]); ]);
} }
} }
export class NewMembership {
constructor() {
this.selectUser = new SelectList('users', ['firstname', 'lastname'], {
view(vnode) {
return m('span', `${vnode.attrs.firstname} ${vnode.attrs.lastname}`);
},
});
}
view() {
return m(this.selectUser);
}
}