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
import m from 'mithril';
import infinite from 'mithril-infinite';
const pageSize = 5;
const getIndex = pageNum => (pageNum - 1) * pageSize;
function item(data, opts, itemIndex){
return m('div', data.firstname);
}
function pageData(pageNum) {
return new Promise((resolve, reject) => {
m.request({
method: 'GET',
dataType: 'jsonp',
headers: {
'Authorization': 'root'
},
url: `https://amiv-api.ethz.ch/users?max_results=5&page=${pageNum}`,
}).then((response) => {
resolve(response._items);
});
});
}
export default {
view: function() {
return m('div', {
style: {
height: '400px'
}
//className: 'experiment_list',
/*header: {
title: 'Users'
},*/
}, m(infinite, {
item,
pageData,
}),
);
}
}
import m from 'mithril';
import { RaisedButton, RadioGroup } from 'polythene-mithril';
import { TextInput } from 'amiv-web-ui-components';
import EditView from '../views/editView';
export default class UserEdit extends EditView {
constructor(vnode) {
super(vnode, 'users', {});
beforeSubmit() {
if ('rfid' in this.form.data && !this.form.data.rfid) delete this.form.data.rfid;
this.submit(this.form.data).then(() => this.controller.changeModus('view'));
}
view() {
// do not render anything if there is no data yet
if (!this.data) return m.trust('');
const submitButton = m(RaisedButton, {
disabled: !this.valid,
label: 'Submit',
events: { onclick: () => { this.submit(); } },
});
return m('form', [
...this.renderPage({
lastname: { type: 'text', label: 'Last Name' },
firstname: { type: 'text', label: 'First Name' },
email: { type: 'text', label: 'Email' },
nethz: { type: 'text', label: 'NETHZ' },
rfid: { type: 'text', label: 'RFID Code' },
}),
m(RadioGroup, {
name: 'Membership',
buttons: [
{
value: 'regular',
label: 'Regular AMIV Member',
defaultChecked: this.data.membership === 'regular',
},
{
value: 'extraordinary',
label: 'Extraordinary Member',
defaultChecked: this.data.membership === 'extraordinary',
},
{
value: 'honory',
label: 'Honorary Member',
defaultChecked: this.data.membership === 'honory',
},
],
onChange: ({ value }) => { this.data.membership = value; },
}),
m(RadioGroup, {
name: 'Sex',
buttons: [
{ value: 'female', label: 'Female', defaultChecked: this.data.gender === 'female' },
{ value: 'male', label: 'Male', defaultChecked: this.data.gender === 'male' },
],
onChange: ({ value }) => { console.log(value); this.data.gender = value; },
}),
m(RadioGroup, {
name: 'Departement',
buttons: [
{ value: 'itet', label: 'ITET', defaultChecked: this.data.department === 'itet' },
{ value: 'mavt', label: 'MAVT', defaultChecked: this.data.department === 'mavt' },
],
onChange: ({ value }) => { this.data.department = value; },
}),
submitButton,
return this.layout([
...this.form.renderSchema(['lastname', 'firstname', 'email', 'phone', 'nethz', 'legi']),
m(TextInput, this.form.bind({
type: 'password',
name: 'password',
label: 'New password',
floatingLabel: true,
})),
...this.form.renderSchema(['rfid', 'send_newsletter', 'membership', 'department']),
]);
}
}
import m from 'mithril';
import UserEdit from './editUser';
import UserView from './viewUser';
import { DatalistController } from 'amiv-web-ui-components';
import EditUser from './editUser';
import ViewUser from './viewUser';
import TableView from '../views/tableView';
import { users as config } from '../resourceConfig.json';
import DatalistController from '../listcontroller';
import ItemController from '../itemcontroller';
import { loadingScreen } from '../layout';
import { ResourceHandler } from '../auth';
export class NewUser extends UserEdit {
constructor(vnode) {
super(vnode);
this.data = {
membership: 'regular',
};
this.valid = false;
// if the creation is finished, UI should switch to new User
this.callback = (response) => { m.route.set(`/users/${response.data._id}`); };
}
}
export class UserModal {
export class UserItem {
constructor() {
this.edit = false;
this.controller = new ItemController('users');
}
view() {
if (this.edit) {
return m(UserEdit, { onfinish: () => { this.edit = false; m.redraw(); } });
if (!this.controller || (!this.controller.data && this.controller.modus !== 'new')) {
return m(loadingScreen);
}
// else
return m('div', [
m('div.btn.btn-default', { onclick: () => { this.edit = true; } }, 'Edit'),
m('br'),
m(UserView),
]);
if (this.controller.modus !== 'view') return m(EditUser, { controller: this.controller });
return m(ViewUser, { controller: this.controller });
}
}
export class UserTable {
constructor() {
this.ctrl = new DatalistController('users', {}, config.tableKeys);
this.handler = new ResourceHandler('users');
this.ctrl = new DatalistController(
(query, search) => this.handler.get({ search, ...query }),
{ sort: [['lastname', 1]] },
);
}
view() {
const tableKeys = ['firstname', 'lastname', 'nethz', 'legi', 'membership'];
return m(TableView, {
controller: this.ctrl,
keys: config.tableKeys,
titles: config.tableKeys.map(key => config.keyDescriptors[key] || key),
keys: tableKeys,
titles: tableKeys.map(key => this.handler.schema.properties[key].title || key),
filters: [[
{ name: 'not members', query: { membership: 'none' } },
{ name: 'regular members', query: { membership: 'regular' } },
{ name: 'extraordinary members', query: { membership: 'extraordinary' } },
{ name: 'honorary member', query: { membership: 'honorary' } },
], [
{ name: 'ITET', query: { department: 'itet' } },
{ name: 'MAVT', query: { department: 'mavt' } },
]],
onAdd: () => { m.route.set('/newuser'); },
});
}
......
import m from 'mithril';
import { Card, Toolbar, ToolbarTitle, Button, Snackbar } from 'polythene-mithril';
import { ListSelect, DatalistController, Chip } from 'amiv-web-ui-components';
import ItemView from '../views/itemView';
import TableView from '../views/tableView';
import SelectList from '../views/selectList';
import { users as config } from '../resourceConfig.json';
import DatalistController from '../listcontroller';
import RelationlistController from '../relationlistcontroller';
import { ResourceHandler } from '../auth';
import { icons, Property } from '../views/elements';
import { colors } from '../style';
export default class UserView extends ItemView {
constructor() {
super('users');
constructor(vnode) {
super(vnode);
// a controller to handle the groupmemberships of this user
this.groupmemberships = new DatalistController('groupmemberships', {
where: { user: this.id },
embedded: { group: 1 },
this.groupmemberships = new RelationlistController({
primary: 'groupmemberships', secondary: 'groups', query: { where: { user: this.data._id } },
});
// a controller to handle the eventsignups of this user
this.eventsignups = new DatalistController('eventsignups', {
where: { user: this.id },
embedded: { event: 1 },
this.eventsignups = new RelationlistController({
primary: 'eventsignups', secondary: 'events', query: { where: { user: this.data._id } },
});
// initially, don't display the choice field for a new group
// (this will be displayed once the user clicks on 'new')
this.groupchoice = false;
// a controller to handle the list of possible groups to join
this.groupcontroller = new DatalistController('groups', {}, ['name']);
this.groupHandler = new ResourceHandler('groups', ['name']);
this.groupController = new DatalistController(
(query, search) => this.groupHandler.get({ search, ...query }),
);
// exclude the groups where the user is already a member
this.groupmemberships.handler.get({ where: { user: this.id } })
this.groupmemberships.handler.get({ where: { user: this.data._id } })
.then((data) => {
const groupIds = data._items.map(item => item.group);
this.groupcontroller.setQuery({
this.groupController.setQuery({
where: { _id: { $nin: groupIds } },
});
});
this.sessionsHandler = new ResourceHandler('sessions');
}
oninit() {
this.handler.getItem(this.id, this.embedded).then((item) => {
this.data = item;
m.redraw();
});
this.groupmemberships.refresh();
}
view() {
// do not render anything if there is no data yet
if (!this.data) return m.trust('');
const stdMargin = { margin: '5px' };
let membershipBadge = m('span.label.label-important', 'No Member');
let membership = m(Chip, {
svg: icons.clear,
svgBackground: colors.amiv_red,
style: stdMargin,
}, 'No Member');
if (this.data.membership === 'regular') {
membershipBadge = m('span.label.label-success', 'Member');
membership = m(Chip, {
svg: icons.checked,
svgBackground: colors.green,
style: stdMargin,
}, 'Regular Member');
} else if (this.data.membership === 'extraordinary') {
membershipBadge = m('span.label.label-success', 'Extraordinary Member');
membership = m(Chip, {
svg: icons.checked,
svgBackground: colors.green,
style: stdMargin,
}, 'Extraordinary Member');
} else if (this.data.membership === 'honorary') {
membershipBadge = m('span.label.label-warning', 'Honorary Member');
membership = m(Chip, {
svg: icons.star,
svgBackground: colors.orange,
style: stdMargin,
}, 'Honorary Member');
}
const detailKeys = [
'email', 'phone', 'nethz', 'legi', 'rfid', 'department', 'gender'];
// Selector that is only displayed if "new" is clicked in the
// groupmemberships. Selects a group to request membership for.
const groupSelect = m(SelectList, {
controller: this.groupcontroller,
listTileAttrs: data => Object.assign({}, { title: data.name }),
const groupSelect = m(ListSelect, {
controller: this.groupController,
listTileAttrs: group => Object.assign({}, { title: group.name }),
selectedText: group => group.name,
onSubmit: (group) => {
this.groupchoice = false;
this.groupmemberships.handler.post({
......@@ -69,30 +83,102 @@ export default class UserView extends ItemView {
group: group._id,
}).then(() => {
this.groupmemberships.refresh();
m.redraw();
});
},
onCancel: () => { this.groupchoice = false; m.redraw(); },
});
return m('div', [
m('h1', `${this.data.firstname} ${this.data.lastname}`),
membershipBadge,
m('table', detailKeys.map(key => m('tr', [
m('td.detail-descriptor', config.keyDescriptors[key]),
m('td', this.data[key] ? this.data[key] : ''),
]))),
m('h2', 'Memberships'), m('br'),
this.groupchoice ? groupSelect : '',
m(TableView, {
controller: this.groupmemberships,
keys: ['group.name', 'expiry'],
titles: ['groupname', 'expiry'],
onAdd: () => { this.groupchoice = true; },
}),
m('h2', 'Signups'), m('br'),
m(TableView, {
controller: this.eventsignups,
keys: ['event.title_de'],
titles: ['event'],
const now = new Date();
return this.layout([
m('div.maincontainer', [
m('h1', `${this.data.firstname} ${this.data.lastname}`),
membership,
this.data.department && m(
Chip,
{ svg: icons.department, style: stdMargin },
this.data.department,
),
m(Chip, {
svg: this.data.send_newsletter ? icons.checked : icons.clear,
style: stdMargin,
}, 'newsletter'),
m('div', { style: { display: 'flex' } }, [
this.data.nethz && m(Property, { title: 'NETHZ', style: stdMargin }, this.data.nethz),
this.data.email && m(Property, { title: 'Email', style: stdMargin }, this.data.email),
m(Property, { title: 'Legi', style: stdMargin }, this.data.legi ? this.data.legi : '-'),
m(Property, { title: 'RFID', style: stdMargin }, this.data.rfid ? this.data.rfid : '-'),
this.data.phone && m(Property, { title: 'Phone', style: stdMargin }, this.data.phone),
]),
]),
m('div.viewcontainer', [
m('div.viewcontainercolumn', m(Card, {
style: { height: '350px' },
content: m('div', [
m(Toolbar, { compact: true }, [
m(ToolbarTitle, { text: 'Event Signups' }),
]),
m(TableView, {
tableHeight: '175px',
controller: this.eventsignups,
tileContent: item => m('div', item.event.title_en || item.event.title_de),
titles: ['Event'],
clickOnRows: (data) => { m.route.set(`/events/${data.event._id}`); },
filters: [[{
name: 'upcoming',
query: { 'event.time_start': { $gte: `${now.toISOString().slice(0, -5)}Z` } },
}, {
name: 'past',
query: { 'event.time_start': { $lt: `${now.toISOString().slice(0, -5)}Z` } },
}]],
// per default, enable the 'upcoming' filter
initFilterIdxs: [[0, 0]],
}),
]),
})),
m('div.viewcontainercolumn', m(Card, {
style: { height: '350px' },
content: m('div', [
this.groupchoice && groupSelect,
m(Toolbar, { compact: true }, [
m(ToolbarTitle, { text: 'Group Memberships' }),
m(Button, {
className: 'blue-button',
label: 'add',
events: { onclick: () => { this.groupchoice = true; } },
}),
]),
m(TableView, {
tableHeight: '225px',
controller: this.groupmemberships,
keys: ['group.name', 'expiry'],
titles: ['Group Name', 'Expires'],
clickOnRows: (data) => { m.route.set(`/groups/${data.group._id}`); },
}),
]),
})),
]),
], [
m(Button, {
label: 'log out all Sessions',
className: 'itemView-delete-button',
border: true,
events: {
onclick: () => {
this.sessionsHandler.get({
where: { user: this.data._id },
}).then((response) => {
if (response._items.length === 0) {
Snackbar.show({ title: 'No active sessions for this user.' });
} else {
response._items.forEach((session) => {
this.sessionsHandler.delete(session);
});
}
});
},
},
}),
]);
}
......
......@@ -18,10 +18,11 @@ export function debounce(func, wait, immediate) {
};
}
export function dateFormatter(datestring) {
export function dateFormatter(datestring, time = true) {
// converts an API datestring into the standard format 01.01.1990, 10:21
if (!datestring) return '';
const date = new Date(datestring);
if (!time) return date.toLocaleDateString('de-DE');
return date.toLocaleString('de-DE', {
day: '2-digit',
month: '2-digit',
......
import Ajv from 'ajv';
import { Checkbox } from 'polythene-mithril';
import { apiUrl } from 'networkConfig';
import m from 'mithril';
import { IconButton, Toolbar, ToolbarTitle, Button } from 'polythene-mithril';
import { Form } from 'amiv-web-ui-components';
import ItemView from './itemView';
import { textInput, datetimeInput, numInput } from './elements';
const m = require('mithril');
// Mapper for resource vs schema-object names
const objectNameForResource = {
users: 'User',
groupmembershipds: 'Groupmembership',
groups: 'Group',
eventsignups: 'Eventsignup',
events: 'Event',
};
import { icons } from './elements';
import { colors } from '../style';
export default class EditView extends ItemView {
/* Extension of ItemView to edit a data item
/**
* Extension of ItemView to edit a data item
*
* Requires:
* - call constructor with vnode, resource, (valid, true by default)
* - call constructor with vnode, resource, (valid, false by default)
* - vnode.attrs.onfinish has to be a callback function that is called after
* the edit is finished
*
* Provides Methods:
* - bind(attrs): binds a form-field against this.data
* - submit
* @param {object} vnode [as provided by mithril]
* @param {string} resource [the API resource of this view, e.g. 'events']
* @param {object} embedded [any embedding query that should be added
* to API requests for this resource]
*/
constructor(vnode, resource, embedded, valid = true) {
super(resource, embedded);
this.changed = false;
this.resource = resource;
// state for validation
this.valid = valid;
this.ajv = new Ajv({
missingRefs: 'ignore',
errorDataPath: 'property',
allErrors: true,
});
this.errors = {};
this.data = {};
// callback when edit is finished
this.callback = vnode.attrs.onfinish;
}
oninit() {
// if this.id is set, this is an edit view of an existing event.
// Therefore, we load the current state of the event from the API.
if (this.id) {
this.handler.getItem(this.id, this.embedded).then((item) => {
this.data = item;
m.redraw();
});
}
// load schema
m.request(`${apiUrl}/docs/api-docs`).then((schema) => {
const objectSchema = schema.definitions[
objectNameForResource[this.resource]];
// console.log(objectSchema);
// filter out any field that is of type media and replace with type
// object
Object.keys(objectSchema.properties).forEach((property) => {
if (objectSchema.properties[property].type === 'media' ||
objectSchema.properties[property].type === 'json_schema_object') {
objectSchema.properties[property].type = 'object';
}
});
// delete objectSchema.properties['_id'];
console.log(this.ajv.addSchema(objectSchema, 'schema'));
}).catch((error) => { console.log(error); });
}
// bind form-fields to the object data and validation
bind(attrs) {
// initialize error-list for every bound field
if (!this.errors[attrs.name]) this.errors[attrs.name] = [];
const boundFormelement = {
onChange: (name, value) => {
this.changed = true;
// bind changed data
this.data[name] = value;
console.log(this.data);
// validate against schema
const validate = this.ajv.getSchema('schema');
this.valid = validate(this.data);
console.log(validate.errors);
if (this.valid) {
Object.keys(this.errors).forEach((field) => {
this.errors[field] = [];
});
} else {
// get errors for respective fields
Object.keys(this.errors).forEach((field) => {
const errors = validate.errors.filter(error =>
`.${field}` === error.dataPath);
this.errors[field] = errors.map(error => error.message);
});
}
},
getErrors: () => this.errors[attrs.name],
value: this.data[attrs.name],
};
// add the given attributes
Object.keys(attrs).forEach((key) => { boundFormelement[key] = attrs[key]; });
return boundFormelement;
constructor(vnode) {
super(vnode);
// the form is valid in case that the item controller is in edit mode
const validInitially = this.controller.modus === 'edit';
// start a form to collect the submit data
this.form = new Form({}, validInitially, 4, Object.assign({}, this.controller.data));
this.form.setSchema(JSON.parse(JSON.stringify(this.handler.schema)));
}
renderPage(page) {
return Object.keys(page).map((key) => {
const field = page[key];
if (field.type === 'text') {
field.name = key;
field.floatingLabel = true;
delete field.type;
return m(textInput, this.bind(field));
} else if (field.type === 'number') {
field.name = key;
field.floatingLabel = true;
delete field.type;
return m(numInput, this.bind(field));
} else if (field.type === 'checkbox') {
field.checked = this.data[key] || false;
field.onChange = (state) => {
this.data[key] = state.checked;
};
delete field.type;
return m(Checkbox, field);
} else if (field.type === 'datetime') {
field.name = key;
delete field.type;
return m(datetimeInput, this.bind(field));
}
return `key '${key}' not found`;
});
}
submit(formData = false) {
if (Object.keys(this.data).length > 0) {
/**
* Submit the changed version of this.data
*
* @param {Boolean} true if the data should be send as FormData instead of
* JSON. Necessary in cases where files are included in the
* changes.
*/
submit(data) {
return new Promise((resolve, reject) => {
let request;
if (this.id) {
// if id is known, this is a patch to an existing item
request = this.handler.patch(this.data, formData);
if (this.controller.modus === 'edit') {
// this is a patch to an existing item
request = this.controller.patch(data);
} else {
request = this.handler.post(this.data);
request = this.controller.post(data);
}
request.then((response) => {
this.callback(response);
resolve(response);
}).catch((error) => {
console.log(error);
// Process the API error
const { response } = error;
if (response.status === 422) {
if ('_issues' in error) {
// there are problems with some fields, display them
Object.keys(response.data._issues).forEach((field) => {
this.errors[field] = [response.data._issues[field]];
Object.keys(error._issues).forEach((field) => {
this.form.errors[field] = [error._issues[field]];
this.form.valid = false;
});
// eslint-disable-next-line no-console
console.log(this.form.errors);
m.redraw();
reject(error);
} else {
// eslint-disable-next-line no-console
console.log(error);
}
});
});
}
beforeSubmit() {
if (Object.keys(this.form.data).length > 0) {
this.submit(this.form.data).then(() => this.controller.changeModus('view'));
} else {
this.callback();
this.controller.changeModus('view');
}
}
layout(children, buttonLabel = 'submit', wrapInContainer = true) {
return m('div', { style: { 'background-color': 'white' } }, [
m(Toolbar, { style: { 'background-color': colors.orange } }, [
m(IconButton, {
icon: { svg: { content: m.trust(icons.clear) } },
events: { onclick: () => { this.controller.cancel(); } },
}),
m(ToolbarTitle, `${((this.controller.modus === 'new') ? 'New' : 'Edit')}`
+ ` ${this.resource.charAt(0).toUpperCase()}${this.resource.slice(1, -1)}`),
m(Button, {
className: 'blue-button-filled',
extraWide: true,
label: buttonLabel,
// NOTE: Temporary fix as the `valid` flag of the `Form` instance does
// not work reliably. This ensures that the form can be submitted.
// disabled: !this.form.valid,
events: { onclick: () => { this.beforeSubmit(); } },
}),
]),
...!this.form.schema ? [''] : [
wrapInContainer && m('div.maincontainer', {
style: { height: 'calc(100vh - 130px)', 'overflow-y': 'scroll', padding: '10px' },
}, children),
!wrapInContainer && children,
],
]);
}
}
import m from 'mithril';
import {
IconButton,
TextField,
Toolbar,
ToolbarTitle,
Card,
} from 'polythene-mithril';
import { Chip } from 'amiv-web-ui-components';
export const icons = {
search: '<svg width="24" height="24" viewBox="0 0 24 24"><path d="M15.5 14h-.79l-.28-.27C15.41 12.59 16 11.11 16 9.5 16 5.91 13.09 3 9.5 3S3 5.91 3 9.5 5.91 16 9.5 16c1.61 0 3.09-.59 4.23-1.57l.27.28v.79l5 4.99L20.49 19l-4.99-5zm-6 0C7.01 14 5 11.99 5 9.5S7.01 5 9.5 5 14 7.01 14 9.5 11.99 14 9.5 14z"/></svg>',
......@@ -15,218 +9,29 @@ export const icons = {
ArrowDown: '<svg fill="#000000" height="24" viewBox="0 0 24 24" width="24" xmlns="http://www.w3.org/2000/svg"><path d="M7.41 7.84L12 12.42l4.59-4.58L18 9.25l-6 6-6-6z"/><path d="M0-.75h24v24H0z" fill="none"/></svg>',
iconUsersSVG: '<svg fill="#000000" height="24" viewBox="0 0 24 24" width="24" xmlns="http://www.w3.org/2000/svg"><path d="M3 5v14c0 1.1.89 2 2 2h14c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2H5c-1.11 0-2 .9-2 2zm12 4c0 1.66-1.34 3-3 3s-3-1.34-3-3 1.34-3 3-3 3 1.34 3 3zm-9 8c0-2 4-3.1 6-3.1s6 1.1 6 3.1v1H6v-1z"/><path d="M0 0h24v24H0z" fill="none"/></svg>',
iconEventSVG: '<svg fill="#000000" height="24" viewBox="0 0 24 24" width="24" xmlns="http://www.w3.org/2000/svg"><path d="M9 11H7v2h2v-2zm4 0h-2v2h2v-2zm4 0h-2v2h2v-2zm2-7h-1V2h-2v2H8V2H6v2H5c-1.11 0-1.99.9-1.99 2L3 20c0 1.1.89 2 2 2h14c1.1 0 2-.9 2-2V6c0-1.1-.9-2-2-2zm0 16H5V9h14v11z"/><path d="M0 0h24v24H0z" fill="none"/></svg>',
iconJobsSVG: '<svg fill="#000000" height="24" viewBox="0 0 24 24" width="24" xmlns="http://www.w3.org/2000/svg"><path d="M0 0h24v24H0z" fill="none"/><path d="M20 6h-4V4c0-1.11-.89-2-2-2h-4c-1.11 0-2 .89-2 2v2H4c-1.11 0-1.99.89-1.99 2L2 19c0 1.11.89 2 2 2h16c1.11 0 2-.89 2-2V8c0-1.11-.89-2-2-2zm-6 0h-4V4h4v2z"/></svg>',
ArrowLeft: '<svg fill="#000000" height="24" viewBox="0 0 24 24" width="24" xmlns="http://www.w3.org/2000/svg"><path d="M15.41 16.09l-4.58-4.59 4.58-4.59L14 5.5l-6 6 6 6z"/><path d="M0-.5h24v24H0z" fill="none"/></svg>',
checked: '<svg fill="#000000" height="24" viewBox="0 0 24 24" width="24" xmlns="http://www.w3.org/2000/svg"><path d="M0 0h24v24H0z" fill="none"/><path d="M9 16.2L4.8 12l-1.4 1.4L9 19 21 7l-1.4-1.4L9 16.2z"/></svg>',
group: '<svg fill="#000000" height="24" viewBox="0 0 24 24" width="24" xmlns="http://www.w3.org/2000/svg"><path d="M0 0h24v24H0z" fill="none"/><path d="M16 11c1.66 0 2.99-1.34 2.99-3S17.66 5 16 5c-1.66 0-3 1.34-3 3s1.34 3 3 3zm-8 0c1.66 0 2.99-1.34 2.99-3S9.66 5 8 5C6.34 5 5 6.34 5 8s1.34 3 3 3zm0 2c-2.33 0-7 1.17-7 3.5V19h14v-2.5c0-2.33-4.67-3.5-7-3.5zm8 0c-.29 0-.62.02-.97.05 1.16.84 1.97 1.97 1.97 3.45V19h6v-2.5c0-2.33-4.67-3.5-7-3.5z"/></svg>',
cloud: '<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path d="M0 0h24v24H0z" fill="none"/><path d="M19.35 10.04C18.67 6.59 15.64 4 12 4 9.11 4 6.6 5.64 5.35 8.04 2.34 8.36 0 10.91 0 14c0 3.31 2.69 6 6 6h13c2.76 0 5-2.24 5-5 0-2.64-2.05-4.78-4.65-4.96z"/></svg>',
star: '<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path d="M0 0h24v24H0z" fill="none"/><path d="M12 17.27L18.18 21l-1.64-7.03L22 9.24l-7.19-.61L12 2 9.19 8.63 2 9.24l5.46 4.73L5.82 21z"/><path d="M0 0h24v24H0z" fill="none"/></svg>',
email: '<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path d="M20 4H4c-1.1 0-1.99.9-1.99 2L2 18c0 1.1.9 2 2 2h16c1.1 0 2-.9 2-2V6c0-1.1-.9-2-2-2zm0 4l-8 5-8-5V6l8 5 8-5v2z"/><path d="M0 0h24v24H0z" fill="none"/></svg>',
department: '<svg xmlns="http://www.w3.org/2000/svg" width="48" height="48" viewBox="0 0 48 48"><path d="M0 0h48v48H0z" fill="none"/><path d="M24 14V6H4v36h40V14H24zM12 38H8v-4h4v4zm0-8H8v-4h4v4zm0-8H8v-4h4v4zm0-8H8v-4h4v4zm8 24h-4v-4h4v4zm0-8h-4v-4h4v4zm0-8h-4v-4h4v4zm0-8h-4v-4h4v4zm20 24H24v-4h4v-4h-4v-4h4v-4h-4v-4h16v20zm-4-16h-4v4h4v-4zm0 8h-4v4h4v-4z"/></svg>',
amivWheel: '<svg width="81.059502" height="80.056625" viewBox="0 0 82 82" id="svg2"><metadata id="metadata8"><rdf:RDF><cc:Work rdf:about=""><dc:format>image/svg+xml</dc:format><dc:type rdf:resource="http://purl.org/dc/dcmitype/StillImage" /><dc:title></dc:title></cc:Work></rdf:RDF></metadata><defs id="defs6"><clipPath id="clipPath18"><path d="m 0,849.563 1960.52,0 L 1960.52,0 0,0 0,849.563 z" id="path20" /></clipPath></defs><g transform="matrix(1.25,0,0,-1.25,-16.34525,92.96925)" id="g10"><g transform="scale(0.1,0.1)" id="g12"><g clip-path="url(#clipPath18)" id="g16"><path d="m 566.012,342.883 c -44.453,-61.184 -130.383,-74.797 -191.563,-30.344 -3.969,2.891 -7.719,5.957 -11.289,9.18 l 41.192,29.922 40.945,-56.375 51.351,117.707 37.684,-51.848 44.727,32.5 -40.387,55.598 41.469,30.132 c 19.257,-43.32 15.679,-95.437 -14.129,-136.472 m -235.504,23.465 c -19.887,43.554 -16.5,96.32 13.601,137.75 44.45,61.179 130.383,74.789 191.559,30.336 4.352,-3.161 8.391,-6.579 12.254,-10.125 l -41.762,-30.344 -40.558,55.82 -44.735,-32.5 40.563,-55.828 -0.067,-0.051 -127.726,-12.449 38.203,-52.578 -41.332,-30.031 z m 366.523,24.668 c 1.41,10.644 2.207,21.48 2.207,32.511 0,11.028 -0.797,21.86 -2.207,32.508 l -57.468,8.922 c -2.571,11.469 -6.196,22.711 -10.864,33.57 l 41.211,40.961 c -5.109,9.438 -10.828,18.676 -17.312,27.598 -6.481,8.922 -13.496,17.223 -20.899,25 l -51.679,-26.52 c -4.372,3.84 -8.93,7.532 -13.731,11.02 -4.84,3.512 -9.801,6.73 -14.84,9.719 l 9.258,57.351 c -9.676,4.641 -19.734,8.75 -30.238,12.16 -10.481,3.407 -21.039,5.993 -31.586,7.938 l -26.262,-51.918 c -11.742,1.07 -23.519,1.031 -35.199,-0.066 l -26.293,51.984 c -10.559,-1.945 -21.109,-4.531 -31.598,-7.938 -10.492,-3.41 -20.551,-7.519 -30.23,-12.148 l 9.269,-57.434 c -10.039,-5.925 -19.582,-12.859 -28.511,-20.707 l -51.746,26.559 c -7.407,-7.777 -14.422,-16.07 -20.903,-25 -6.492,-8.922 -12.211,-18.16 -17.32,-27.598 l 41.258,-41.011 c -4.715,-10.922 -8.36,-22.137 -10.887,-33.512 l -57.481,-8.93 c -1.421,-10.64 -2.218,-21.48 -2.218,-32.508 0,-11.031 0.797,-21.855 2.218,-32.511 l 57.563,-8.934 c 2.559,-11.445 6.168,-22.668 10.82,-33.496 L 240.09,307.57 c 5.109,-9.445 10.828,-18.683 17.32,-27.597 6.481,-8.926 13.488,-17.227 20.903,-25 l 51.675,26.523 c 4.41,-3.867 9,-7.59 13.84,-11.105 4.801,-3.485 9.723,-6.688 14.723,-9.657 l -9.258,-57.336 c 9.687,-4.636 19.746,-8.75 30.238,-12.156 10.489,-3.418 21.039,-5.996 31.598,-7.929 l 26.219,51.843 c 11.773,-1.093 23.57,-1.062 35.285,0.039 l 26.238,-51.894 c 10.559,1.945 21.117,4.523 31.598,7.941 10.504,3.406 20.551,7.52 30.238,12.149 l -9.246,57.285 c 10.078,5.957 19.648,12.898 28.617,20.789 l 51.621,-26.492 c 7.403,7.773 14.41,16.074 20.899,25 6.484,8.914 12.203,18.152 17.312,27.597 l -41.148,40.907 c 4.73,10.957 8.379,22.207 10.929,33.644 l 57.34,8.895" id="path30" style="fill:#f03d30;fill-opacity:1;fill-rule:nonzero;stroke:none" /></g></g></g></svg>',
menu: '<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path d="M0 0h24v24H0z" fill="none"/><path d="M3 18h18v-2H3v2zm0-5h18v-2H3v2zm0-7v2h18V6H3z"/></svg>',
studydoc: '<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path d="M0 0h24v24H0z" fill="none"/><path d="M5 13.18v4L12 21l7-3.82v-4L12 17l-7-3.82zM12 3L1 9l11 6 9-4.91V17h2V9L12 3z"/></svg>',
blacklist: '<svg style="width:24px;height:24px" viewBox="0 0 24 24"><path fill="#000000" d="M22.5,2.09C21.6,3 20.13,3.73 18.31,4.25C16.59,2.84 14.39,2 12,2C9.61,2 7.41,2.84 5.69,4.25C3.87,3.73 2.4,3 1.5,2.09C1.53,3.72 2.35,5.21 3.72,6.4C2.63,8 2,9.92 2,12A10,10 0 0,0 12,22A10,10 0 0,0 22,12C22,9.92 21.37,8 20.28,6.4C21.65,5.21 22.47,3.72 22.5,2.09M7.5,8.5L10.5,10C10.5,10.8 9.8,11.5 9,11.5C8.2,11.5 7.5,10.8 7.5,10V8.5M12,17.23C10.25,17.23 8.71,16.5 7.81,15.42L9.23,14C9.68,14.72 10.75,15.23 12,15.23C13.25,15.23 14.32,14.72 14.77,14L16.19,15.42C15.29,16.5 13.75,17.23 12,17.23M16.5,10C16.5,10.8 15.8,11.5 15,11.5C14.2,11.5 13.5,10.8 13.5,10L16.5,8.5V10Z" /></svg>',
error: '<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path fill="none" d="M0 0h24v24H0V0z"/><path d="M11 15h2v2h-2zm0-8h2v6h-2zm.99-5C6.47 2 2 6.48 2 12s4.47 10 9.99 10C17.52 22 22 17.52 22 12S17.52 2 11.99 2zM12 20c-4.42 0-8-3.58-8-8s3.58-8 8-8 8 3.58 8 8-3.58 8-8 8z"/></svg>',
sortingArrow: '<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path d="M7 10l5 5 5-5z"/><path d="M0 0h24v24H0z" fill="none"/></svg>',
};
export class textInput {
constructor({ attrs: { getErrors, name } }) {
// Link the error-getting function from the binding
this.getErrors = () => [];
this.name = name;
if (getErrors) {
this.getErrors = getErrors;
}
this.value = '';
}
view({ attrs }) {
// set display-settings accoridng to error-state
const errors = this.getErrors();
const attributes = Object.assign({}, attrs);
attributes.valid = errors.length === 0;
attributes.error = errors.join(', ');
attributes.onChange = ({ value }) => {
if (value !== this.value) {
this.value = value;
attrs.onChange(this.name, value);
}
};
return m(TextField, attributes);
}
}
export class numInput extends textInput {
view({ attrs }) {
// set display-settings accoridng to error-state
const errors = this.getErrors();
const attributes = Object.assign({}, attrs);
attributes.type = 'number';
attributes.valid = errors.length === 0;
attributes.error = errors.join(', ');
attributes.onChange = ({ value }) => {
if (value !== this.value) {
this.value = value;
attrs.onChange(this.name, parseInt(value, 10));
}
};
return m(TextField, attributes);
}
}
export class datetimeInput {
constructor({ attrs: { getErrors, name, onChange } }) {
// Link the error-getting function from the binding
this.getErrors = () => [];
this.name = name;
if (getErrors) { this.getErrors = getErrors; }
this.value = '';
this.date = false;
this.time = false;
this.onChangeCallback = onChange;
}
onChange() {
if (this.date && this.time) {
const date = new Date(this.date);
const splitted = this.time.split(':');
date.setHours(splitted[0]);
date.setMinutes(splitted[1]);
if (this.onChangeCallback) {
// the ISO String contains 3 positions for microseconds, this kind of fomrat
// is not accepted by the API
this.onChangeCallback(this.name, `${date.toISOString().slice(0, -5)}Z`);
}
}
}
view({ attrs: { label, value } }) {
// set display-settings accoridng to error-state
const errors = this.getErrors();
const initialValue = value || 'T';
const initialDate = initialValue.split('T')[0];
const initialTime = initialValue.split('T')[1].substring(0, 5);
const date = {
type: 'date',
style: {
width: '150px',
float: 'left',
},
onChange: ({ value: newDate }) => {
if (newDate !== this.date) {
this.date = newDate;
this.onChange();
}
},
valid: errors.length === 0,
error: errors.join(', '),
value: this.date || initialDate,
};
const time = {
type: 'time',
style: {
width: '100px',
},
onChange: ({ value: newTime }) => {
if (newTime !== this.time) {
this.time = newTime;
this.onChange();
}
},
valid: errors.length === 0,
value: this.time || initialTime,
};
return m('div', [
m(TextField, {
label,
disabled: true,
style: {
width: '200px',
float: 'left',
},
}),
m(TextField, date),
m(TextField, time),
]);
}
}
export class fileInput {
constructor({ attrs: { getErrors, name, onChange } }) {
// Link the error-getting function from the binding
this.getErrors = () => [];
this.name = name;
if (getErrors) { this.getErrors = getErrors; }
this.onChangeCallback = onChange;
this.file = null;
}
view({ attrs: { label, accept } }) {
// set display-settings accoridng to error-state
const errors = this.getErrors();
const image = {
type: 'file',
accept,
onchange: ({ target: { files: [file] } }) => {
if (file !== this.file) {
// as we only accept one file, it is always the first element
// of the list
this.file = file;
console.log(this.file);
this.onChangeCallback(this.name, this.file);
}
},
};
return m('div', { style: { display: 'flex' } }, [
m(TextField, {
label,
disabled: true,
style: {
width: '200px',
float: 'left',
},
valid: errors.length === 0,
error: errors.join(', '),
}),
m('input', image),
]);
}
}
// a card that is usually collapsed, but pops out when clicked on the title
export class DropdownCard {
constructor() {
this.expand = false;
}
view({ attrs: { title }, children }) {
const toolbar = m(Toolbar, {
compact: true,
events: { onclick: () => { this.expand = !this.expand; } },
}, [
m(IconButton, {
icon: {
svg: m.trust(this.expand ? icons.ArrowDown : icons.ArrowRight),
},
}),
m(ToolbarTitle, { text: title }),
]);
const card = m(Card, {
style: { padding: '10px', 'font-size': '15sp' },
content: children.map(child => ({ any: { content: child } })),
});
return m('div', [toolbar, this.expand ? card : '']);
}
}
// Property as specified by material design: small, grey title and larger
// darker content text below
// attrs is the title, children the text
// therefore, you can call it with m(Property, title, text)
export class Property {
view({ attrs: { title, style }, children }) {
return m('div', { style }, [
view({ attrs: { title, leftAlign = true, ...restAttrs }, children }) {
return m('div', { style: { margin: '5px' }, ...restAttrs }, [
m('span', {
style: {
'margin-top': '10px',
......@@ -234,7 +39,12 @@ export class Property {
color: 'rgba(0, 0, 0, 0.54)',
},
}, m.trust(title)),
m('p', { style: { color: 'rgba(0, 0, 0, 0.87)' } }, children),
m('p', {
style: {
color: 'rgba(0, 0, 0, 0.87)',
'text-align': leftAlign ? 'left' : 'right',
},
}, children),
]);
}
}
......@@ -262,23 +72,18 @@ export class submitButton {
}
}
export const BackButton = {
view: ({ attrs }) => m(IconButton, {
icon: { svg: m.trust(icons.back) },
ink: false,
events: { onclick: attrs.leave },
}),
};
export const ClearButton = {
view: ({ attrs }) => m(IconButton, {
icon: { svg: m.trust(icons.clear) },
ink: false,
events: { onclick: attrs.clear },
}),
};
export const SearchIcon = {
view: () => m(IconButton, {
icon: { svg: m.trust(icons.search) },
inactive: true,
}),
};
export class FilterChip {
view({ attrs: { selected = false, onclick = () => { } }, children }) {
return m(Chip, {
'margin-left': '5px',
'margin-right': '5px',
background: selected ? '#aaaaaa' : '#dddddd',
svgBackground: '#aaaaaa',
textColor: selected ? '#000000' : '#999999',
svgColor: '#000000',
svg: selected ? icons.checked : null,
onclick,
onClick: onclick,
}, children);
}
}
import { ResourceHandler } from '../auth';
import m from 'mithril';
import { IconButton, Toolbar, Dialog, Button } from 'polythene-mithril';
import { ButtonCSS } from 'polythene-css';
import { colors } from '../style';
import { loadingScreen } from '../layout';
import { icons } from './elements';
const m = require('mithril');
ButtonCSS.addStyle('.itemView-edit-button', {
color_light_background: colors.light_blue,
color_light_text: 'white',
});
ButtonCSS.addStyle('.itemView-delete-button', {
color_light_text: colors.amiv_red,
color_light_border: colors.amiv_red,
});
export default class ItemView {
/* Basic class show a data item
/* Basic class to show a data item
*
* Required:
* - call constructor with 'resource'
* - either make sure m.route.params('id') exists or set this.id in
* constructor
* - gets attribute 'controller' when rendered
*/
constructor(resource, embedded) {
this.data = null;
this.id = m.route.param('id');
this.handler = new ResourceHandler(resource);
this.embedded = embedded || {};
constructor({ attrs: { controller, onDelete } }) {
this.controller = controller;
this.handler = this.controller.handler;
this.data = this.controller.data;
this.resource = this.controller.resource;
if (!onDelete) this.onDelete = () => { m.route.set(`/${controller.resource}`); };
else this.onDelete = onDelete;
}
oninit() {
this.handler.getItem(this.id, this.embedded).then((item) => {
this.data = item;
m.redraw();
delete() {
Dialog.show({
body: 'Are you sure you want to delete this item?',
backdrop: true,
footerButtons: [
m(Button, {
label: 'Cancel',
events: { onclick: () => Dialog.hide() },
}),
m(Button, {
label: 'Delete',
events: {
onclick: () => {
Dialog.hide();
this.controller.handler.delete(this.data).then(this.onDelete);
},
},
})],
});
}
layout(children, buttons = []) {
if (!this.controller || !this.controller.data) return m(loadingScreen);
// update the data reference
this.data = this.controller.data;
return m('div', [
m(Toolbar, [
this.data._links.self.methods.indexOf('PATCH') > -1 && m('div', {
style: { width: 'calc(100% - 48px)' },
}, m('div.pe-button-row', [
m(Button, {
element: 'div',
className: 'itemView-edit-button',
label: `Edit ${this.resource.charAt(0).toUpperCase()}${this.resource.slice(1, -1)}`,
events: { onclick: () => { this.controller.changeModus('edit'); } },
}),
m(Button, {
label: `Delete ${this.resource.charAt(0).toUpperCase()}${this.resource.slice(1, -1)}`,
className: 'itemView-delete-button',
border: true,
events: { onclick: () => this.delete() },
}),
...buttons,
])),
m(IconButton, {
style: { 'margin-left': 'auto', 'margin-right': '0px' },
icon: { svg: { content: m.trust(icons.clear) } },
events: { onclick: () => { this.controller.cancel(); } },
}),
]),
m('div', {
style: { height: 'calc(100vh - 130px)', 'overflow-y': 'scroll' },
}, children),
]);
}
}
import m from 'mithril';
import Stream from 'mithril/stream';
import {
List, ListTile, Search, IconButton, Button, Shadow, Toolbar,
ToolbarTitle,
} from 'polythene-mithril';
import infinite from 'mithril-infinite';
import { debounce } from '../utils';
import { icons, BackButton, ClearButton, SearchIcon } from './elements';
class SearchField {
oninit() {
this.value = Stream('');
this.setInputState = Stream();
// const clear = () => setInputState()({ value: '', focus: false});
this.clear = () => this.value('');
this.leave = () => this.value('');
}
view({ state, attrs }) {
// incoming value and focus added for result list example:
const value = attrs.value !== undefined ? attrs.value : state.value();
const onCancel = attrs.onCancel !== undefined ? attrs.onCancel : () => {};
const ExitButton = {
view() {
return m(Button, {
label: 'Cancel',
className: 'blue-button',
events: { onclick: onCancel },
});
},
};
return m(Search, Object.assign(
{},
{
textfield: {
label: 'type here',
onChange: (newState) => {
state.value(newState.value);
state.setInputState(newState.setInputState);
// onChange callback added for result list example:
if (attrs.onChange) attrs.onChange(newState, state.setInputState);
},
value,
defaultValue: attrs.defaultValue,
},
buttons: {
none: {
before: m(SearchIcon),
after: m(ExitButton),
},
focus: {
before: m(BackButton, { leave: state.leave }),
after: m(ExitButton),
},
focus_dirty: {
before: m(BackButton, { leave: state.leave }),
after: m(ClearButton, { clear: state.clear }),
},
dirty: {
before: m(BackButton, { leave: state.leave }),
after: m(ClearButton, { clear: state.clear }),
},
},
before: m(Shadow),
},
attrs,
));
}
}
export default class SelectList {
constructor({ attrs: { listTileAttrs } }) {
this.selected = null;
this.showList = false;
this.searchValue = '';
this.listTileAttrs = listTileAttrs;
}
item() {
return (data) => {
const attrs = {
compactFront: true,
hoverable: true,
className: 'themed-list-tile',
events: {
onclick: () => { this.selected = data; this.showList = false; },
},
};
// Overwrite default attrs
Object.assign(attrs, this.listTileAttrs(data));
return m(ListTile, attrs);
};
}
view({
attrs: {
controller,
onSubmit = () => {},
onCancel = () => {},
selectedText,
},
}) {
return m('div', [
this.selected ? m(Toolbar, { compact: true, style: { background: 'rgb(78, 242, 167)' } }, [
m(IconButton, {
icon: { svg: m.trust(icons.clear) },
ink: false,
events: { onclick: () => { this.selected = null; } },
}),
m(ToolbarTitle, { text: selectedText(this.selected) }),
m(Button, {
label: 'Submit',
className: 'blue-button',
events: {
onclick: () => {
onSubmit(this.selected);
this.selected = null;
controller.setSearch('');
controller.refresh();
},
},
}),
]) : m(SearchField, Object.assign({}, {
style: { background: 'rgb(78, 242, 167)' },
onChange: ({ value, focus }) => {
if (focus) {
this.showList = true;
}
if (value !== this.searchValue) {
// if we always update the search value, this would also happen
// immidiately in the moment where we click on the listitem.
// Then, the list get's updated before the click is registered.
// So, we make sure this state change is due to value change and
// not due to focus change.
this.searchValue = value;
controller.setSearch(value);
debounce(() => { controller.refresh(); }, 500);
}
},
onCancel,
defaultValue: '',
})),
(this.showList && !this.selected) ? m(List, {
className: 'scrollTable',
tiles: m(infinite, controller.infiniteScrollParams(this.item())),
}) : null,
]);
}
}
import m from 'mithril';
import infinite from 'mithril-infinite';
import { List, ListTile, Toolbar, Search, Button } from 'polythene-mithril';
import { List, ListTile, Toolbar, Search, Button, Icon } from 'polythene-mithril';
import 'polythene-css';
import { styler } from 'polythene-core-css';
import { debounce } from '../utils';
import { FilterChip, icons } from './elements';
const tableStyles = [
{
'.tabletool': {
display: 'grid',
height: '100%',
'grid-template-rows': '48px calc(100% - 48px)',
},
'.toolbar': {
'grid-row': 1,
display: 'flex',
},
'.scrollTable': {
'grid-row': 2,
'background-color': 'white',
},
'.tableTile': {
padding: '10px',
'border-bottom': '1px solid rgba(0, 0, 0, 0.12)',
......@@ -41,12 +32,46 @@ export default class TableView {
* Works with embedded resources, i.e. if you add
* { embedded: { event: 1 } } to a list of eventsignups,
* you can display event.title_de as a table key
* - filters: list of list of objects, each inner list is a group of mutual exclusive
* filters.
* A filter can have properties 'name', 'query' and optionally 'selected' for
* the initial selection state.
*/
constructor({ attrs: { keys, tileContent, clickOnRows = true } }) {
constructor({
attrs: {
keys,
titles,
tileContent,
filters = null,
clickOnRows = (data) => { m.route.set(`/${data._links.self.href}`); },
clickOnTitles = (controller, title) => { controller.setSort([[title, 1]]); },
},
}) {
this.search = '';
this.tableKeys = keys;
this.tableKeys = keys || [];
this.tableTitles = titles;
this.tileContent = tileContent;
this.clickOnRows = clickOnRows;
this.clickOnTitles = clickOnTitles;
this.searchValue = '';
// make a copy of filters so we can toggle the selected status
this.filters = filters ? filters.map(
filterGroup => filterGroup.map(filter => Object.assign({}, filter)),
) : null;
}
/*
* initFilterIdxs lets you specify the filters that are active at initialization.
* They are specified as index to the nexted filterGroups array.
*/
oninit({ attrs: { controller, initFilterIdxs = [] } }) {
if (this.filters) {
initFilterIdxs.forEach((filterIdx) => {
this.filters[filterIdx[0]][filterIdx[1]].selected = true;
});
// update filters in controller
controller.setFilter(this.getSelectedFilterQuery());
}
}
getItemData(data) {
......@@ -65,12 +90,12 @@ export default class TableView {
item() {
return data => m(ListTile, {
className: 'themed-list-tile',
hoverable: true,
hoverable: this.clickOnRows,
compactFront: true,
compact: true,
content: m('div', {
onclick: () => {
if (this.clickOnRows) { m.route.set(`/${data._links.self.href}`); }
if (this.clickOnRows) this.clickOnRows(data);
},
className: 'tableTile',
style: { width: '100%', display: 'flex' },
......@@ -79,18 +104,46 @@ export default class TableView {
}
getSelectedFilterQuery() {
// produce a list of queries from the filters that are currently selected
const selectedFilters = [].concat(...this.filters.map(filterGroup => filterGroup.filter(
filter => filter.selected === true,
).map(filter => filter.query)));
// now merge all queries into one new object
return Object.assign({}, ...selectedFilters);
}
// Display an arrow at the table title that allows sorting
arrowOrNot(controller, title) {
const titleText = title.width ? title.text : title;
if (!controller.sort) return false;
let i;
for (i = 0; i < this.tableTitles.length; i += 1) {
const tableTitlei = this.tableTitles[i].width
? this.tableTitles[i].text : this.tableTitles[i];
if (tableTitlei === titleText) break;
}
return this.tableKeys[i] === controller.sort[0][0];
}
view({
attrs: {
controller,
titles,
onAdd = () => {},
onAdd = false,
buttons = [],
tableHeight = false,
},
}) {
const updateList = debounce(() => {
controller.refresh();
}, 500);
return m('div.tabletool', [
return m('div.tabletool', {
style: {
display: 'grid',
height: '100%',
'grid-template-rows': this.filters
? '48px 40px calc(100% - 120px)' : '48px calc(100% - 80px)',
'background-color': 'white',
},
}, [
m(Toolbar, {
className: 'toolbar',
compact: true,
......@@ -99,33 +152,95 @@ export default class TableView {
textfield: {
label: 'Search',
onChange: ({ value }) => {
controller.setSearch(value);
updateList();
// this is called not only if the value changes, but also the focus.
// we only want to change the search of the value is changed, therefore we
// have to track changes in the search value
if (value !== this.searchValue) controller.debouncedSearch(value);
this.searchValue = value;
},
},
fullWidth: false,
}),
m(Button, {
...buttons.map(b => m(Button, {
className: 'blue-button',
style: {
'margin-right': '5px',
},
events: {
onclick: b.onclick,
},
label: b.text,
})),
onAdd ? m(Button, {
className: 'blue-button',
borders: true,
label: 'Add',
events: { onclick: () => { onAdd(); } },
}),
}) : '',
],
}),
// please beare with this code, it is the only way possible to track the selection
// status of all the filters of the same group and make sure that they are really
// mutually exclusive (that way when you click on one filter in the group, the other
// ones in this group will be deselected)
this.filters && m('div', {
style: {
height: '50px',
'overflow-x': 'auto',
'overflow-y': 'hidden',
'white-space': 'nowrap',
padding: '0px 5px',
},
}, [].concat(['Filters: '], ...[...this.filters.keys()].map(
filterGroupIdx => [...this.filters[filterGroupIdx].keys()].map((filterIdx) => {
const thisFilter = this.filters[filterGroupIdx][filterIdx];
return m(FilterChip, {
selected: thisFilter.selected,
onclick: () => {
if (!thisFilter.selected) {
// set all filters in this group to false
[...this.filters[filterGroupIdx].keys()].forEach((i) => {
this.filters[filterGroupIdx][i].selected = false;
});
// now set this filter to selected
this.filters[filterGroupIdx][filterIdx].selected = true;
} else {
this.filters[filterGroupIdx][filterIdx].selected = false;
}
// update filters in controller
controller.setFilter(this.getSelectedFilterQuery());
},
}, thisFilter.name);
}),
))),
m(List, {
className: 'scrollTable',
style: {
'grid-row': this.filters ? 3 : 2,
...tableHeight ? { height: tableHeight } : {},
},
tiles: [
m(ListTile, {
className: 'tableTile',
hoverable: this.clickOnTitles,
content: m(
'div',
{ style: { width: '100%', display: 'flex' } },
// Either titles is a list of titles that are distributed equally,
// or it is a list of objects with text and width
titles.map(title => m('div', {
style: { width: title.width || `${98 / this.tableKeys.length}%` },
}, title.width ? title.text : title)),
titles.map((title, i) => m(
'div', {
onclick: () => {
if (this.clickOnTitles && this.tableKeys[i]) {
this.clickOnTitles(controller, this.tableKeys[i]);
}
},
style: { width: title.width || `${98 / this.tableKeys.length}%` },
},
[title.width ? title.text : title,
this.arrowOrNot(controller, title)
? m(Icon, { svg: { content: m.trust(icons.sortingArrow) } }) : ''],
)),
),
}),
m(infinite, controller.infiniteScrollParams(this.item())),
......@@ -134,4 +249,3 @@ export default class TableView {
]);
}
}
const webpack = require('webpack');
const CompressionPlugin = require('compression-webpack-plugin');
// Start with dev config
const config = require('./webpack.config.js');
// Remove local server and code map
config.devServer = undefined;
//config.devtool = '';
config.mode = 'production';
config.optimization = {
usedExports: true,
sideEffects: true,
splitChunks: {
chunks: 'async', // TODO possibly set to all
automaticNameDelimiter: '-',
name: true,
},
};
// Add optimization plugins
config.plugins.push(
new CompressionPlugin({
algorithm: 'gzip',
test: /\.js$|\.css$|\.html$/,
threshold: 10240,
minRatio: 0.8,
}),
);
// Replace local with development server config
config.resolve.alias.networkConfig = `${__dirname}/src/networkConfig.dev.json`;
module.exports = config;
......@@ -38,26 +38,33 @@ const config = {
rules: [
{
test: /\.js$/,
enforce: "pre",
enforce: 'pre',
exclude: /node_modules/,
loader: 'eslint-loader',
options: {
emitWarning: true // don't fail the build for linting errors
}
emitWarning: true, // don't fail the build for linting errors
},
},
{
test: /\.js$/, // Check for all js files
include: [
path.resolve(__dirname, './src'),
path.resolve(__dirname, 'node_modules/@material'),
path.resolve(__dirname, 'node_modules/amiv-web-ui-components'),
],
use: [{
loader: 'babel-loader',
options: {
presets: ['env'],
plugins: ['transform-object-rest-spread'],
use: [
{
loader: 'babel-loader',
options: {
//presets: [['@babel/preset-env', { targets: 'last 2 years' }]],
presets: ['@babel/preset-env'],
plugins: [
'@babel/plugin-proposal-object-rest-spread',
//'@babel/plugin-syntax-dynamic-import',
],
},
},
}],
],
},
{
test: /\.(png|jpe?g|gif|svg)$/,
......@@ -71,7 +78,7 @@ const config = {
],
},
{
test: /node_modules\/announcetool.*\.(html|css)$/,
test: /\.(html)$/,
use: [
{
loader: 'file-loader',
......@@ -81,6 +88,10 @@ const config = {
},
],
},
{
test: /\.css$/,
use: ['style-loader', 'css-loader'],
},
],
},
......
const webpack = require('webpack');
const CompressionPlugin = require('compression-webpack-plugin');
// Start with dev config
const config = require('./webpack.config.js');
// Remove development server and code map
config.devServer = undefined;
config.devtool = '';
config.mode = 'production';
config.optimization = {
usedExports: true,
sideEffects: true,
splitChunks: {
chunks: 'async', // TODO possibly set to all
automaticNameDelimiter: '-',
name: true,
},
};
// Add optimization plugins
config.plugins.push(
new CompressionPlugin({
algorithm: 'gzip',
test: /\.js$|\.css$|\.html$/,
threshold: 10240,
minRatio: 0.8,
}),
);
// Replace development with production config
config.resolve.alias.networkConfig = `${__dirname}/src/networkConfig.local.json`;
module.exports = config;
......@@ -7,22 +7,31 @@ const config = require('./webpack.config.js');
// Remove development server and code map
config.devServer = undefined;
config.devtool = '';
config.mode = 'production';
config.optimization = {
usedExports: true,
sideEffects: true,
splitChunks: {
chunks: 'async', // TODO possibly set to all
automaticNameDelimiter: '-',
name: true,
},
};
// Add optimization plugins
config.plugins = [
new webpack.optimize.UglifyJsPlugin(),
new webpack.optimize.AggressiveMergingPlugin(),
config.plugins.push(
new CompressionPlugin({
asset: '[path].gz[query]',
algorithm: 'gzip',
test: /\.js$|\.css$|\.html$/,
threshold: 10240,
minRatio: 0.8,
}),
];
);
// Replace development with production config
config.resolve.alias.config = `${__dirname}/config.${process.env.BUILD_CONFIG_POSTFIX}.js`;
// Replace development with production config
config.resolve.alias.networkConfig = `${__dirname}/src/networkConfig.prod.json`;
module.exports = config;
const webpack = require('webpack');
const CompressionPlugin = require('compression-webpack-plugin');
// Start with dev config
const config = require('./webpack.config.js');
// Remove local server and code map
config.devServer = undefined;
//config.devtool = '';
config.mode = 'production';
config.optimization = {
usedExports: true,
sideEffects: true,
splitChunks: {
chunks: 'async', // TODO possibly set to all
automaticNameDelimiter: '-',
name: true,
},
};
// Add optimization plugins
config.plugins.push(
new CompressionPlugin({
algorithm: 'gzip',
test: /\.js$|\.css$|\.html$/,
threshold: 10240,
minRatio: 0.8,
}),
);
// Replace local with staging server config
config.resolve.alias.networkConfig = `${__dirname}/src/networkConfig.staging.json`;
module.exports = config;