From e1b738b82c084a37525dcb24b474f4c797b36271 Mon Sep 17 00:00:00 2001 From: Abhishek Kumar Date: Fri, 5 Sep 2025 22:25:08 +0530 Subject: [PATCH 1/3] api,server,ui: improve listing public ip for associate Currently, acquire or associate IP action in the UI will load only 500 addresses. ALso, it will load all 500 of them together. This PR uses InfiniteScrollSelect to iteratively load more addresses. To facilitate retrieving both Free and Reserved addresses api and server change has been made for the listPublicIpAddresses API to pass a comma-separated list of states for state parameter. Signed-off-by: Abhishek Kumar --- .../address/ListPublicIpAddressesCmd.java | 2 +- .../cloud/server/ManagementServerImpl.java | 37 +++++--- .../server/ManagementServerImplTest.java | 21 ++--- .../widgets/InfiniteScrollSelect.vue | 24 +++++- ui/src/views/network/IpAddressesTab.vue | 84 ++++++++----------- 5 files changed, 91 insertions(+), 77 deletions(-) diff --git a/api/src/main/java/org/apache/cloudstack/api/command/user/address/ListPublicIpAddressesCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/user/address/ListPublicIpAddressesCmd.java index 357f0c83ed74..57fd733d51d2 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/user/address/ListPublicIpAddressesCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/user/address/ListPublicIpAddressesCmd.java @@ -53,7 +53,7 @@ public class ListPublicIpAddressesCmd extends BaseListRetrieveOnlyResourceCountC @Parameter(name = ApiConstants.ALLOCATED_ONLY, type = CommandType.BOOLEAN, description = "limits search results to allocated public IP addresses") private Boolean allocatedOnly; - @Parameter(name = ApiConstants.STATE, type = CommandType.STRING, description = "lists all public IP addresses by state") + @Parameter(name = ApiConstants.STATE, type = CommandType.STRING, description = "lists all public IP addresses by state. A comma-separated list of states can be passed") private String state; @Parameter(name = ApiConstants.FOR_VIRTUAL_NETWORK, type = CommandType.BOOLEAN, description = "the virtual network for the IP address") diff --git a/server/src/main/java/com/cloud/server/ManagementServerImpl.java b/server/src/main/java/com/cloud/server/ManagementServerImpl.java index 3245acfbbf2c..b6f0ff3bb5e2 100644 --- a/server/src/main/java/com/cloud/server/ManagementServerImpl.java +++ b/server/src/main/java/com/cloud/server/ManagementServerImpl.java @@ -2451,19 +2451,29 @@ public Pair, Integer> searchForIPAddresses(final ListP final Long vpcId = cmd.getVpcId(); final String state = cmd.getState(); + List states = new ArrayList<>(); + if (StringUtils.isNotBlank(state)) { + for (String s : StringUtils.split(state, ",")) { + try { + states.add(IpAddress.State.valueOf(s)); + } catch (IllegalArgumentException e) { + throw new InvalidParameterValueException("Invalid state: " + s); + } + } + } Boolean isAllocated = cmd.isAllocatedOnly(); if (isAllocated == null) { - if (state != null && (state.equalsIgnoreCase(IpAddress.State.Free.name()) || state.equalsIgnoreCase(IpAddress.State.Reserved.name()))) { + if (states.contains(IpAddress.State.Free) || states.contains(IpAddress.State.Reserved)) { isAllocated = Boolean.FALSE; } else { isAllocated = Boolean.TRUE; // default } } else { - if (state != null && (state.equalsIgnoreCase(IpAddress.State.Free.name()) || state.equalsIgnoreCase(IpAddress.State.Reserved.name()))) { + if (states.contains(IpAddress.State.Free) || states.contains(IpAddress.State.Reserved)) { if (isAllocated) { throw new InvalidParameterValueException("Conflict: allocatedonly is true but state is Free"); } - } else if (state != null && state.equalsIgnoreCase(IpAddress.State.Allocated.name())) { + } else if (states.contains(IpAddress.State.Allocated)) { isAllocated = Boolean.TRUE; } } @@ -2543,7 +2553,7 @@ public Pair, Integer> searchForIPAddresses(final ListP final List permittedAccounts = new ArrayList<>(); ListProjectResourcesCriteria listProjectResourcesCriteria = null; Boolean isAllocatedOrReserved = false; - if (isAllocated || IpAddress.State.Reserved.name().equalsIgnoreCase(state)) { + if (isAllocated || states.contains(IpAddress.State.Allocated)) { isAllocatedOrReserved = true; } if (isAllocatedOrReserved || (vlanType == VlanType.VirtualNetwork && (caller.getType() != Account.Type.ADMIN || cmd.getDomainId() != null))) { @@ -2559,7 +2569,7 @@ public Pair, Integer> searchForIPAddresses(final ListP buildParameters(sb, cmd, vlanType == VlanType.VirtualNetwork ? true : isAllocated); SearchCriteria sc = sb.create(); - setParameters(sc, cmd, vlanType, isAllocated); + setParameters(sc, cmd, vlanType, isAllocated, states); if (isAllocatedOrReserved || (vlanType == VlanType.VirtualNetwork && (caller.getType() != Account.Type.ADMIN || cmd.getDomainId() != null))) { _accountMgr.buildACLSearchCriteria(sc, domainId, isRecursive, permittedAccounts, listProjectResourcesCriteria); @@ -2628,7 +2638,7 @@ public Pair, Integer> searchForIPAddresses(final ListP buildParameters(searchBuilder, cmd, false); SearchCriteria searchCriteria = searchBuilder.create(); - setParameters(searchCriteria, cmd, vlanType, false); + setParameters(searchCriteria, cmd, vlanType, false, states); searchCriteria.setParameters("state", IpAddress.State.Free.name()); addrs.addAll(_publicIpAddressDao.search(searchCriteria, searchFilter)); // Free IPs on shared network } @@ -2641,7 +2651,7 @@ public Pair, Integer> searchForIPAddresses(final ListP sb2.and("quarantinedPublicIpsIdsNIN", sb2.entity().getId(), SearchCriteria.Op.NIN); SearchCriteria sc2 = sb2.create(); - setParameters(sc2, cmd, vlanType, isAllocated); + setParameters(sc2, cmd, vlanType, isAllocated, states); sc2.setParameters("ids", freeAddrIds.toArray()); _publicIpAddressDao.buildQuarantineSearchCriteria(sc2); addrs.addAll(_publicIpAddressDao.search(sc2, searchFilter)); // Allocated + Free @@ -2671,7 +2681,7 @@ private void buildParameters(final SearchBuilder sb, final ListPubl sb.and("isSourceNat", sb.entity().isSourceNat(), SearchCriteria.Op.EQ); sb.and("isStaticNat", sb.entity().isOneToOneNat(), SearchCriteria.Op.EQ); sb.and("vpcId", sb.entity().getVpcId(), SearchCriteria.Op.EQ); - sb.and("state", sb.entity().getState(), SearchCriteria.Op.EQ); + sb.and("state", sb.entity().getState(), SearchCriteria.Op.IN); sb.and("display", sb.entity().isDisplay(), SearchCriteria.Op.EQ); sb.and(FOR_SYSTEMVMS, sb.entity().isForSystemVms(), SearchCriteria.Op.EQ); @@ -2714,7 +2724,8 @@ private void buildParameters(final SearchBuilder sb, final ListPubl } } - protected void setParameters(SearchCriteria sc, final ListPublicIpAddressesCmd cmd, VlanType vlanType, Boolean isAllocated) { + protected void setParameters(SearchCriteria sc, final ListPublicIpAddressesCmd cmd, VlanType vlanType, + Boolean isAllocated, List states) { final Object keyword = cmd.getKeyword(); final Long physicalNetworkId = cmd.getPhysicalNetworkId(); final Long sourceNetworkId = cmd.getNetworkId(); @@ -2726,7 +2737,6 @@ protected void setParameters(SearchCriteria sc, final ListPublicIpA final Boolean sourceNat = cmd.isSourceNat(); final Boolean staticNat = cmd.isStaticNat(); final Boolean forDisplay = cmd.getDisplay(); - final String state = cmd.getState(); final Boolean forSystemVms = cmd.getForSystemVMs(); final boolean forProvider = cmd.isForProvider(); final Map tags = cmd.getTags(); @@ -2783,13 +2793,14 @@ protected void setParameters(SearchCriteria sc, final ListPublicIpA sc.setParameters("display", forDisplay); } - if (state != null) { - sc.setParameters("state", state); + if (CollectionUtils.isNotEmpty(states)) { + sc.setParameters("state", states.toArray()); } else if (isAllocated != null && isAllocated) { sc.setParameters("state", IpAddress.State.Allocated); } - if (IpAddressManagerImpl.getSystemvmpublicipreservationmodestrictness().value() && IpAddress.State.Free.name().equalsIgnoreCase(state)) { + if (IpAddressManagerImpl.getSystemvmpublicipreservationmodestrictness().value() && + states.contains(IpAddress.State.Free)) { sc.setParameters(FOR_SYSTEMVMS, false); } else { sc.setParameters(FOR_SYSTEMVMS, forSystemVms); diff --git a/server/src/test/java/com/cloud/server/ManagementServerImplTest.java b/server/src/test/java/com/cloud/server/ManagementServerImplTest.java index c31db2c6dd7f..d91fe4a4cefb 100644 --- a/server/src/test/java/com/cloud/server/ManagementServerImplTest.java +++ b/server/src/test/java/com/cloud/server/ManagementServerImplTest.java @@ -26,6 +26,7 @@ import java.lang.reflect.Field; import java.util.ArrayList; import java.util.Arrays; +import java.util.Collections; import java.util.List; import org.apache.cloudstack.annotation.dao.AnnotationDao; @@ -253,14 +254,14 @@ public void setParametersTestWhenStateIsFreeAndSystemVmPublicIsTrue() throws Ill Mockito.when(cmd.getId()).thenReturn(null); Mockito.when(cmd.isSourceNat()).thenReturn(null); Mockito.when(cmd.isStaticNat()).thenReturn(null); - Mockito.when(cmd.getState()).thenReturn(IpAddress.State.Free.name()); Mockito.when(cmd.getTags()).thenReturn(null); - spy.setParameters(sc, cmd, VlanType.VirtualNetwork, Boolean.FALSE); + List states = Collections.singletonList(IpAddress.State.Free); + spy.setParameters(sc, cmd, VlanType.VirtualNetwork, Boolean.FALSE, states); Mockito.verify(sc, Mockito.times(1)).setJoinParameters("vlanSearch", "vlanType", VlanType.VirtualNetwork); Mockito.verify(sc, Mockito.times(1)).setParameters("display", false); Mockito.verify(sc, Mockito.times(1)).setParameters("sourceNetworkId", 10L); - Mockito.verify(sc, Mockito.times(1)).setParameters("state", "Free"); + Mockito.verify(sc, Mockito.times(1)).setParameters("state", states.toArray()); Mockito.verify(sc, Mockito.times(1)).setParameters("forsystemvms", false); } @@ -276,14 +277,14 @@ public void setParametersTestWhenStateIsFreeAndSystemVmPublicIsFalse() throws No Mockito.when(cmd.getId()).thenReturn(null); Mockito.when(cmd.isSourceNat()).thenReturn(null); Mockito.when(cmd.isStaticNat()).thenReturn(null); - Mockito.when(cmd.getState()).thenReturn(IpAddress.State.Free.name()); Mockito.when(cmd.getTags()).thenReturn(null); - spy.setParameters(sc, cmd, VlanType.VirtualNetwork, Boolean.FALSE); + List states = Collections.singletonList(IpAddress.State.Free); + spy.setParameters(sc, cmd, VlanType.VirtualNetwork, Boolean.FALSE, states); Mockito.verify(sc, Mockito.times(1)).setJoinParameters("vlanSearch", "vlanType", VlanType.VirtualNetwork); Mockito.verify(sc, Mockito.times(1)).setParameters("display", false); Mockito.verify(sc, Mockito.times(1)).setParameters("sourceNetworkId", 10L); - Mockito.verify(sc, Mockito.times(1)).setParameters("state", "Free"); + Mockito.verify(sc, Mockito.times(1)).setParameters("state", states.toArray()); Mockito.verify(sc, Mockito.times(1)).setParameters("forsystemvms", false); } @@ -299,13 +300,13 @@ public void setParametersTestWhenStateIsNullAndSystemVmPublicIsFalse() throws No Mockito.when(cmd.getId()).thenReturn(null); Mockito.when(cmd.isSourceNat()).thenReturn(null); Mockito.when(cmd.isStaticNat()).thenReturn(null); - Mockito.when(cmd.getState()).thenReturn(null); Mockito.when(cmd.getTags()).thenReturn(null); - spy.setParameters(sc, cmd, VlanType.VirtualNetwork, Boolean.TRUE); + spy.setParameters(sc, cmd, VlanType.VirtualNetwork, Boolean.TRUE, Collections.emptyList()); Mockito.verify(sc, Mockito.times(1)).setJoinParameters("vlanSearch", "vlanType", VlanType.VirtualNetwork); Mockito.verify(sc, Mockito.times(1)).setParameters("display", false); Mockito.verify(sc, Mockito.times(1)).setParameters("sourceNetworkId", 10L); + Mockito.verify(sc, Mockito.times(1)).setParameters("state", IpAddress.State.Allocated); Mockito.verify(sc, Mockito.times(1)).setParameters("forsystemvms", false); } @@ -321,13 +322,13 @@ public void setParametersTestWhenStateIsNullAndSystemVmPublicIsTrue() throws NoS Mockito.when(cmd.getId()).thenReturn(null); Mockito.when(cmd.isSourceNat()).thenReturn(null); Mockito.when(cmd.isStaticNat()).thenReturn(null); - Mockito.when(cmd.getState()).thenReturn(null); Mockito.when(cmd.getTags()).thenReturn(null); - spy.setParameters(sc, cmd, VlanType.VirtualNetwork, Boolean.TRUE); + spy.setParameters(sc, cmd, VlanType.VirtualNetwork, Boolean.TRUE, Collections.emptyList()); Mockito.verify(sc, Mockito.times(1)).setJoinParameters("vlanSearch", "vlanType", VlanType.VirtualNetwork); Mockito.verify(sc, Mockito.times(1)).setParameters("display", false); Mockito.verify(sc, Mockito.times(1)).setParameters("sourceNetworkId", 10L); + Mockito.verify(sc, Mockito.times(1)).setParameters("state", IpAddress.State.Allocated); Mockito.verify(sc, Mockito.times(1)).setParameters("forsystemvms", false); } diff --git a/ui/src/components/widgets/InfiniteScrollSelect.vue b/ui/src/components/widgets/InfiniteScrollSelect.vue index c11f96864469..87166bebdf2e 100644 --- a/ui/src/components/widgets/InfiniteScrollSelect.vue +++ b/ui/src/components/widgets/InfiniteScrollSelect.vue @@ -43,6 +43,7 @@ - defaultOption (Object, optional): Preselected object to include initially - 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' + - autoSelectFirstOption (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 @@ -81,7 +82,7 @@ - {{ option[optionLabelKey] }} + {{ optionLabelFn ? optionLabelFn(option) : option[optionLabelKey] }} @@ -120,6 +121,10 @@ export default { type: String, default: 'name' }, + optionLabelFn: { + type: Function, + default: null + }, defaultOption: { type: Object, default: null @@ -135,6 +140,10 @@ export default { pageSize: { type: Number, default: null + }, + autoSelectFirstOption: { + type: Boolean, + default: false } }, data () { @@ -147,11 +156,12 @@ export default { searchTimer: null, scrollHandlerAttached: false, preselectedOptionValue: null, - successiveFetches: 0 + successiveFetches: 0, + canSelectFirstOption: false } }, created () { - this.addDefaultOptionIfNeeded(true) + this.addDefaultOptionIfNeeded() }, mounted () { this.preselectedOptionValue = this.$attrs.value @@ -208,6 +218,7 @@ export default { }).catch(error => { this.$notifyError(error) }).finally(() => { + this.canSelectFirstOption = true if (this.successiveFetches === 0) { this.loading = false } @@ -218,6 +229,12 @@ export default { (Array.isArray(this.preselectedOptionValue) && this.preselectedOptionValue.length === 0) || this.successiveFetches >= this.maxSuccessiveFetches) { this.resetPreselectedOptionValue() + if (!this.canSelectFirstOption && this.autoSelectFirstOption && this.options.length > 0) { + this.$nextTick(() => { + this.preselectedOptionValue = this.options[0][this.optionValueKey] + this.onChange(this.preselectedOptionValue) + }) + } return } const matchValue = Array.isArray(this.preselectedOptionValue) ? this.preselectedOptionValue[0] : this.preselectedOptionValue @@ -239,6 +256,7 @@ export default { }, addDefaultOptionIfNeeded () { if (this.defaultOption) { + this.canSelectFirstOption = true this.options.push(this.defaultOption) } }, diff --git a/ui/src/views/network/IpAddressesTab.vue b/ui/src/views/network/IpAddressesTab.vue index ee9d1eff42bc..37ebc29f19d9 100644 --- a/ui/src/views/network/IpAddressesTab.vue +++ b/ui/src/views/network/IpAddressesTab.vue @@ -148,20 +148,17 @@ - - {{ ip.ipaddress }} ({{ ip.state }}) - + api="listPublicIpAddresses" + :apiParams="listApiParamsForAssociate" + resourceType="publicipaddress" + optionValueKey="ipaddress" + :optionLabelFn="ip => ip.ipaddress + ' (' + ip.state + ')'" + defaultIcon="environment-outlined" + :autoSelectFirstOption="true" + @change-option-value="(ip) => acquireIp = ip" />
{{ $t('label.cancel') }} @@ -212,13 +209,15 @@ import Status from '@/components/widgets/Status' import TooltipButton from '@/components/widgets/TooltipButton' import BulkActionView from '@/components/view/BulkActionView' import eventBus from '@/config/eventBus' +import InfiniteScrollSelect from '@/components/widgets/InfiniteScrollSelect' export default { name: 'IpAddressesTab', components: { Status, TooltipButton, - BulkActionView + BulkActionView, + InfiniteScrollSelect }, props: { resource: { @@ -281,7 +280,6 @@ export default { showAcquireIp: false, acquireLoading: false, acquireIp: null, - listPublicIpAddress: [], changeSourceNat: false, zoneExtNetProvider: '' } @@ -302,6 +300,26 @@ export default { } }, inject: ['parentFetchData'], + computed: { + listApiParams () { + const params = { + zoneid: this.resource.zoneid, + domainid: this.resource.domainid, + account: this.resource.account, + forvirtualnetwork: true, + allocatedonly: false + } + if (['nsx', 'netris'].includes(this.zoneExtNetProvider?.toLowerCase())) { + params.forprovider = true + } + return params + }, + listApiParamsForAssociate () { + const params = this.listApiParams + params.state = 'Free,Reserved' + return params + } + }, methods: { fetchData () { const params = { @@ -344,19 +362,9 @@ export default { }).catch(reject) }) }, - fetchListPublicIpAddress () { + fetchListPublicIpAddress (state) { return new Promise((resolve, reject) => { - const params = { - zoneid: this.resource.zoneid, - domainid: this.resource.domainid, - account: this.resource.account, - forvirtualnetwork: true, - allocatedonly: false - } - if (['nsx', 'netris'].includes(this.zoneExtNetProvider?.toLowerCase())) { - params.forprovider = true - } - getAPI('listPublicIpAddresses', params).then(json => { + getAPI('listPublicIpAddresses', this.listApiParams).then(json => { const listPublicIps = json.listpublicipaddressesresponse.publicipaddress || [] resolve(listPublicIps) }).catch(reject) @@ -554,30 +562,6 @@ export default { }, async onShowAcquireIp () { this.showAcquireIp = true - this.acquireLoading = true - this.listPublicIpAddress = [] - - try { - const listPublicIpAddress = await this.fetchListPublicIpAddress() - listPublicIpAddress.forEach(item => { - if (item.state === 'Free' || item.state === 'Reserved') { - this.listPublicIpAddress.push({ - ipaddress: item.ipaddress, - state: item.state - }) - } - }) - this.listPublicIpAddress.sort(function (a, b) { - if (a.ipaddress < b.ipaddress) { return -1 } - if (a.ipaddress > b.ipaddress) { return 1 } - return 0 - }) - this.acquireIp = this.listPublicIpAddress && this.listPublicIpAddress.length > 0 ? this.listPublicIpAddress[0].ipaddress : null - this.acquireLoading = false - } catch (e) { - this.acquireLoading = false - this.$notifyError(e) - } }, onCloseModal () { this.showAcquireIp = false From 4378548b4b00c6dddd7a0dade88b01830df30410 Mon Sep 17 00:00:00 2001 From: Abhishek Kumar Date: Wed, 10 Sep 2025 14:38:55 +0530 Subject: [PATCH 2/3] fix Signed-off-by: Abhishek Kumar --- server/src/main/java/com/cloud/server/ManagementServerImpl.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/src/main/java/com/cloud/server/ManagementServerImpl.java b/server/src/main/java/com/cloud/server/ManagementServerImpl.java index b6f0ff3bb5e2..305d058cc793 100644 --- a/server/src/main/java/com/cloud/server/ManagementServerImpl.java +++ b/server/src/main/java/com/cloud/server/ManagementServerImpl.java @@ -2553,7 +2553,7 @@ public Pair, Integer> searchForIPAddresses(final ListP final List permittedAccounts = new ArrayList<>(); ListProjectResourcesCriteria listProjectResourcesCriteria = null; Boolean isAllocatedOrReserved = false; - if (isAllocated || states.contains(IpAddress.State.Allocated)) { + if (isAllocated || states.contains(IpAddress.State.Reserved)) { isAllocatedOrReserved = true; } if (isAllocatedOrReserved || (vlanType == VlanType.VirtualNetwork && (caller.getType() != Account.Type.ADMIN || cmd.getDomainId() != null))) { From 68bfa1b47f22cb231b0288fc076b056ecc22fe52 Mon Sep 17 00:00:00 2001 From: Abhishek Kumar Date: Wed, 10 Sep 2025 16:51:26 +0530 Subject: [PATCH 3/3] minor change Signed-off-by: Abhishek Kumar --- ui/src/components/widgets/InfiniteScrollSelect.vue | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/ui/src/components/widgets/InfiniteScrollSelect.vue b/ui/src/components/widgets/InfiniteScrollSelect.vue index 87166bebdf2e..ee1628148d45 100644 --- a/ui/src/components/widgets/InfiniteScrollSelect.vue +++ b/ui/src/components/widgets/InfiniteScrollSelect.vue @@ -39,11 +39,12 @@ - apiParams (Object, optional): Additional parameters passed to the API - resourceType (String, required): The key in the API response containing the resource array (e.g., 'account') - optionValueKey (String, optional): Property to use as the value for options (e.g., 'name'). Default is 'id' + - optionLabelFn (Function, optional): Function to generate the label for options. Receives the option object as argument. If provided, takes precedence over optionLabelKey. Preferred over optionLabelKey. - 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 - 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' - - autoSelectFirstOption (Boolean, optional): Whether to automatically select the first option when options are loaded. Default is false + - autoSelectFirstOption (Boolean, optional): Whether to automatically select the first option when options are loaded. Default is false. Works only when there is no preselected value or defaultOption 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 @@ -157,7 +158,7 @@ export default { scrollHandlerAttached: false, preselectedOptionValue: null, successiveFetches: 0, - canSelectFirstOption: false + canSelectFirstOption: true } }, created () { @@ -218,7 +219,7 @@ export default { }).catch(error => { this.$notifyError(error) }).finally(() => { - this.canSelectFirstOption = true + this.canSelectFirstOption = false if (this.successiveFetches === 0) { this.loading = false } @@ -229,7 +230,7 @@ export default { (Array.isArray(this.preselectedOptionValue) && this.preselectedOptionValue.length === 0) || this.successiveFetches >= this.maxSuccessiveFetches) { this.resetPreselectedOptionValue() - if (!this.canSelectFirstOption && this.autoSelectFirstOption && this.options.length > 0) { + if (this.canSelectFirstOption && this.autoSelectFirstOption && this.options.length > 0) { this.$nextTick(() => { this.preselectedOptionValue = this.options[0][this.optionValueKey] this.onChange(this.preselectedOptionValue) @@ -256,7 +257,7 @@ export default { }, addDefaultOptionIfNeeded () { if (this.defaultOption) { - this.canSelectFirstOption = true + this.canSelectFirstOption = false this.options.push(this.defaultOption) } },