Skip to content

Commit f3d5432

Browse files
committed
changes
Signed-off-by: Abhishek Kumar <abhishek.mrt22@gmail.com>
1 parent 8cf8583 commit f3d5432

2 files changed

Lines changed: 93 additions & 17 deletions

File tree

ui/src/components/header/ProjectMenu.vue

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
v-model:value="selectedProjectId"
2323
class="project-select"
2424
api="listProjects"
25+
:apiParams="apiParams"
2526
resourceType="project"
2627
:defaultOption="defaultOption"
2728
defaultIcon="project-outlined"
@@ -56,6 +57,11 @@ export default {
5657
},
5758
defaultOption () {
5859
return { id: 0, name: this.$t('label.default.view') }
60+
},
61+
apiParams () {
62+
return {
63+
listall: true
64+
}
5965
}
6066
},
6167
methods: {

ui/src/components/widgets/InfiniteScrollSelect.vue

Lines changed: 87 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,44 @@
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"
@@ -37,11 +74,13 @@
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

Comments
 (0)