diff --git a/package.json b/package.json index 7f4a2450943b1f262b43298d5bf58c04e15c04c6..8a2f2f6f2ccc6c68f2365a9f30f445f1235b471f 100644 --- a/package.json +++ b/package.json @@ -49,6 +49,7 @@ "vue-demi": "^0.14.6", "vue-i18n": "^8.28.2", "vue-multiselect": "^2.1.8", + "vue-observe-visibility": "^1.0.0", "vue-router": "^3.6.5", "vue-select": "^3.20.2", "vue-sidebar-menu": "^4.8.1" diff --git a/src/modules/resource/components/create-resource/ApplicationProfile.vue b/src/modules/resource/components/create-resource/ApplicationProfile.vue index dc7633fcc03b5ffd65ab6e9d7f84867e5eb15a74..c2d70383f765b13a9b619f62581e4291f3c5379e 100644 --- a/src/modules/resource/components/create-resource/ApplicationProfile.vue +++ b/src/modules/resource/components/create-resource/ApplicationProfile.vue @@ -55,7 +55,18 @@ : false, }" @input="setSelectedApplicationProfile" + @search-change="applicationProfileSearchChange" > + <template slot="afterList"> + <div + v-if="groupedApplicationProfiles.length && !allResultsLoaded" + v-observe-visibility="reachedEndOfAPList" + > + <div class="d-flex justify-content-left my-3 pl-3"> + <b-spinner v-if="isLoadingResults" small /> + </div> + </div> + </template> <span slot="noResult"> {{ $t("page.createResource.multiselect.noResults") }} </span> @@ -121,6 +132,7 @@ import { defineComponent, reactive, ref, type PropType } from "vue"; import useResourceStore from "../../store"; import useUserStore from "@/modules/user/store"; import "@/plugins/form-generator"; +import "@/plugins/vue-observe-visibility"; import type { Dataset } from "@rdfjs/types"; import type { BilingualLabel, @@ -147,6 +159,10 @@ export default defineComponent({ type: Object as PropType<ResourceForCreationDto>, required: true, }, + allResultsLoaded: { + default: false, + type: Boolean, + }, applicationProfile: { type: [Object, null] as PropType<Dataset | null>, default: null, @@ -155,6 +171,10 @@ export default defineComponent({ type: Array as PropType<ApplicationProfileDto[]>, required: true, }, + isLoadingResults: { + default: false, + type: Boolean, + }, isLoadingFormGenerator: { type: Boolean, required: true, @@ -165,8 +185,10 @@ export default defineComponent({ }, }, emits: { - valid: (_: boolean) => true, + applicationProfileSearchChange: (_: string) => true, input: (_: ResourceForCreationDto) => true, + reachedEndOfAPList: () => true, + valid: (_: boolean) => true, }, setup(props) { const resourceStore = useResourceStore(); @@ -227,6 +249,12 @@ export default defineComponent({ }, methods: { + /** + * Trigger the search term change in the parent component. + */ + applicationProfileSearchChange(searchTerm: string) { + this.$emit("applicationProfileSearchChange", searchTerm); + }, /** * Initializes the content of the selected application profile based on the current state of the resource for creation. */ @@ -327,6 +355,15 @@ export default defineComponent({ return groupedProfiles; }, + /** + * Propagate that end of AP list has been hit. + */ + reachedEndOfAPList() { + if (!this.allResultsLoaded) { + this.$emit("reachedEndOfAPList"); + } + }, + /** * Sets the selected application profile. */ diff --git a/src/modules/resource/pages/CreateResource.vue b/src/modules/resource/pages/CreateResource.vue index ace030cd227b444cfab2fd0e4d55359c938de890..fc91ee441152e02791b8ef0fb80e2db9b4249580 100644 --- a/src/modules/resource/pages/CreateResource.vue +++ b/src/modules/resource/pages/CreateResource.vue @@ -27,9 +27,13 @@ <ApplicationProfile v-else-if="currentTab === 1" v-model="resourceForCreation" + :all-results-loaded="allResultsLoaded" :application-profile="applicationProfileDefinition" :application-profiles="applicationProfiles" :is-loading-form-generator="isLoadingFormGenerator" + :is-loading-results="isLoadingResults" + @applicationProfileSearchChange="applicationProfileSearchChange" + @reachedEndOfAPList="reachedEndOfAPList" @valid="setNextTab" /> <ResourceMetadata @@ -107,6 +111,8 @@ export default defineComponent({ data() { return { + allResultsLoaded: false, + apPageNumber: 1, currentTab: 0, tabsStatus: [true, false, false, false], resourceForCreation: { @@ -129,6 +135,8 @@ export default defineComponent({ isLoadingFormGenerator: false, isWaitingForResponse: false, isLoading: false, + isLoadingResults: false, + searchTerm: "", }; }, @@ -182,6 +190,11 @@ export default defineComponent({ this.getApplicationProfile(); } }, + searchTerm() { + this.applicationProfiles = []; + this.apPageNumber = 1; + this.getApplicationProfiles(); + }, }, async created() { @@ -217,13 +230,22 @@ export default defineComponent({ } }, async getApplicationProfiles() { - const applicationProfileDtos = + this.isLoadingResults = true; + const applicationProfilesObject = await this.resourceStore.getApplicationProfiles( "displayName asc, baseUrl asc", + this.searchTerm, + this.apPageNumber, ); - if (applicationProfileDtos) { - this.applicationProfiles = applicationProfileDtos; + this.apPageNumber++; + if (applicationProfilesObject) { + this.applicationProfiles = [ + ...this.applicationProfiles, + ...applicationProfilesObject.applicationProfiles, + ]; + this.allResultsLoaded = applicationProfilesObject.allResultsLoaded; } + this.isLoadingResults = false; }, async getApplicationProfile() { if (this.resourceForCreation?.applicationProfile) { @@ -238,6 +260,12 @@ export default defineComponent({ this.applicationProfileDefinition = null; } }, + reachedEndOfAPList() { + this.getApplicationProfiles(); + }, + applicationProfileSearchChange(searchTerm: string) { + this.searchTerm = searchTerm; + }, back() { this.currentTab -= 1; }, diff --git a/src/modules/resource/store.ts b/src/modules/resource/store.ts index f44172102b227abe4a544b6c60f40097967888f3..c08d3bc6b9e29c15aeeb1cfb983b359df61150bc 100644 --- a/src/modules/resource/store.ts +++ b/src/modules/resource/store.ts @@ -1,6 +1,7 @@ import { defineStore } from "pinia"; import type { ApplicationProfileDefinition, + ApplicationProfilesPaged, BilingualLabel, BilingualLabels, ResourceState, @@ -30,7 +31,6 @@ import factory from "rdf-ext"; import fileSaver from "file-saver"; import type { RdfFormat, - ApplicationProfileDto, GitlabBranchDto, GitlabProjectDto, MetadataTreeForCreationDto, @@ -253,18 +253,23 @@ export const useResourceStore = defineStore({ async getApplicationProfiles( orderBy: string, - ): Promise<ApplicationProfileDto[] | undefined> { + searchTerm: string, + pageNumber: number, + pageSize: number = 10, + ): Promise<ApplicationProfilesPaged | undefined> { const notificationStore = useNotificationStore(); try { - return await wrapListRequest((pageNumber: number) => - ApplicationProfileApi.getApplicationProfiles({ - searchTerm: "", - modules: false, - orderBy, - pageNumber, - pageSize: 50, - }), - ); + const response = await ApplicationProfileApi.getApplicationProfiles({ + searchTerm, + modules: false, + orderBy, + pageNumber, + pageSize, + }); + return { + applicationProfiles: response.data.data ?? [], + allResultsLoaded: !response.data.pagination?.hasNext ?? false, + }; } catch (error) { // Handle other Status Codes notificationStore.postApiErrorNotification(error as AxiosError); diff --git a/src/modules/resource/types.ts b/src/modules/resource/types.ts index a1c08a6fb2e25d4569f988c69b8048aa35f4a8af..b47720a3ba91d13167cdea43761feebb2ccdb416 100644 --- a/src/modules/resource/types.ts +++ b/src/modules/resource/types.ts @@ -252,3 +252,8 @@ export interface ApplicationProfileDefinition { definition: Dataset; displayName: string; } + +export type ApplicationProfilesPaged = { + applicationProfiles: ApplicationProfileDto[]; + allResultsLoaded: boolean; +}; diff --git a/src/plugins/vue-observe-visibility.ts b/src/plugins/vue-observe-visibility.ts new file mode 100644 index 0000000000000000000000000000000000000000..38710d77cce723f83649b1407c663ad9b9d79766 --- /dev/null +++ b/src/plugins/vue-observe-visibility.ts @@ -0,0 +1,4 @@ +import Vue from "vue"; +import VueObserveVisibility from "vue-observe-visibility"; + +Vue.use(VueObserveVisibility); diff --git a/yarn.lock b/yarn.lock index 98fcfd67c1120c97c947164c488d77989e64358a..96e6190f1f96e532515e0f9808402e619c501251 100644 --- a/yarn.lock +++ b/yarn.lock @@ -461,7 +461,6 @@ __metadata: vue-material-design-icons: "npm:^5.2.0" vue-multiselect: "npm:^2.1.8" vue-observe-visibility: "npm:^1.0.0" - vue-observe-visibility: "npm:^1.0.0" peerDependencies: "@vue/composition-api": ^1.0.0-rc.1 vue: ^2.0.0 @@ -13240,6 +13239,7 @@ __metadata: vue-demi: "npm:^0.14.6" vue-i18n: "npm:^8.28.2" vue-multiselect: "npm:^2.1.8" + vue-observe-visibility: "npm:^1.0.0" vue-router: "npm:^3.6.5" vue-select: "npm:^3.20.2" vue-sidebar-menu: "npm:^4.8.1" @@ -13989,13 +13989,6 @@ __metadata: languageName: node linkType: hard -"vue-observe-visibility@npm:^1.0.0": - version: 1.0.0 - resolution: "vue-observe-visibility@npm:1.0.0" - checksum: 46b2e6415d53e42ed53875622f241458af3d8493d31acfcc41d0f53a26f8aaa37378fa49ff3ebdcc5438dbac6d6b77c578d234ca7431917f8a1629fb06c2dfb5 - languageName: node - linkType: hard - "vue-router@npm:^3.6.5": version: 3.6.5 resolution: "vue-router@npm:3.6.5"