mirror of
https://github.com/ckaczor/azuredatastudio.git
synced 2026-02-16 18:46:40 -05:00
Add custom option support on Connection dialog + move Encrypt to connection dialog (#20959)
This commit is contained in:
@@ -228,7 +228,8 @@
|
|||||||
"objectType": null,
|
"objectType": null,
|
||||||
"categoryValues": null,
|
"categoryValues": null,
|
||||||
"isRequired": false,
|
"isRequired": false,
|
||||||
"isArray": false
|
"isArray": false,
|
||||||
|
"showOnConnectionDialog": true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"specialValueType": null,
|
"specialValueType": null,
|
||||||
|
|||||||
@@ -1046,7 +1046,8 @@
|
|||||||
"objectType": null,
|
"objectType": null,
|
||||||
"categoryValues": null,
|
"categoryValues": null,
|
||||||
"isRequired": false,
|
"isRequired": false,
|
||||||
"isArray": false
|
"isArray": false,
|
||||||
|
"showOnConnectionDialog": true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"specialValueType": null,
|
"specialValueType": null,
|
||||||
|
|||||||
@@ -5367,6 +5367,12 @@ Error: {1}</source>
|
|||||||
<trans-unit id="serverGroup">
|
<trans-unit id="serverGroup">
|
||||||
<source xml:lang="en">Server group</source>
|
<source xml:lang="en">Server group</source>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
|
<trans-unit id="boolean.true">
|
||||||
|
<source xml:lang="en">True</source>
|
||||||
|
</trans-unit>
|
||||||
|
<trans-unit id="boolean.false">
|
||||||
|
<source xml:lang="en">False</source>
|
||||||
|
</trans-unit>
|
||||||
</body></file>
|
</body></file>
|
||||||
<file original="src/sql/workbench/services/connection/browser/localizedConstants" source-language="en" datatype="plaintext"><body>
|
<file original="src/sql/workbench/services/connection/browser/localizedConstants" source-language="en" datatype="plaintext"><body>
|
||||||
<trans-unit id="onDidConnectMessage">
|
<trans-unit id="onDidConnectMessage">
|
||||||
@@ -6544,4 +6550,4 @@ Error: {1}</source>
|
|||||||
<source xml:lang="en">Show Getting Started</source>
|
<source xml:lang="en">Show Getting Started</source>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
</body></file>
|
</body></file>
|
||||||
</xliff>
|
</xliff>
|
||||||
|
|||||||
6
src/sql/azdata.proposed.d.ts
vendored
6
src/sql/azdata.proposed.d.ts
vendored
@@ -469,6 +469,12 @@ declare module 'azdata' {
|
|||||||
|
|
||||||
export interface ConnectionOption {
|
export interface ConnectionOption {
|
||||||
defaultValueOsOverrides?: DefaultValueOsOverride[];
|
defaultValueOsOverrides?: DefaultValueOsOverride[];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* When set to true, the respective connection option will be rendered on the main connection dialog
|
||||||
|
* and not the Advanced Options window.
|
||||||
|
*/
|
||||||
|
showOnConnectionDialog?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface TaskInfo {
|
export interface TaskInfo {
|
||||||
|
|||||||
@@ -215,7 +215,7 @@ export class ProviderConnectionInfo extends Disposable implements azdata.Connect
|
|||||||
let idNames = [];
|
let idNames = [];
|
||||||
if (this.serverCapabilities) {
|
if (this.serverCapabilities) {
|
||||||
idNames = this.serverCapabilities.connectionOptions.map(o => {
|
idNames = this.serverCapabilities.connectionOptions.map(o => {
|
||||||
if ((o.specialValueType || o.isIdentity)
|
if ((o.specialValueType || o.isIdentity || o.showOnConnectionDialog)
|
||||||
&& o.specialValueType !== ConnectionOptionSpecialType.password
|
&& o.specialValueType !== ConnectionOptionSpecialType.password
|
||||||
&& o.specialValueType !== ConnectionOptionSpecialType.connectionName) {
|
&& o.specialValueType !== ConnectionOptionSpecialType.connectionName) {
|
||||||
return o.name;
|
return o.name;
|
||||||
|
|||||||
@@ -116,6 +116,7 @@ suite('SQL ProviderConnectionInfo tests', () => {
|
|||||||
defaultValue: undefined!,
|
defaultValue: undefined!,
|
||||||
isIdentity: false,
|
isIdentity: false,
|
||||||
isRequired: false,
|
isRequired: false,
|
||||||
|
showOnConnectionDialog: true,
|
||||||
specialValueType: undefined!,
|
specialValueType: undefined!,
|
||||||
valueType: ServiceOptionType.string
|
valueType: ServiceOptionType.string
|
||||||
}
|
}
|
||||||
@@ -200,7 +201,7 @@ suite('SQL ProviderConnectionInfo tests', () => {
|
|||||||
|
|
||||||
test('constructor should initialize the options given a valid model with options', () => {
|
test('constructor should initialize the options given a valid model with options', () => {
|
||||||
let options: { [key: string]: string } = {};
|
let options: { [key: string]: string } = {};
|
||||||
options['encrypt'] = 'test value';
|
options['encrypt'] = 'true';
|
||||||
let conn2 = Object.assign({}, connectionProfile, { options: options });
|
let conn2 = Object.assign({}, connectionProfile, { options: options });
|
||||||
let conn = new ProviderConnectionInfo(capabilitiesService, conn2);
|
let conn = new ProviderConnectionInfo(capabilitiesService, conn2);
|
||||||
|
|
||||||
@@ -210,12 +211,15 @@ suite('SQL ProviderConnectionInfo tests', () => {
|
|||||||
assert.strictEqual(conn.authenticationType, conn2.authenticationType);
|
assert.strictEqual(conn.authenticationType, conn2.authenticationType);
|
||||||
assert.strictEqual(conn.password, conn2.password);
|
assert.strictEqual(conn.password, conn2.password);
|
||||||
assert.strictEqual(conn.userName, conn2.userName);
|
assert.strictEqual(conn.userName, conn2.userName);
|
||||||
assert.strictEqual(conn.options['encrypt'], 'test value');
|
assert.strictEqual(conn.options['encrypt'], 'true');
|
||||||
});
|
});
|
||||||
|
|
||||||
test('getOptionsKey should create a valid unique id', () => {
|
test('getOptionsKey should create a valid unique id', () => {
|
||||||
|
let options: { [key: string]: string } = {};
|
||||||
|
options['encrypt'] = 'true';
|
||||||
|
connectionProfile.options = options;
|
||||||
let conn = new ProviderConnectionInfo(capabilitiesService, connectionProfile);
|
let conn = new ProviderConnectionInfo(capabilitiesService, connectionProfile);
|
||||||
let expectedId = 'providerName:MSSQL|authenticationType:|databaseName:database|serverName:new server|userName:user';
|
let expectedId = 'providerName:MSSQL|authenticationType:|databaseName:database|encrypt:true|serverName:new server|userName:user';
|
||||||
let id = conn.getOptionsKey();
|
let id = conn.getOptionsKey();
|
||||||
assert.strictEqual(id, expectedId);
|
assert.strictEqual(id, expectedId);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -147,8 +147,8 @@ export function updateOptions(options: { [optionName: string]: any }, optionsMap
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export let trueInputValue: string = 'True';
|
export let trueInputValue: string = localize('boolean.true', 'True');
|
||||||
export let falseInputValue: string = 'False';
|
export let falseInputValue: string = localize('boolean.false', 'False');
|
||||||
|
|
||||||
export function findElement(container: HTMLElement, className: string): HTMLElement {
|
export function findElement(container: HTMLElement, className: string): HTMLElement {
|
||||||
let elementBuilder = container;
|
let elementBuilder = container;
|
||||||
|
|||||||
@@ -40,7 +40,7 @@ export class ConnectionController implements IConnectionComponentController {
|
|||||||
this._callback = callback;
|
this._callback = callback;
|
||||||
this._providerOptions = connectionProperties.connectionOptions;
|
this._providerOptions = connectionProperties.connectionOptions;
|
||||||
let specialOptions = this._providerOptions.filter(
|
let specialOptions = this._providerOptions.filter(
|
||||||
(property) => (property.specialValueType !== null && property.specialValueType !== undefined));
|
(property) => (property.specialValueType !== null && property.specialValueType !== undefined || property.showOnConnectionDialog));
|
||||||
this._connectionWidget = this._instantiationService.createInstance(ConnectionWidget, specialOptions, {
|
this._connectionWidget = this._instantiationService.createInstance(ConnectionWidget, specialOptions, {
|
||||||
onSetConnectButton: (enable: boolean) => this._callback.onSetConnectButton(enable),
|
onSetConnectButton: (enable: boolean) => this._callback.onSetConnectButton(enable),
|
||||||
onCreateNewServerGroup: () => this.onCreateNewServerGroup(),
|
onCreateNewServerGroup: () => this.onCreateNewServerGroup(),
|
||||||
@@ -123,7 +123,7 @@ export class ConnectionController implements IConnectionComponentController {
|
|||||||
this._advancedController = this._instantiationService.createInstance(AdvancedPropertiesController, () => this._connectionWidget.focusOnAdvancedButton());
|
this._advancedController = this._instantiationService.createInstance(AdvancedPropertiesController, () => this._connectionWidget.focusOnAdvancedButton());
|
||||||
}
|
}
|
||||||
let advancedOption = this._providerOptions.filter(
|
let advancedOption = this._providerOptions.filter(
|
||||||
(property) => (property.specialValueType === undefined || property.specialValueType === null));
|
(property) => (property.specialValueType === undefined || property.specialValueType === null && !property.showOnConnectionDialog));
|
||||||
this._advancedController.showDialog(advancedOption, this._model.options);
|
this._advancedController.showDialog(advancedOption, this._model.options);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ import { Checkbox } from 'sql/base/browser/ui/checkbox/checkbox';
|
|||||||
import { InputBox } from 'sql/base/browser/ui/inputBox/inputBox';
|
import { InputBox } from 'sql/base/browser/ui/inputBox/inputBox';
|
||||||
import * as DialogHelper from 'sql/workbench/browser/modal/dialogHelper';
|
import * as DialogHelper from 'sql/workbench/browser/modal/dialogHelper';
|
||||||
import { IConnectionComponentCallbacks } from 'sql/workbench/services/connection/browser/connectionDialogService';
|
import { IConnectionComponentCallbacks } from 'sql/workbench/services/connection/browser/connectionDialogService';
|
||||||
import { IConnectionProfile } from 'sql/platform/connection/common/interfaces';
|
import { IConnectionProfile, ServiceOptionType } from 'sql/platform/connection/common/interfaces';
|
||||||
import { ConnectionOptionSpecialType } from 'sql/workbench/api/common/sqlExtHostTypes';
|
import { ConnectionOptionSpecialType } from 'sql/workbench/api/common/sqlExtHostTypes';
|
||||||
import { ConnectionProfileGroup, IConnectionProfileGroup } from 'sql/platform/connection/common/connectionProfileGroup';
|
import { ConnectionProfileGroup, IConnectionProfileGroup } from 'sql/platform/connection/common/connectionProfileGroup';
|
||||||
import { IConnectionManagementService } from 'sql/platform/connection/common/connectionManagement';
|
import { IConnectionManagementService } from 'sql/platform/connection/common/connectionManagement';
|
||||||
@@ -68,11 +68,13 @@ export class ConnectionWidget extends lifecycle.Disposable {
|
|||||||
protected _container: HTMLElement;
|
protected _container: HTMLElement;
|
||||||
protected _serverGroupSelectBox: SelectBox;
|
protected _serverGroupSelectBox: SelectBox;
|
||||||
protected _authTypeSelectBox: SelectBox;
|
protected _authTypeSelectBox: SelectBox;
|
||||||
|
protected _customOptions: azdata.ConnectionOption[];
|
||||||
protected _optionsMaps: { [optionType: number]: azdata.ConnectionOption };
|
protected _optionsMaps: { [optionType: number]: azdata.ConnectionOption };
|
||||||
protected _tableContainer: HTMLElement;
|
protected _tableContainer: HTMLElement;
|
||||||
protected _providerName: string;
|
protected _providerName: string;
|
||||||
protected _connectionNameInputBox: InputBox;
|
protected _connectionNameInputBox: InputBox;
|
||||||
protected _databaseNameInputBox: Dropdown;
|
protected _databaseNameInputBox: Dropdown;
|
||||||
|
protected _customOptionWidgets: (InputBox | SelectBox)[];
|
||||||
protected _advancedButton: Button;
|
protected _advancedButton: Button;
|
||||||
private static readonly _authTypes: AuthenticationType[] =
|
private static readonly _authTypes: AuthenticationType[] =
|
||||||
[AuthenticationType.AzureMFA, AuthenticationType.AzureMFAAndUser, AuthenticationType.Integrated, AuthenticationType.SqlLogin, AuthenticationType.DSTSAuth, AuthenticationType.None];
|
[AuthenticationType.AzureMFA, AuthenticationType.AzureMFAAndUser, AuthenticationType.Integrated, AuthenticationType.SqlLogin, AuthenticationType.DSTSAuth, AuthenticationType.None];
|
||||||
@@ -115,6 +117,7 @@ export class ConnectionWidget extends lifecycle.Disposable {
|
|||||||
) {
|
) {
|
||||||
super();
|
super();
|
||||||
this._callbacks = callbacks;
|
this._callbacks = callbacks;
|
||||||
|
this._customOptions = options.filter(a => a.showOnConnectionDialog === true);
|
||||||
this._optionsMaps = {};
|
this._optionsMaps = {};
|
||||||
for (let i = 0; i < options.length; i++) {
|
for (let i = 0; i < options.length; i++) {
|
||||||
let option = options[i];
|
let option = options[i];
|
||||||
@@ -177,6 +180,7 @@ export class ConnectionWidget extends lifecycle.Disposable {
|
|||||||
this.addAuthenticationTypeOption(authTypeChanged);
|
this.addAuthenticationTypeOption(authTypeChanged);
|
||||||
this.addLoginOptions();
|
this.addLoginOptions();
|
||||||
this.addDatabaseOption();
|
this.addDatabaseOption();
|
||||||
|
this.addCustomConnectionOptions();
|
||||||
this.addServerGroupOption();
|
this.addServerGroupOption();
|
||||||
this.addConnectionNameOptions();
|
this.addConnectionNameOptions();
|
||||||
this.addAdvancedOptions();
|
this.addAdvancedOptions();
|
||||||
@@ -234,6 +238,12 @@ export class ConnectionWidget extends lifecycle.Disposable {
|
|||||||
const userNameOption: azdata.ConnectionOption = this._optionsMaps[ConnectionOptionSpecialType.userName];
|
const userNameOption: azdata.ConnectionOption = this._optionsMaps[ConnectionOptionSpecialType.userName];
|
||||||
this._serverNameInputBox.required = !this.useConnectionString;
|
this._serverNameInputBox.required = !this.useConnectionString;
|
||||||
this._userNameInputBox.required = (!this.useConnectionString) && userNameOption?.isRequired;
|
this._userNameInputBox.required = (!this.useConnectionString) && userNameOption?.isRequired;
|
||||||
|
this._userNameInputBox.value = '';
|
||||||
|
if (this.useConnectionString) {
|
||||||
|
this._tableContainer.classList.add('hide-customOptions');
|
||||||
|
} else {
|
||||||
|
this._tableContainer.classList.remove('hide-customOptions');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
protected addAuthenticationTypeOption(authTypeChanged: boolean = false): void {
|
protected addAuthenticationTypeOption(authTypeChanged: boolean = false): void {
|
||||||
@@ -243,6 +253,32 @@ export class ConnectionWidget extends lifecycle.Disposable {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected addCustomConnectionOptions(): void {
|
||||||
|
if (this._customOptions.length > 0) {
|
||||||
|
this._customOptionWidgets = [];
|
||||||
|
this._customOptions.forEach((option, i) => {
|
||||||
|
let customOptionsContainer = DialogHelper.appendRow(this._tableContainer, option.displayName, 'connection-label', 'connection-input', 'custom-connection-options');
|
||||||
|
switch (option.valueType) {
|
||||||
|
case ServiceOptionType.boolean:
|
||||||
|
this._customOptionWidgets[i] = new SelectBox([localize('boolean.true', 'True'), localize('boolean.false', 'False')], option.defaultValue, this._contextViewService, customOptionsContainer, { ariaLabel: option.displayName });
|
||||||
|
DialogHelper.appendInputSelectBox(customOptionsContainer, this._customOptionWidgets[i] as SelectBox);
|
||||||
|
this._register(styler.attachSelectBoxStyler(this._customOptionWidgets[i] as SelectBox, this._themeService));
|
||||||
|
break;
|
||||||
|
case ServiceOptionType.category:
|
||||||
|
this._customOptionWidgets[i] = new SelectBox(option.categoryValues.map(c => c.displayName), option.defaultValue, this._contextViewService, customOptionsContainer, { ariaLabel: option.displayName });
|
||||||
|
DialogHelper.appendInputSelectBox(customOptionsContainer, this._customOptionWidgets[i] as SelectBox);
|
||||||
|
this._register(styler.attachSelectBoxStyler(this._customOptionWidgets[i] as SelectBox, this._themeService));
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
this._customOptionWidgets[i] = new InputBox(customOptionsContainer, this._contextViewService, { ariaLabel: option.displayName });
|
||||||
|
this._register(styler.attachInputBoxStyler(this._customOptionWidgets[i] as InputBox, this._themeService));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
this._register(this._customOptionWidgets[i]);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
protected addServerNameOption(): void {
|
protected addServerNameOption(): void {
|
||||||
// Server name
|
// Server name
|
||||||
let serverNameOption = this._optionsMaps[ConnectionOptionSpecialType.serverName];
|
let serverNameOption = this._optionsMaps[ConnectionOptionSpecialType.serverName];
|
||||||
@@ -491,6 +527,8 @@ export class ConnectionWidget extends lifecycle.Disposable {
|
|||||||
|
|
||||||
protected onAuthTypeSelected(selectedAuthType: string) {
|
protected onAuthTypeSelected(selectedAuthType: string) {
|
||||||
let currentAuthType = this.getMatchingAuthType(selectedAuthType);
|
let currentAuthType = this.getMatchingAuthType(selectedAuthType);
|
||||||
|
this._userNameInputBox.value === '';
|
||||||
|
this._passwordInputBox.value === '';
|
||||||
this._userNameInputBox.hideMessage();
|
this._userNameInputBox.hideMessage();
|
||||||
this._passwordInputBox.hideMessage();
|
this._passwordInputBox.hideMessage();
|
||||||
this._azureAccountDropdown.hideMessage();
|
this._azureAccountDropdown.hideMessage();
|
||||||
@@ -734,6 +772,16 @@ export class ConnectionWidget extends lifecycle.Disposable {
|
|||||||
this._tableContainer.classList.add('hide-azure-accounts');
|
this._tableContainer.classList.add('hide-azure-accounts');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (this._customOptionWidgets) {
|
||||||
|
this._customOptionWidgets.forEach((widget, i) => {
|
||||||
|
if (widget instanceof SelectBox) {
|
||||||
|
widget.selectWithOptionName(this.getModelValue(connectionInfo.options[this._customOptions[i].name]));
|
||||||
|
} else {
|
||||||
|
widget.value = this.getModelValue(connectionInfo.options[this._customOptions[i].name]);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
if (this.authType === AuthenticationType.AzureMFA || this.authType === AuthenticationType.AzureMFAAndUser) {
|
if (this.authType === AuthenticationType.AzureMFA || this.authType === AuthenticationType.AzureMFAAndUser) {
|
||||||
this.fillInAzureAccountOptions().then(async () => {
|
this.fillInAzureAccountOptions().then(async () => {
|
||||||
let tenantId = connectionInfo.azureTenantId;
|
let tenantId = connectionInfo.azureTenantId;
|
||||||
@@ -802,7 +850,11 @@ export class ConnectionWidget extends lifecycle.Disposable {
|
|||||||
if (this._authTypeSelectBox) {
|
if (this._authTypeSelectBox) {
|
||||||
this._authTypeSelectBox.disable();
|
this._authTypeSelectBox.disable();
|
||||||
}
|
}
|
||||||
|
if (this._customOptionWidgets) {
|
||||||
|
this._customOptionWidgets.forEach(widget => {
|
||||||
|
widget.disable();
|
||||||
|
});
|
||||||
|
}
|
||||||
if (this._connectionStringOptions.isEnabled) {
|
if (this._connectionStringOptions.isEnabled) {
|
||||||
this._connectionStringInputBox.disable();
|
this._connectionStringInputBox.disable();
|
||||||
this._defaultInputOptionRadioButton.enabled = false;
|
this._defaultInputOptionRadioButton.enabled = false;
|
||||||
@@ -840,6 +892,11 @@ export class ConnectionWidget extends lifecycle.Disposable {
|
|||||||
if (this._databaseNameInputBox) {
|
if (this._databaseNameInputBox) {
|
||||||
this._databaseNameInputBox.enabled = true;
|
this._databaseNameInputBox.enabled = true;
|
||||||
}
|
}
|
||||||
|
if (this._customOptionWidgets) {
|
||||||
|
this._customOptionWidgets.forEach(widget => {
|
||||||
|
widget.enable();
|
||||||
|
});
|
||||||
|
}
|
||||||
if (this._connectionStringOptions.isEnabled) {
|
if (this._connectionStringOptions.isEnabled) {
|
||||||
this._connectionStringInputBox.enable();
|
this._connectionStringInputBox.enable();
|
||||||
this._defaultInputOptionRadioButton.enabled = true;
|
this._defaultInputOptionRadioButton.enabled = true;
|
||||||
@@ -964,6 +1021,11 @@ export class ConnectionWidget extends lifecycle.Disposable {
|
|||||||
model.savePassword = this._rememberPasswordCheckBox.checked;
|
model.savePassword = this._rememberPasswordCheckBox.checked;
|
||||||
model.connectionName = this.connectionName;
|
model.connectionName = this.connectionName;
|
||||||
model.databaseName = this.databaseName;
|
model.databaseName = this.databaseName;
|
||||||
|
if (this._customOptionWidgets) {
|
||||||
|
this._customOptionWidgets.forEach((widget, i) => {
|
||||||
|
model.options[this._customOptions[i].name] = widget.value;
|
||||||
|
});
|
||||||
|
}
|
||||||
if (this._serverGroupSelectBox) {
|
if (this._serverGroupSelectBox) {
|
||||||
if (this._serverGroupSelectBox.value === this.DefaultServerGroup.name) {
|
if (this._serverGroupSelectBox.value === this.DefaultServerGroup.name) {
|
||||||
model.groupFullName = '';
|
model.groupFullName = '';
|
||||||
|
|||||||
@@ -123,6 +123,10 @@
|
|||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.hide-customOptions .custom-connection-options {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
.hide-refresh-link .azure-account-row.refresh-credentials-link {
|
.hide-refresh-link .azure-account-row.refresh-credentials-link {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -215,6 +215,7 @@ suite('SQL Object Explorer Service tests', () => {
|
|||||||
defaultValue: undefined,
|
defaultValue: undefined,
|
||||||
isIdentity: false,
|
isIdentity: false,
|
||||||
isRequired: false,
|
isRequired: false,
|
||||||
|
showOnConnectionDialog: true,
|
||||||
specialValueType: undefined,
|
specialValueType: undefined,
|
||||||
valueType: ServiceOptionType.string
|
valueType: ServiceOptionType.string
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user