From d4dd473a8c55f7135049cc4da6c775fbd6e8ec04 Mon Sep 17 00:00:00 2001
From: Hermann Blum <hermannsblum@yahoo.de>
Date: Sat, 27 Jan 2018 19:57:48 +0100
Subject: [PATCH] split tables and lists between View and Controller

In order to be able to refresh single data lists (e.g. the groupmemberships after
a membership got added), this commit disentangles data from view and enables the
outside userTool to refresh single lists.
---
 src/listcontroller.js   | 34 +++++++++++++++++++
 src/userTool.js         | 73 +++++++++++++++++++++++++++--------------
 src/views/selectList.js | 40 ++++++++++------------
 src/views/tableView.js  | 49 ++++++++-------------------
 4 files changed, 113 insertions(+), 83 deletions(-)
 create mode 100644 src/listcontroller.js

diff --git a/src/listcontroller.js b/src/listcontroller.js
new file mode 100644
index 0000000..d2fbdb6
--- /dev/null
+++ b/src/listcontroller.js
@@ -0,0 +1,34 @@
+import * as m from 'mithril';
+import { ResourceHandler } from './auth';
+
+export default class DatalistController {
+  constructor(resource, query = {}, searchKeys = false) {
+    this.handler = new ResourceHandler(resource, searchKeys);
+    this.query = query;
+    this.items = [];
+    this.refresh();
+  }
+
+  addItem(item) {
+    this.items.push(item);
+    m.redraw();
+  }
+
+  refresh() {
+    this.handler.get(this.query).then((data) => {
+      this.items = data._items;
+      m.redraw();
+    });
+  }
+
+  setSearch(search) {
+    this.query.search = search;
+    this.refresh();
+  }
+
+  setQuery(query) {
+    this.query = query;
+    this.refresh();
+  }
+}
+
diff --git a/src/userTool.js b/src/userTool.js
index 8e302c3..93c8b1e 100644
--- a/src/userTool.js
+++ b/src/userTool.js
@@ -4,15 +4,44 @@ import TableView from './views/tableView';
 import { inputGroup, selectGroup, submitButton } from './views/elements';
 import SelectList from './views/selectList';
 import { users as config } from './config.json';
-import { getSession } from './auth';
+import DatalistController from './listcontroller';
 
 const m = require('mithril');
 
 class UserView extends ItemView {
   constructor() {
     super('users');
-    this.memberships = [];
+    // a controller to handle the groupmemberships of this user
+    this.groupmemberships = new DatalistController('groupmemberships', {
+      where: { user: this.id },
+      embedded: { group: 1 },
+    });
+    // a controller to handle the eventsignups of this user
+    this.eventsignups = new DatalistController('eventsignups', {
+      where: { user: this.id },
+      embedded: { event: 1 },
+    });
+    // initially, don't display the choice field for a new group
+    // (this will be displayed once the user clicks on 'new')
     this.groupchoice = false;
+    // a controller to handle the list of possible groups to join
+    this.groupcontroller = new DatalistController('groups', {}, ['name']);
+    // exclude the groups where the user is already a member
+    this.groupmemberships.handler.get({ where: { user: this.id } })
+      .then((data) => {
+        const groupIds = data._items.map(item => item.group);
+        this.groupcontroller.setQuery({
+          where: { _id: { $nin: groupIds } },
+        });
+      });
+  }
+
+  oninit() {
+    this.handler.getItem(this.id, this.embedded).then((item) => {
+      this.data = item;
+      m.redraw();
+    });
+    this.groupmemberships.refresh();
   }
 
   view() {
@@ -31,18 +60,20 @@ class UserView extends ItemView {
     const detailKeys = [
       'email', 'phone', 'nethz', 'legi', 'rfid', 'department', 'gender'];
 
+    // Selector that is only displayed if "new" is clicked in the
+    // groupmemberships. Selects a group to request membership for.
     const groupSelect = m(SelectList, {
-      resource: 'groups',
-      searchKeys: ['name'],
+      controller: this.groupcontroller,
       itemView: {
         view({ attrs }) { return m('span', attrs.name); },
       },
       onSubmit: (group) => {
-        getSession().then((apiSession) => {
-          apiSession.post('groupmemberships', {
-            user: this.data._id,
-            group: group._id,
-          });
+        this.groupmemberships.handler.post({
+          user: this.data._id,
+          group: group._id,
+        }).then((data) => {
+          this.groupmemberships.refresh();
+          this.groupchoice = false;
         });
       },
     });
@@ -57,25 +88,16 @@ class UserView extends ItemView {
       m('h2', 'Memberships'), m('br'),
       this.groupchoice ? groupSelect : '',
       m(TableView, {
-        resource: 'groupmemberships',
+        controller: this.groupmemberships,
         keys: ['group.name', 'expiry'],
-        searchKeys: ['group.name'],
-        query: {
-          where: { user: this.id },
-          embedded: { group: 1 },
-        },
-        onAdd: () => {
-          this.groupchoice = true;
-        },
+        titles: ['groupname', 'expiry'],
+        onAdd: () => { this.groupchoice = true; },
       }),
       m('h2', 'Signups'), m('br'),
       m(TableView, {
-        resource: 'eventsignups',
+        controller: this.eventsignups,
         keys: ['event.title_de'],
-        query: {
-          where: { user: this.id },
-          embedded: { event: 1 },
-        },
+        titles: ['event'],
       }),
     ]);
   }
@@ -179,9 +201,12 @@ export class UserModal {
 }
 
 export class UserTable {
+  constructor() {
+    this.ctrl = new DatalistController('users', {}, config.tableKeys);
+  }
   view() {
     return m(TableView, {
-      resource: 'users',
+      controller: this.ctrl,
       keys: config.tableKeys,
       titles: config.tableKeys.map(key => config.keyDescriptors[key] || key),
       onAdd: () => { m.route.set('/newuser'); },
diff --git a/src/views/selectList.js b/src/views/selectList.js
index bbb56c1..b322a2c 100644
--- a/src/views/selectList.js
+++ b/src/views/selectList.js
@@ -1,4 +1,3 @@
-import TableView from './tableView';
 import { submitButton } from './elements';
 
 const m = require('mithril');
@@ -22,56 +21,46 @@ function debounce(func, wait, immediate) {
     timeout = setTimeout(later, wait);
     if (callNow) func.apply(context, args);
   };
-};
+}
 
-export default class SelectList extends TableView {
-  constructor({
-    attrs: {
-      resource,
-      searchKeys,
-      itemView,
-      onSubmit = () => {},
-    },
-  }) {
-    super({ attrs: { resource, keys: searchKeys } });
-    this.itemView = itemView;
+export default class SelectList {
+  constructor() {
     this.selected = null;
     this.showList = false;
-    this.onSubmit = onSubmit;
   }
 
-  view() {
+  view({ attrs: { controller, itemView, onSubmit = () => {} } }) {
     // input search and select field
     const updateList = debounce(() => {
-      this.buildList();
+      controller.refresh();
     }, 500);
 
     let input = m('input.form-control', {
       onfocus: () => { this.showList = true; },
       onblur: debounce(() => { this.showList = false; m.redraw(); }, 100),
       onkeyup: (e) => {
-        this.query.search = e.target.value;
+        controller.setSearch(e.target.value);
         updateList();
       },
     });
     if (this.selected !== null) {
       input = m('div.btn-group', [
-        m('div.btn.btn-default', m(this.itemView, this.selected)),
+        m('div.btn.btn-default', m(itemView, this.selected)),
         m('div.btn.btn-primary', {
           onclick: () => {
             this.selected = null;
-            this.query = {};
-            this.buildList();
+            controller.setSearch('');
+            controller.refresh();
           },
         }, m('span.glyphicon.glyphicon-remove-sign')),
       ]);
     }
 
     // list of items
-    const list = m('ul.list-group', this.items.map(item =>
+    const list = m('ul.list-group', controller.items.map(item =>
       m('button.list-group-item', {
         onclick: () => { this.selected = item; this.showList = false; },
-      }, m(this.itemView, item))));
+      }, m(itemView, item))));
 
     return m('div', {
     }, [
@@ -83,7 +72,12 @@ export default class SelectList extends TableView {
             active: this.selected !== null,
             args: {
               class: 'btn-primary',
-              onclick: () => { this.onSubmit(this.selected); },
+              onclick: () => {
+                onSubmit(this.selected);
+                this.selected = null;
+                controller.setSearch('');
+                controller.refresh();
+              },
             },
           }),
         ]),
diff --git a/src/views/tableView.js b/src/views/tableView.js
index 33c4a00..03c748d 100644
--- a/src/views/tableView.js
+++ b/src/views/tableView.js
@@ -1,5 +1,3 @@
-import { ResourceHandler } from '../auth';
-
 const m = require('mithril');
 
 class TableRow {
@@ -23,7 +21,6 @@ class TableRow {
   }
 }
 
-
 export default class TableView {
   /* Shows a table of objects for a given API resource.
    *
@@ -39,61 +36,41 @@ export default class TableView {
    *       https://docs.mongodb.com/v3.2/reference/operator/query/
    *       e.g. : { where: {name: somename } }
    */
-  constructor({
+  constructor() {
+    this.search = '';
+  }
+
+  view({
     attrs: {
       keys,
       titles,
-      resource,
-      query = false,
-      searchKeys = false,
+      controller,
       onAdd = () => {},
     },
   }) {
-    this.items = [];
-    this.showKeys = keys;
-    this.titles = titles || keys;
-    this.handler = new ResourceHandler(resource, searchKeys);
-    // the querystring is either given or will be parsed from the url
-    this.query = query || m.route.param();
-    this.onAdd = onAdd;
-  }
-
-  buildList() {
-    this.handler.get(this.query).then((data) => {
-      this.items = data._items;
-      console.log(this.items);
-      m.redraw();
-    });
-  }
-
-  oninit() {
-    this.buildList();
-  }
-
-  view() {
     return m('div', [
       m('div.row', [
         m('div.col-xs-4', [
           m('div.input-group', [
             m('input[name=search].form-control', {
-              value: this.query.search,
-              onchange: m.withAttr('value', (value) => { this.query.search = value; }),
+              value: this.search,
+              onchange: m.withAttr('value', (value) => { this.search = value; }),
             }),
             m('span.input-group-btn', m('button.btn.btn-default', {
-              onclick: () => { this.buildList(); },
+              onclick: () => { controller.setSearch(this.search); },
             }, 'Search')),
           ]),
         ]),
         m('div.col-xs-4', [
           m('div.btn.btn-default', {
-            onclick: () => { this.onAdd(); },
+            onclick: () => { onAdd(); },
           }, 'New'),
         ]),
       ]),
       m('table.table.table-hover', [
-        m('thead', m('tr', this.titles.map(title => m('th', title)))),
-        m('tbody', this.items.map(item =>
-          m(TableRow, { showKeys: this.showKeys, data: item }))),
+        m('thead', m('tr', titles.map(title => m('th', title)))),
+        m('tbody', controller.items.map(item =>
+          m(TableRow, { showKeys: keys, data: item }))),
       ]),
     ]);
   }
-- 
GitLab