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