Use faster, editable dropdown for Collations in database dialogs (#23974)

* Also fixed an issue where a manually edited text field doesn't get updated when selecting the same dropdown value from before the manual edit.
---------

Co-authored-by: Charles Gagnon <chgagnon@microsoft.com>
This commit is contained in:
Cory Rivera 2023-07-26 10:04:29 -07:00 коммит произвёл GitHub
Родитель 5f1801d6d4
Коммит cc778ad69f
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: 4AEE18F83AFDEB23
7 изменённых файлов: 67 добавлений и 32 удалений

Просмотреть файл

@ -161,7 +161,7 @@ export const MemberSectionHeader = localize('objectManagement.membersLabel', "Me
export const SchemaText = localize('objectManagement.schemaLabel', "Schema");
// Database
export const DatabaseExistsError = (dbName: string) => localize('objectManagement.databaseExistsError', "Database '{0}' already exists. Choose a different database name.", dbName);
export const CollationNotValidError = (collationName: string) => localize('objectManagement.collationNotValidError', "The selected collation '{0}' is not valid. Please choose a different collation.", collationName);
export const CollationText = localize('objectManagement.collationLabel', "Collation");
export const RecoveryModelText = localize('objectManagement.recoveryModelLabel', "Recovery Model");
export const CompatibilityLevelText = localize('objectManagement.compatibilityLevelLabel', "Compatibility Level");

Просмотреть файл

@ -134,6 +134,14 @@ export class DatabaseDialog extends ObjectManagementDialogBase<Database, Databas
}
}
protected override async validateInput(): Promise<string[]> {
let errors = await super.validateInput();
if (this.viewInfo.collationNames?.length > 0 && !this.viewInfo.collationNames.some(name => name.toLowerCase() === this.objectInfo.collationName?.toLowerCase())) {
errors.push(localizedConstants.CollationNotValidError(this.objectInfo.collationName ?? ''));
}
return errors;
}
//#region Create Database
private initializeGeneralSection(): azdata.GroupContainer {
let containers: azdata.Component[] = [];
@ -167,7 +175,7 @@ export class DatabaseDialog extends ObjectManagementDialogBase<Database, Databas
this.objectInfo.collationName = this.viewInfo.collationNames[0];
let collationDropbox = this.createDropdown(localizedConstants.CollationText, async () => {
this.objectInfo.collationName = collationDropbox.value as string;
}, this.viewInfo.collationNames, this.viewInfo.collationNames[0]);
}, this.viewInfo.collationNames, this.viewInfo.collationNames[0], true, DefaultInputWidth, true, true);
containers.push(this.createLabelInputContainer(localizedConstants.CollationText, collationDropbox));
}
@ -277,7 +285,7 @@ export class DatabaseDialog extends ObjectManagementDialogBase<Database, Databas
// Collation
let collationDropbox = this.createDropdown(localizedConstants.CollationText, async (newValue) => {
this.objectInfo.collationName = newValue as string;
}, this.viewInfo.collationNames, this.objectInfo.collationName);
}, this.viewInfo.collationNames, this.objectInfo.collationName, true, DefaultInputWidth, true, true);
containers.push(this.createLabelInputContainer(localizedConstants.CollationText, collationDropbox));
// Recovery Model

Просмотреть файл

@ -27,7 +27,6 @@ export const DefaultTableListItemEnabledStateGetter: TableListItemEnabledStateGe
export const DefaultTableListItemValueGetter: TableListItemValueGetter<any> = (item: any) => [item?.toString() ?? ''];
export const DefaultTableListItemComparer: TableListItemComparer<any> = (item1: any, item2: any) => item1 === item2;
export abstract class DialogBase<DialogResult> {
protected readonly disposables: vscode.Disposable[] = [];
protected readonly dialogObject: azdata.window.Dialog;
@ -287,7 +286,7 @@ export abstract class DialogBase<DialogResult> {
return this.createButtonContainer([addButton, removeButton]);
}
protected createDropdown(ariaLabel: string, handler: (newValue: string) => Promise<void>, values: string[], value: string | undefined, enabled: boolean = true, width: number = DefaultInputWidth): azdata.DropDownComponent {
protected createDropdown(ariaLabel: string, handler: (newValue: string) => Promise<void>, values: string[], value: string | undefined, enabled: boolean = true, width: number = DefaultInputWidth, editable?: boolean, strictSelection?: boolean): azdata.DropDownComponent {
// Automatically add an empty item to the beginning of the list if the current value is not specified.
// This is needed when no meaningful default value can be provided.
// Create a new array so that the original array isn't modified.
@ -301,7 +300,9 @@ export abstract class DialogBase<DialogResult> {
values: dropdownValues,
value: value,
width: width,
enabled: enabled
enabled: enabled,
editable: editable,
strictSelection: strictSelection
}).component();
this.disposables.push(dropdown.onValueChanged(async () => {
await handler(<string>dropdown.value!);

11
src/sql/azdata.proposed.d.ts поставляемый
Просмотреть файл

@ -1826,14 +1826,21 @@ declare module 'azdata' {
/**
* Corresponds to the aria-live accessibility attribute for this component
*/
ariaLive?: AriaLiveValue
ariaLive?: AriaLiveValue;
}
export interface ContainerProperties extends ComponentProperties {
/**
* Corresponds to the aria-live accessibility attribute for this component
*/
ariaLive?: AriaLiveValue
ariaLive?: AriaLiveValue;
}
export interface DropDownProperties {
/**
* Whether or not an option in the list must be selected or a "new" option can be set. Only applicable when 'editable' is true. Default false.
*/
strictSelection?: boolean;
}
export interface NodeInfo {

Просмотреть файл

@ -112,9 +112,11 @@ export class Dropdown extends Disposable implements IListVirtualDelegate<string>
this._inputContainer = DOM.append(this._el, DOM.$('.dropdown-input.select-container'));
this._inputContainer.style.width = '100%';
this._inputContainer.style.height = '100%';
this._selectListContainer = DOM.$('div');
this._selectListContainer.style.backgroundColor = opt.contextBackground;
this._selectListContainer.style.outline = `1px solid ${opt.contextBorder}`;
this._input = new InputBox(this._inputContainer, contextViewService, {
validationOptions: {
// @SQLTODO
@ -141,11 +143,11 @@ export class Dropdown extends Disposable implements IListVirtualDelegate<string>
}));
const inputTracker = this._register(DOM.trackFocus(this._input.inputElement));
inputTracker.onDidBlur(() => {
this._register(inputTracker.onDidBlur(() => {
if (!this._selectList.isDOMFocused()) {
this._onBlur.fire();
}
});
}));
/*
This event listener is intended to close the expanded drop down when the ADS shell window is resized
@ -167,14 +169,12 @@ export class Dropdown extends Disposable implements IListVirtualDelegate<string>
break;
case KeyCode.Escape:
if (this._isDropDownVisible) {
this._input.validate();
this._onBlur.fire();
this._hideList();
e.stopPropagation();
}
break;
case KeyCode.Tab:
this._input.validate();
this._onBlur.fire();
this._hideList();
break;
@ -244,7 +244,7 @@ export class Dropdown extends Disposable implements IListVirtualDelegate<string>
}
}));
this._input.onDidChange(e => {
this._register(this._input.onDidChange(e => {
if (this._dataSource.values.length > 0) {
this._dataSource.filter = e;
if (this._isDropDownVisible) {
@ -254,12 +254,11 @@ export class Dropdown extends Disposable implements IListVirtualDelegate<string>
if (this.fireOnTextChange) {
this.value = e;
}
});
}));
this.onBlur(() => {
this._register(this.onBlur(() => {
this._hideList();
this._input.validate();
});
}));
this._register(this._selectList);
this._register(this._input);
@ -316,6 +315,8 @@ export class Dropdown extends Disposable implements IListVirtualDelegate<string>
private _hideList(): void {
this.contextViewService.hideContextView();
this._inputContainer.setAttribute('aria-expanded', 'false');
// Show error for input box in case the user closed the dropdown without selecting anything, like by hitting Escape
this.input.validate();
}
private _updateDropDownList(): void {
@ -323,17 +324,9 @@ export class Dropdown extends Disposable implements IListVirtualDelegate<string>
const selectedIndex = this._dataSource.filteredValues.indexOf(this.value);
this._selectList.setSelection(selectedIndex !== -1 ? [selectedIndex] : []);
let width = this._inputContainer.clientWidth;
// Find the longest option in the list and set our width to that (max 500px)
const longestOption = this._dataSource.filteredValues.reduce((previous, current) => {
return previous.length > current.length ? previous : current;
}, '');
this._widthControlElement.innerText = longestOption;
const inputContainerWidth = DOM.getContentWidth(this._inputContainer);
const longestOptionWidth = DOM.getTotalWidth(this._widthControlElement);
width = clamp(longestOptionWidth, inputContainerWidth, 500);
let width = clamp(longestOptionWidth, inputContainerWidth, 500);
const height = Math.min(this._dataSource.filteredValues.length * this.getHeight(), this._options.maxHeight ?? 500);
this._selectListContainer.style.width = `${width}px`;
@ -345,6 +338,13 @@ export class Dropdown extends Disposable implements IListVirtualDelegate<string>
if (vals) {
this._dataSource.filter = undefined;
this._dataSource.values = vals;
// Find the longest option in the list to set the width of the dropdown
let longestOption = this._dataSource.values.reduce((previous, current) => {
return previous.length > current.length ? previous : current;
}, '');
this._widthControlElement.innerText = longestOption;
if (this._isDropDownVisible) {
this._updateDropDownList();
}
@ -357,9 +357,12 @@ export class Dropdown extends Disposable implements IListVirtualDelegate<string>
}
public set value(val: string) {
this._input.value = val;
if (this._previousValue !== val) {
// A value can be changed either by selecting an option from the dropdown list or editing the text field directly.
// If you try to select the same dropdown value again after changing the text field directly, that change should
// still be applied, which is why we check both _previousValue and _input.value.
if (this._previousValue !== val || this._input.value !== val) {
this._previousValue = val;
this._input.value = val;
this._onValueChange.fire(val);
}
}
@ -378,7 +381,7 @@ export class Dropdown extends Disposable implements IListVirtualDelegate<string>
}
private _inputValidator(value: string): IMessage | null {
if (!this._input.hasFocus() && this._input.isEnabled() && !this._selectList.isDOMFocused() && !this._dataSource.values.some(i => i === value)) {
if (this._input.isEnabled() && !this._selectList.isDOMFocused() && !this._isDropDownVisible && !this._dataSource.values.some(i => i === value)) {
if (this._options.strictSelection && this._options.errorMessage) {
return {
content: this._options.errorMessage,
@ -418,4 +421,8 @@ export class Dropdown extends Disposable implements IListVirtualDelegate<string>
public get options(): IDropdownOptions {
return this._options;
}
public set strictSelection(val: boolean | undefined) {
this._options.strictSelection = val;
}
}

Просмотреть файл

@ -68,4 +68,12 @@ suite('Editable dropdown tests', () => {
dropdown.input.value = options.values[0];
assert.strictEqual(count, 3, 'onValueChange event was fired with input box value change even after setting the fireOnTextChange to false');
});
test('selecting same dropdown value again after changing text field should update text field', () => {
const dropdown = new Dropdown(container, undefined, options);
dropdown.value = options.values[0];
dropdown.input.value = 'NotARealValue';
dropdown.value = options.values[0];
assert.strictEqual(dropdown.input.value, options.values[0]);
});
});

Просмотреть файл

@ -81,7 +81,7 @@ export default class DropDownComponent extends ComponentBase<azdata.DropDownProp
if (this._editableDropDownContainer) {
let dropdownOptions: IDropdownOptions = {
values: [],
strictSelection: false,
strictSelection: this.strictSelection ?? false,
placeholder: this.placeholder,
maxHeight: 125,
ariaLabel: '',
@ -178,7 +178,7 @@ export default class DropDownComponent extends ComponentBase<azdata.DropDownProp
}
this._editableDropdown.enabled = this.enabled;
this._editableDropdown.fireOnTextChange = this.fireOnTextChange;
this._editableDropdown.strictSelection = this.strictSelection;
if (this.placeholder) {
this._editableDropdown.input.setPlaceHolder(this.placeholder);
}
@ -338,7 +338,11 @@ export default class DropDownComponent extends ComponentBase<azdata.DropDownProp
}
public get placeholder(): string | undefined {
return this.getPropertyOrDefault<string>((props) => props.placeholder, undefined);
return this.getPropertyOrDefault<string | undefined>((props) => props.placeholder, undefined);
}
public get strictSelection(): boolean | undefined {
return this.getPropertyOrDefault<boolean | undefined>((props) => props.strictSelection, undefined);
}
public get validationErrorMessages(): string[] | undefined {