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 425 additions and 515 deletions
......@@ -2,21 +2,35 @@ import m from 'mithril';
import Stream from 'mithril/stream';
import { ResourceHandler } from './auth';
import { debounce } from './utils';
// IS NOT IN USE
// IS NOT IN USE
// IS NOT IN USE
// IS NOT IN USE
// IS NOT IN USE
// IS NOT IN USE
// IS NOT IN USE
// IS NOT IN USE
// IS NOT IN USE
// IS NOT IN USE
export default class RelationlistController {
/*
* Controller for a list of data embedding a relationship.
* The secondary api endpoint is embedded into the list items of the primary endpoint results.
* Searches are applied to both resources, queries and filters need to be specified for each.
*
* @param {bool} includeWithoutRelation - Specifies what to do in case the relation is undefined.
* By default, such items are excluded, if true they will be included into the list.
*/
constructor(
constructor({
primary,
secondary,
query = {},
searchKeys = false,
searchKeys = [],
secondaryQuery = {},
secondarySearchKeys = false,
) {
secondarySearchKeys = [],
includeWithoutRelation = false,
}) {
this.handler = new ResourceHandler(primary, searchKeys);
this.handler2 = new ResourceHandler(secondary, secondarySearchKeys);
this.secondaryKey = secondary.slice(0, -1);
......@@ -24,6 +38,8 @@ export default class RelationlistController {
this.query2 = secondaryQuery || {};
this.filter = null;
this.filter2 = null;
this.sort = null;
this.includeWithoutRelation = includeWithoutRelation;
// state pointer that is counted up every time the table is refreshed so
// we can tell infinite scroll that the data-version has changed.
this.stateCounter = Stream(0);
......@@ -46,6 +62,7 @@ export default class RelationlistController {
item,
pageData: pageNum => this.getPageData(pageNum),
pageKey: pageNum => `${pageNum}-${this.stateCounter()}`,
maxPages: this.totalPages ? this.totalPages : undefined,
};
}
......@@ -54,22 +71,21 @@ export default class RelationlistController {
// resource for the items specified by the relation in the primary resource
// We apply Queries for both resources seperately.
const query = Object.assign({}, this.query);
query.max_results = 10;
query.max_results = 50;
query.page = pageNum;
query.where = { ...this.filter, ...this.query.where };
console.log(query.search);
query.sort = this.sort || query.sort;
return new Promise((resolve) => {
this.handler.get(query).then((data) => {
// update total number of pages
this.totalPages = Math.ceil(data._meta.total / 10);
this.totalPages = Math.ceil(data._meta.total / 50);
console.log(data._items.map(item => item._id));
const itemsWithoutRelation = data._items.filter(item => !(this.secondaryKey in item));
const itemsWithRelation = data._items.filter(item => (this.secondaryKey in item));
const query2 = Object.assign({}, this.query2);
query2.where = {
_id: { $in: data._items.map(item => item[this.secondaryKey]) },
_id: { $in: itemsWithRelation.map(item => item[this.secondaryKey]) },
...this.filter2,
...this.query2.where,
};
......@@ -78,15 +94,20 @@ export default class RelationlistController {
const secondaryIds = secondaryData._items.map(item => item._id);
// filter the primary list to only include those items that have a relation to
// the queried secondary IDs
const filteredPrimaries = data._items.filter(item =>
secondaryIds.includes(item[this.secondaryKey]));
// now return the list of filteredPrimaries with the secondary data embedded
resolve(filteredPrimaries.map((item) => {
const filteredPrimaries = itemsWithRelation.filter(
item => secondaryIds.includes(item[this.secondaryKey]),
);
// embed the secondary data
const embeddedList = filteredPrimaries.map((item) => {
const itemCopy = Object.assign({}, item);
itemCopy[this.secondaryKey] = secondaryData._items.find(relItem =>
relItem._id === item[this.secondaryKey]);
itemCopy[this.secondaryKey] = secondaryData._items.find(
relItem => relItem._id === item[this.secondaryKey],
);
return itemCopy;
}));
});
// now return the list of filteredPrimaries with the secondary data embedded
if (this.includeWithoutRelation) resolve([...embeddedList, ...itemsWithoutRelation]);
else resolve(embeddedList);
});
});
});
......@@ -103,7 +124,6 @@ export default class RelationlistController {
// save totalPages as a constant to avoid race condition with pages added during this
// process
const { totalPages } = this;
console.log(totalPages);
if (totalPages === 1) {
resolve(firstPage);
......@@ -114,8 +134,10 @@ export default class RelationlistController {
this.getPageData(pageNum).then((newPage) => {
pages[pageNum] = newPage;
// look if all pages were collected
const missingPages = Array.from(new Array(totalPages), (x, i) => i + 1).filter(i =>
!(i in pages));
const missingPages = Array.from(new Array(totalPages), (x, i) => i + 1).filter(
i => !(i in pages),
);
// eslint-disable-next-line no-console
console.log('missingPages', missingPages);
if (missingPages.length === 0) {
// collect all the so-far loaded pages in order (sorted keys)
......@@ -151,5 +173,9 @@ export default class RelationlistController {
this.query = Object.assign({}, query, { search: this.query.search });
this.refresh();
}
}
setSort(sort) {
this.sort = sort;
this.refresh();
}
}
{
"apiUrl": "https://api-dev.amiv.ethz.ch/",
"events": {
"keyDescriptors": {
"title_de": "German Title",
"title_en": "English Title",
"location": "Location",
"show_website": "Event is shown on the website",
"priority": "Priority",
"time_end": "Ending time",
"time_register_end": "Deadline for registration",
"time_start": "Starting time",
"spots": "Spots available",
"allow_email_signup": "Event open for non-AMIV members",
"price": "Price",
"signup_count": "Signed-up participants",
"catchphrase_en": "Catchphrase in English. Announce and Website.",
"catchphrase_de": "Schlagwort auf Deutsch",
"description_de": "Beschreibung auf Deutsch",
"description_en": "Description in English",
"img_banner": "Banner as png",
"img_poster": "Poster as png",
"img_thumbnail": "Thumbnail as png",
"show_infoscreen": "Does the event show on the infoscreen?",
"img_infoscreen": "Infoscreen as png",
"time_advertising_end": "Advertisment ends on",
"time_advertising_start": "Advertisement starts on",
"selection_strategy": "TODO what is this?",
"show_announce": "Does it belong to announce?"
},
"tableKeys": [
"title_de",
"time_start",
"time_end",
"time_register_end",
"show_website",
"priority"
],
"notPatchableKeys": [
"signup_count"
],
"searchKeys": [
"title_de",
"title_en",
"location"
]
},
"users": {
"keyDescriptors": {
"legi": "Legi Number",
"firstname": "First Name",
"lastname": "Last Name",
"rfid": "RFID",
"phone": "Phone",
"nethz": "nethz Account",
"gender": "Gender",
"department": "Department",
"email": "Email"
},
"tableKeys": [
"firstname",
"lastname",
"nethz",
"legi",
"membership"
],
"searchKeys": [
"firstname",
"lastname",
"nethz",
"legi",
"email"
],
"notPatchableKeys": [
"password_set"
]
},
"joboffers":{
"keyDescriptors": {
"company": "Company",
"email": "Email",
"description_en": "Job description",
"description_de": "Job Beschreibung",
"logo": "Logo as png",
"pdf": "PDF provided by company",
"time_end": "Application deadline",
"title_de": "Stelle auf Deutsch",
"title_en": "Position title in English",
"show_website": "Is the job listed on the website?",
"_id":"Job ID."
},
"tableKeys": [
"title_de",
"time_end",
"show_website"
]
},
"groups": {
"keyDescriptors": {
"name": "Name"
},
"searchKeys": ["name"],
"patchableKeys": ["name"]
},
"groupmemberships": {
"patchableKeys": ["user", "group"]
},
"eventsignups": {
"patchableKeys": ["event"],
"tableKeys": [
"_created",
"user.lastname",
"user.firstname",
"email"
],
"searchKeys": []
},
"sessions": {
"searchKeys": []
},
"studydocuments": {
"searchKeys": [
"title",
"lecture",
"professor",
"author",
"uploader"
]
}
}
import m from 'mithril';
import { RadioGroup } from 'polythene-mithril';
import { FileInput } from 'amiv-web-ui-components';
import { Button, List, ListTile, Snackbar } from 'polythene-mithril';
import EditView from '../views/editView';
import { getSchema } from '../auth';
export default class editDoc extends EditView {
// constructor zu file upload
constructor(vnode) {
// remove the files list as it is impossible to validate
const docSchema = getSchema().definitions['Study Document'];
delete docSchema.properties.files;
super(vnode);
if (!('files' in this.form.data)) {
this.form.data.files = [{ name: 'add file' }];
}
}
beforeSubmit() {
// check if there are files uploaded
const files = [];
Object.keys(this.form.data).forEach((key) => {
if (key.startsWith('new_file_') && this.form.data[key]) {
files.push(this.form.data[key]);
delete this.form.data[key];
}
});
// in case that there are no files, eject an error
if (this.controller.modus === 'new' && files.length === 0) {
Snackbar.show({ title: 'You need to upload at least one file.' });
this.form.valid = false;
return;
}
// now post all together as FormData
const submitData = new FormData();
Object.keys(this.form.data).forEach((key) => {
if (key !== 'files') submitData.append(key, this.form.data[key]);
});
files.forEach((file) => { submitData.append('files', file); });
this.submit(submitData).then(() => this.controller.changeModus('view'));
}
view() {
return this.layout([
m('h3', 'Add a New Studydocument'),
...this.form.renderPage({
// uploader
author: { type: 'text', label: 'Author' },
files: { type: 'text', label: 'File' }, // buggy only singel file possible
lecture: { type: 'text', label: 'Lecture' },
title: { type: 'text', label: 'Title' },
professor: { type: 'text', label: 'Professor' },
course_year: { type: 'number', lable: 'Year' }, // semester unterscheidung, plausibility
this.form._renderField('semester', {
...this.form.schema.properties.semester,
style: { width: '100px' },
}),
// department //drop-down-list
m('div', 'Semester'), // formatieren
m(RadioGroup, {
name: 'semester',
buttons: [
{ value: '1', label: '1.', defaultChecked: this.form.data.gender === '1' },
{ value: '2', label: '2', defaultChecked: this.form.data.gender === '2' },
{ value: '3', label: '3', defaultChecked: this.form.data.gender === '3' },
{ value: '4', label: '4', defaultChecked: this.form.data.gender === '4' },
{ value: '5', label: '5+', defaultChecked: this.form.data.gender === '5' },
],
onChange: ({ value }) => { console.log(value); this.form.data.gender = value; },
}),
m(RadioGroup, {
name: 'type',
buttons: [{
value: 'exames',
label: 'exames',
defaultChecked: this.form.data.gender === 'exames',
}, {
value: 'cheat_sheet',
label: 'cheat sheet',
defaultChecked: this.form.data.gender === 'cheat_sheet',
}, {
value: 'lecture_documents',
label: 'lecture documents',
defaultChecked: this.form.data.gender === 'lecture_documents',
}, {
value: 'exercise',
label: 'exercise',
defaultChecked: this.form.data.gender === 'exercise',
}],
onChange: ({ value }) => { console.log(value); this.form.data.gender = value; },
}),
...this.form.renderSchema(['type', 'lecture', 'title', 'course_year', 'professor', 'author']),
// file upload: work in progress, so far all files get deleted with a patch
m('div', [
'WARNING: Files added here will remove all files currently uploaded. If you want to add',
'/edit a file in this studydoc, reupload all other files as well.',
m(List, {
tiles: [...this.form.data.files.entries()].map(numAndFile => m(ListTile, {
content: [
m(FileInput, this.form.bind({
name: `new_file_${numAndFile[0]}`,
label: numAndFile[1].name,
})),
],
})),
}),
// additional file
m(Button, {
label: 'Additional File',
className: 'blue-button',
border: true,
events: { onclick: () => { this.form.data.files.push({ name: 'add file' }); } },
}),
]),
]);
}
}
......@@ -10,7 +10,9 @@ export default class studydocItem {
}
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(editDoc, { controller: this.controller });
return m(viewDoc, { controller: this.controller });
}
......
import m from 'mithril';
import { DatalistController } from 'amiv-web-ui-components';
import { studydocuments as config } from '../resourceConfig.json';
import TableView from '../views/tableView';
import { ResourceHandler } from '../auth';
......@@ -8,29 +7,36 @@ import { ResourceHandler } from '../auth';
/* Table of all studydocuments */
export default class StudydocTable {
constructor() {
this.handler = new ResourceHandler('studydocuments', config.tableKeys);
this.handler = new ResourceHandler('studydocuments');
this.ctrl = new DatalistController((query, search) => this.handler.get({ search, ...query }));
}
getItemData(data) {
return [
m('div', { style: { width: 'calc(100% - 30em)' } }, data.title),
m('div', { style: { width: '6em' } }, data.department.toUpperCase()),
m('div', { style: { width: '6em' } }, data.semester),
m('div', { style: { width: '18em' } }, data.lecture),
m('div', { style: { width: 'calc(100% - 36em)' } }, data.title),
m('div', { style: { width: '8em' } }, data.author),
m('div', { style: { width: '4em' } }, data.course_year),
m('div', { style: { width: '4em' } }, data.semester),
m('div', { style: { width: '10em' } }, data.lecture),
m('div', { style: { width: '10em' } }, data.files.map((file) => {
const splittedFilenames = file.name.split('.');
return `.${splittedFilenames[splittedFilenames.length - 1]} `;
})),
];
}
view() {
return m(TableView, {
controller: this.ctrl,
keys: config.tableKeys,
keys: ['title', 'author', 'course_year', 'semester', 'lecture'],
tileContent: this.getItemData,
titles: [
{ text: 'Titel', width: 'calc(100% - 30em)' },
{ text: 'Department', width: '6em' },
{ text: 'Semester', width: '6em' },
{ text: 'Lecture', width: '18em' },
{ text: 'Title', width: 'calc(100% - 36em)' },
{ text: 'Author', width: '8em' },
{ text: 'Year', width: '4em' },
{ text: 'Sem.', width: '4em' },
{ text: 'Lecture', width: '10em' },
{ text: 'Files', width: '10em' },
],
onAdd: () => { m.route.set('/newstudydocument'); },
});
......
......@@ -41,4 +41,3 @@ export default class viewDoc extends ItemView {
]));
}
}
......@@ -33,6 +33,14 @@ ButtonCSS.addStyle('.red-row-button', {
margin_h: 0,
});
ButtonCSS.addStyle('.blue-row-button', {
color_light_text: 'white',
color_light_background: colors.light_blue,
padding_h: 0,
font_size: 12,
margin_h: 0,
});
CardCSS.addStyle('.pe-card', {
border_radius: '4',
});
......@@ -64,6 +72,9 @@ const style = [
p: {
margin: '0',
},
a: {
color: 'rgba(0, 0, 0, 0.87)',
},
},
];
styler.add('containers', style);
import m from 'mithril';
import { RadioGroup } from 'amiv-web-ui-components';
import { TextInput } from 'amiv-web-ui-components';
import EditView from '../views/editView';
export default class UserEdit extends EditView {
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() {
const style = 'display: inline-block; vertical-align: top; padding-right: 80px';
return this.layout([
...this.form.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(
'div', { style },
m(RadioGroup, {
name: 'Membership',
default: this.form.data.membership,
values: [
{
value: 'none',
label: 'No Member',
},
{
value: 'regular',
label: 'Regular AMIV Member',
},
{
value: 'extraordinary',
label: 'Extraordinary Member',
},
{
value: 'honorary',
label: 'Honorary Member',
},
],
onchange: (value) => {
this.form.data.membership = value;
this.form.validate();
},
}),
),
m(
'div', { style },
m(RadioGroup, {
name: 'Sex',
default: this.form.data.gender,
values: [
{ value: 'female', label: 'Female' },
{ value: 'male', label: 'Male' },
],
onchange: (value) => {
this.form.data.gender = value;
this.form.validate();
},
}),
),
m(
'div', { style },
m(RadioGroup, {
name: 'Departement',
default: this.form.data.department,
values: [
{ value: 'itet', label: 'ITET' },
{ value: 'mavt', label: 'MAVT' },
{ value: null, label: 'None' },
],
onchange: (value) => {
this.form.data.department = value;
this.form.validate();
},
}),
),
...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']),
]);
}
}
......@@ -3,7 +3,6 @@ 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 ItemController from '../itemcontroller';
import { loadingScreen } from '../layout';
import { ResourceHandler } from '../auth';
......@@ -14,7 +13,9 @@ export class UserItem {
}
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(EditUser, { controller: this.controller });
return m(ViewUser, { controller: this.controller });
}
......@@ -28,11 +29,13 @@ export class UserTable {
{ 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' } },
......
import m from 'mithril';
import { Card, Toolbar, ToolbarTitle, Button } from 'polythene-mithril';
import { ListSelect, DatalistController } from 'amiv-web-ui-components';
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 RelationlistController from '../relationlistcontroller';
import { ResourceHandler } from '../auth';
import { chip, icons, Property } from '../views/elements';
import { icons, Property } from '../views/elements';
import { colors } from '../style';
export default class UserView extends ItemView {
constructor(vnode) {
super(vnode);
// a controller to handle the groupmemberships of this user
this.groupmemberships = new RelationlistController('groupmemberships', 'groups', {
where: { user: this.data._id },
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 RelationlistController('eventsignups', 'events', {
where: { user: this.data._id },
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.groupHandler = new ResourceHandler('groups', ['name']);
this.groupController = new DatalistController((query, search) =>
this.groupHandler.get({ search, ...query }));
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.data._id } })
.then((data) => {
const groupIds = data._items.map(item => item.group);
this.groupcontroller.setQuery({
this.groupController.setQuery({
where: { _id: { $nin: groupIds } },
});
});
......@@ -44,28 +45,28 @@ export default class UserView extends ItemView {
view() {
const stdMargin = { margin: '5px' };
let membership = m(chip, {
let membership = m(Chip, {
svg: icons.clear,
svgBackground: colors.amiv_red,
...stdMargin,
style: stdMargin,
}, 'No Member');
if (this.data.membership === 'regular') {
membership = m(chip, {
membership = m(Chip, {
svg: icons.checked,
svgBackground: colors.green,
...stdMargin,
style: stdMargin,
}, 'Regular Member');
} else if (this.data.membership === 'extraordinary') {
membership = m(chip, {
membership = m(Chip, {
svg: icons.checked,
svgBackground: colors.green,
...stdMargin,
style: stdMargin,
}, 'Extraordinary Member');
} else if (this.data.membership === 'honorary') {
membership = m(chip, {
membership = m(Chip, {
svg: icons.star,
svgBackground: colors.orange,
...stdMargin,
style: stdMargin,
}, 'Honorary Member');
}
......@@ -95,16 +96,19 @@ export default class UserView extends ItemView {
m('h1', `${this.data.firstname} ${this.data.lastname}`),
membership,
this.data.department && m(
chip,
{ svg: icons.department, ...stdMargin },
Chip,
{ svg: icons.department, style: stdMargin },
this.data.department,
),
this.data.gender && m(chip, { margin: '5px' }, this.data.gender),
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),
this.data.legi && m(Property, { title: 'Legi', style: stdMargin }, this.data.legi),
this.data.rfid && m(Property, { title: 'RFID', style: stdMargin }, this.data.rfid),
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),
]),
]),
......@@ -119,7 +123,7 @@ export default class UserView extends ItemView {
tableHeight: '175px',
controller: this.eventsignups,
tileContent: item => m('div', item.event.title_en || item.event.title_de),
titles: ['event'],
titles: ['Event'],
clickOnRows: (data) => { m.route.set(`/events/${data.event._id}`); },
filters: [[{
name: 'upcoming',
......@@ -149,7 +153,7 @@ export default class UserView extends ItemView {
tableHeight: '225px',
controller: this.groupmemberships,
keys: ['group.name', 'expiry'],
titles: ['groupname', 'expiry'],
titles: ['Group Name', 'Expires'],
clickOnRows: (data) => { m.route.set(`/groups/${data.group._id}`); },
}),
]),
......@@ -165,10 +169,13 @@ export default class UserView extends ItemView {
this.sessionsHandler.get({
where: { user: this.data._id },
}).then((response) => {
response._items.forEach((session) => {
this.sessionsHandler.delete(session);
});
console.log(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 m from 'mithril';
import { IconButton, Toolbar, ToolbarTitle, Button } from 'polythene-mithril';
import { Form } from 'amiv-web-ui-components';
// eslint-disable-next-line import/extensions
import { apiUrl } from 'networkConfig';
import ItemView from './itemView';
import { icons } from './elements';
import { colors } from '../style';
// Mapper for resource vs schema-object names
const objectNameForResource = {
users: 'User',
groupmembershipds: 'Groupmembership',
groups: 'Group',
eventsignups: 'Eventsignup',
events: 'Event',
};
export default class EditView extends ItemView {
/**
* Extension of ItemView to edit a data item
......@@ -34,14 +23,8 @@ export default class EditView extends ItemView {
// 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, Object.assign({}, this.controller.data));
}
oninit() {
// load schema
m.request(`${apiUrl}/docs/api-docs`).then((schema) => {
this.form.setSchema(schema.definitions[objectNameForResource[this.resource]]);
}).catch((error) => { console.log(error); });
this.form = new Form({}, validInitially, 4, Object.assign({}, this.controller.data));
this.form.setSchema(JSON.parse(JSON.stringify(this.handler.schema)));
}
/**
......@@ -51,17 +34,18 @@ export default class EditView extends ItemView {
* JSON. Necessary in cases where files are included in the
* changes.
*/
submit(formData = false) {
if (Object.keys(this.form.data).length > 0) {
submit(data) {
return new Promise((resolve, reject) => {
let request;
if (this.controller.modus === 'edit') {
// if id is known, this is a patch to an existing item
request = this.controller.patch(this.form.data, formData);
// this is a patch to an existing item
request = this.controller.patch(data);
} else {
request = this.controller.post(this.form.data);
request = this.controller.post(data);
}
request.catch((error) => {
console.log(error);
request.then((response) => {
resolve(response);
}).catch((error) => {
// Process the API error
if ('_issues' in error) {
// there are problems with some fields, display them
......@@ -69,41 +53,51 @@ export default class EditView extends ItemView {
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);
}
});
} else {
this.controller.changeModus('view');
}
});
}
beforeSubmit() {
this.submit();
if (Object.keys(this.form.data).length > 0) {
this.submit(this.form.data).then(() => this.controller.changeModus('view'));
} else {
this.controller.changeModus('view');
}
}
layout(children, buttonLabel = 'submit') {
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(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,
disabled: !this.form.valid,
// 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(); } },
}),
]),
m('div.maincontainer', {
style: { height: 'calc(100vh - 130px)', 'overflow-y': 'scroll', padding: '10px' },
}, children),
...!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 { 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>',
......@@ -20,6 +20,9 @@ export const icons = {
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>',
};
// Property as specified by material design: small, grey title and larger
......@@ -27,8 +30,8 @@ export const icons = {
// 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',
......@@ -36,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),
]);
}
}
......@@ -54,46 +62,6 @@ export class selectGroup {
}
}
export class chip {
view({
attrs: {
svg,
background = '#ffffff',
textColor = '#000000',
svgColor = '#000000',
svgBackground = '#dddddd',
...attrs
},
children,
}) {
return m('div', {
style: {
height: '32px',
'background-color': background,
color: textColor,
'border-radius': '16px',
// if there is a border, things are weirdly shifted
padding: attrs.border ? '3px 8px 4px 6px' : '4px 8px',
display: 'inline-flex',
...attrs,
},
...attrs.onclick ? { onclick: attrs.onclick } : {},
}, [
svg && m('div', {
style: {
'background-color': svgBackground,
'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: { svgColor } })),
m('div', { style: { 'line-height': '24px' } }, children),
]);
}
}
export class submitButton {
view({ attrs: { args, active, text } }) {
const argsCopy = args;
......@@ -103,3 +71,19 @@ export class submitButton {
return m('div.btn', argsCopy, text);
}
}
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);
}
}
......@@ -53,9 +53,13 @@ export default class ItemView {
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', { style: { width: 'calc(100% - 48px)' } }, m('div.pe-button-row', [
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',
......
import m from 'mithril';
import '@material/select/dist/mdc.select.css';
import '@material/select/dist/mdc.select';
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 { chip, icons } from './elements';
import { FilterChip, icons } from './elements';
const tableStyles = [
{
......@@ -21,22 +21,6 @@ const tableStyles = [
styler.add('tableview', tableStyles);
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,
}, children);
}
}
export default class TableView {
/* Shows a table of objects for a given API resource.
*
......@@ -56,19 +40,24 @@ export default class TableView {
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.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;
this.filters = filters ? filters.map(
filterGroup => filterGroup.map(filter => Object.assign({}, filter)),
) : null;
}
/*
......@@ -117,18 +106,32 @@ 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)));
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,
},
}) {
......@@ -136,8 +139,8 @@ export default class TableView {
style: {
display: 'grid',
height: '100%',
'grid-template-rows': this.filters ?
'48px 40px calc(100% - 78px)' : '48px calc(100% - 78px)',
'grid-template-rows': this.filters
? '48px 40px calc(100% - 120px)' : '48px calc(100% - 80px)',
'background-color': 'white',
},
}, [
......@@ -158,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,
......@@ -172,19 +185,20 @@ export default class TableView {
// ones in this group will be deselected)
this.filters && m('div', {
style: {
height: '40px',
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) => {
}, [].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
// set all filters in this group to false
[...this.filters[filterGroupIdx].keys()].forEach((i) => {
this.filters[filterGroupIdx][i].selected = false;
});
......@@ -197,7 +211,8 @@ export default class TableView {
controller.setFilter(this.getSelectedFilterQuery());
},
}, thisFilter.name);
})))),
}),
))),
m(List, {
className: 'scrollTable',
style: {
......@@ -207,14 +222,25 @@ export default class TableView {
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())),
......@@ -223,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,12 +38,12 @@ 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
......@@ -52,13 +52,19 @@ const config = {
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;