Add dependent field provider to resource deployment (#13664)

* Add dependent field provider to resource deployment

* Change name to value provider service

* Add error handling

* providerId -> id

* Set dropdown value correctly

* missed id

* back to providerId

* fix updating missed id

* remove placeholder
This commit is contained in:
Charles Gagnon
2020-12-04 17:21:30 -08:00
committed by GitHub
parent 757ac1d4aa
commit a70dce7855
12 changed files with 147 additions and 15 deletions

View File

@@ -315,10 +315,10 @@
"type": "text", "type": "text",
"required": true, "required": true,
"defaultValue": "", "defaultValue": "",
"placeHolder": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx", "enabled": false,
"enabled": { "valueProvider": {
"target": "AZDATA_NB_VAR_ARC_DATA_CONTROLLER_CONNECTIVITY_MODE", "providerId": "subscription-id-to-tenant-id",
"value": "direct" "triggerField": "AZDATA_NB_VAR_ARC_SUBSCRIPTION"
}, },
"validations" : [{ "validations" : [{
"type": "regex_match", "type": "regex_match",

View File

@@ -17,7 +17,7 @@ import { AzureArcTreeDataProvider } from '../ui/tree/azureArcTreeDataProvider';
*/ */
export class ArcControllersOptionsSourceProvider implements rd.IOptionsSourceProvider { export class ArcControllersOptionsSourceProvider implements rd.IOptionsSourceProvider {
private _cacheManager = new CacheManager<string, string>(); private _cacheManager = new CacheManager<string, string>();
readonly optionsSourceId = 'arc.controllers'; readonly id = 'arc.controllers';
constructor(private _treeProvider: AzureArcTreeDataProvider) { } constructor(private _treeProvider: AzureArcTreeDataProvider) { }
async getOptions(): Promise<string[] | azdata.CategoryValue[]> { async getOptions(): Promise<string[] | azdata.CategoryValue[]> {

View File

@@ -10,7 +10,7 @@ import * as azdataExt from 'azdata-ext';
* Class that provides options sources for an Arc Data Controller * Class that provides options sources for an Arc Data Controller
*/ */
export class ArcControllerConfigProfilesOptionsSource implements rd.IOptionsSourceProvider { export class ArcControllerConfigProfilesOptionsSource implements rd.IOptionsSourceProvider {
readonly optionsSourceId = 'arc.controller.config.profiles'; readonly id = 'arc.controller.config.profiles';
constructor(private _azdataExtApi: azdataExt.IExtension) { } constructor(private _azdataExtApi: azdataExt.IExtension) { }
async getOptions(): Promise<string[]> { async getOptions(): Promise<string[]> {
const isEulaAccepted = await this._azdataExtApi.isEulaAccepted(); const isEulaAccepted = await this._azdataExtApi.isEulaAccepted();

View File

@@ -8,6 +8,7 @@ import * as vscode from 'vscode';
import { promises as fs } from 'fs'; import { promises as fs } from 'fs';
import * as path from 'path'; import * as path from 'path';
import * as os from 'os'; import * as os from 'os';
import * as resourceDeployment from 'resource-deployment';
import { AppContext } from './appContext'; import { AppContext } from './appContext';
import { AzureAccountProviderService } from './account-provider/azureAccountProviderService'; import { AzureAccountProviderService } from './account-provider/azureAccountProviderService';
@@ -86,8 +87,8 @@ export async function activate(context: vscode.ExtensionContext): Promise<azurec
registerAzureServices(appContext); registerAzureServices(appContext);
const azureResourceTree = new AzureResourceTreeProvider(appContext); const azureResourceTree = new AzureResourceTreeProvider(appContext);
const connectionDialogTree = new ConnectionDialogTreeProvider(appContext); const connectionDialogTree = new ConnectionDialogTreeProvider(appContext);
pushDisposable(vscode.window.registerTreeDataProvider('connectionDialog/azureResourceExplorer', connectionDialogTree));
pushDisposable(vscode.window.registerTreeDataProvider('azureResourceExplorer', azureResourceTree)); pushDisposable(vscode.window.registerTreeDataProvider('azureResourceExplorer', azureResourceTree));
pushDisposable(vscode.window.registerTreeDataProvider('connectionDialog/azureResourceExplorer', connectionDialogTree));
pushDisposable(vscode.workspace.onDidChangeConfiguration(e => onDidChangeConfiguration(e), this)); pushDisposable(vscode.workspace.onDidChangeConfiguration(e => onDidChangeConfiguration(e), this));
registerAzureResourceCommands(appContext, azureResourceTree, connectionDialogTree); registerAzureResourceCommands(appContext, azureResourceTree, connectionDialogTree);
azdata.dataprotocol.registerDataGridProvider(new AzureDataGridProvider(appContext)); azdata.dataprotocol.registerDataGridProvider(new AzureDataGridProvider(appContext));
@@ -105,6 +106,40 @@ export async function activate(context: vscode.ExtensionContext): Promise<azurec
} }
}); });
// Don't block on this since there's a bit of a circular dependency here with the extension activation since resource deployment
// depends on this extension too. It's fine to wait a bit for that to finish before registering the provider
vscode.extensions.getExtension(resourceDeployment.extension.name).activate().then((api: resourceDeployment.IExtension) => {
api.registerValueProvider({
id: 'subscription-id-to-tenant-id',
getValue: async (triggerValue: string) => {
if (triggerValue === '') {
return '';
}
let accounts: azdata.Account[] = [];
try {
accounts = await azdata.accounts.getAllAccounts();
} catch (err) {
console.warn(`Error fetching accounts for subscription-id-to-tenant-id provider : ${err}`);
return '';
}
for (const account of accounts) {
// Ignore any errors - they'll be logged in the called function and we still want to look
// at any subscriptions that are returned - worst case we'll just return an empty string if we didn't
// find the matching subscription
const subs = await azureResourceUtils.getSubscriptions(appContext, account, true);
const sub = subs.subscriptions.find(sub => sub.id === triggerValue);
if (sub) {
return sub.tenant;
}
}
console.error(`Unable to find subscription with ID ${triggerValue} when mapping subscription ID to tenant ID`);
return '';
}
});
});
return { return {
getSubscriptions(account?: azdata.Account, ignoreErrors?: boolean, selectedOnly: boolean = false): Thenable<azurecore.GetSubscriptionsResult> { getSubscriptions(account?: azdata.Account, ignoreErrors?: boolean, selectedOnly: boolean = false): Thenable<azurecore.GetSubscriptionsResult> {
return selectedOnly return selectedOnly

View File

@@ -7,4 +7,5 @@
/// <reference path='../../../../src/vs/vscode.proposed.d.ts'/> /// <reference path='../../../../src/vs/vscode.proposed.d.ts'/>
/// <reference path='../../../../src/sql/azdata.d.ts'/> /// <reference path='../../../../src/sql/azdata.d.ts'/>
/// <reference path='../../../../src/sql/azdata.proposed.d.ts'/> /// <reference path='../../../../src/sql/azdata.proposed.d.ts'/>
/// <reference path='../../../resource-deployment/src/typings/resource-deployment.d.ts'/>
/// <reference types='@types/node'/> /// <reference types='@types/node'/>

View File

@@ -4,11 +4,13 @@
*--------------------------------------------------------------------------------------------*/ *--------------------------------------------------------------------------------------------*/
import * as rd from 'resource-deployment'; import * as rd from 'resource-deployment';
import { valueProviderService } from './services/valueProviderService';
import { optionsSourcesService } from './services/optionSourcesService'; import { optionsSourcesService } from './services/optionSourcesService';
export function getExtensionApi(): rd.IExtension { export function getExtensionApi(): rd.IExtension {
return { return {
registerOptionsSourceProvider: (provider: rd.IOptionsSourceProvider) => optionsSourcesService.registerOptionsSourceProvider(provider) registerOptionsSourceProvider: (provider: rd.IOptionsSourceProvider) => optionsSourcesService.registerOptionsSourceProvider(provider),
registerValueProvider: (provider: rd.IValueProvider) => valueProviderService.registerValueProvider(provider)
}; };
} }

View File

@@ -261,6 +261,11 @@ export interface DynamicEnablementInfo {
value: string value: string
} }
export interface ValueProviderInfo {
providerId: string,
triggerField: string
}
export interface FieldInfoBase { export interface FieldInfoBase {
labelWidth?: string; labelWidth?: string;
inputWidth?: string; inputWidth?: string;
@@ -307,9 +312,8 @@ export interface FieldInfo extends SubFieldInfo, FieldInfoBase {
editable?: boolean; // for editable drop-down, editable?: boolean; // for editable drop-down,
enabled?: boolean | DynamicEnablementInfo; enabled?: boolean | DynamicEnablementInfo;
isEvaluated?: boolean; isEvaluated?: boolean;
valueLookup?: string; // for fetching dropdown options
validationLookup?: string // for fetching text field validations
validations?: ValidationInfo[]; validations?: ValidationInfo[];
valueProvider?: ValueProviderInfo;
} }
export interface KubeClusterContextFieldInfo extends FieldInfo { export interface KubeClusterContextFieldInfo extends FieldInfo {

View File

@@ -28,7 +28,9 @@ export const NewResourceGroupAriaLabel = localize('azure.resourceGroup.NewResour
export const realm = localize('deployCluster.Realm', "Realm"); export const realm = localize('deployCluster.Realm', "Realm");
export const unknownFieldTypeError = (type: FieldType) => localize('UnknownFieldTypeError', "Unknown field type: \"{0}\"", type); export const unknownFieldTypeError = (type: FieldType) => localize('UnknownFieldTypeError', "Unknown field type: \"{0}\"", type);
export const optionsSourceAlreadyDefined = (optionsSourceId: string) => localize('optionsSource.alreadyDefined', "Options Source with id:{0} is already defined", optionsSourceId); export const optionsSourceAlreadyDefined = (optionsSourceId: string) => localize('optionsSource.alreadyDefined', "Options Source with id:{0} is already defined", optionsSourceId);
export const valueProviderAlreadyDefined = (providerId: string) => localize('valueProvider.alreadyDefined', "Value Provider with id:{0} is already defined", providerId);
export const noOptionsSourceDefined = (optionsSourceId: string) => localize('optionsSource.notDefined', "No Options Source defined for id: {0}", optionsSourceId); export const noOptionsSourceDefined = (optionsSourceId: string) => localize('optionsSource.notDefined', "No Options Source defined for id: {0}", optionsSourceId);
export const noValueProviderDefined = (providerId: string) => localize('valueProvider.notDefined', "No Value Provider defined for id: {0}", providerId);
export const variableValueFetchForUnsupportedVariable = (variableName: string) => localize('getVariableValue.unknownVariableName', "Attempt to get variable value for unknown variable:{0}", variableName); export const variableValueFetchForUnsupportedVariable = (variableName: string) => localize('getVariableValue.unknownVariableName', "Attempt to get variable value for unknown variable:{0}", variableName);
export const isPasswordFetchForUnsupportedVariable = (variableName: string) => localize('getIsPassword.unknownVariableName', "Attempt to get isPassword for unknown variable:{0}", variableName); export const isPasswordFetchForUnsupportedVariable = (variableName: string) => localize('getIsPassword.unknownVariableName', "Attempt to get isPassword for unknown variable:{0}", variableName);
export const optionsNotDefined = (fieldType: FieldType) => localize('optionsNotDefined', "FieldInfo.options was not defined for field type: {0}", fieldType); export const optionsNotDefined = (fieldType: FieldType) => localize('optionsNotDefined', "FieldInfo.options was not defined for field type: {0}", fieldType);

View File

@@ -9,10 +9,10 @@ import * as loc from '../localizedConstants';
class OptionsSourcesService { class OptionsSourcesService {
private _optionsSourceStore = new Map<string, rd.IOptionsSourceProvider>(); private _optionsSourceStore = new Map<string, rd.IOptionsSourceProvider>();
registerOptionsSourceProvider(provider: rd.IOptionsSourceProvider): void { registerOptionsSourceProvider(provider: rd.IOptionsSourceProvider): void {
if (this._optionsSourceStore.has(provider.optionsSourceId)) { if (this._optionsSourceStore.has(provider.id)) {
throw new Error(loc.optionsSourceAlreadyDefined(provider.optionsSourceId)); throw new Error(loc.optionsSourceAlreadyDefined(provider.id));
} }
this._optionsSourceStore.set(provider.optionsSourceId, provider); this._optionsSourceStore.set(provider.id, provider);
} }
getOptionsSource(optionsSourceProviderId: string): rd.IOptionsSourceProvider { getOptionsSource(optionsSourceProviderId: string): rd.IOptionsSourceProvider {

View File

@@ -0,0 +1,27 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as rd from 'resource-deployment';
import * as loc from '../localizedConstants';
class ValueProviderService {
private _valueProviderStore = new Map<string, rd.IValueProvider>();
registerValueProvider(provider: rd.IValueProvider): void {
if (this._valueProviderStore.has(provider.id)) {
throw new Error(loc.valueProviderAlreadyDefined(provider.id));
}
this._valueProviderStore.set(provider.id, provider);
}
getValueProvider(providerId: string): rd.IValueProvider {
const valueProvider = this._valueProviderStore.get(providerId);
if (valueProvider === undefined) {
throw new Error(loc.noValueProviderDefined(providerId));
}
return valueProvider;
}
}
export const valueProviderService = new ValueProviderService();

View File

@@ -17,12 +17,17 @@ declare module 'resource-deployment' {
name = 'Microsoft.resource-deployment' name = 'Microsoft.resource-deployment'
} }
export interface IOptionsSourceProvider { export interface IOptionsSourceProvider {
readonly optionsSourceId: string, readonly id: string,
getOptions(): Promise<string[] | azdata.CategoryValue[]> | string[] | azdata.CategoryValue[]; getOptions(): Promise<string[] | azdata.CategoryValue[]> | string[] | azdata.CategoryValue[];
getVariableValue?: (variableName: string, input: string) => Promise<string> | string; getVariableValue?: (variableName: string, input: string) => Promise<string> | string;
getIsPassword?: (variableName: string) => boolean | Promise<boolean>; getIsPassword?: (variableName: string) => boolean | Promise<boolean>;
} }
export interface IValueProvider {
readonly id: string,
getValue(triggerValue: string): Promise<string>;
}
/** /**
* Covers defining what the resource-deployment extension exports to other extensions * Covers defining what the resource-deployment extension exports to other extensions
* *
@@ -31,6 +36,7 @@ declare module 'resource-deployment' {
*/ */
export interface IExtension { export interface IExtension {
registerOptionsSourceProvider(provider: IOptionsSourceProvider): void registerOptionsSourceProvider(provider: IOptionsSourceProvider): void,
registerValueProvider(provider: IValueProvider): void
} }
} }

View File

@@ -14,6 +14,7 @@ import { getDateTimeString, getErrorMessage, throwUnless } from '../common/utils
import { AzureAccountFieldInfo, AzureLocationsFieldInfo, ComponentCSSStyles, DialogInfoBase, FieldInfo, FieldType, FilePickerFieldInfo, instanceOfDynamicEnablementInfo, IOptionsSource, KubeClusterContextFieldInfo, LabelPosition, NoteBookEnvironmentVariablePrefix, OptionsInfo, OptionsType, PageInfoBase, RowInfo, SectionInfo, TextCSSStyles } from '../interfaces'; import { AzureAccountFieldInfo, AzureLocationsFieldInfo, ComponentCSSStyles, DialogInfoBase, FieldInfo, FieldType, FilePickerFieldInfo, instanceOfDynamicEnablementInfo, IOptionsSource, KubeClusterContextFieldInfo, LabelPosition, NoteBookEnvironmentVariablePrefix, OptionsInfo, OptionsType, PageInfoBase, RowInfo, SectionInfo, TextCSSStyles } from '../interfaces';
import * as loc from '../localizedConstants'; import * as loc from '../localizedConstants';
import { apiService } from '../services/apiService'; import { apiService } from '../services/apiService';
import { valueProviderService } from '../services/valueProviderService';
import { getDefaultKubeConfigPath, getKubeConfigClusterContexts } from '../services/kubeService'; import { getDefaultKubeConfigPath, getKubeConfigClusterContexts } from '../services/kubeService';
import { optionsSourcesService } from '../services/optionSourcesService'; import { optionsSourcesService } from '../services/optionSourcesService';
import { KubeCtlTool, KubeCtlToolName } from '../services/tools/kubeCtlTool'; import { KubeCtlTool, KubeCtlToolName } from '../services/tools/kubeCtlTool';
@@ -38,6 +39,7 @@ export type InputComponentInfo<T extends InputComponent> = {
component: T; component: T;
labelComponent?: azdata.TextComponent; labelComponent?: azdata.TextComponent;
getValue: () => Promise<InputValueType>; getValue: () => Promise<InputValueType>;
setValue: (value: InputValueType) => void;
getDisplayValue?: () => Promise<string>; getDisplayValue?: () => Promise<string>;
onValueChanged: vscode.Event<void>; onValueChanged: vscode.Event<void>;
isPassword?: boolean isPassword?: boolean
@@ -200,6 +202,7 @@ export function createInputBoxInputInfo(view: azdata.ModelView, inputInfo: Input
return { return {
component: component, component: component,
getValue: async (): Promise<InputValueType> => component.value, getValue: async (): Promise<InputValueType> => component.value,
setValue: (value: InputValueType) => component.value = value?.toString(),
onValueChanged: component.onTextChanged onValueChanged: component.onTextChanged
}; };
} }
@@ -240,6 +243,7 @@ export function createCheckboxInputInfo(view: azdata.ModelView, info: { initialV
return { return {
component: checkbox, component: checkbox,
getValue: async () => checkbox.checked ? 'true' : 'false', getValue: async () => checkbox.checked ? 'true' : 'false',
setValue: (value: InputValueType) => checkbox.checked = value?.toString().toLowerCase() === 'true' ? true : false,
onValueChanged: checkbox.onChanged onValueChanged: checkbox.onChanged
}; };
} }
@@ -265,6 +269,7 @@ export function createDropdownInputInfo(view: azdata.ModelView, info: { defaultV
return { return {
component: dropdown, component: dropdown,
getValue: async (): Promise<InputValueType> => typeof dropdown.value === 'string' ? dropdown.value : dropdown.value?.name, getValue: async (): Promise<InputValueType> => typeof dropdown.value === 'string' ? dropdown.value : dropdown.value?.name,
setValue: (value: InputValueType) => setDropdownValue(dropdown, value?.toString()),
getDisplayValue: async (): Promise<string> => (typeof dropdown.value === 'string' ? dropdown.value : dropdown.value?.displayName) || '', getDisplayValue: async (): Promise<string> => (typeof dropdown.value === 'string' ? dropdown.value : dropdown.value?.displayName) || '',
onValueChanged: dropdown.onValueChanged, onValueChanged: dropdown.onValueChanged,
}; };
@@ -331,6 +336,7 @@ export function initializeWizardPage(context: WizardPageContext): void {
}); });
})); }));
await hookUpDynamicEnablement(context); await hookUpDynamicEnablement(context);
await hookUpValueProviders(context);
const formBuilder = view.modelBuilder.formContainer().withFormItems( const formBuilder = view.modelBuilder.formContainer().withFormItems(
sections.map(section => { return { title: '', component: section }; }), sections.map(section => { return { title: '', component: section }; }),
{ {
@@ -397,6 +403,35 @@ async function hookUpDynamicEnablement(context: WizardPageContext): Promise<void
})); }));
} }
async function hookUpValueProviders(context: WizardPageContext): Promise<void> {
await Promise.all(context.pageInfo.sections.map(async section => {
if (!section.fields) {
return;
}
await Promise.all(section.fields.map(async field => {
if (field.valueProvider) {
const fieldKey = field.variableName || field.label;
const fieldComponent = context.inputComponents[fieldKey];
const targetComponent = context.inputComponents[field.valueProvider.triggerField];
if (!targetComponent) {
console.error(`Could not find target component ${field.valueProvider.triggerField} when hooking up value providers for ${field.label}`);
return;
}
const provider = valueProviderService.getValueProvider(field.valueProvider.providerId);
const updateFields = async () => {
const targetComponentValue = await targetComponent.getValue();
const newFieldValue = await provider.getValue(targetComponentValue?.toString() ?? '');
fieldComponent.setValue(newFieldValue);
};
targetComponent.onValueChanged(() => {
updateFields();
});
await updateFields();
}
}));
}));
}
export async function createSection(context: SectionContext): Promise<azdata.GroupContainer> { export async function createSection(context: SectionContext): Promise<azdata.GroupContainer> {
const components: azdata.Component[] = []; const components: azdata.Component[] = [];
context.sectionInfo.inputWidth = context.sectionInfo.inputWidth || DefaultInputWidth; context.sectionInfo.inputWidth = context.sectionInfo.inputWidth || DefaultInputWidth;
@@ -630,6 +665,7 @@ async function configureOptionsSourceSubfields(context: FieldContext, optionsSou
throw e; throw e;
} }
}, },
setValue: (_value: InputValueType) => { throw new Error('Setting value of radio group isn\'t currently supported'); },
onValueChanged: optionsComponent.onValueChanged onValueChanged: optionsComponent.onValueChanged
}); });
} }
@@ -666,6 +702,7 @@ function processNumberField(context: FieldContext): void {
const value = await input.getValue(); const value = await input.getValue();
return typeof value === 'string' && value.length > 0 ? parseFloat(value) : value; return typeof value === 'string' && value.length > 0 ? parseFloat(value) : value;
}, },
setValue: (value: InputValueType) => input.component.value = value?.toString(),
onValueChanged: input.onValueChanged onValueChanged: input.onValueChanged
}); });
} }
@@ -762,6 +799,7 @@ function processEvaluatedTextField(context: FieldContext): ReadOnlyFieldInputs {
readOnlyField.text!.value = await substituteVariableValues(context.inputComponents, context.fieldInfo.defaultValue); readOnlyField.text!.value = await substituteVariableValues(context.inputComponents, context.fieldInfo.defaultValue);
return readOnlyField.text!.value; return readOnlyField.text!.value;
}, },
setValue: (value: InputValueType) => readOnlyField.text!.value = value?.toString(),
onValueChanged: onChangedEmitter.event, onValueChanged: onChangedEmitter.event,
}); });
return readOnlyField; return readOnlyField;
@@ -945,6 +983,7 @@ async function createRadioOptions(context: FieldContext, getRadioButtonInfo?: ((
component: radioGroupLoadingComponentBuilder, component: radioGroupLoadingComponentBuilder,
labelComponent: label, labelComponent: label,
getValue: async (): Promise<InputValueType> => radioGroupLoadingComponentBuilder.value, getValue: async (): Promise<InputValueType> => radioGroupLoadingComponentBuilder.value,
setValue: (value: InputValueType) => { throw new Error('Setting value of radio group isn\'t currently supported'); },
getDisplayValue: async (): Promise<string> => radioGroupLoadingComponentBuilder.displayValue, getDisplayValue: async (): Promise<string> => radioGroupLoadingComponentBuilder.displayValue,
onValueChanged: radioGroupLoadingComponentBuilder.onValueChanged, onValueChanged: radioGroupLoadingComponentBuilder.onValueChanged,
}); });
@@ -1140,6 +1179,7 @@ function createAzureSubscriptionDropdown(
const inputValue = (await subscriptionDropdown.getValue())?.toString() || ''; const inputValue = (await subscriptionDropdown.getValue())?.toString() || '';
return subscriptionValueToSubscriptionMap.get(inputValue)?.id || inputValue; return subscriptionValueToSubscriptionMap.get(inputValue)?.id || inputValue;
}, },
setValue: (value: InputValueType) => setDropdownValue(subscriptionDropdown.component, value?.toString()),
getDisplayValue: subscriptionDropdown.getDisplayValue, getDisplayValue: subscriptionDropdown.getDisplayValue,
onValueChanged: subscriptionDropdown.onValueChanged onValueChanged: subscriptionDropdown.onValueChanged
}); });
@@ -1401,3 +1441,18 @@ export function isInputBoxEmpty(input: azdata.InputBoxComponent): boolean {
return input.value === undefined || input.value === ''; return input.value === undefined || input.value === '';
} }
/**
* Sets the dropdown value to the corresponding value from the list of current values, converting
* into a CategoryValue if necessary (using the name field).
* @param dropdown The dropdown component to set the value for
* @param value The value to set - either the direct string value or the name of the CategoryValue to use
*/
function setDropdownValue(dropdown: azdata.DropDownComponent, value: string = ''): void {
const values = dropdown.values ?? [];
if (typeof values[0] === 'object') {
dropdown.value = (<azdata.CategoryValue[]>values).find(v => v.name === value);
} else {
dropdown.value = value;
}
}