Skip to content

Commit db1577e

Browse files
vishesh92dhslove
authored andcommitted
Use infinite scroll select (apache#11991)
* addresses the domain selection (listed after the page size) with keyword search
1 parent 33287d9 commit db1577e

9 files changed

Lines changed: 611 additions & 763 deletions

File tree

ui/src/components/view/DedicateDomain.vue

Lines changed: 55 additions & 74 deletions
Original file line numberDiff line numberDiff line change
@@ -18,52 +18,44 @@
1818
<template>
1919
<div class="form">
2020
<div class="form__item" :class="{'error': domainError}">
21-
<a-spin :spinning="domainsLoading">
22-
<p class="form__label">{{ $t('label.domain') }}<span class="required">*</span></p>
23-
<p class="required required-label">{{ $t('label.required') }}</p>
24-
<a-select
25-
style="width: 100%"
26-
showSearch
27-
optionFilterProp="label"
28-
:filterOption="(input, option) => {
29-
return option.label.toLowerCase().indexOf(input.toLowerCase()) >= 0
30-
}"
31-
@change="handleChangeDomain"
32-
v-focus="true"
33-
v-model:value="domainId">
34-
<a-select-option
35-
v-for="(domain, index) in domainsList"
36-
:value="domain.id"
37-
:key="index"
38-
:label="domain.path || domain.name || domain.description">
39-
{{ domain.path || domain.name || domain.description }}
40-
</a-select-option>
41-
</a-select>
42-
</a-spin>
21+
<p class="form__label">{{ $t('label.domain') }}<span class="required">*</span></p>
22+
<p class="required required-label">{{ $t('label.required') }}</p>
23+
<infinite-scroll-select
24+
style="width: 100%"
25+
v-model:value="domainId"
26+
api="listDomains"
27+
:apiParams="domainsApiParams"
28+
resourceType="domain"
29+
optionValueKey="id"
30+
optionLabelKey="path"
31+
defaultIcon="block-outlined"
32+
v-focus="true"
33+
@change-option-value="handleChangeDomain" />
4334
</div>
44-
<div class="form__item" v-if="accountsList">
35+
<div class="form__item">
4536
<p class="form__label">{{ $t('label.account') }}</p>
46-
<a-select
37+
<infinite-scroll-select
4738
style="width: 100%"
48-
@change="handleChangeAccount"
49-
showSearch
50-
optionFilterProp="value"
51-
:filterOption="(input, option) => {
52-
return option.value.toLowerCase().indexOf(input.toLowerCase()) >= 0
53-
}" >
54-
<a-select-option v-for="(account, index) in accountsList" :value="account.name" :key="index">
55-
{{ account.name }}
56-
</a-select-option>
57-
</a-select>
39+
v-model:value="selectedAccount"
40+
api="listAccounts"
41+
:apiParams="accountsApiParams"
42+
resourceType="account"
43+
optionValueKey="name"
44+
optionLabelKey="name"
45+
defaultIcon="team-outlined"
46+
@change-option-value="handleChangeAccount" />
5847
</div>
5948
</div>
6049
</template>
6150

6251
<script>
63-
import { getAPI } from '@/api'
52+
import InfiniteScrollSelect from '@/components/widgets/InfiniteScrollSelect.vue'
6453
6554
export default {
6655
name: 'DedicateDomain',
56+
components: {
57+
InfiniteScrollSelect
58+
},
6759
props: {
6860
error: {
6961
type: Boolean,
@@ -72,59 +64,48 @@ export default {
7264
},
7365
data () {
7466
return {
75-
domainsLoading: false,
7667
domainId: null,
77-
accountsList: null,
78-
domainsList: null,
68+
selectedAccount: null,
7969
domainError: false
8070
}
8171
},
72+
computed: {
73+
domainsApiParams () {
74+
return {
75+
listall: true,
76+
details: 'min'
77+
}
78+
},
79+
accountsApiParams () {
80+
if (!this.domainId) {
81+
return {
82+
listall: true,
83+
showicon: true
84+
}
85+
}
86+
return {
87+
showicon: true,
88+
domainid: this.domainId
89+
}
90+
}
91+
},
8292
watch: {
8393
error () {
8494
this.domainError = this.error
8595
}
8696
},
8797
created () {
88-
this.fetchData()
8998
},
9099
methods: {
91-
fetchData () {
92-
this.domainsLoading = true
93-
getAPI('listDomains', {
94-
listAll: true,
95-
details: 'min'
96-
}).then(response => {
97-
this.domainsList = response.listdomainsresponse.domain
98-
99-
if (this.domainsList[0]) {
100-
this.domainId = this.domainsList[0].id
101-
this.handleChangeDomain(this.domainId)
102-
}
103-
}).catch(error => {
104-
this.$notifyError(error)
105-
}).finally(() => {
106-
this.domainsLoading = false
107-
})
108-
},
109-
fetchAccounts () {
110-
getAPI('listAccounts', {
111-
domainid: this.domainId
112-
}).then(response => {
113-
this.accountsList = response.listaccountsresponse.account || []
114-
if (this.accountsList && this.accountsList.length === 0) {
115-
this.handleChangeAccount(null)
116-
}
117-
}).catch(error => {
118-
this.$notifyError(error)
119-
})
120-
},
121-
handleChangeDomain (e) {
122-
this.$emit('domainChange', e)
100+
handleChangeDomain (domainId) {
101+
this.domainId = domainId
102+
this.selectedAccount = null
103+
this.$emit('domainChange', domainId)
123104
this.domainError = false
124-
this.fetchAccounts()
125105
},
126-
handleChangeAccount (e) {
127-
this.$emit('accountChange', e)
106+
handleChangeAccount (accountName) {
107+
this.selectedAccount = accountName
108+
this.$emit('accountChange', accountName)
128109
}
129110
}
130111
}

ui/src/components/widgets/InfiniteScrollSelect.vue

Lines changed: 78 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -41,9 +41,11 @@
4141
- optionValueKey (String, optional): Property to use as the value for options (e.g., 'name'). Default is 'id'
4242
- optionLabelKey (String, optional): Property to use as the label for options (e.g., 'name'). Default is 'name'
4343
- defaultOption (Object, optional): Preselected object to include initially
44+
- allowClear (Boolean, optional): Whether to allow clearing the selection. Default is false
4445
- showIcon (Boolean, optional): Whether to show icon for the options. Default is true
4546
- defaultIcon (String, optional): Icon to be shown when there is no resource icon for the option. Default is 'cloud-outlined'
4647
- autoSelectFirstOption (Boolean, optional): Whether to automatically select the first option when options are loaded. Default is false
48+
- selectFirstOption (Boolean, optional): Whether to automatically select the first option when options are loaded. Default is false
4749
4850
Events:
4951
- @change-option-value (Function): Emits the selected option value(s) when value(s) changes. Do not use @change as it will give warnings and may not work
@@ -59,6 +61,7 @@
5961
:filter-option="false"
6062
:loading="loading"
6163
show-search
64+
:allowClear="allowClear"
6265
placeholder="Select"
6366
@search="onSearchTimed"
6467
@popupScroll="onScroll"
@@ -76,9 +79,9 @@
7679
</div>
7780
</div>
7881
</template>
79-
<a-select-option v-for="option in options" :key="option.id" :value="option[optionValueKey]">
82+
<a-select-option v-for="option in selectableOptions" :key="option.id" :value="option[optionValueKey]">
8083
<span>
81-
<span v-if="showIcon && option.showicon !== false">
84+
<span v-if="showIcon && option.id !== null && option.id !== undefined">
8285
<resource-icon v-if="option.icon && option.icon.base64image" :image="option.icon.base64image" size="1x" style="margin-right: 5px"/>
8386
<render-icon v-else :icon="defaultIcon" style="margin-right: 5px" />
8487
</span>
@@ -129,6 +132,10 @@ export default {
129132
type: Object,
130133
default: null
131134
},
135+
allowClear: {
136+
type: Boolean,
137+
default: false
138+
},
132139
showIcon: {
133140
type: Boolean,
134141
default: true
@@ -141,7 +148,7 @@ export default {
141148
type: Number,
142149
default: null
143150
},
144-
autoSelectFirstOption: {
151+
selectFirstOption: {
145152
type: Boolean,
146153
default: false
147154
}
@@ -157,7 +164,7 @@ export default {
157164
scrollHandlerAttached: false,
158165
preselectedOptionValue: null,
159166
successiveFetches: 0,
160-
canSelectFirstOption: false
167+
hasAutoSelectedFirst: false
161168
}
162169
},
163170
created () {
@@ -176,6 +183,36 @@ export default {
176183
},
177184
formattedSearchFooterMessage () {
178185
return `${this.$t('label.showing.results.for').replace('%x', this.searchQuery)}`
186+
},
187+
selectableOptions () {
188+
const currentValue = this.$attrs.value
189+
// Only filter out null/empty options when the current value is also null/undefined/empty
190+
// This prevents such options from being selected and allows the placeholder to show instead
191+
if (currentValue === null || currentValue === undefined || currentValue === '') {
192+
return this.options.filter(option => {
193+
const optionValue = option[this.optionValueKey]
194+
return optionValue !== null && optionValue !== undefined && optionValue !== ''
195+
})
196+
}
197+
// When a valid value is selected, show all options
198+
return this.options
199+
},
200+
apiOptionsCount () {
201+
if (this.defaultOption) {
202+
const defaultOptionValue = this.defaultOption[this.optionValueKey]
203+
return this.options.filter(option => option[this.optionValueKey] !== defaultOptionValue).length
204+
}
205+
return this.options.length
206+
},
207+
preselectedMatchValue () {
208+
// Extract the first value from preselectedOptionValue if it's an array, otherwise return the value itself
209+
if (!this.preselectedOptionValue) return null
210+
return Array.isArray(this.preselectedOptionValue) ? this.preselectedOptionValue[0] : this.preselectedOptionValue
211+
},
212+
preselectedMatch () {
213+
// Find the matching option for the preselected value
214+
if (!this.preselectedMatchValue) return null
215+
return this.options.find(entry => entry[this.optionValueKey] === this.preselectedMatchValue) || null
179216
}
180217
},
181218
watch: {
@@ -221,6 +258,7 @@ export default {
221258
this.canSelectFirstOption = true
222259
if (this.successiveFetches === 0) {
223260
this.loading = false
261+
this.autoSelectFirstOptionIfNeeded()
224262
}
225263
})
226264
},
@@ -237,19 +275,18 @@ export default {
237275
}
238276
return
239277
}
240-
const matchValue = Array.isArray(this.preselectedOptionValue) ? this.preselectedOptionValue[0] : this.preselectedOptionValue
241-
const match = this.options.find(entry => entry[this.optionValueKey] === matchValue)
242-
if (!match) {
278+
if (!this.preselectedMatch) {
243279
this.successiveFetches++
244-
if (this.options.length < this.totalCount) {
280+
// Exclude defaultOption from count when comparing with totalCount
281+
if (this.apiOptionsCount < this.totalCount) {
245282
this.fetchItems()
246283
} else {
247284
this.resetPreselectedOptionValue()
248285
}
249286
return
250287
}
251288
if (Array.isArray(this.preselectedOptionValue) && this.preselectedOptionValue.length > 1) {
252-
this.preselectedOptionValue = this.preselectedOptionValue.filter(o => o !== match)
289+
this.preselectedOptionValue = this.preselectedOptionValue.filter(o => o !== this.preselectedMatchValue)
253290
} else {
254291
this.resetPreselectedOptionValue()
255292
}
@@ -264,6 +301,36 @@ export default {
264301
this.preselectedOptionValue = null
265302
this.successiveFetches = 0
266303
},
304+
autoSelectFirstOptionIfNeeded () {
305+
if (!this.selectFirstOption || this.hasAutoSelectedFirst) {
306+
return
307+
}
308+
// Don't auto-select if there's a preselected value being fetched
309+
if (this.preselectedOptionValue) {
310+
return
311+
}
312+
const currentValue = this.$attrs.value
313+
if (currentValue !== undefined && currentValue !== null && currentValue !== '') {
314+
return
315+
}
316+
if (this.options.length === 0) {
317+
return
318+
}
319+
if (this.searchQuery && this.searchQuery.length > 0) {
320+
return
321+
}
322+
// Only auto-select after initial load is complete (no more successive fetches)
323+
if (this.successiveFetches > 0) {
324+
return
325+
}
326+
const firstOption = this.options[0]
327+
if (firstOption) {
328+
const firstValue = firstOption[this.optionValueKey]
329+
this.hasAutoSelectedFirst = true
330+
this.$emit('change-option-value', firstValue)
331+
this.$emit('change-option', firstOption)
332+
}
333+
},
267334
onSearchTimed (value) {
268335
clearTimeout(this.searchTimer)
269336
this.searchTimer = setTimeout(() => {
@@ -282,7 +349,8 @@ export default {
282349
},
283350
onScroll (e) {
284351
const nearBottom = e.target.scrollTop + e.target.clientHeight >= e.target.scrollHeight - 10
285-
const hasMore = this.options.length < this.totalCount
352+
// Exclude defaultOption from count when comparing with totalCount
353+
const hasMore = this.apiOptionsCount < this.totalCount
286354
if (nearBottom && hasMore && !this.loading) {
287355
this.fetchItems()
288356
}

0 commit comments

Comments
 (0)