Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
129 changes: 55 additions & 74 deletions ui/src/components/view/DedicateDomain.vue
Original file line number Diff line number Diff line change
Expand Up @@ -18,52 +18,44 @@
<template>
<div class="form">
<div class="form__item" :class="{'error': domainError}">
<a-spin :spinning="domainsLoading">
<p class="form__label">{{ $t('label.domain') }}<span class="required">*</span></p>
<p class="required required-label">{{ $t('label.required') }}</p>
<a-select
style="width: 100%"
showSearch
optionFilterProp="label"
:filterOption="(input, option) => {
return option.label.toLowerCase().indexOf(input.toLowerCase()) >= 0
}"
@change="handleChangeDomain"
v-focus="true"
v-model:value="domainId">
<a-select-option
v-for="(domain, index) in domainsList"
:value="domain.id"
:key="index"
:label="domain.path || domain.name || domain.description">
{{ domain.path || domain.name || domain.description }}
</a-select-option>
</a-select>
</a-spin>
<p class="form__label">{{ $t('label.domain') }}<span class="required">*</span></p>
<p class="required required-label">{{ $t('label.required') }}</p>
<infinite-scroll-select
style="width: 100%"
v-model:value="domainId"
api="listDomains"
:apiParams="domainsApiParams"
resourceType="domain"
optionValueKey="id"
optionLabelKey="path"
defaultIcon="block-outlined"
v-focus="true"
@change-option-value="handleChangeDomain" />
</div>
<div class="form__item" v-if="accountsList">
<div class="form__item">
<p class="form__label">{{ $t('label.account') }}</p>
<a-select
<infinite-scroll-select
style="width: 100%"
@change="handleChangeAccount"
showSearch
optionFilterProp="value"
:filterOption="(input, option) => {
return option.value.toLowerCase().indexOf(input.toLowerCase()) >= 0
}" >
<a-select-option v-for="(account, index) in accountsList" :value="account.name" :key="index">
{{ account.name }}
</a-select-option>
</a-select>
v-model:value="selectedAccount"
api="listAccounts"
:apiParams="accountsApiParams"
resourceType="account"
optionValueKey="name"
optionLabelKey="name"
defaultIcon="team-outlined"
@change-option-value="handleChangeAccount" />
</div>
</div>
</template>

<script>
import { api } from '@/api'
import InfiniteScrollSelect from '@/components/widgets/InfiniteScrollSelect.vue'

export default {
name: 'DedicateDomain',
components: {
InfiniteScrollSelect
},
props: {
error: {
type: Boolean,
Expand All @@ -72,59 +64,48 @@ export default {
},
data () {
return {
domainsLoading: false,
domainId: null,
accountsList: null,
domainsList: null,
selectedAccount: null,
domainError: false
}
},
computed: {
domainsApiParams () {
return {
listall: true,
details: 'min'
}
},
accountsApiParams () {
if (!this.domainId) {
return {
listall: true,
showicon: true
}
}
return {
showicon: true,
domainid: this.domainId
}
}
},
watch: {
error () {
this.domainError = this.error
}
},
created () {
this.fetchData()
},
methods: {
fetchData () {
this.domainsLoading = true
api('listDomains', {
listAll: true,
details: 'min'
}).then(response => {
this.domainsList = response.listdomainsresponse.domain

if (this.domainsList[0]) {
this.domainId = this.domainsList[0].id
this.handleChangeDomain(this.domainId)
}
}).catch(error => {
this.$notifyError(error)
}).finally(() => {
this.domainsLoading = false
})
},
fetchAccounts () {
api('listAccounts', {
domainid: this.domainId
}).then(response => {
this.accountsList = response.listaccountsresponse.account || []
if (this.accountsList && this.accountsList.length === 0) {
this.handleChangeAccount(null)
}
}).catch(error => {
this.$notifyError(error)
})
},
handleChangeDomain (e) {
this.$emit('domainChange', e)
handleChangeDomain (domainId) {
this.domainId = domainId
this.selectedAccount = null
this.$emit('domainChange', domainId)
this.domainError = false
this.fetchAccounts()
},
handleChangeAccount (e) {
this.$emit('accountChange', e)
handleChangeAccount (accountName) {
this.selectedAccount = accountName
this.$emit('accountChange', accountName)
}
}
}
Expand Down
89 changes: 81 additions & 8 deletions ui/src/components/widgets/InfiniteScrollSelect.vue
Original file line number Diff line number Diff line change
Expand Up @@ -41,8 +41,10 @@
- optionValueKey (String, optional): Property to use as the value for options (e.g., 'name'). Default is 'id'
- optionLabelKey (String, optional): Property to use as the label for options (e.g., 'name'). Default is 'name'
- defaultOption (Object, optional): Preselected object to include initially
- allowClear (Boolean, optional): Whether to allow clearing the selection. Default is false
- showIcon (Boolean, optional): Whether to show icon for the options. Default is true
- defaultIcon (String, optional): Icon to be shown when there is no resource icon for the option. Default is 'cloud-outlined'
- selectFirstOption (Boolean, optional): Whether to automatically select the first option when options are loaded. Default is false

Events:
- @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
Expand All @@ -58,6 +60,7 @@
:filter-option="false"
:loading="loading"
show-search
:allowClear="allowClear"
placeholder="Select"
@search="onSearchTimed"
@popupScroll="onScroll"
Expand All @@ -75,9 +78,9 @@
</div>
</div>
</template>
<a-select-option v-for="option in options" :key="option.id" :value="option[optionValueKey]">
<a-select-option v-for="option in selectableOptions" :key="option.id" :value="option[optionValueKey]">
<span>
<span v-if="showIcon">
<span v-if="showIcon && option.id !== null && option.id !== undefined">
<resource-icon v-if="option.icon && option.icon.base64image" :image="option.icon.base64image" size="1x" style="margin-right: 5px"/>
<render-icon v-else :icon="defaultIcon" style="margin-right: 5px" />
</span>
Expand Down Expand Up @@ -124,6 +127,10 @@
type: Object,
default: null
},
allowClear: {
type: Boolean,
default: false
},
showIcon: {
type: Boolean,
default: true
Expand All @@ -135,6 +142,10 @@
pageSize: {
type: Number,
default: null
},
selectFirstOption: {
type: Boolean,
default: false
}
},
data () {
Expand All @@ -147,7 +158,8 @@
searchTimer: null,
scrollHandlerAttached: false,
preselectedOptionValue: null,
successiveFetches: 0
successiveFetches: 0,
hasAutoSelectedFirst: false
}
},
created () {
Expand All @@ -166,6 +178,36 @@
},
formattedSearchFooterMessage () {
return `${this.$t('label.showing.results.for').replace('%x', this.searchQuery)}`
},
selectableOptions () {
const currentValue = this.$attrs.value
// Only filter out null/empty options when the current value is also null/undefined/empty
// This prevents such options from being selected and allows the placeholder to show instead
if (currentValue === null || currentValue === undefined || currentValue === '') {
return this.options.filter(option => {
const optionValue = option[this.optionValueKey]
return optionValue !== null && optionValue !== undefined && optionValue !== ''
})
}
// When a valid value is selected, show all options
return this.options
Comment on lines +182 to +193
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What is the benefit of this differentiation? Maybe we can show the null/empty option always or is it beneficial when using the value from route?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I added this to maintain consistency with the existing behavior in UI.

},
apiOptionsCount () {
if (this.defaultOption) {
const defaultOptionValue = this.defaultOption[this.optionValueKey]
return this.options.filter(option => option[this.optionValueKey] !== defaultOptionValue).length
}
return this.options.length
},
preselectedMatchValue () {
// Extract the first value from preselectedOptionValue if it's an array, otherwise return the value itself
if (!this.preselectedOptionValue) return null
return Array.isArray(this.preselectedOptionValue) ? this.preselectedOptionValue[0] : this.preselectedOptionValue
},
preselectedMatch () {
// Find the matching option for the preselected value
if (!this.preselectedMatchValue) return null
return this.options.find(entry => entry[this.optionValueKey] === this.preselectedMatchValue) || null
}
},
watch: {
Expand Down Expand Up @@ -210,6 +252,7 @@
}).finally(() => {
if (this.successiveFetches === 0) {
this.loading = false
this.autoSelectFirstOptionIfNeeded()
}
})
},
Expand All @@ -220,11 +263,10 @@
this.resetPreselectedOptionValue()
return
}
const matchValue = Array.isArray(this.preselectedOptionValue) ? this.preselectedOptionValue[0] : this.preselectedOptionValue
const match = this.options.find(entry => entry[this.optionValueKey] === matchValue)
if (!match) {
if (!this.preselectedMatch) {
this.successiveFetches++
if (this.options.length < this.totalCount) {
// Exclude defaultOption from count when comparing with totalCount
if (this.apiOptionsCount < this.totalCount) {
this.fetchItems()
} else {
this.resetPreselectedOptionValue()
Expand All @@ -232,7 +274,7 @@
return
}
if (Array.isArray(this.preselectedOptionValue) && this.preselectedOptionValue.length > 1) {
this.preselectedOptionValue = this.preselectedOptionValue.filter(o => o !== match)

Check failure on line 277 in ui/src/components/widgets/InfiniteScrollSelect.vue

View workflow job for this annotation

GitHub Actions / build

'match' is not defined
} else {
this.resetPreselectedOptionValue()
}
Expand All @@ -246,6 +288,36 @@
this.preselectedOptionValue = null
this.successiveFetches = 0
},
autoSelectFirstOptionIfNeeded () {
if (!this.selectFirstOption || this.hasAutoSelectedFirst) {
return
}
// Don't auto-select if there's a preselected value being fetched
if (this.preselectedOptionValue) {
return
}
const currentValue = this.$attrs.value
if (currentValue !== undefined && currentValue !== null && currentValue !== '') {
return
}
if (this.options.length === 0) {
return
}
if (this.searchQuery && this.searchQuery.length > 0) {
return
}
// Only auto-select after initial load is complete (no more successive fetches)
if (this.successiveFetches > 0) {
return
}
const firstOption = this.options[0]
if (firstOption) {
const firstValue = firstOption[this.optionValueKey]
this.hasAutoSelectedFirst = true
this.$emit('change-option-value', firstValue)
this.$emit('change-option', firstOption)
}
},
onSearchTimed (value) {
clearTimeout(this.searchTimer)
this.searchTimer = setTimeout(() => {
Expand All @@ -264,7 +336,8 @@
},
onScroll (e) {
const nearBottom = e.target.scrollTop + e.target.clientHeight >= e.target.scrollHeight - 10
const hasMore = this.options.length < this.totalCount
// Exclude defaultOption from count when comparing with totalCount
const hasMore = this.apiOptionsCount < this.totalCount
if (nearBottom && hasMore && !this.loading) {
this.fetchItems()
}
Expand Down
Loading
Loading