From cab8d84759fc7e92ede682621b1d7613dea64fb0 Mon Sep 17 00:00:00 2001
From: Nils Brinckmann <nils.brinckmann@gfz-potsdam.de>
Date: Thu, 3 Aug 2023 12:14:30 +0200
Subject: [PATCH] Resolve "No pages implemented for search for devices &
 platforms in the mount wizzard"

---
 components/configurations/MountWizard.vue     |   4 +-
 .../MountWizardEntitySelect.vue               | 226 ++++++++++++++++--
 components/shared/BaseMountList.vue           |  46 +++-
 services/sms/DeviceApi.ts                     |  23 --
 services/sms/PlatformApi.ts                   |  23 --
 store/devices.ts                              |   7 -
 store/platforms.ts                            |   9 -
 7 files changed, 241 insertions(+), 97 deletions(-)

diff --git a/components/configurations/MountWizard.vue b/components/configurations/MountWizard.vue
index 69d3e709f..f008e3d1e 100644
--- a/components/configurations/MountWizard.vue
+++ b/components/configurations/MountWizard.vue
@@ -274,8 +274,8 @@ import MountActionDetailsForm from '@/components/configurations/MountActionDetai
     ...mapState('contacts', ['contacts'])
   },
   methods: {
-    ...mapActions('devices', ['searchDevices', 'clearDeviceAvailabilities']),
-    ...mapActions('platforms', ['searchPlatforms', 'clearPlatformAvailabilities']),
+    ...mapActions('devices', ['clearDeviceAvailabilities']),
+    ...mapActions('platforms', ['clearPlatformAvailabilities']),
     ...mapActions('configurations', ['addDeviceMountAction', 'addPlatformMountAction', 'loadConfiguration', 'loadMountingConfigurationForDate'])
   }
 })
diff --git a/components/configurations/MountWizardEntitySelect.vue b/components/configurations/MountWizardEntitySelect.vue
index b5d8037ea..7684c8f95 100644
--- a/components/configurations/MountWizardEntitySelect.vue
+++ b/components/configurations/MountWizardEntitySelect.vue
@@ -60,7 +60,7 @@ permissions and limitations under the Licence.
                       </v-col>
                       <v-col
                         cols="12"
-                        md="7"
+                        md="5"
                         align-self="center"
                       >
                         <v-btn
@@ -78,28 +78,52 @@ permissions and limitations under the Licence.
                           Clear
                         </v-btn>
                       </v-col>
+                      <v-col cols="12" md="7">
+                        <v-subheader>
+                          <page-size-select
+                            v-model="deviceSearchSearchPageSize"
+                            :items="devicePageSizeItems"
+                            label="Items per page"
+                          />
+                        </v-subheader>
+                      </v-col>
                       <ProgressIndicator
                         v-model="isLoading"
                       />
                     </v-row>
-                    <div v-if="devices.length>0 && deviceAvailabilities.length>0">
+                    <div v-if="devicesTotalCount > 0 && deviceAvailabilities.length>0">
                       <v-subheader>
-                        <template v-if="devices.length == 1">
+                        <template v-if="devicesTotalCount == 1">
                           1 device found
                         </template>
                         <template v-else>
-                          {{ devices.length }} devices found
+                          {{ devicesTotalCount }} devices found
                         </template>
                         <v-spacer />
                       </v-subheader>
+                      <v-pagination
+                        v-if="devicesSearchPage != 1 || devicesTotalPages > 1"
+                        v-model="devicesSearchPage"
+                        :disabled="isLoading"
+                        :length="devicesTotalPages"
+                        :total-visible="7"
+                      />
                       <base-mount-list
-                        v-model="syncedSelectedDevices"
+                        :value="syncedSelectedDevices"
                         :items="devices"
                         :availabilities="deviceAvailabilities"
+                        keep-values-that-are-not-in-items
                         @selectEntity="setDeviceSelection($event)"
                       />
+                      <v-pagination
+                        v-if="devicesSearchPage != 1 || devicesTotalPages > 1"
+                        v-model="devicesSearchPage"
+                        :disabled="isLoading"
+                        :length="devicesTotalPages"
+                        :total-visible="7"
+                      />
                     </div>
-                    <div v-else-if="devices.length <=0 && hasSearchedDevice">
+                    <div v-else-if="devicesTotalCount <=0 && hasSearchedDevice">
                       <v-subheader>
                         There are no devices that match your search criteria.
                       </v-subheader>
@@ -122,7 +146,7 @@ permissions and limitations under the Licence.
                       </v-col>
                       <v-col
                         cols="12"
-                        md="7"
+                        md="5"
                         align-self="center"
                       >
                         <v-btn
@@ -140,28 +164,52 @@ permissions and limitations under the Licence.
                           Clear
                         </v-btn>
                       </v-col>
+                      <v-col cols="12" md="7">
+                        <v-subheader>
+                          <page-size-select
+                            v-model="platformSearchSearchPageSize"
+                            :items="platformPageSizeItems"
+                            label="Items per page"
+                          />
+                        </v-subheader>
+                      </v-col>
                       <ProgressIndicator
                         v-model="isLoading"
                       />
                     </v-row>
-                    <div v-if="platforms.length>0 && platformAvailabilities.length>0">
+                    <div v-if="platformsTotalCount > 0 && platformAvailabilities.length>0">
                       <v-subheader>
-                        <template v-if="platforms.length == 1">
+                        <template v-if="platformsTotalCount == 1">
                           1 platform found
                         </template>
                         <template v-else>
-                          {{ platforms.length }} platforms found
+                          {{ platformsTotalCount }} platforms found
                         </template>
                         <v-spacer />
                       </v-subheader>
+                      <v-pagination
+                        v-if="platformsSearchPage != 1 || platformsTotalPages > 1"
+                        v-model="platformsSearchPage"
+                        :disabled="isLoading"
+                        :length="platformsTotalPages"
+                        :total-visible="7"
+                      />
                       <base-mount-list
-                        v-model="syncedSelectedPlatforms"
+                        :value="syncedSelectedPlatforms"
                         :items="platforms"
                         :availabilities="platformAvailabilities"
+                        keep-values-that-are-not-in-items
                         @selectEntity="setPlatformSelection($event)"
                       />
+                      <v-pagination
+                        v-if="platformsSearchPage != 1 || platformsTotalPages > 1"
+                        v-model="platformsSearchPage"
+                        :disabled="isLoading"
+                        :length="platformsTotalPages"
+                        :total-visible="7"
+                      />
                     </div>
-                    <div v-else-if="platforms.length <=0 && hasSearchedPlatform">
+                    <div v-else-if="platformsTotalCount <=0 && hasSearchedPlatform">
                       <v-subheader>
                         There are no platforms that match your search criteria.
                       </v-subheader>
@@ -195,13 +243,13 @@ permissions and limitations under the Licence.
 </template>
 
 <script lang="ts">
-import { Component, Vue, PropSync, InjectReactive } from 'nuxt-property-decorator'
-import { mapActions, mapState } from 'vuex'
+import { Component, Vue, PropSync, InjectReactive, Watch } from 'nuxt-property-decorator'
+import { mapActions, mapGetters, mapState } from 'vuex'
 
 import { DateTime } from 'luxon'
 
-import { DevicesState, SearchDevicesAction, LoadDeviceAvailabilitiesAction } from '@/store/devices'
-import { PlatformsState, SearchPlatformsAction, LoadPlatformAvailabilitiesAction } from '@/store/platforms'
+import { DevicesState, LoadDeviceAvailabilitiesAction, SearchDevicesPaginatedAction } from '@/store/devices'
+import { PlatformsState, SearchPlatformsPaginatedAction, LoadPlatformAvailabilitiesAction } from '@/store/platforms'
 
 import { Device } from '@/models/Device'
 import { DeviceMountAction } from '@/models/DeviceMountAction'
@@ -213,6 +261,7 @@ import BaseMountList from '@/components/shared/BaseMountList.vue'
 
 import PlatformsListItem from '@/components/platforms/PlatformsListItem.vue'
 import DevicesListItem from '@/components/devices/DevicesListItem.vue'
+import PageSizeSelect from '@/components/shared/PageSizeSelect.vue'
 
 import ProgressIndicator from '@/components/ProgressIndicator.vue'
 
@@ -222,15 +271,33 @@ import ProgressIndicator from '@/components/ProgressIndicator.vue'
     PlatformsListItem,
     DevicesListItem,
     BaseList,
-    BaseMountList
+    BaseMountList,
+    PageSizeSelect
+
   },
   computed: {
-    ...mapState('devices', ['devices', 'deviceAvailabilities']),
-    ...mapState('platforms', ['platforms', 'platformAvailabilities'])
+    ...mapState('devices', {
+      devices: 'devices',
+      deviceAvailabilities: 'deviceAvailabilities',
+      devicesTotalCount: 'totalCount',
+      devicesTotalPages: 'totalPages'
+    }),
+    ...mapState('platforms', {
+      platforms: 'platforms',
+      platformAvailabilities: 'platformAvailabilities',
+      platformsTotalCount: 'totalCount',
+      platformsTotalPages: 'totalPages'
+    }),
+    ...mapGetters('devices', {
+      devicePageSizeItems: 'pageSizes'
+    }),
+    ...mapGetters('platforms', {
+      platformPageSizeItems: 'pageSizes'
+    })
   },
   methods: {
-    ...mapActions('devices', ['searchDevices', 'loadDeviceAvailabilities']),
-    ...mapActions('platforms', ['searchPlatforms', 'loadPlatformAvailabilities'])
+    ...mapActions('devices', ['searchDevicesPaginated', 'loadDeviceAvailabilities']),
+    ...mapActions('platforms', ['searchPlatformsPaginated', 'loadPlatformAvailabilities'])
   }
 })
 export default class MountWizardEntitySelect extends Vue {
@@ -261,12 +328,17 @@ export default class MountWizardEntitySelect extends Vue {
   @InjectReactive() selectedDate!: DateTime
   @InjectReactive() selectedEndDate!: DateTime | null
 
+  private resetDeviceSearchToFirstPage = false
+  private resetPlatformSearchToFirstPage = false
+
   // vuex definition for typescript check
   devices!: DevicesState['devices']
   platforms!: PlatformsState['platforms']
-  searchDevices!: SearchDevicesAction
+  devicesTotalPages!: DevicesState['totalPages']
+  platformsTotalPages!: PlatformsState['totalPages']
+  searchDevicesPaginated!: SearchDevicesPaginatedAction
   loadDeviceAvailabilities!: LoadDeviceAvailabilitiesAction
-  searchPlatforms!: SearchPlatformsAction
+  searchPlatformsPaginated!: SearchPlatformsPaginatedAction
   loadPlatformAvailabilities!: LoadPlatformAvailabilitiesAction
 
   private tab = null
@@ -278,21 +350,47 @@ export default class MountWizardEntitySelect extends Vue {
   private hasSearchedDevice = false
   private hasSearchedPlatform = false
 
+  mounted () {
+    // Start with some clean state for devices & platforms search
+    this.$store.commit('devices/setDevices', [])
+    this.$store.commit('devices/setTotalCount', 0)
+    this.$store.commit('devices/setPageNumber', 1)
+    this.$store.commit('platforms/setPlatforms', [])
+    this.$store.commit('platforms/setTotalCount', 0)
+    this.$store.commit('platforms/setPageNumber', 1)
+  }
+
   clearBasicSearchPlatforms () {
     this.searchTextPlatforms = ''
     this.hasSearchedPlatform = false
   }
 
   clearBasicSearchDevices () {
-    this.searchTextPlatforms = ''
+    this.searchTextDevices = ''
     this.hasSearchedDevice = false
   }
 
   async searchDevicesForMount () {
+    if (this.resetDeviceSearchToFirstPage) {
+      this.$store.commit('devices/setPageNumber', 1)
+      this.resetDeviceSearchToFirstPage = false
+    }
     try {
       this.isLoading = true
-      await this.searchDevices(this.searchTextDevices)
+      await this.searchDevicesPaginated({
+        searchText: this.searchTextDevices,
+        manufacturer: [],
+        states: [],
+        types: [],
+        permissionGroups: [],
+        onlyOwnDevices: false,
+        includeArchivedDevices: false
+      })
       await this.checkAvailabilities('device')
+      if (this.devicesSearchPage > this.devicesTotalPages) {
+        // triggers also a new search
+        this.devicesSearchPage = this.devicesTotalPages
+      }
     } catch (e) {
       this.$store.commit('snackbar/setError', 'Loading of devices failed')
     } finally {
@@ -302,10 +400,28 @@ export default class MountWizardEntitySelect extends Vue {
   }
 
   async searchPlatformsForMount () {
+    if (this.resetPlatformSearchToFirstPage) {
+      this.$store.commit('platforms/setPageNumber', 1)
+      this.resetPlatformSearchToFirstPage = false
+    }
     try {
       this.isLoading = true
-      await this.searchPlatforms(this.searchTextPlatforms)
+
+      // Not bound as methods, as there can be name conflicts with the devices.
+      this.$store.dispatch('platforms/setSearchText', this.searchTextPlatforms)
+      this.$store.dispatch('platforms/setSelectedSearchManufacturers', [])
+      this.$store.dispatch('platforms/setSelectedSearchStates', [])
+      this.$store.dispatch('platforms/setSelectedSearchPlatformTypes', [])
+      this.$store.dispatch('platforms/setSelectedSearchPermissionGroups', [])
+      this.$store.dispatch('platforms/setOnlyOwnPlatforms', false)
+      this.$store.dispatch('platforms/setIncludeArchivedPlatforms', false)
+
+      await this.searchPlatformsPaginated()
       await this.checkAvailabilities('platform')
+      if (this.platformsSearchPage > this.platformsTotalPages) {
+        // triggers also a new search
+        this.platformsSearchPage = this.platformsTotalPages
+      }
     } catch (e) {
       this.$store.commit('snackbar/setError', 'Loading of platforms failed')
     } finally {
@@ -349,6 +465,64 @@ export default class MountWizardEntitySelect extends Vue {
   get selectedEntities () {
     return [...this.syncedSelectedPlatforms, ...this.syncedSelectedDevices]
   }
+
+  get deviceSearchSearchPageSize (): number {
+    return this.$store.state.devices.pageSize
+  }
+
+  set deviceSearchSearchPageSize (newVal: number) {
+    const oldVal = this.deviceSearchSearchPageSize
+    if (oldVal !== newVal) {
+      this.$store.dispatch('devices/setPageSize', newVal)
+      this.searchDevicesForMount()
+    }
+  }
+
+  get devicesSearchPage (): number {
+    return this.$store.state.devices.pageNumber
+  }
+
+  set devicesSearchPage (newVal: number) {
+    const oldVal = this.devicesSearchPage
+    if (oldVal !== newVal) {
+      this.$store.dispatch('devices/setPageNumber', newVal)
+      this.searchDevicesForMount()
+    }
+  }
+
+  get platformSearchSearchPageSize (): number {
+    return this.$store.state.platforms.pageSize
+  }
+
+  set platformSearchSearchPageSize (newVal: number) {
+    const oldVal = this.platformSearchSearchPageSize
+    if (oldVal !== newVal) {
+      this.$store.dispatch('platforms/setPageSize', newVal)
+      this.searchPlatformsForMount()
+    }
+  }
+
+  get platformsSearchPage (): number {
+    return this.$store.state.platforms.pageNumber
+  }
+
+  set platformsSearchPage (newVal: number) {
+    const oldVal = this.platformsSearchPage
+    if (oldVal !== newVal) {
+      this.$store.dispatch('platforms/setPageNumber', newVal)
+      this.searchPlatformsForMount()
+    }
+  }
+
+  @Watch('searchTextDevices')
+  onSearchTextDevicesChanged () {
+    this.resetDeviceSearchToFirstPage = true
+  }
+
+  @Watch('searchTextPlatforms')
+  onSearchTextPlatformsChanged () {
+    this.resetPlatformSearchToFirstPage = true
+  }
 }
 </script>
 
diff --git a/components/shared/BaseMountList.vue b/components/shared/BaseMountList.vue
index 833d8420e..3539adce8 100644
--- a/components/shared/BaseMountList.vue
+++ b/components/shared/BaseMountList.vue
@@ -2,7 +2,7 @@
 Web client of the Sensor Management System software developed within the
 Helmholtz DataHub Initiative by GFZ and UFZ.
 
-Copyright (C) 2020, 2021
+Copyright (C) 2020 - 2023
 - Nils Brinckmann (GFZ, nils.brinckmann@gfz-potsdam.de)
 - Marc Hanisch (GFZ, marc.hanisch@gfz-potsdam.de)
 - Tobias Kuhnert (UFZ, tobias.kuhnert@ufz.de)
@@ -33,14 +33,15 @@ permissions and limitations under the Licence.
   <div>
     <v-list>
       <v-list-item-group
-        v-model="selectedEntities"
+        :value="value"
         :value-comparator="compareEntities"
         multiple
         color="primary"
+        @change="change"
       >
         <template v-for="(item, i) in items">
           <v-list-item
-            :key="`item-${i}`"
+            :key="itemKey(item, i)"
             :value="item"
             active-class="primary--text text--accent-4"
             :disabled="!isAvailable(item)"
@@ -117,6 +118,12 @@ export default class BaseMountList extends Vue {
   })
   private availabilities!: Availability[]
 
+  @Prop({
+    default: false,
+    type: Boolean
+  })
+  private keepValuesThatAreNotInItems!: boolean
+
   private availabilitiesWithConfigs: Availability[] = this.availabilities
 
   async fetch () {
@@ -134,12 +141,37 @@ export default class BaseMountList extends Vue {
   async created () {
   }
 
-  get selectedEntities (): [Platform | Device] {
-    return this.value
+  itemKey (item: Platform | Device, i: number): string {
+    if (item.id) {
+      return `item-id-${item.id}`
+    }
+    return `item-index-${i}`
   }
 
-  set selectedEntities (entities: [Platform | Device]) {
-    this.$emit('selectEntity', entities)
+  change (entities: [Platform | Device]) {
+    const result: [Platform | Device] = [...entities]
+
+    if (this.keepValuesThatAreNotInItems) {
+      this.value.forEach((x: Platform | Device) => {
+        if (this.items.findIndex(i => i.id === x.id) < 0) {
+          if (entities.findIndex(e => e.id === x.id) < 0) {
+            // Ok, here we are in this case that the new selection does not
+            // contain an entry that is in the value - and the entry is not
+            // covered by the items nor the entities list.
+            // For those cases we want to stay with those values.
+            result.push(x)
+          }
+        }
+      })
+      // You may wonder why this is needed? In some cases this is the default
+      // behaviour, but in some it wasn't.
+      // The problem was on adding the pagination to the mount wizzard.
+      // After switching to another page, there was an emty entities entry
+      // (while we actually wanted the values to stay in the selection).
+      // So, this is the workaround here.
+    }
+
+    this.$emit('selectEntity', result)
   }
 
   getAvailability (entity: Platform | Device): Availability | undefined {
diff --git a/services/sms/DeviceApi.ts b/services/sms/DeviceApi.ts
index 3f5209166..cad8b53e6 100644
--- a/services/sms/DeviceApi.ts
+++ b/services/sms/DeviceApi.ts
@@ -251,29 +251,6 @@ export class DeviceApi {
     })
   }
 
-  async searchAll () {
-    this.prepareSearch()
-    // set the permission groups for the serializer
-    if (this.permissionFetcher) {
-      this.serializer.permissionGroups = await this.permissionFetcher()
-    }
-    return this.axiosApi.get(
-      this.basePath,
-      {
-        params: {
-          ...this.commonParams
-        }
-      }
-    ).then((rawResponse: any) => {
-      const rawData = rawResponse.data
-      // We don't ask the api to load the contacts, so we just add dummy objects
-      // to stay with the relationships
-      return this.serializer
-        .convertJsonApiObjectListToModelList(rawData)
-        .map(deviceWithMetaToDeviceByAddingDummyObjects)
-    })
-  }
-
   async searchRecentlyUpdated (amount: number) {
     this.prepareSearch()
     return await this.axiosApi.get(
diff --git a/services/sms/PlatformApi.ts b/services/sms/PlatformApi.ts
index 5a9c59f94..5c6b19cb1 100644
--- a/services/sms/PlatformApi.ts
+++ b/services/sms/PlatformApi.ts
@@ -242,29 +242,6 @@ export class PlatformApi {
     })
   }
 
-  async searchAll () {
-    this.prepareSearch()
-    // set the permission groups for the serializer
-    if (this.permissionFetcher) {
-      this.serializer.permissionGroups = await this.permissionFetcher()
-    }
-    return this.axiosApi.get(
-      this.basePath,
-      {
-        params: {
-          ...this.commonParams
-        }
-      }
-    ).then((rawResponse: any) => {
-      const rawData = rawResponse.data
-      // We don't ask the api to load the contacts, so we just add dummy objects
-      // to stay with the relationships
-      return this.serializer
-        .convertJsonApiObjectListToModelList(rawData)
-        .map(platformWithMetaToPlatformByAddingDummyObjects)
-    })
-  }
-
   async getSensorML (platformId: string): Promise<Blob> {
     const url = this.basePath + '/' + platformId + '/sensorml'
     const response = await this.axiosApi.get(url)
diff --git a/store/devices.ts b/store/devices.ts
index f4bfa4ad9..e13a5a233 100644
--- a/store/devices.ts
+++ b/store/devices.ts
@@ -239,7 +239,6 @@ export type RemoveDeviceContactRoleAction = (params: { deviceContactRoleId: stri
 export type ReplaceDeviceInDevicesAction = (newDevice: Device) => void
 export type RestoreDeviceAction = (id: string) => Promise<void>
 export type SaveDeviceAction = (device: Device) => Promise<Device>
-export type SearchDevicesAction = (id: string) => Promise<void>
 export type SearchDevicesPaginatedAction = (searchParams: IDeviceSearchParams) => Promise<void>
 export type SetChosenKindOfDeviceActionAction = (newval: IOptionsForActionType | null) => void
 export type SetPageNumberAction = (newPageNumber: number) => void
@@ -287,12 +286,6 @@ const actions: ActionTree<DevicesState, RootState> = {
     commit('setTotalPages', totalPages)
     commit('setTotalCount', totalCount)
   },
-  async searchDevices ({ commit }: { commit: Commit }, searchText: string = ''): Promise<void> {
-    const devices = await this.$api.devices
-      .setSearchText(searchText)
-      .searchAll()
-    commit('setDevices', devices)
-  },
   async loadDevice ({ commit }: { commit: Commit },
     {
       deviceId,
diff --git a/store/platforms.ts b/store/platforms.ts
index 5a456ac14..df3f6e434 100644
--- a/store/platforms.ts
+++ b/store/platforms.ts
@@ -238,7 +238,6 @@ export type RemovePlatformContactRoleAction = (params: {platformContactRoleId: s
 export type ReplacePlatformInPlatformsAction = (newPlatform: Platform) => void
 export type RestorePlatformAction = (id: string) => Promise<void>
 export type SavePlatformAction = (platform: Platform) => Promise<Platform>
-export type SearchPlatformsAction = (searchtext: string) => Promise<void>
 export type SearchPlatformsPaginatedAction = () => Promise<void>
 export type SetChosenKindOfPlatformActionAction = (newval: IOptionsForActionType | null) => void
 export type SetIncludeArchivedPlatformsAction = (includeArchivedPlatforms: boolean) => void
@@ -290,14 +289,6 @@ const actions: ActionTree<PlatformsState, RootState> = {
     commit('setTotalPages', totalPages)
     commit('setTotalCount', totalCount)
   },
-  async searchPlatforms ({
-    commit
-  }: { commit: Commit }, searchtext: string = ''): Promise<void> {
-    const platforms = await this.$api.platforms
-      .setSearchText(searchtext)
-      .searchAll()
-    commit('setPlatforms', platforms)
-  },
   async loadPlatform ({ commit }: { commit: Commit },
     {
       platformId,
-- 
GitLab