1414// KIND, either express or implied. See the License for the
1515// specific language governing permissions and limitations
1616// under the License.
17+ <!--
18+ InfiniteScrollSelect.vue
1719
20+ A reusable select component that supports:
21+ - Infinite scrolling with paginated API
22+ - Dynamic search filtering. Needs minimum
23+ - Deduplicated option loading
24+ - Auto-fetching of preselected value if not present in the initial result
25+
26+ Usage Example:
27+
28+ <infinite-scroll-select
29+ v-model:value="form.account"
30+ api="listAccounts"
31+ :apiParams="accountsApiParams"
32+ resourceType="account"
33+ optionValueKey="name"
34+ optionLabelKey="name"
35+ @change-option="handleAccountChange" />
36+
37+ Props:
38+ - api (String, required): API command name (e.g., 'listAccounts')
39+ - apiParams (Object, optional): Additional parameters passed to the API
40+ - resourceType (String, required): The key in the API response containing the resource array (e.g., 'account')
41+ - optionValueKey (String, optional): Property to use as the value for options (e.g., 'name'). Default is 'id'
42+ - optionLabelKey (String, optional): Property to use as the label for options (e.g., 'name'). Default is 'name'
43+ - defaultOption (Object, optional): Preselected object to include initially
44+ - showIcon (Boolean, optional): Whether to show icon for the options. Default is true
45+ - defaultIcon (String, optional): Icon to be shown when there is no resource icon for the option. Default is 'cloud-outlined'
46+
47+ Events:
48+ - @change-option (Function): Emits the selected option object when value changes. Do not use @change as it will give warnings and may not work
49+
50+ Features:
51+ - Debounced remote filtering
52+ - Custom dropdown footer/header (e.g., clear search button)
53+ - Handles preselection and fetches missing option automatically
54+ -->
1855<template >
1956 <a-select
2057 :filter-option =" false"
3774 </div >
3875 </div >
3976 </template >
40- <a-select-option v-for =" option in options" :key =" option.id" :value =" option.id " >
77+ <a-select-option v-for =" option in options" :key =" option.id" :value =" option[optionValueKey] " >
4178 <span >
42- <resource-icon v-if =" option.icon && option.icon.base64image" :option =" option.icon.base64image" size =" 1x" style =" margin-right : 5px " />
43- <render-icon :icon =" defaultIcon" style =" margin-right : 5px " />
44- <span >{{ option.name }}</span >
79+ <span v-if =" showIcon" >
80+ <resource-icon v-if =" option.icon && option.icon.base64image" :image =" option.icon.base64image" size =" 1x" style =" margin-right : 5px " />
81+ <render-icon v-else :icon =" defaultIcon" style =" margin-right : 5px " />
82+ </span >
83+ <span >{{ option[optionLabelKey] }}</span >
4584 </span >
4685 </a-select-option >
4786 </a-select >
@@ -64,14 +103,30 @@ export default {
64103 type: String ,
65104 required: true
66105 },
106+ apiParams: {
107+ type: Object ,
108+ required: null
109+ },
67110 resourceType: {
68111 type: String ,
69112 required: true
70113 },
114+ optionValueKey: {
115+ type: String ,
116+ default: ' id'
117+ },
118+ optionLabelKey: {
119+ type: String ,
120+ default: ' name'
121+ },
71122 defaultOption: {
72123 type: Object ,
73124 default: null
74125 },
126+ showIcon: {
127+ type: Boolean ,
128+ default: true
129+ },
75130 defaultIcon: {
76131 type: String ,
77132 default: ' cloud-outlined'
@@ -89,15 +144,15 @@ export default {
89144 loading: false ,
90145 searchQuery: ' ' ,
91146 scrollHandlerAttached: false ,
92- preselectedOptionId : null ,
147+ preselectedOptionValue : null ,
93148 successiveFetches: 0
94149 }
95150 },
96151 created () {
97152 this .addDefaultOptionIfNeeded (true )
98153 },
99154 mounted () {
100- this .preselectedOptionId = this .$attrs .value
155+ this .preselectedOptionValue = this .$attrs .value
101156 this .fetchItems ()
102157 },
103158 computed: {
@@ -108,38 +163,50 @@ export default {
108163 return this .pageSize || this .$store .getters .defaultListViewPageSize
109164 }
110165 },
166+ watch: {
167+ apiParams () {
168+ this .onSearch ()
169+ }
170+ },
111171 emits: [' change-option' ],
112172 methods: {
113173 async fetchItems () {
114174 if (this .successiveFetches === 0 && this .loading ) return
115175 this .loading = true
116176 const params = {
117177 page: this .page ,
118- pagesize: this .computedPageSize ,
119- keyword: this .searchQuery ,
120- listall: true
178+ pagesize: this .computedPageSize
179+ }
180+ if (this .searchQuery && this .searchQuery .length > 0 ) {
181+ params .keyword = this .searchQuery
182+ }
183+ if (this .apiParams ) {
184+ Object .assign (params, this .apiParams )
185+ }
186+ if (this .showIcon ) {
187+ params .showicon = true
121188 }
122189 api (this .api , params).then (json => {
123190 const response = json[this .api .toLowerCase () + ' response' ] || {}
124191 if (this .totalCount === null ) {
125192 this .totalCount = response .count || 0
126193 }
127194 const newOpts = response[this .resourceType ] || []
128- const existingIds = new Set (this .options .map (o => o . id ))
195+ const existingOptions = new Set (this .options .map (o => o[ this . optionValueKey ] ))
129196 newOpts .forEach (opt => {
130- if (! existingIds .has (opt . id )) {
197+ if (! existingOptions .has (opt[ this . optionValueKey ] )) {
131198 this .options .push (opt)
132199 }
133200 })
134201 this .page ++
135202
136- if (this .preselectedOptionId && this .successiveFetches < this .maxSuccessiveFetches ) {
137- const match = this .options .find (entry => entry . id === this .preselectedOptionId )
203+ if (this .preselectedOptionValue && this .successiveFetches < this .maxSuccessiveFetches ) {
204+ const match = this .options .find (entry => entry[ this . optionValueKey ] === this .preselectedOptionValue )
138205 if (! match) {
139206 this .successiveFetches ++
140207 this .fetchItems ()
141208 } else {
142- this .preselectedOptionId = null
209+ this .preselectedOptionValue = null
143210 this .successiveFetches = 0
144211 }
145212 } else {
@@ -159,6 +226,9 @@ export default {
159226 }
160227 },
161228 onSearch (value ) {
229+ if (value && value .length > 0 && value .length < 3 ) {
230+ return
231+ }
162232 this .searchQuery = value
163233 this .page = 1
164234 this .totalCount = null
@@ -175,9 +245,9 @@ export default {
175245 this .fetchItems ()
176246 }
177247 },
178- onChange (id ) {
179- this .preselectedOptionId = null
180- const match = this .options .find (entry => entry . id === id )
248+ onChange (keyValue ) {
249+ this .preselectedOptionValue = null
250+ const match = this .options .find (entry => entry[ this . optionValueKey ] === keyValue )
181251 if (match) {
182252 this .$emit (' change-option' , match)
183253 }
0 commit comments