From 45d7f2518eecaf18002c3795c1cce5eed40a6945 Mon Sep 17 00:00:00 2001
From: Hermann <blumh@ethz.ch>
Date: Sat, 1 Sep 2018 13:02:19 +0200
Subject: [PATCH] add extra llistcontroller for resource embeddings

---
 src/relationlistcontroller.js | 158 ++++++++++++++++++++++++++++++++++
 1 file changed, 158 insertions(+)
 create mode 100644 src/relationlistcontroller.js

diff --git a/src/relationlistcontroller.js b/src/relationlistcontroller.js
new file mode 100644
index 0000000..4a3e6fd
--- /dev/null
+++ b/src/relationlistcontroller.js
@@ -0,0 +1,158 @@
+import m from 'mithril';
+import Stream from 'mithril/stream';
+import { ResourceHandler } from './auth';
+import { debounce } from './utils';
+
+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.
+   */
+  constructor(
+    primary,
+    secondary,
+    query = {},
+    searchKeys = false,
+    secondaryQuery = {},
+    secondarySearchKeys = false,
+  ) {
+    this.handler = new ResourceHandler(primary, searchKeys);
+    this.handler2 = new ResourceHandler(secondary, secondarySearchKeys);
+    this.secondaryKey = secondary.slice(0, -1);
+    this.query = query || {};
+    this.query2 = secondaryQuery || {};
+    this.search = null;
+    this.filter = null;
+    this.filter2 = null;
+    // 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);
+    this.refresh();
+    this.debouncedSearch = debounce((search) => {
+      this.setSearch(search);
+      this.refresh();
+      m.redraw();
+    }, 100);
+    // keep track of the total number of pages
+    this.totalPages = null;
+  }
+
+  refresh() {
+    this.stateCounter(this.stateCounter() + 1);
+  }
+
+  infiniteScrollParams(item) {
+    return {
+      item,
+      pageData: pageNum => this.getPageData(pageNum),
+      pageKey: pageNum => `${pageNum}-${this.stateCounter()}`,
+    };
+  }
+
+  getPageData(pageNum) {
+    // We first query the primary resource for a list of items and then query the secondary
+    // 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.page = pageNum;
+    query.where = { ...this.filter, ...this.query.where };
+
+    console.log(query.search);
+
+    return new Promise((resolve) => {
+      this.handler.get(query).then((data) => {
+        // update total number of pages
+        this.totalPages = Math.ceil(data._meta.total / 10);
+
+        console.log(data._items.map(item => item._id));
+
+        const query2 = Object.assign({}, this.query2);
+        query2.where = {
+          _id: { $in: data._items.map(item => item[this.secondaryKey]) },
+          ...this.filter2,
+          ...this.query2.where,
+        };
+        this.handler2.get(query2).then((secondaryData) => {
+          // check which secondary items were filtered out
+          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 itemCopy = Object.assign({}, item);
+            itemCopy[this.secondaryKey] = secondaryData._items.find(relItem =>
+              relItem._id === item[this.secondaryKey]);
+            return itemCopy;
+          }));
+        });
+      });
+    });
+  }
+
+  /*
+   * Get all available pages
+   */
+  getFullList() {
+    return new Promise((resolve) => {
+      // get first page to refresh total page count
+      this.getPageData(1).then((firstPage) => {
+        const pages = { 1: firstPage };
+        // 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);
+        }
+
+        // now fetch all the missing pages
+        Array.from(new Array(totalPages - 1), (x, i) => i + 2).forEach((pageNum) => {
+          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));
+            console.log('missingPages', missingPages);
+            if (missingPages.length === 0) {
+              // collect all the so-far loaded pages in order (sorted keys)
+              // and flatten them into 1 array
+              resolve([].concat(...Object.keys(pages).sort().map(key => pages[key])));
+            }
+          });
+        });
+      });
+    });
+  }
+
+  setSearch(search) {
+    this.search = search;
+    this.query.search = search;
+    this.query2.search = search;
+  }
+
+  setFilter(filter) {
+    this.filter = {};
+    this.filter2 = {};
+    // split up filters between resouce and relation
+    Object.keys(filter).forEach((key) => {
+      if (key.startsWith(`${this.secondaryKey}.`)) {
+        this.filter2[key.slice(this.secondaryKey.length + 1)] = filter[key];
+      } else {
+        this.filter[key] = filter[key];
+      }
+    });
+    this.refresh();
+  }
+
+  setQuery(query) {
+    this.query = query;
+    this.query.search = this.search;
+    this.refresh();
+  }
+}
+
-- 
GitLab