Ver Fonte

implements typescript (ongoing)

Olivier Massot há 4 anos atrás
pai
commit
c018e57899

+ 38 - 22
components/Ui/Map/Structures.vue

@@ -23,26 +23,37 @@
   </LayoutContainer>
 </template>
 
-<script>
+<script lang="ts">
 import 'leaflet/dist/leaflet.css'
-let L
+import {
+  GridLayer,
+  LatLngBounds,
+  LatLngBoundsExpression, LatLngBoundsLiteral,
+  LatLngTuple,
+  MapOptions
+} from 'leaflet'
+import Vue from 'vue'
+
+let L: any
 if (typeof window !== 'undefined') {
   // only require leaflet on client-side
   L = require('leaflet')
 }
 
-export default {
+export default Vue.extend({
   props: {
     structures: {
-      type: Array,
+      type: Array as () => Array<Structure>,
       required: false,
       default: () => []
     }
   },
   data () {
+    const options: MapOptions = { scrollWheelZoom: false, zoomSnap: 0.25 }
+
     return {
-      map: null,
-      defaultBounds: [[51.03, -5.78], [41.2, 9.70]],
+      map: L.map('map', options),
+      defaultBounds: new LatLngBounds([51.03, -5.78], [41.2, 9.70]),
       shortcuts: [
         { src: '/images/metropole.png', alt: 'Metropole', bounds: [[51.03, -5.78], [41.2, 9.70]] },
         { src: '/images/guadeloupe.png', alt: 'Guadeloupe', bounds: [[16.62, -62.03], [15.74, -60.97]] },
@@ -55,8 +66,8 @@ export default {
     }
   },
   watch: {
-    structures (oldval, newval) {
-      if (oldval !== newval) {
+    structures (oldVal: Array<Structure>, newVal: Array<Structure>) {
+      if (oldVal !== newVal) {
         this.populateMarkers()
       }
 
@@ -67,13 +78,11 @@ export default {
     }
   },
   mounted () {
-    const options = { scrollWheelZoom: false, zoomSnap: 0.25 }
-    const defaultCenter = [46.71, 1.94]
-    const defaultZoom = 5.75
-    const layerSource = 'http://{s}.tile.osm.org/{z}/{x}/{y}.png'
-    const attribution = '&copy; <a href="http://osm.org/copyright">OpenStreetMap</a> contributors'
+    const defaultCenter: Array<number> = [46.71, 1.94]
+    const defaultZoom: number = 5.75
+    const layerSource: string = 'http://{s}.tile.osm.org/{z}/{x}/{y}.png'
+    const attribution: string = '&copy; <a href="http://osm.org/copyright">OpenStreetMap</a> contributors'
 
-    this.map = L.map('map', options)
     this.map.setView(defaultCenter, defaultZoom)
     L.tileLayer(layerSource, { attribution }).addTo(this.map)
 
@@ -91,7 +100,7 @@ export default {
   methods: {
     populateMarkers () {
       // remove any existant marker layers
-      this.map.eachLayer((layer) => {
+      this.map.eachLayer((layer: GridLayer) => {
         if (layer instanceof L.MarkerClusterGroup) {
           this.map.removeLayer(layer)
         }
@@ -108,7 +117,7 @@ export default {
       }
       this.map.addLayer(clusters)
     },
-    setMapBounds (bounds) {
+    setMapBounds (bounds: LatLngBoundsExpression) {
       this.map.fitBounds(bounds) // << without this, the new bounds may not be properly set when the current view overlaps the new bounds.
     },
     resetBounds () {
@@ -118,17 +127,24 @@ export default {
       this.zoomRequired = true
     },
     _fitBoundsToMarkers () {
-      const coords = this.structures.filter(
-        (s) => { return s.latitude && s.longitude }
+      const coords: LatLngBoundsLiteral = this.structures.filter(
+        (s) => {
+          return (
+            s.latitude && typeof s.latitude !== 'undefined' &&
+            s.longitude && typeof s.latitude !== 'undefined'
+          )
+        }
       ).map(
-        (s) => { return [s.latitude, s.longitude] })
-      if (!coords.length > 0) {
+        (s) => { return [s.latitude as number, s.longitude as number] as LatLngTuple }
+      )
+
+      if (!coords) {
         return
       }
       this.setMapBounds(coords)
     }
   }
-}
+})
 </script>
 
 <style scoped>
@@ -144,7 +160,7 @@ export default {
     font-size: 0.9rem;
     width: 100%;
   }
-  .map-shortcuts .col {
+  .map-shortcuts {
     padding: 12px 6px;
   }
   .map-shortcuts img {

+ 51 - 16
components/Ui/Search/Address.vue

@@ -27,8 +27,41 @@
   </div>
 </template>
 
-<script>
-export default {
+<script lang="ts">
+
+import Vue from 'vue'
+
+/**
+ * Address properties as returned by the https://geo.api.gouv.fr/adresse API
+ */
+interface AddressProperties {
+  label: string,
+  score: number,
+  housenumber: string,
+  id: string,
+  name: string,
+  postcode: string,
+  citycode: string,
+  x: number,
+  y: number,
+  city: string,
+  district: string,
+  context: string,
+  type: 'housenumber' | 'street' | 'locality' | 'municipality'
+  importance: number,
+  street: string
+}
+
+/**
+ * Localized addresses as returned by the https://geo.api.gouv.fr/adresse API
+ */
+interface Feature {
+  type: 'Feature',
+  geometry: { type: 'Point', coordinates: [ number, number ]},
+  properties: AddressProperties
+}
+
+export default Vue.extend({
   props: {
     value: {
       type: String,
@@ -39,7 +72,7 @@ export default {
       type: String,
       required: false,
       default: 'housenumber',
-      validator (value) {
+      validator (value: string) {
         return ['housenumber', 'street', 'locality', 'municipality'].includes(value)
       }
     },
@@ -56,26 +89,26 @@ export default {
   },
   data () {
     return {
-      model: null,
+      model: null as Address | null,
       search: null,
-      features: [],
+      features: [] as Array<Feature>,
       loading: false,
       errorMsg: ''
     }
   },
   computed: {
-    items () {
-      return this.features.map((f) => {
+    items (): Array<Address> {
+      return this.features.map((f: Feature) => {
         return {
           text: f.properties.name + ' (' + f.properties.postcode + ')',
-          value: { longitude: f.geometry.coordinates[0], latitude: f.geometry.coordinates[1] },
+          value: { longitude: f.geometry.coordinates[0] as number, latitude: f.geometry.coordinates[1] as number },
           disabled: false
         }
       })
     }
   },
   watch: {
-    search (val) {
+    search (val: string) {
       if (!val) {
         this.features = []
         return
@@ -89,6 +122,7 @@ export default {
                      `&autocomplete=${this.autocomplete ? 1 : 0}` +
                      `&limit=${this.limit}` +
                      `&q=${val}`
+
       fetch(encodeURI(apiUrl))
         .then(res => res.json())
         .then(({ features }) => {
@@ -114,21 +148,22 @@ export default {
     geolocalizeUser () {
       if (navigator.geolocation) {
         navigator.geolocation.getCurrentPosition(
-          (position) => {
-            this.latitude = position.coords.latitude
-            this.longitude = position.coords.longitude
-            this.text = this.$t('my_position')
+          (position: { coords: { longitude: number, latitude: number }}) => {
+            this.model = {
+              text: this.$t('my_position') as string,
+              value: { longitude: position.coords.longitude, latitude: position.coords.latitude }
+            }
           },
           () => {
-            this.errorMsg = this.$t('geolocation_error')
+            this.errorMsg = this.$t('geolocation_error') as string
           }
         )
       } else {
-        this.errorMsg = this.$t('geolocation_not_supported')
+        this.errorMsg = this.$t('geolocation_not_supported') as string
       }
     }
   }
-}
+})
 </script>
 
 <style>

+ 1 - 1
nuxt.config.js

@@ -105,7 +105,7 @@ export default {
     baseURL: process.env.NODE_ENV === 'production' ? 'https://local.api.opentalent.fr' : 'https://local.api.opentalent.fr',
     axios: {
       https: true,
-      browserBaseURL: process.env.NODE_ENV === 'production' ? 'https://local.new.api.opentalent.fr' : 'https://local.new.api.opentalent.fr'
+      browserBaseURL: process.env.NODE_ENV === 'production' ? 'https://local.api.opentalent.fr' : 'https://local.api.opentalent.fr'
     }
   },
   privateRuntimeConfig: {

+ 1 - 0
package.json

@@ -34,6 +34,7 @@
     "@nuxtjs/eslint-config-typescript": "^6.0.1",
     "@nuxtjs/eslint-module": "^3.0.2",
     "@nuxtjs/vuetify": "^1.12.1",
+    "@types/leaflet": "^1.7.5",
     "@vue/test-utils": "^1.2.1",
     "babel-core": "7.0.0-bridge.0",
     "babel-jest": "^27.0.5",

+ 50 - 45
pages/structures/index.vue

@@ -266,31 +266,33 @@
   </LayoutContainer>
 </template>
 
-<script>
+<script lang="ts">
+import Vue from 'vue'
 import departments from '@/enums/departments'
 import practices from '@/enums/practices'
 import sphericDistance from '@/services/utils/geo'
 import StructuresProvider from '~/services/data/StructuresProvider'
+import { LatLngBoundsExpression } from 'leaflet'
 
-export default {
+export default Vue.extend({
   data () {
     return {
-      structures: [],
-      filteredStructures: [],
-      federations: [],
+      structures: [] as Array<Structure>,
+      filteredStructures: [] as Array<Structure>,
+      federations: [] as Array<{ id: number | null, name: string | null }>,
       loading: true,
       page: 1,
       itemsPerPage: 8,
       mapview: true,
-      departments,
-      practices,
-      textFilter: '',
-      locationFilter: null,
-      practicesFilter: null,
-      departmentFilter: null,
-      federationFilter: null,
-      distanceFilter: null,
-      mapBoundsFilter: null,
+      departments: departments as {code: string, label: string}[],
+      practices: practices as {id: string}[],
+      textFilter: null as string | null,
+      locationFilter: null as Address | null,
+      practicesFilter: null as string | null,
+      departmentFilter: null as string | null,
+      federationFilter: null as number | null,
+      distanceFilter: null as number | null,
+      mapBoundsFilter: null as LatLngBoundsExpression | null,
       mapBoundsFilterStarted: false // map bounds filter is only activated when the map bounds are updated
     }
   },
@@ -313,54 +315,54 @@ export default {
       })
   },
   computed: {
-    totalRecords () {
+    onMapFilteredStructures (): Array<Structure> {
+      if (this.mapBoundsFilterStarted) {
+        return this.filteredStructures.filter((s) => {
+          return this.matchMapBounds(s)
+        })
+      } else {
+        return this.filteredStructures
+      }
+    },
+    totalRecords (): number {
       return this.onMapFilteredStructures.length
     },
-    pageCount () {
+    pageCount (): number {
       return Math.floor(this.totalRecords / this.itemsPerPage) + 1
     },
-    listview () {
+    listview (): boolean {
       return !this.mapview
     },
-    translatedPractices () {
+    translatedPractices (): Array<{ id: string, label: string }> {
       const tPractices = []
       for (const practice of this.practices) {
-        tPractices.push({ id: practice.id, label: this.$t(practice.id) })
+        tPractices.push({ id: practice.id, label: this.$t(practice.id) as string })
       }
       return tPractices
-    },
-    onMapFilteredStructures () {
-      if (this.mapBoundsFilterStarted) {
-        return this.filteredStructures.filter((s) => {
-          return this.matchMapBounds(s)
-        })
-      } else {
-        return this.filteredStructures
-      }
     }
   },
   methods: {
-    viewChanged (e) {
+    viewChanged (e: number) {
       this.mapview = (e === 0)
     },
-    textFilterChanged (newVal) {
+    textFilterChanged (newVal: string) {
       this.textFilter = newVal
     },
-    locationFilterChanged (newVal) {
+    locationFilterChanged (newVal: Address) {
       this.locationFilter = newVal
       if (this.distanceFilter === null) {
         this.distanceFilter = 10
       }
       this.search()
     },
-    fitMapToResults () {
-      this.$refs.map.zoomOnResults()
+    fitMapToResults (): void {
+      (this.$refs.map as any).zoomOnResults()
     },
-    mapBoundsFilterChanged (newBounds) {
+    mapBoundsFilterChanged (newBounds: LatLngBoundsExpression) {
       this.mapBoundsFilterStarted = true
       this.mapBoundsFilter = newBounds
     },
-    reinitializeFilters () {
+    reinitializeFilters (): void {
       this.textFilter = null
       this.locationFilter = null
       this.practicesFilter = null
@@ -377,7 +379,7 @@ export default {
      * @param structure
      * @returns {boolean}
      */
-    matchTextFilter (structure) {
+    matchTextFilter (structure: Structure): boolean {
       if (!this.textFilter) { return true }
 
       return structure.name.toLowerCase().includes(this.textFilter.toLowerCase())
@@ -387,7 +389,7 @@ export default {
      * @param structure
      * @returns {boolean}
      */
-    matchLocationFilter (structure) {
+    matchLocationFilter (structure: Structure): boolean {
       if (!this.locationFilter) { return true }
       if (!structure.latitude || !structure.longitude) { return false }
 
@@ -402,7 +404,7 @@ export default {
      * @param structure
      * @returns {boolean}
      */
-    matchPracticesFilter (structure) {
+    matchPracticesFilter (structure: Structure): boolean {
       if (!this.practicesFilter) { return true }
       return structure.practices && structure.practices.includes(this.practicesFilter)
     },
@@ -411,7 +413,7 @@ export default {
      * @param structure
      * @returns {boolean}
      */
-    matchDepartmentFilter (structure) {
+    matchDepartmentFilter (structure: Structure): boolean {
       if (!this.departmentFilter) { return true }
       return structure.postalCode.startsWith(this.departmentFilter)
     },
@@ -420,7 +422,7 @@ export default {
      * @param structure
      * @returns {boolean}
      */
-    matchFederationFilter (structure) {
+    matchFederationFilter (structure: Structure): boolean {
       if (!this.federationFilter) { return true }
       return structure.parents.includes(Number(this.federationFilter))
     },
@@ -429,9 +431,12 @@ export default {
      * @param structure
      * @returns {boolean}
      */
-    matchMapBounds (structure) {
+    matchMapBounds (structure: Structure): boolean {
       if (!this.mapBoundsFilter) { return true }
-      return this.mapBoundsFilter.getSouth() <= structure.latitude &&
+      return structure.latitude && structure.longitude &&
+           typeof structure.latitude !== 'undefined' &&
+           typeof structure.longitude !== 'undefined' &&
+           this.mapBoundsFilter.getSouth() <= structure.latitude &&
            structure.latitude <= this.mapBoundsFilter.getNorth() &&
            this.mapBoundsFilter.getWest() <= structure.longitude &&
            structure.longitude <= this.mapBoundsFilter.getEast()
@@ -441,7 +446,7 @@ export default {
      * @param structure
      * @returns {boolean}
      */
-    matchFilters (structure) {
+    matchFilters (structure: Structure): boolean {
       return this.matchTextFilter(structure) &&
         this.matchLocationFilter(structure) &&
         this.matchPracticesFilter(structure) &&
@@ -452,12 +457,12 @@ export default {
     /**
      * Update the filteredStructures array
      */
-    search () {
+    search (): void {
       this.filteredStructures = this.structures.filter((s) => { return this.matchFilters(s) })
       this.fitMapToResults()
     }
   }
-}
+})
 </script>
 
 <style scoped lang="scss">

+ 37 - 0
types/interfaces.d.ts

@@ -0,0 +1,37 @@
+/**
+ * Structures data as returned by the API and consumed by the structures page
+ */
+interface Structure {
+  readonly id: number,
+  name: string,
+  logoId?: string,
+  principalType?: string,
+  website?: string,
+  latitude?: number,
+  longitude?: number,
+  streetAddress?: string,
+  postalCode?: string,
+  addressCity?: string,
+  country?: string,
+  telphone?: string,
+  email?: string,
+  facebook?: string,
+  twitter?: string,
+  practices: Array<string>,
+  n1Id: number | null,
+  n1Name: string | null,
+  n2Id: number | null,
+  n3Id: number | null,
+  n4Id: number | null,
+  n5Id: number | null,
+  parents: Array<string>
+}
+
+/**
+ * Items of the UiSearchAddress component
+ */
+interface Address {
+  text: string,
+  value: { longitude: number, latitude: number },
+  disabled?: boolean
+}

+ 7 - 0
yarn.lock

@@ -1976,6 +1976,13 @@
   dependencies:
     "@types/geojson" "*"
 
+"@types/leaflet@^1.7.5":
+  version "1.7.5"
+  resolved "https://registry.yarnpkg.com/@types/leaflet/-/leaflet-1.7.5.tgz#7b2bcf1271fb7b8c305e3c468eaad65b6dbac472"
+  integrity sha512-+Myo00Yb5OuvUyrH+vUwn9DRgOaBJsF/etIMdMcNhWGBMo58Mo1cxLInvCd0ZpvItju/AeDYFB/Od2pLiHB3VA==
+  dependencies:
+    "@types/geojson" "*"
+
 "@types/less@3.0.2":
   version "3.0.2"
   resolved "https://registry.yarnpkg.com/@types/less/-/less-3.0.2.tgz#2761d477678c8374cb9897666871662eb1d1115e"