Skip to content
Snippets Groups Projects
Commit 93ea9bce authored by Hermann's avatar Hermann
Browse files

breaking changes: seperate controller for viewing data items

this comes together with a more unified layout of edit views
the new structure simplifies data handling and needs less api communication, making it more responsive
parent 7a2f7dda
No related branches found
No related tags found
No related merge requests found
...@@ -2,31 +2,28 @@ import m from 'mithril'; ...@@ -2,31 +2,28 @@ import m from 'mithril';
import { TextField } from 'polythene-mithril'; import { TextField } from 'polythene-mithril';
import SelectList from '../views/selectList'; import SelectList from '../views/selectList';
import DatalistController from '../listcontroller'; import DatalistController from '../listcontroller';
import { EditView, EditLayout } from '../views/editView'; import EditView from '../views/editView';
export default class NewGroup extends EditView { export default class NewGroup extends EditView {
constructor(vnode) { constructor(vnode) {
super(vnode, 'groups', { moderator: 1 }); super(vnode);
this.userController = new DatalistController( this.userController = new DatalistController(
'users', {}, 'users', {},
['firstname', 'lastname', 'email', 'nethz'], ['firstname', 'lastname', 'email', 'nethz'],
); );
console.log(this.data);
} }
view({ attrs: { onCancel = () => {} }}) { beforeSubmit() {
if (!this.data) return ''; // exchange moderator object with string of id
return m(EditLayout, { const { moderator } = this.data;
title: 'Edit Group', if (moderator) { this.data.moderator = `${moderator._id}`; }
onSubmit: () => { this.submit();
// excahgne moderator object with string of id }
const { moderator } = this.data;
if (moderator) { this.data.moderator = `${moderator._id}`; } view() {
this.submit(); return this.layout([
},
onCancel,
}, m('div.mywrapper', [
m('h3', 'Add a New Group'),
...this.renderPage({ ...this.renderPage({
name: { type: 'text', label: 'Group Name' }, name: { type: 'text', label: 'Group Name' },
allow_self_enrollment: { allow_self_enrollment: {
...@@ -50,6 +47,6 @@ export default class NewGroup extends EditView { ...@@ -50,6 +47,6 @@ export default class NewGroup extends EditView {
}, },
})), })),
]), ]),
])); ]);
} }
} }
import m from 'mithril';
import viewGroup from './viewGroup';
import newGroup from './newGroup';
import ItemTool from '../views/itemTool';
export default class GroupTool extends ItemTool {
constructor() {
super('groups', { moderator: 1 });
}
detailView() {
return m(viewGroup, { handler: this.handler, data: this.data });
}
editView() {
return m(newGroup, { onCancel: () => { this.modus = 'view'; } });
}
}
import m from 'mithril';
import viewGroup from './viewGroup';
import editGroup from './editGroup';
import ItemController from '../itemcontroller';
export default class GroupItem {
constructor() {
this.controller = new ItemController('groups', { moderator: 1 });
}
view() {
if (!this.controller || !this.controller.data) return '';
if (this.controller.modus !== 'view') return m(editGroup, { controller: this.controller });
return m(viewGroup, { controller: this.controller });
}
}
...@@ -3,7 +3,7 @@ import { Card } from 'polythene-mithril'; ...@@ -3,7 +3,7 @@ import { Card } from 'polythene-mithril';
import DatalistController from '../listcontroller'; import DatalistController from '../listcontroller';
class GroupItem { class GroupListItem {
view({ attrs: { name, _id } }) { view({ attrs: { name, _id } }) {
return m('div', { return m('div', {
style: { style: {
...@@ -34,7 +34,7 @@ export default class GroupList { ...@@ -34,7 +34,7 @@ export default class GroupList {
return m( return m(
'div', { style: { display: 'flex', 'flex-wrap': 'wrap' } }, 'div', { style: { display: 'flex', 'flex-wrap': 'wrap' } },
this.data.map(item => m(GroupItem, item)), this.data.map(item => m(GroupListItem, item)),
m('div', { m('div', {
style: { style: {
padding: '20px', padding: '20px',
......
...@@ -163,44 +163,44 @@ class EmailTable { ...@@ -163,44 +163,44 @@ class EmailTable {
} }
} }
export default class viewGroup { export default class viewGroup extends ItemView {
view({ attrs: { handler, data } }) { view() {
return m('div.maincontainer', [ return this.layout([
// this div is the title line // this div is the title line
m('div.maincontainer', [ m('div.maincontainer', [
m('h1', { style: { 'margin-top': '0px', 'margin-bottom': '0px' } }, data.name), m('h1', { style: { 'margin-top': '0px', 'margin-bottom': '0px' } }, this.data.name),
data.moderator ? m(Property, { this.data.moderator ? m(Property, {
title: 'Moderator', title: 'Moderator',
onclick: () => { m.route.set(`/users/${data.moderator._id}`); }, onclick: () => { m.route.set(`/users/${this.data.moderator._id}`); },
}, `${data.moderator.firstname} ${data.moderator.lastname}`) : '', }, `${this.data.moderator.firstname} ${this.data.moderator.lastname}`) : '',
]), ]),
m('div.viewcontainer', [ m('div.viewcontainer', [
// now-column layout: This first column are the members // now-column layout: This first column are the members
m('div.viewcontainercolumn', [ m('div.viewcontainercolumn', [
m('h4', 'Members'), m('h4', 'Members'),
m(MembersTable, { group: data._id }), m(MembersTable, { group: this.data._id }),
]), ]),
// the second column contains receive_from and forward_to emails // the second column contains receive_from and forward_to emails
m('div.viewcontainercolumn', [ m('div.viewcontainercolumn', [
m(EmailTable, { m(EmailTable, {
list: data.receive_from || [], list: this.data.receive_from || [],
onSubmit: (newItem) => { onSubmit: (newItem) => {
const oldList = data.receive_from || []; const oldList = this.data.receive_from || [];
handler.patch({ this.handler.patch({
_id: data._id, _id: this.data._id,
_etag: data._etag, _etag: this.data._etag,
receive_from: [...oldList, newItem], receive_from: [...oldList, newItem],
}); });
}, },
onRemove: (item) => { onRemove: (item) => {
const oldList = data.receive_from; const oldList = this.data.receive_from;
// remove the first occurence of the given item-string // remove the first occurence of the given item-string
const index = oldList.indexOf(item); const index = oldList.indexOf(item);
if (index !== -1) { if (index !== -1) {
oldList.splice(index, 1); oldList.splice(index, 1);
handler.patch({ this.handler.patch({
_id: data._id, _id: this.data._id,
_etag: data._etag, _etag: this.data._etag,
receive_from: oldList, receive_from: oldList,
}).then(() => m.redraw()); }).then(() => m.redraw());
} }
......
import m from 'mithril'; import m from 'mithril';
import { OauthRedirect } from './auth'; import { OauthRedirect } from './auth';
import GroupList from './groups/overview'; import GroupList from './groups/list';
import GroupView from './groups/groupTool'; import GroupItem from './groups/item';
import { UserModal, UserTable, NewUser } from './users/userTool'; import { UserModal, UserTable, NewUser } from './users/userTool';
import { MembershipView } from './membershipTool'; import { MembershipView } from './membershipTool';
import NewGroup from './groups/newGroup';
import EventTable from './events/table'; import EventTable from './events/table';
import newEvent from './events/newEvent'; import newEvent from './events/newEvent';
import EventModal from './events/eventModal'; import EventModal from './events/eventModal';
...@@ -38,8 +37,8 @@ m.route(root, '/users', { ...@@ -38,8 +37,8 @@ m.route(root, '/users', {
'/draftevent': layoutWith(eventDraft), '/draftevent': layoutWith(eventDraft),
'/eventwithexport': layoutWith(eventWithExport), '/eventwithexport': layoutWith(eventWithExport),
'/groups': layoutWith(GroupList), '/groups': layoutWith(GroupList),
'/groups/:id': layoutWith(GroupView), '/groups/:id': layoutWith(GroupItem),
'/newgroup': layoutWith(NewGroup), '/newgroup': layoutWith(GroupItem),
'/oauthcallback': OauthRedirect, '/oauthcallback': OauthRedirect,
'/joboffers': layoutWith(jobTable), '/joboffers': layoutWith(jobTable),
'/newjoboffer': layoutWith(newJob), '/newjoboffer': layoutWith(newJob),
......
import m from 'mithril';
import { ResourceHandler } from './auth';
export default class ItemController {
constructor(resource, embedded) {
this.resource = resource;
this.id = m.route.param('id');
if (this.id) {
this.modus = 'view';
} else {
this.modus = 'new';
this.data = {};
}
this.handler = new ResourceHandler(resource, false);
this.embedded = embedded || {};
if (this.id) {
this.handler.getItem(this.id, this.embedded).then((item) => {
this.data = item;
m.redraw();
});
}
}
post(data) {
return new Promise((resolve, reject) => {
this.handler.post(data).then((response) => {
this.id = response._id;
this.changeModus('view');
}).catch(reject);
});
}
patch(data, formData = false) {
return new Promise((resolve, reject) => {
this.handler.patch(data, formData).then(() => { this.changeModus('view'); }).catch(reject);
});
}
cancel() {
if (this.modus === 'edit') this.changeModus('view');
if (this.modus === 'new') m.route.set(`/${this.resource}`);
}
changeModus(newModus) {
this.modus = newModus;
if (this.modus === 'view') {
// reload item to current state, patches do not return embeddinds...
this.handler.getItem(this.id, this.embedded).then((item) => {
this.data = item;
m.redraw();
});
}
}
}
...@@ -15,23 +15,7 @@ const objectNameForResource = { ...@@ -15,23 +15,7 @@ const objectNameForResource = {
events: 'Event', events: 'Event',
}; };
export class EditLayout { export default class EditView extends ItemView {
view({ attrs: { title, onSubmit = () => {}, onCancel = () => {} }, children }) {
return m('div', { style: { 'background-color': 'white' } }, [
m(Toolbar, [
m(IconButton, {
icon: { svg: { content: m.trust(icons.clear) } },
events: { onclick: onCancel },
}),
m(ToolbarTitle, title),
m(Button, { label: 'submit', events: { onclick: onSubmit } }),
]),
children,
]);
}
}
export class EditView extends ItemView {
/* Extension of ItemView to edit a data item /* Extension of ItemView to edit a data item
* *
* Requires: * Requires:
...@@ -43,10 +27,9 @@ export class EditView extends ItemView { ...@@ -43,10 +27,9 @@ export class EditView extends ItemView {
* - bind(attrs): binds a form-field against this.data * - bind(attrs): binds a form-field against this.data
* - submit * - submit
*/ */
constructor(vnode, resource, embedded, valid = true) { constructor(vnode, valid = true) {
super(resource, embedded); super(vnode);
this.changed = false; this.changed = false;
this.resource = resource;
// state for validation // state for validation
this.valid = valid; this.valid = valid;
...@@ -56,28 +39,12 @@ export class EditView extends ItemView { ...@@ -56,28 +39,12 @@ export class EditView extends ItemView {
allErrors: true, allErrors: true,
}); });
this.errors = {}; this.errors = {};
this.data = {}; // copy a local version of the controller data to manipulate before submission
// (changes will therefore not be applied if edit is cancelled)
// callback when edit is finished this.data = Object.assign({}, this.controller.data);
if (vnode.attrs.onfinish) this.callback = vnode.attrs.onfinish;
else {
this.callback = (item) => {
console.log(item);
if (item) m.route.set(`/${resource}/${item._id}`);
else m.route.set(`/${resource}`);
};
}
} }
oninit() { 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 // load schema
m.request(`${apiUrl}/docs/api-docs`).then((schema) => { m.request(`${apiUrl}/docs/api-docs`).then((schema) => {
const objectSchema = schema.definitions[ const objectSchema = schema.definitions[
...@@ -170,15 +137,13 @@ export class EditView extends ItemView { ...@@ -170,15 +137,13 @@ export class EditView extends ItemView {
submit(formData = false) { submit(formData = false) {
if (Object.keys(this.data).length > 0) { if (Object.keys(this.data).length > 0) {
let request; let request;
if (this.id) { if (this.controller.modus === 'edit') {
// if id is known, this is a patch to an existing item // if id is known, this is a patch to an existing item
request = this.handler.patch(this.data, formData); request = this.controller.patch(this.data, formData);
} else { } else {
request = this.handler.post(this.data); request = this.controller.post(this.data);
} }
request.then((response) => { request.catch((error) => {
this.callback(response);
}).catch((error) => {
console.log(error); console.log(error);
// Process the API error // Process the API error
const { response } = error; const { response } = error;
...@@ -193,7 +158,26 @@ export class EditView extends ItemView { ...@@ -193,7 +158,26 @@ export class EditView extends ItemView {
} }
}); });
} else { } else {
this.callback(); this.controller.changeModus('view');
} }
} }
beforeSubmit() {
this.submit();
}
layout(children) {
return m('div', { style: { 'background-color': 'white' } }, [
m(Toolbar, { style: { 'background-color': '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, { label: 'submit', events: { onclick: () => { this.beforeSubmit(); } } }),
]),
m('div.maincontainer', children),
]);
}
} }
import m from 'mithril';
import {
RaisedButton,
Dialog,
Button,
Toolbar,
IconButton,
ToolbarTitle,
} from 'polythene-mithril';
import { ResourceHandler } from '../auth';
import { icons } from './elements';
export default class ItemTool {
constructor(resource, embedded) {
this.id = m.route.param('id');
if (this.id) {
this.modus = 'view';
} else {
this.modus = 'new';
}
this.handler = new ResourceHandler(resource, false, (newData) => { this.data = newData; });
this.embedded = embedded || {};
// by default, we go to the resource list after deletion of an item
// this may be overwritten with other behaviour
this.onDelete = () => { console.log('on delete'); m.route.set(`/${resource}`); };
}
oninit() {
if (this.id) {
this.handler.getItem(this.id, this.embedded).then((item) => {
this.data = item;
m.redraw();
});
}
}
creationView() {
console.log(`creation for ${this.resource} not found`);
return null;
}
/*
* Should display this.data
*/
detailView() {
console.log(`detail view for ${this.resource} not found`);
return null;
}
/*
* To edit an existing item. Has to implement this.onSubmit, which is called
*/
editView() {
console.log(`edit view for ${this.resource} not found`);
return null;
}
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.handler.delete(this.data).then(this.onDelete);
},
},
})],
});
}
view() {
if (this.modus === 'new') {
return this.creationView();
} else if (this.modus === 'edit') {
return this.editView();
}
if (!this.data) return '';
return m('div', { style: { height: '100%', 'overflow-y': 'scroll' } }, [
m('div', { style: { display: 'flex' } }, [
m(RaisedButton, {
element: 'div',
label: 'Edit',
border: true,
events: { onclick: () => { this.modus = 'edit'; } },
}),
m(RaisedButton, {
className: 'red-row-button',
label: 'Delete',
border: true,
events: { onclick: () => this.delete() },
}),
]),
this.detailView(),
]);
}
}
import m from 'mithril'; import m from 'mithril';
import { ResourceHandler } from '../auth'; import { Dialog, Button, RaisedButton } from 'polythene-mithril';
import { Dialog, Button } from 'polythene-mithril';
export default class ItemView { export default class ItemView {
/* Basic class to show a data item /* Basic class to show a data item
* *
* Required: * Required:
* - call constructor with 'resource' * - gets attribute 'controller' when rendered
* - either make sure m.route.params('id') exists or set this.id in
* constructor
*/ */
constructor(resource, embedded) { constructor({ attrs: { controller, onDelete } }) {
this.data = null; this.controller = controller;
this.id = m.route.param('id'); this.data = this.controller.data;
this.handler = new ResourceHandler(resource); this.handler = this.controller.handler;
this.embedded = embedded || {}; this.resource = this.controller.resource;
if (!onDelete) this.onDelete = () => { m.route.set(`/${controller.resource}`); };
else this.onDelete = onDelete;
} }
oninit() { delete() {
this.handler.getItem(this.id, this.embedded).then((item) => { Dialog.show({
this.data = item; body: 'Are you sure you want to delete this item?',
m.redraw(); 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) {
if (!this.controller || !this.controller.data) return '';
return m('div', { style: { height: '100%', 'overflow-y': 'scroll' } }, [
m('div', { style: { display: 'flex' } }, [
m(RaisedButton, {
element: 'div',
label: 'Edit',
border: true,
events: { onclick: () => { this.controller.changeModus('edit'); } },
}),
m(RaisedButton, {
className: 'red-row-button',
label: 'Delete',
border: true,
events: { onclick: () => this.delete() },
}),
]),
children,
]);
}
} }
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment