changeset 2738:add2d47c2567

client: tables: implemented simple default sorting
author Markus Kottlaender <markus@intevation.de>
date Tue, 19 Mar 2019 18:59:40 +0100
parents 4a5c0e7cb75b
children 8057662812f1
files client/src/components/Bottlenecks.vue client/src/components/ImportStretches.vue client/src/components/importoverview/ImportOverview.vue client/src/components/importschedule/Importschedule.vue client/src/components/systemconfiguration/PDFTemplates.vue client/src/components/ui/UITableHeader.vue client/src/components/usermanagement/Usermanagement.vue client/src/lib/filters.js client/src/lib/mixins.js
diffstat 9 files changed, 248 insertions(+), 244 deletions(-) [+]
line wrap: on
line diff
--- a/client/src/components/Bottlenecks.vue	Tue Mar 19 18:07:50 2019 +0100
+++ b/client/src/components/Bottlenecks.vue	Tue Mar 19 18:59:40 2019 +0100
@@ -7,18 +7,17 @@
     />
     <UITableHeader
       :columns="[
-        { id: 'name', title: 'Name', class: 'col-4' },
+        { id: 'properties.name', title: 'Name', class: 'col-4' },
         {
-          id: 'latestMeasurement',
+          id: 'properties.current',
           title: 'Latest Measurement',
           class: 'col-3'
         },
-        { id: 'chainage', title: 'Chainage', class: 'col-3' }
+        { id: 'properties.from', title: 'Chainage', class: 'col-3' }
       ]"
-      @sortingChanged="sortBy"
     />
     <UITableBody
-      :data="filteredAndSortedBottlenecks()"
+      :data="filteredBottlenecks() | sortTable(sortColumn, sortDirection)"
       :maxHeight="(showSplitscreen ? 18 : 35) + 'rem'"
       :active="openBottleneck"
       v-slot="{ item: bottleneck }"
@@ -124,14 +123,15 @@
  */
 import { mapState } from "vuex";
 import { HTTP } from "@/lib/http";
-import { displayError } from "@/lib/errors.js";
+import { displayError } from "@/lib/errors";
+import { sortTable } from "@/lib/mixins";
 
 export default {
   name: "bottlenecks",
+  mixins: [sortTable],
   data() {
     return {
-      sortColumn: "name",
-      sortDirection: "ASC",
+      sortColumn: "properties.name",
       openBottleneck: null,
       openBottleneckSurveys: null,
       loading: null
@@ -151,51 +151,12 @@
     }
   },
   methods: {
-    filteredAndSortedBottlenecks() {
-      return this.bottlenecksList
-        .filter(bn => {
-          return bn.properties.name
-            .toLowerCase()
-            .includes(this.searchQuery.toLowerCase());
-        })
-        .sort((bnA, bnB) => {
-          switch (this.sortColumn) {
-            case "name":
-              if (
-                bnA.properties.name.toLowerCase() <
-                bnB.properties.name.toLowerCase()
-              )
-                return this.sortDirection === "ASC" ? -1 : 1;
-              if (
-                bnA.properties.name.toLowerCase() >
-                bnB.properties.name.toLowerCase()
-              )
-                return this.sortDirection === "ASC" ? 1 : -1;
-              return 0;
-
-            case "latestMeasurement": {
-              if (
-                (bnA.properties.current || "") < (bnB.properties.current || "")
-              )
-                return this.sortDirection === "ASC" ? -1 : 1;
-              if (
-                (bnA.properties.current || "") > (bnB.properties.current || "")
-              )
-                return this.sortDirection === "ASC" ? 1 : -1;
-              return 0;
-            }
-
-            case "chainage":
-              if (bnA.properties.from < bnB.properties.from)
-                return this.sortDirection === "ASC" ? -1 : 1;
-              if (bnA.properties.from > bnB.properties.from)
-                return this.sortDirection === "ASC" ? 1 : -1;
-              return 0;
-
-            default:
-              return 0;
-          }
-        });
+    filteredBottlenecks() {
+      return this.bottlenecksList.filter(bn => {
+        return bn.properties.name
+          .toLowerCase()
+          .includes(this.searchQuery.toLowerCase());
+      });
     },
     selectSurvey(survey, bottleneck) {
       this.$store
@@ -231,10 +192,6 @@
           });
         });
     },
-    sortBy(sorting) {
-      this.sortColumn = sorting.sortColumn;
-      this.sortDirection = sorting.sortDirection;
-    },
     loadSurveys(bottleneck) {
       if (bottleneck === this.openBottleneck) {
         this.openBottleneck = null;
--- a/client/src/components/ImportStretches.vue	Tue Mar 19 18:07:50 2019 +0100
+++ b/client/src/components/ImportStretches.vue	Tue Mar 19 18:59:40 2019 +0100
@@ -8,13 +8,19 @@
     <div v-if="!edit" class="mb-3">
       <UITableHeader
         :columns="[
-          { id: 'name', title: 'Name', class: 'col-4' },
-          { id: 'date', title: 'Date', class: 'col-2' },
-          { id: 'srcorg', title: 'Source organization', class: 'col-3' }
+          { id: 'properties.name', title: 'Name', class: 'col-4' },
+          { id: 'properties.date_info', title: 'Date', class: 'col-2' },
+          {
+            id: 'properties.source_organization',
+            title: 'Source organization',
+            class: 'col-3'
+          }
         ]"
-        :sortable="false"
       />
-      <UITableBody :data="stretches" v-slot="{ item: stretch }">
+      <UITableBody
+        :data="filteredStretches() | sortTable(sortColumn, sortDirection)"
+        v-slot="{ item: stretch }"
+      >
         <div class="py-1 col-4 ">
           <a
             class="linkto text-info"
@@ -34,10 +40,10 @@
           }}</a>
         </div>
         <div class="py-1 col-2">
-          {{ stretch.properties["date_info"] | surveyDate }}
+          {{ stretch.properties.date_info | surveyDate }}
         </div>
         <div class="py-1 col-3">
-          {{ stretch.properties["source_organization"] }}
+          {{ stretch.properties.source_organization }}
         </div>
         <div class="py-1 col text-right">
           <button
@@ -279,12 +285,14 @@
  * Tom Gottfried <tom.gottfried@intevation.de>
  */
 import { mapState, mapGetters } from "vuex";
-import { displayError, displayInfo } from "@/lib/errors.js";
-import { LAYERS } from "@/store/map.js";
+import { displayError, displayInfo } from "@/lib/errors";
+import { LAYERS } from "@/store/map";
 import { HTTP } from "@/lib/http";
+import { sortTable } from "@/lib/mixins";
 
 export default {
   name: "importstretches",
+  mixins: [sortTable],
   data() {
     return {
       staging: [],
@@ -314,24 +322,53 @@
       countryCodeError: false
     };
   },
-  mounted() {
-    this.edit = false;
-    this.loadStretches().catch(error => {
-      const { status, data } = error.response;
-      displayError({
-        title: this.$gettext("Backend Error"),
-        message: `${status}: ${data.message || data}`
-      });
-    });
-    this.loadStagingData().catch(error => {
-      const { status, data } = error.response;
-      displayError({
-        title: this.$gettext("Backend Error"),
-        message: `${status}: ${data.message || data}`
-      });
-    });
+  computed: {
+    ...mapState("application", ["searchQuery"]),
+    ...mapState("map", ["identifiedFeatures", "currentMeasurement"]),
+    ...mapGetters("user", ["isSysAdmin"]),
+    ...mapState("imports", ["stretches"]),
+    stretchesInStaging() {
+      const result = [];
+      for (let stretch of this.stretches) {
+        for (let s of this.staging) {
+          if (s.kind == "st" && s.summary.stretch == stretch.properties.name) {
+            result.push({ name: s.summary.stretch, id: s.id });
+          }
+        }
+      }
+      return result;
+    },
+    pointsValid() {
+      if (!this.startrhm || !this.endrhm) return true;
+      const start = this.startrhm.replace(/\D+/g, "") * 1;
+      const end = this.endrhm.replace(/\D+/g, "") * 1;
+      const result = start < end;
+      return result;
+    }
+  },
+  watch: {
+    identifiedFeatures() {
+      const filterDistanceMarks = x => {
+        return /^distance_marks/.test(x["id_"]);
+      };
+      const distanceMark = this.identifiedFeatures.filter(filterDistanceMarks);
+      if (distanceMark.length > 0) {
+        const value = distanceMark[0].getProperties()["location"];
+        this.startrhm = this.pipetteStart ? value : this.startrhm;
+        this.endrhm = this.pipetteEnd ? value : this.endrhm;
+        this.pipetteStart = false;
+        this.pipetteEnd = false;
+      }
+    }
   },
   methods: {
+    filteredStretches() {
+      return this.stretches.filter(s => {
+        return (s.properties.name + s.properties.source_organization)
+          .toLowerCase()
+          .includes(this.searchQuery.toLowerCase());
+      });
+    },
     gotoStaging(id) {
       this.$router.push("/review/" + id);
     },
@@ -531,43 +568,22 @@
         });
     }
   },
-  watch: {
-    identifiedFeatures() {
-      const filterDistanceMarks = x => {
-        return /^distance_marks/.test(x["id_"]);
-      };
-      const distanceMark = this.identifiedFeatures.filter(filterDistanceMarks);
-      if (distanceMark.length > 0) {
-        const value = distanceMark[0].getProperties()["location"];
-        this.startrhm = this.pipetteStart ? value : this.startrhm;
-        this.endrhm = this.pipetteEnd ? value : this.endrhm;
-        this.pipetteStart = false;
-        this.pipetteEnd = false;
-      }
-    }
-  },
-  computed: {
-    ...mapState("map", ["identifiedFeatures", "currentMeasurement"]),
-    ...mapGetters("user", ["isSysAdmin"]),
-    ...mapState("imports", ["stretches"]),
-    stretchesInStaging() {
-      const result = [];
-      for (let stretch of this.stretches) {
-        for (let s of this.staging) {
-          if (s.kind == "st" && s.summary.stretch == stretch.properties.name) {
-            result.push({ name: s.summary.stretch, id: s.id });
-          }
-        }
-      }
-      return result;
-    },
-    pointsValid() {
-      if (!this.startrhm || !this.endrhm) return true;
-      const start = this.startrhm.replace(/\D+/g, "") * 1;
-      const end = this.endrhm.replace(/\D+/g, "") * 1;
-      const result = start < end;
-      return result;
-    }
+  mounted() {
+    this.edit = false;
+    this.loadStretches().catch(error => {
+      const { status, data } = error.response;
+      displayError({
+        title: this.$gettext("Backend Error"),
+        message: `${status}: ${data.message || data}`
+      });
+    });
+    this.loadStagingData().catch(error => {
+      const { status, data } = error.response;
+      displayError({
+        title: this.$gettext("Backend Error"),
+        message: `${status}: ${data.message || data}`
+      });
+    });
   }
 };
 </script>
--- a/client/src/components/importoverview/ImportOverview.vue	Tue Mar 19 18:07:50 2019 +0100
+++ b/client/src/components/importoverview/ImportOverview.vue	Tue Mar 19 18:59:40 2019 +0100
@@ -29,47 +29,25 @@
       </div>
       <UITableHeader
         :columns="[
-          { id: 'id', title: 'Id', width: '79px', disableSorting: true },
-          {
-            id: 'kind',
-            title: 'Kind',
-            width: '53px',
-            disableSorting: true
-          },
-          {
-            id: 'enqueued',
-            title: 'Enqueued',
-            width: '138px',
-            disableSorting: true
-          },
-          {
-            id: 'user',
-            title: 'User',
-            width: '105px',
-            disableSorting: true
-          },
-          {
-            id: 'signer',
-            title: 'Signer',
-            width: '105px',
-            disableSorting: true
-          },
-          {
-            id: 'state',
-            title: 'Status',
-            width: '72px',
-            disableSorting: true
-          },
-          {
-            id: 'warning',
-            icon: 'exclamation-triangle',
-            width: '44px',
-            disableSorting: true
-          }
+          { id: 'id', title: 'Id', width: '79px' },
+          { id: 'kind', title: 'Kind', width: '53px' },
+          { id: 'enqueued', title: 'Enqueued', width: '138px' },
+          { id: 'user', title: 'User', width: '105px' },
+          { id: 'signer', title: 'Signer', width: '105px' },
+          { id: 'state', title: 'Status', width: '72px' },
+          { id: 'warnings', icon: 'exclamation-triangle', width: '44px' }
         ]"
-        @sortingChanged="sortBy"
       />
-      <UITableBody :data="imports" maxHeight="80vh" v-slot="{ item: entry }">
+      <!--
+      For server-side sorting, etc simply don't use the sortTable filter.
+      Instead you could just pass a function that loads the imports, like:
+      :data="loadImports(sortColumn, sortDirection)"
+     -->
+      <UITableBody
+        :data="filteredImports() | sortTable(sortColumn, sortDirection)"
+        maxHeight="80vh"
+        v-slot="{ item: entry }"
+      >
         <LogEntry :entry="entry"></LogEntry>
       </UITableBody>
     </div>
@@ -100,14 +78,17 @@
  *
  * Author(s):
  * Thomas Junk <thomas.junk@intevation.de>
+ * Markus Kottländer <markus.kottlaender@intevation.de>
  */
 
 import { mapState, mapGetters } from "vuex";
-import { displayError, displayInfo } from "@/lib/errors.js";
-import { STATES } from "@/store/imports.js";
+import { displayError, displayInfo } from "@/lib/errors";
+import { STATES } from "@/store/imports";
+import { sortTable } from "@/lib/mixins";
 
 export default {
   name: "importoverviewalt",
+  mixins: [sortTable],
   components: {
     Filters: () => import("./Filters.vue"),
     LogEntry: () => import("./LogEntry.vue")
@@ -118,11 +99,18 @@
     };
   },
   computed: {
+    ...mapState("application", ["searchQuery"]),
     ...mapState("imports", ["imports", "reviewed"]),
     ...mapGetters("imports", ["filters"])
   },
   methods: {
-    sortBy() {},
+    filteredImports() {
+      return this.imports.filter(i => {
+        return (i.kind + i.id)
+          .toLowerCase()
+          .includes(this.searchQuery.toLowerCase());
+      });
+    },
     loadLogs() {
       this.loading = true;
       this.$store
--- a/client/src/components/importschedule/Importschedule.vue	Tue Mar 19 18:07:50 2019 +0100
+++ b/client/src/components/importschedule/Importschedule.vue	Tue Mar 19 18:59:40 2019 +0100
@@ -24,14 +24,16 @@
         <UITableHeader
           :columns="[
             { id: 'id', title: 'ID', class: 'col-1' },
-            { id: 'type', title: 'Type', class: 'col-2' },
-            { id: 'author', title: 'Author', class: 'col-2' },
-            { id: 'schedule', title: 'Schedule', class: 'col-2' },
-            { id: 'email', title: 'Email', class: 'col-2' }
+            { id: 'kind', title: 'Type', class: 'col-2' },
+            { id: 'user', title: 'Author', class: 'col-2' },
+            { id: 'config.cron', title: 'Schedule', class: 'col-2' },
+            { id: 'config.send-email', title: 'Email', class: 'col-2' }
           ]"
-          :sortable="false"
         />
-        <UITableBody :data="schedules" v-slot="{ item: schedule }">
+        <UITableBody
+          :data="filteredSchedules() | sortTable(sortColumn, sortDirection)"
+          v-slot="{ item: schedule }"
+        >
           <div class="py-1 col-1">{{ schedule.id }}</div>
           <div class="py-1 col-2">{{ schedule.kind.toUpperCase() }}</div>
           <div class="py-1 col-2">{{ schedule.user }}</div>
@@ -86,6 +88,23 @@
   </div>
 </template>
 
+<style lang="sass" scoped>
+th
+  border-top: 0px
+
+.card-body
+  padding-bottom: $small-offset
+
+.schedulecard
+  margin-right: $small-offset
+  min-height: 20rem
+
+.schedulecard-body
+  width: 100%
+  margin-left: auto
+  margin-right: auto
+</style>
+
 <script>
 /* This is Free Software under GNU Affero General Public License v >= 3.0
  * without warranty, see README.md and license for details.
@@ -104,10 +123,12 @@
 
 import { mapState } from "vuex";
 import { HTTP } from "@/lib/http";
-import { displayInfo, displayError } from "@/lib/errors.js";
+import { displayInfo, displayError } from "@/lib/errors";
+import { sortTable } from "@/lib/mixins";
 
 export default {
   name: "importschedule",
+  mixins: [sortTable],
   components: {
     Importscheduledetail: () => import("./Importscheduledetail"),
     Spacer: () => import("@/components/Spacer")
@@ -117,10 +138,27 @@
       searchQuery: ""
     };
   },
-  mounted() {
-    this.getSchedules();
+  computed: {
+    ...mapState("application", ["showSidebar"]),
+    ...mapState("importschedule", ["schedules", "importScheduleDetailVisible"]),
+    spacerStyle() {
+      return [
+        "spacer ml-3",
+        {
+          "spacer-expanded": this.showSidebar,
+          "spacer-collapsed": !this.showSidebar
+        }
+      ];
+    }
   },
   methods: {
+    filteredSchedules() {
+      return this.schedules.filter(s => {
+        return (s.id + s.kind)
+          .toLowerCase()
+          .includes(this.searchQuery.toLowerCase());
+      });
+    },
     editSchedule(id) {
       this.$store
         .dispatch("importschedule/loadSchedule", id)
@@ -205,39 +243,8 @@
       });
     }
   },
-  computed: {
-    ...mapState("application", ["showSidebar"]),
-    ...mapState("importschedule", ["schedules", "importScheduleDetailVisible"]),
-    spacerStyle() {
-      return [
-        "spacer ml-3",
-        {
-          "spacer-expanded": this.showSidebar,
-          "spacer-collapsed": !this.showSidebar
-        }
-      ];
-    }
+  mounted() {
+    this.getSchedules();
   }
 };
 </script>
-
-<style lang="scss" scoped>
-th {
-  border-top: 0px;
-}
-
-.card-body {
-  padding-bottom: $small-offset;
-}
-
-.schedulecard {
-  margin-right: $small-offset;
-  min-height: 20rem;
-}
-
-.schedulecard-body {
-  width: 100%;
-  margin-left: auto;
-  margin-right: auto;
-}
-</style>
--- a/client/src/components/systemconfiguration/PDFTemplates.vue	Tue Mar 19 18:07:50 2019 +0100
+++ b/client/src/components/systemconfiguration/PDFTemplates.vue	Tue Mar 19 18:59:40 2019 +0100
@@ -14,12 +14,14 @@
       <UITableHeader
         :columns="[
           { id: 'name', title: 'Name', class: 'col-4' },
-          { id: 'date', title: 'Date', class: 'col-4' },
+          { id: 'time', title: 'Date', class: 'col-4' },
           { id: 'country', title: 'Country', class: 'col-2' }
         ]"
-        :sortable="false"
       />
-      <UITableBody :data="templates" v-slot="{ item: template }">
+      <UITableBody
+        :data="templates | sortTable(sortColumn, sortDirection)"
+        v-slot="{ item: template }"
+      >
         <div class="py-1 col-4">{{ template.name }}</div>
         <div class="py-1 col-4">{{ template.time }}</div>
         <div class="py-1 col-2" v-if="template.country">
@@ -78,10 +80,12 @@
  * Fadi Abbud <fadi.abbud@intevation.de>
  */
 import { HTTP } from "@/lib/http";
-import { displayError, displayInfo } from "@/lib/errors.js";
+import { displayError, displayInfo } from "@/lib/errors";
+import { sortTable } from "@/lib/mixins";
 
 export default {
   name: "pdftemplates",
+  mixins: [sortTable],
   data() {
     return {
       templates: [],
--- a/client/src/components/ui/UITableHeader.vue	Tue Mar 19 18:07:50 2019 +0100
+++ b/client/src/components/ui/UITableHeader.vue	Tue Mar 19 18:59:40 2019 +0100
@@ -2,7 +2,7 @@
   <div :class="['table-header row no-gutters bg-light', { sortable }]">
     <a
       v-for="column in columns"
-      @click.prevent="!column.disableSorting && sortBy(column.id)"
+      @click.prevent="!column.disableSorting && sortTable(column.id)"
       :key="column.id"
       :class="[
         'd-flex py-1 align-items-center justify-content-center small ' +
@@ -79,11 +79,11 @@
       }
       return "sort";
     },
-    sortBy(id) {
+    sortTable(id) {
       if (this.sortable) {
         this.sortColumn = id;
         this.sortDirection = this.sortDirection === "ASC" ? "DESC" : "ASC";
-        this.$emit("sortingChanged", {
+        this.$parent.sortTable({
           sortColumn: this.sortColumn,
           sortDirection: this.sortDirection
         });
--- a/client/src/components/usermanagement/Usermanagement.vue	Tue Mar 19 18:07:50 2019 +0100
+++ b/client/src/components/usermanagement/Usermanagement.vue	Tue Mar 19 18:59:40 2019 +0100
@@ -12,10 +12,9 @@
               { id: 'country', title: 'Country', class: 'col-2' },
               { id: 'email', title: 'Email', class: 'col-3' }
             ]"
-            @sortingChanged="sortBy"
           />
           <UITableBody
-            :data="sortedUsers"
+            :data="users | sortTable(sortColumn, sortDirection, page, pageSize)"
             maxHeight="47rem"
             :active="currentUser"
             v-slot="{ item: user }"
@@ -57,15 +56,15 @@
           <div class="d-flex mx-auto align-items-center">
             <button
               @click="prevPage"
-              v-if="this.currentPage !== 1"
+              v-if="this.page !== 1"
               class="mr-2 btn btn-sm btn-light align-self-center"
             >
               <font-awesome-icon icon="angle-left"></font-awesome-icon>
             </button>
-            {{ this.currentPage }} / {{ this.pages }}
+            {{ this.page }} / {{ this.pages }}
             <button
               @click="nextPage"
-              v-if="this.currentPage !== this.pages"
+              v-if="this.page !== this.pages"
               class="ml-2 btn btn-sm btn-light align-self-center"
             >
               <font-awesome-icon icon="angle-right"></font-awesome-icon>
@@ -143,6 +142,7 @@
 import { HTTP } from "@/lib/http";
 import Vue from "vue";
 import { VTooltip, VPopover, VClosePopover } from "v-tooltip";
+import { sortTable } from "@/lib/mixins";
 
 Vue.directive("tooltip", VTooltip);
 Vue.directive("close-popover", VClosePopover);
@@ -150,12 +150,10 @@
 
 export default {
   name: "userview",
+  mixins: [sortTable],
   data() {
     return {
-      sortColumn: "user",
-      sortDirection: "ASC",
-      pageSize: 20,
-      currentPage: 1
+      sortColumn: "user" // overriding the sortTable mixin's empty default value
     };
   },
   components: {
@@ -175,23 +173,6 @@
     deleteUserLabel() {
       return this.$gettext("Delete user");
     },
-    sortedUsers() {
-      const start = (this.currentPage - 1) * this.pageSize;
-      return this.users
-        .filter(u => u) // to clone the array and leave the original store value intact
-        .sort((a, b) => {
-          if (
-            a[this.sortColumn].toLowerCase() < b[this.sortColumn].toLowerCase()
-          )
-            return this.sortDirection === "ASC" ? -1 : 1;
-          if (
-            a[this.sortColumn].toLowerCase() > b[this.sortColumn].toLowerCase()
-          )
-            return this.sortDirection === "ASC" ? 1 : -1;
-          return 0;
-        })
-        .slice(start, start + this.pageSize);
-    },
     pages() {
       return Math.ceil(this.users.length / this.pageSize);
     },
@@ -237,19 +218,15 @@
         });
     },
     nextPage() {
-      if (this.currentPage < this.pages) {
-        this.currentPage += 1;
+      if (this.page < this.pages) {
+        this.page += 1;
       }
     },
     prevPage() {
-      if (this.currentPage > 0) {
-        this.currentPage -= 1;
+      if (this.page > 0) {
+        this.page -= 1;
       }
     },
-    sortBy(sorting) {
-      this.sortColumn = sorting.sortColumn;
-      this.sortDirection = sorting.sortDirection;
-    },
     deleteUser(name) {
       this.$store.commit("application/popup", {
         icon: "trash",
--- a/client/src/lib/filters.js	Tue Mar 19 18:07:50 2019 +0100
+++ b/client/src/lib/filters.js	Tue Mar 19 18:59:40 2019 +0100
@@ -39,5 +39,28 @@
         hour12: false
       })
     );
+  },
+  sortTable(data, sortColumn, sortDirection, page, pageSize) {
+    // clone the array and leave the original intact
+    let sortedData = data.filter(d => d);
+
+    if (sortColumn && sortDirection) {
+      sortedData.sort((a, b) => {
+        let valB = sortColumn.split(".").reduce((o, i) => o[i], a) || "";
+        let valA = sortColumn.split(".").reduce((o, i) => o[i], b) || "";
+
+        if (valA > valB) return sortDirection === "ASC" ? -1 : 1;
+        if (valA < valB) return sortDirection === "ASC" ? 1 : -1;
+
+        return 0;
+      });
+    }
+
+    if (page && pageSize) {
+      let start = page * pageSize - pageSize;
+      sortedData = sortedData.slice(start, start + pageSize);
+    }
+
+    return sortedData;
   }
 };
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/client/src/lib/mixins.js	Tue Mar 19 18:59:40 2019 +0100
@@ -0,0 +1,32 @@
+/* This is Free Software under GNU Affero General Public License v >= 3.0
+ * without warranty, see README.md and license for details.
+ *
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ * License-Filename: LICENSES/AGPL-3.0.txt
+ *
+ * Copyright (C) 2018 by via donau
+ *   – Österreichische Wasserstraßen-Gesellschaft mbH
+ * Software engineering by Intevation GmbH
+ *
+ * Author(s):
+ * Markus Kottländer <markus.kottlaender@intevation.de>
+ */
+
+const sortTable = {
+  data() {
+    return {
+      sortColumn: "",
+      sortDirection: "ASC",
+      pageSize: 20,
+      page: 1
+    };
+  },
+  methods: {
+    sortTable(sorting) {
+      this.sortColumn = sorting.sortColumn;
+      this.sortDirection = sorting.sortDirection;
+    }
+  }
+};
+
+export { sortTable };