view client/src/components/Search.vue @ 4231:6f31a99cd92d

clinet: fix translations process and update source strings * move strings for translations from *.po files to the component itself to let gettext() mark only the strings without the html elements. (make makemessages complains to have html elements in the .po files and stops the process).
author Fadi Abbud <fadi.abbud@intevation.de>
date Wed, 21 Aug 2019 11:13:12 +0200
parents 33180c25bd87
children 1cdd246860ae
line wrap: on
line source

<template>
  <div :class="searchbarContainerStyle">
    <div class="input-group-prepend m-0 d-print-none">
      <span @click="toggleSearchbar" :class="searchButtonStyle" for="search">
        <font-awesome-icon icon="search" />
      </span>
    </div>
    <div
      :class="[
        'searchgroup',
        {
          'searchgroup-collapsed': !showSearchbar,
          big:
            showContextBox &&
            ['bottlenecks', 'staging', 'stretches'].indexOf(
              contextBoxContent
            ) !== -1
        }
      ]"
    >
      <input
        @keyup.enter="triggerEnter"
        id="search"
        v-model="searchQuery"
        type="text"
        :class="searchInputStyle"
      />
    </div>
    <div
      v-if="showSearchbar && searchResults !== null && !showContextBox"
      class="searchresults border-top ui-element bg-white rounded-bottom d-print-none position-absolute"
    >
      <div
        v-for="(entry, index) of searchResults"
        :key="index"
        class="border-top text-left"
      >
        <a
          href="#"
          @click.prevent="moveToSearchResult(entry)"
          class="p-2 d-block text-nowrap"
        >
          <font-awesome-icon
            icon="ship"
            v-if="entry.type === 'bottleneck'"
            class="mr-1"
            fixed-width
          />
          <font-awesome-icon
            icon="water"
            v-if="entry.type === 'rhm'"
            class="mr-1"
            fixed-width
          />
          <font-awesome-icon
            icon="city"
            v-if="entry.type === 'city'"
            class="mr-1"
            fixed-width
          />
          <font-awesome-icon
            icon="ruler-vertical"
            v-if="entry.type === 'gauge'"
            class="mr-1"
            fixed-width
          />
          <font-awesome-icon
            icon="road"
            v-if="['stretch', 'section'].includes(entry.type)"
            class="mr-1"
            fixed-width
          />
          {{ entry.name }}
          <span v-if="entry.location || entry.locationcode"
            >({{ entry.location
            }}<span v-if="entry.location && entry.locationcode">, </span
            >{{ entry.locationcode }})</span
          >
        </a>
      </div>
    </div>
  </div>
</template>

<style lang="scss" scoped>
.searchcontainer {
  opacity: 0.96;
  width: 668px;
}

.searchcontainer .searchbar {
  border-top-left-radius: 0 !important;
  border-bottom-left-radius: 0 !important;
}

.searchgroup {
  width: 635px;
  overflow: hidden;
}

.searchgroup-collapsed {
  width: 0;
}

.searchbar {
  height: 2rem !important;
  box-shadow: none !important;
}

.searchbar.rounded-top-right {
  border-radius: 0 !important;
  border-top-right-radius: 0.25rem !important;
}

.searchlabel.rounded-top-left {
  border-radius: 0 !important;
  border-top-left-radius: 0.25rem !important;
}

.input-group-text {
  height: 2rem;
  width: 2rem;
}

.input-group-prepend svg path {
  fill: #666;
}

.searchresults {
  box-shadow: 0 0.1rem 0.5rem rgba(0, 0, 0, 0.2);
  top: 2rem;
  left: 0;
  right: 0;
  max-height: 24rem;
  overflow: auto;
}

.searchresults > div:first-child {
  border-top: 0 !important;
}

.searchresults a {
  text-decoration: none;
}

.searchresults a:hover {
  background: #f8f8f8;
}

.smallbox {
  width: 2rem;
}
</style>

<script>
/* 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>
 */
import debounce from "lodash.debounce";
import { mapState, mapGetters } from "vuex";

import { displayError } from "@/lib/errors";
import { HTTP } from "@/lib/http";
import { format } from "date-fns";

const setFocus = () => document.querySelector("#search").focus();

export default {
  name: "search",
  data() {
    return {
      searchQueryIsDirty: false,
      searchResults: null,
      isSearching: false
    };
  },
  computed: {
    ...mapState("application", [
      "showSearchbar",
      "showContextBox",
      "contextBoxContent"
    ]),
    ...mapState("imports", ["startDate", "endDate"]),
    ...mapGetters("imports", ["filters"]),
    ...mapGetters("map", ["openLayersMap"]),
    searchQuery: {
      get() {
        return this.$store.state.application.searchQuery;
      },
      set(value) {
        this.$store.commit("application/searchQuery", value);
      }
    },
    searchIndicator: function() {
      if (this.isSearching) {
        return "⟳";
      } else if (this.searchQueryIsDirty) {
        return "";
      } else {
        return "✓";
      }
    },
    searchbarContainerStyle() {
      return [
        "input-group searchcontainer shadow-xs rounded",
        {
          "d-flex": this.contextBoxContent !== "imports",
          "d-none": this.contextBoxContent === "imports" && this.showContextBox,
          smallbox: !this.showSearchbar
        }
      ];
    },
    searchInputStyle() {
      return [
        "form-control ui-element search searchbar d-print-none border-0",
        { "rounded-top-right": this.showContextBox || this.searchResults }
      ];
    },
    searchButtonStyle() {
      return [
        "ui-element input-group-text p-0 d-flex border-0 justify-content-center searchlabel bg-white d-print-none",
        {
          rounded: !this.showSearchbar,
          "rounded-left": this.showSearchbar,
          "rounded-top-left":
            this.showSearchbar && (this.showContextBox || this.searchResults)
        }
      ];
    }
  },
  watch: {
    searchQuery: function() {
      this.searchQueryIsDirty = true;
      if (!this.showContextBox) this.triggerSearch();
    }
  },
  methods: {
    loadLogs() {
      this.$store
        .dispatch("imports/getImports", {
          filter: this.filters,
          from: format(this.startDate, "YYYY-MM-DDTHH:mm:ss.SSS"),
          to: format(this.endDate, "YYYY-MM-DDTHH:mm:ss.SSS"),
          query: this.searchQuery
        })
        .then(() => {})
        .catch(error => {
          const { status, data } = error.response;
          displayError({
            title: this.$gettext("Backend Error"),
            message: `${status}: ${data.message || data}`
          });
        });
    },
    triggerEnter() {
      if (this.showContextBox && this.contextBoxContent === "importoverview") {
        this.loadLogs();
      }
      if (!this.searchResults || this.searchResults.length != 1) return;
      this.moveToSearchResult(this.searchResults[0]);
    },
    triggerSearch: debounce(function() {
      this.doSearch();
    }, 500),
    doSearch() {
      this.isCalculating = true;
      this.searchResults = null;

      if (this.searchQuery == "") {
        return;
      }

      HTTP.post(
        "/search",
        { string: this.searchQuery },
        {
          headers: {
            "X-Gemma-Auth": localStorage.getItem("token"),
            "Content-type": "text/xml; charset=UTF-8"
          }
        }
      )
        .then(response => {
          this.searchResults = response.data;
        })
        .catch(error => {
          const { status, data } = error.response;
          displayError({
            title: this.$gettext("Backend Error"),
            message: `${status}: ${data.message || data}`
          });
        });

      this.isCalculating = false;
      this.searchQueryIsDirty = false;
    },
    moveToSearchResult(resultEntry) {
      let zoom = 16;
      if (resultEntry.type === "bottleneck") {
        this.openLayersMap()
          .getLayer("BOTTLENECKS")
          .setVisible(true);
      }
      if (resultEntry.type === "rhm") {
        this.openLayersMap()
          .getLayer("DISTANCEMARKSAXIS")
          .setVisible(true);
      }
      if (resultEntry.type === "gauge") {
        this.openLayersMap()
          .getLayer("GAUGES")
          .setVisible(true);
      }
      if (resultEntry.type === "stretch") {
        this.$store.commit(
          "imports/selectedStretchId",
          "stretches_geoserver." + resultEntry.id
        );
        this.openLayersMap()
          .getLayer("STRETCHES")
          .setVisible(true);
      }
      if (resultEntry.type === "section") {
        this.$store.commit(
          "imports/selectedSectionId",
          "sections_geoserver." + resultEntry.id
        );
        this.openLayersMap()
          .getLayer("SECTIONS")
          .setVisible(true);
      }
      if (resultEntry.type === "city") zoom = 13;

      if (resultEntry.geom.type == "Point") {
        this.$store.dispatch("map/moveMap", {
          coordinates: resultEntry.geom.coordinates,
          zoom,
          preventZoomOut: true
        });
      } else if (resultEntry.geom.type == "Polygon") {
        const boundingBox = [
          Math.min(...resultEntry.geom.coordinates[0].map(c => c[0])),
          Math.min(...resultEntry.geom.coordinates[0].map(c => c[1])),
          Math.max(...resultEntry.geom.coordinates[0].map(c => c[0])),
          Math.max(...resultEntry.geom.coordinates[0].map(c => c[1]))
        ];
        this.$store.dispatch("map/moveToBoundingBox", { boundingBox, zoom });
      }
      // this.searchQuery = ""; // clear search query again
      this.toggleSearchbar();
    },
    toggleSearchbar() {
      if (!this.showContextBox) {
        if (!this.showSearchbar) {
          setTimeout(setFocus, 300);
        }
        this.$store.commit("application/showSearchbar", !this.showSearchbar);
      }
    }
  }
};
</script>