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 {
IconButton,
TextField,
Toolbar,
ToolbarTitle,
Card,
Icon,
} 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>',
......@@ -24,228 +17,21 @@ export const icons = {
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.style = Object.assign({
'margin-top': '-10px',
'margin-bottom': '-10px',
}, attributes.style);
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.style = Object.assign({
'margin-top': '-10px',
'margin-bottom': '-10px',
}, attributes.style);
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, ...kwargs }, 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 content = [{ any: { content: toolbar } }];
if (this.expand) {
content.push(...children.map(child => ({
any: {
style: {
padding: '0 10px 10px 10px',
},
content: child,
},
})));
}
return m(Card, { content, ...kwargs });
}
}
// 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, ...restAttrs }, children }) {
return m('div', restAttrs, [
view({ attrs: { title, leftAlign = true, ...restAttrs }, children }) {
return m('div', { style: { margin: '5px' }, ...restAttrs }, [
m('span', {
style: {
'margin-top': '10px',
......@@ -253,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),
]);
}
}
......@@ -271,34 +62,6 @@ export class selectGroup {
}
}
export class chip {
view({ attrs: { svg, color = '#000000', background = '#dddddd', ...styleAttrs }, children }) {
return m('div', {
style: {
height: '32px',
'background-color': '#ffffff',
'border-radius': '16px',
// if there is a border, things are weirdly shifted
padding: styleAttrs.border ? '3px 8px 4px 6px' : '4px 8px',
display: 'inline-flex',
...styleAttrs,
},
}, [
svg && m('div', {
style: {
'background-color': background,
'border-radius': '12px',
margin: '0px 4px 0px -2px',
height: '24px',
width: '24px',
padding: '2px 2px 2px 4px',
},
}, m(Icon, { svg: { content: m.trust(svg) }, size: 'small', style: { color } })),
m('div', { style: { 'line-height': '24px' } }, children),
]);
}
}
export class submitButton {
view({ attrs: { args, active, text } }) {
const argsCopy = args;
......@@ -309,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 m from 'mithril';
import { Toolbar, Dialog, Button } from 'polythene-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';
ButtonCSS.addStyle('.itemView-edit-button', {
color_light_background: colors.light_blue,
......@@ -21,8 +23,8 @@ export default class ItemView {
*/
constructor({ attrs: { controller, onDelete } }) {
this.controller = controller;
this.data = this.controller.data;
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;
......@@ -49,23 +51,36 @@ export default class ItemView {
});
}
layout(children) {
if (!this.controller || !this.controller.data) return '';
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, 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(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(Button, {
label: `Delete ${this.resource.charAt(0).toUpperCase()}${this.resource.slice(1, -1)}`,
className: 'itemView-delete-button',
border: true,
events: { onclick: () => this.delete() },
}),
])),
]),
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, Toolbar,
ToolbarTitle,
} from 'polythene-mithril';
import infinite from 'mithril-infinite';
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();
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: {
focus: {
before: m(BackButton, { leave: state.leave }),
},
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 }),
},
},
},
attrs,
));
}
}
export default class SelectList {
constructor({ attrs: { listTileAttrs, onSelect = false } }) {
this.showList = false;
this.searchValue = '';
this.listTileAttrs = listTileAttrs;
this.onSelect = onSelect;
// initialize the Selection
this.selected = null;
}
onupdate({ attrs: { selection = null } }) {
// make it possible to change the selection from outside, e.g. to set the field to an
// existing group moderator
if (selection) this.selected = selection;
}
item() {
return (data) => {
const attrs = {
compactFront: true,
hoverable: true,
className: 'themed-list-tile',
events: {
onclick: () => {
if (this.onSelect) { this.onSelect(data); }
this.selected = data;
this.showList = false;
},
},
};
// Overwrite default attrs
Object.assign(attrs, this.listTileAttrs(data));
return m(ListTile, attrs);
};
}
view({
attrs: {
controller,
onSubmit = false,
onCancel = false,
selectedText,
},
}) {
return m('div', [
m(Toolbar, { compact: true, style: { background: 'rgb(78, 242, 167)' } }, this.selected ? [
m(IconButton, {
icon: { svg: m.trust(icons.clear) },
ink: false,
events: {
onclick: () => {
if (this.onSelect) { this.onSelect(null); }
this.selected = null;
},
},
}),
m(ToolbarTitle, { text: selectedText(this.selected) }),
onSubmit ? 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 }) => {
// onChange is called either if the value or focus fo the SearchField
// changes.
// At value change we want to update the search
// at focus changt we hie the list of results. As focus change also
// happens while clicking on an item in the list of results, the list
// is hidden after a short Timeout that has to be sufficiently long
// to register the onclick of the listitem. Can be a problem for different
// OS and browsers.
if (focus) {
this.showList = true;
} else if (!focus) {
// don't close the list immidiately, as 'out of focus' could
// also mean that the user is clicking on a list item
setTimeout(() => { this.showList = false; m.redraw(); }, 500);
}
if (value !== this.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.debouncedSearch(value);
}
},
})),
onCancel ? m(Button, {
label: 'cancel',
className: 'blue-button',
events: { onclick: onCancel },
}) : '',
]),
(this.showList && !this.selected) ? m(List, {
style: { height: '400px', 'background-color': 'white' },
tiles: m(infinite, controller.infiniteScrollParams(this.item())),
}) : '',
]);
}
}
import m from 'mithril';
import '@material/select/dist/mdc.select.css';
import '@material/select/dist/mdc.select';
//@import 'material/select/dist/mdc.select.css';
import stream from 'mithril/stream';
import { Menu, List, ListTile } from 'polythene-mithril';
/**
* form element to select from multiple options.
*
* Copied from
* https://github.com/ArthurClemens/polythene/blob/master/docs/components/mithril/menu.md
*
* @class SelectOptions (name)
*/
export class SelectOptions {
oninit({ name }) {
this.isOpen = stream(false);
this.selectedIndex = stream(0);
// target has to be a unique ID, therefore we take the name of the assigned value
this.target = name;
}
view({ attrs: { name, options, onChange } }) {
const isOpen = this.isOpen();
const selectedIndex = this.selectedIndex();
return m('div', { style: { position: 'relative' } }, [
m(Menu, {
target: `#${this.target}`,
show: isOpen,
hideDelay: 0.240,
didHide: () => this.isOpen(false),
size: 5,
content: m(List, {
tiles: options.map((setting, index) =>
m(ListTile, {
title: setting,
ink: true,
hoverable: true,
events: {
onclick: () => {
this.selectedIndex(index);
onChange(name, options[index]);
},
},
})),
}),
}),
m(ListTile, {
id: this.target,
title: options[selectedIndex],
events: { onclick: () => this.isOpen(true) },
}),
]);
}
}
export class MDCSelect {
view({ attrs: { options, name, onchange = () => {}, ...kwargs } }) {
return m('div.mdc-select', { style: { height: '41px' } }, [
m('select.mdc-select__native-control', {
style: { 'padding-top': '10px' },
onchange: ({ target: { value } }) => { onchange(value); },
...kwargs,
}, options.map(option => m('option', { value: option }, option)),
),
m('label.mdc-floating-label', ''),
m('div.mdc-line-ripple'),
]);
}
}
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 { FilterChip, icons } from './elements';
const tableStyles = [
{
'.tabletool': {
display: 'grid',
height: '100%',
'grid-template-rows': '48px calc(100% - 78px)',
'background-color': 'white',
},
'.toolbar': {
'grid-row': 1,
display: 'flex',
},
'.scrollTable': {
'grid-row': 2,
},
'.tableTile': {
padding: '10px',
'border-bottom': '1px solid rgba(0, 0, 0, 0.12)',
......@@ -40,19 +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,
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) {
......@@ -85,15 +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 = false,
buttons = [],
tableHeight = false,
},
}) {
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,
......@@ -111,6 +161,16 @@ export default class TableView {
},
fullWidth: false,
}),
...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,
......@@ -119,20 +179,68 @@ export default class TableView {
}) : '',
],
}),
// 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: tableHeight ? { height: tableHeight } : {},
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())),
......@@ -141,4 +249,3 @@ export default class TableView {
]);
}
}
// Start with prod config
const config = require('./webpack.config.prod.js');
// Replace development with production 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 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)$/,
......
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.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;