Mercurial > gemma
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 };