mirror of
https://github.com/ckaczor/azuredatastudio.git
synced 2026-01-27 01:25:36 -05:00
make the designer event based (#17472)
* make the designer event based * pr comments
This commit is contained in:
@@ -3,12 +3,12 @@
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { DesignerComponentInput, DesignerEditType, DesignerTab, DesignerEdit, DesignerEditIdentifier, DesignerViewModel, DesignerDataPropertyInfo, DesignerTableComponentRowData, DesignerTableProperties, InputBoxProperties, DropDownProperties, CheckBoxProperties, DesignerComponentTypeName } from 'sql/base/browser/ui/designer/interfaces';
|
||||
import { DesignerComponentInput, DesignerEditType, DesignerTab, DesignerEdit, DesignerEditIdentifier, DesignerViewModel, DesignerDataPropertyInfo, DesignerTableComponentRowData, DesignerTableProperties, InputBoxProperties, DropDownProperties, CheckBoxProperties, DesignerComponentTypeName, DesignerEditProcessedEventArgs, DesignerStateChangedEventArgs, DesignerAction, DesignerUIState } from 'sql/base/browser/ui/designer/interfaces';
|
||||
import { IPanelTab, ITabbedPanelStyles, TabbedPanel } from 'sql/base/browser/ui/panel/panel';
|
||||
import * as DOM from 'vs/base/browser/dom';
|
||||
import { Event } from 'vs/base/common/event';
|
||||
import { Orientation, Sizing, SplitView } from 'vs/base/browser/ui/splitview/splitview';
|
||||
import { Disposable } from 'vs/base/common/lifecycle';
|
||||
import { Disposable, DisposableStore } from 'vs/base/common/lifecycle';
|
||||
import { IInputBoxStyles, InputBox } from 'sql/base/browser/ui/inputBox/inputBox';
|
||||
import { IContextViewProvider } from 'vs/base/browser/ui/contextview/contextview';
|
||||
import 'vs/css!./media/designer';
|
||||
@@ -63,6 +63,8 @@ export class Designer extends Disposable implements IThemable {
|
||||
private _tableCellEditorFactory: TableCellEditorFactory;
|
||||
private _propertiesPane: DesignerPropertiesPane;
|
||||
private _buttons: Button[] = [];
|
||||
private _inputDisposable: DisposableStore;
|
||||
private _loadingTimeoutHandle: any;
|
||||
|
||||
constructor(private readonly _container: HTMLElement,
|
||||
private readonly _contextViewProvider: IContextViewProvider) {
|
||||
@@ -72,8 +74,8 @@ export class Designer extends Disposable implements IThemable {
|
||||
valueGetter: (item, column): string => {
|
||||
return item[column.field].value;
|
||||
},
|
||||
valueSetter: async (context: string, row: number, item: DesignerTableComponentRowData, column: Slick.Column<Slick.SlickData>, value: string): Promise<void> => {
|
||||
await this.handleEdit({
|
||||
valueSetter: (context: string, row: number, item: DesignerTableComponentRowData, column: Slick.Column<Slick.SlickData>, value: string): void => {
|
||||
this.handleEdit({
|
||||
type: DesignerEditType.Update,
|
||||
property: {
|
||||
parentProperty: context,
|
||||
@@ -196,37 +198,127 @@ export class Designer extends Disposable implements IThemable {
|
||||
}
|
||||
|
||||
|
||||
public async setInput(input: DesignerComponentInput): Promise<void> {
|
||||
public setInput(input: DesignerComponentInput): void {
|
||||
// Save state
|
||||
if (this._input) {
|
||||
this._input.designerUIState = this.getUIState();
|
||||
}
|
||||
|
||||
// Clean up
|
||||
if (this._loadingTimeoutHandle) {
|
||||
this.stopLoading();
|
||||
}
|
||||
this._buttons = [];
|
||||
this._componentMap.clear();
|
||||
DOM.clearNode(this._topContentContainer);
|
||||
this._tabbedPanel.clearTabs();
|
||||
this._propertiesPane.clear();
|
||||
this._inputDisposable?.dispose();
|
||||
|
||||
|
||||
// Initialize with new input
|
||||
this._input = input;
|
||||
await this.initializeDesignerView();
|
||||
this._inputDisposable = new DisposableStore();
|
||||
this._inputDisposable.add(this._input.onInitialized(() => {
|
||||
this.initializeDesigner();
|
||||
}));
|
||||
this._inputDisposable.add(this._input.onEditProcessed((args) => {
|
||||
this.handleEditProcessedEvent(args);
|
||||
}));
|
||||
this._inputDisposable.add(this._input.onStateChange((args) => {
|
||||
this.handleInputStateChangedEvent(args);
|
||||
}));
|
||||
|
||||
if (this._input.view === undefined) {
|
||||
this._input.initialize();
|
||||
} else {
|
||||
this.initializeDesigner();
|
||||
}
|
||||
if (this._input.pendingAction) {
|
||||
this.updateLoadingStatus(this._input.pendingAction, true, false);
|
||||
}
|
||||
}
|
||||
|
||||
private async initializeDesignerView(): Promise<void> {
|
||||
this._propertiesPane.clear();
|
||||
DOM.clearNode(this._topContentContainer);
|
||||
// For initialization, we would want to show the loading indicator immediately.
|
||||
const handle = this.startLoading(localize('designer.loadingDesigner', "Loading designer..."), 0);
|
||||
const view = await this._input.getView();
|
||||
this.stopLoading(handle, localize('designer.loadingDesignerCompleted', "Loading designer completed"));
|
||||
public override dispose(): void {
|
||||
super.dispose();
|
||||
this._inputDisposable?.dispose();
|
||||
}
|
||||
|
||||
private initializeDesigner(): void {
|
||||
const view = this._input.view;
|
||||
if (view.components) {
|
||||
view.components.forEach(component => {
|
||||
this.createComponent(this._topContentContainer, component, component.propertyName, true, true);
|
||||
});
|
||||
}
|
||||
this._tabbedPanel.clearTabs();
|
||||
view.tabs.forEach(tab => {
|
||||
this._tabbedPanel.pushTab(this.createTabView(tab));
|
||||
});
|
||||
this.layoutTabbedPanel();
|
||||
await this.updateComponentValues();
|
||||
this.updateComponentValues();
|
||||
this.restoreUIState();
|
||||
}
|
||||
|
||||
private handleEditProcessedEvent(args: DesignerEditProcessedEventArgs): void {
|
||||
const edit = args.edit;
|
||||
const result = args.result;
|
||||
if (result.isValid) {
|
||||
this._supressEditProcessing = true;
|
||||
this.updateComponentValues();
|
||||
if (edit.type === DesignerEditType.Add) {
|
||||
// Move focus to the first cell of the newly added row.
|
||||
const propertyName = edit.property as string;
|
||||
const tableData = this._input.viewModel[propertyName] as DesignerTableProperties;
|
||||
const table = this._componentMap.get(propertyName).component as Table<Slick.SlickData>;
|
||||
table.setActiveCell(tableData.data.length - 1, 0);
|
||||
}
|
||||
this._supressEditProcessing = false;
|
||||
} else {
|
||||
//TODO: add error notification
|
||||
}
|
||||
}
|
||||
|
||||
private handleInputStateChangedEvent(args: DesignerStateChangedEventArgs): void {
|
||||
if (args.previousState.pendingAction !== args.currentState.pendingAction) {
|
||||
const showLoading = args.currentState.pendingAction !== undefined;
|
||||
const action = args.currentState.pendingAction || args.previousState.pendingAction;
|
||||
this.updateLoadingStatus(action, showLoading, true);
|
||||
}
|
||||
}
|
||||
|
||||
private updateLoadingStatus(action: DesignerAction, showLoading: boolean, useDelay: boolean): void {
|
||||
let message;
|
||||
let timeout;
|
||||
switch (action) {
|
||||
case 'save':
|
||||
message = showLoading ? localize('designer.savingChanges', "Saving changes...") : localize('designer.savingChangesCompleted', "Changes have been saved");
|
||||
timeout = 0;
|
||||
break;
|
||||
case 'initialize':
|
||||
message = showLoading ? localize('designer.loadingDesigner', "Loading designer...") : localize('designer.loadingDesignerCompleted', "Designer is loaded");
|
||||
timeout = 0;
|
||||
break;
|
||||
case 'processEdit':
|
||||
message = showLoading ? localize('designer.processingChanges', "Processing changes...") : localize('designer.processingChangesCompleted', "Changes have been processed");
|
||||
// To make the edit experience smoother, only show the loading indicator if the request is not returning in 500ms.
|
||||
timeout = 500;
|
||||
break;
|
||||
default:
|
||||
return;
|
||||
}
|
||||
if (showLoading) {
|
||||
this.startLoading(message, useDelay ? timeout : 0);
|
||||
} else {
|
||||
this.stopLoading(message);
|
||||
}
|
||||
}
|
||||
|
||||
private layoutTabbedPanel() {
|
||||
this._tabbedPanel.layout(new DOM.Dimension(this._tabbedPanelContainer.clientWidth, this._tabbedPanelContainer.clientHeight));
|
||||
}
|
||||
|
||||
private async updatePropertiesPane(newContext: PropertiesPaneObjectContext): Promise<void> {
|
||||
const viewModel = await this._input.getViewModel();
|
||||
private updatePropertiesPane(newContext: PropertiesPaneObjectContext): void {
|
||||
const viewModel = this._input.viewModel;
|
||||
let type: string;
|
||||
let components: DesignerDataPropertyInfo[];
|
||||
let inputViewModel: DesignerViewModel;
|
||||
@@ -260,42 +352,25 @@ export class Designer extends Disposable implements IThemable {
|
||||
}
|
||||
}
|
||||
|
||||
private async updateComponentValues(): Promise<void> {
|
||||
const viewModel = await this._input.getViewModel();
|
||||
private updateComponentValues(): void {
|
||||
const viewModel = this._input.viewModel;
|
||||
// data[ScriptPropertyName] -- todo- set the script editor
|
||||
this._componentMap.forEach((value) => {
|
||||
this.setComponentValue(value.defintion, value.component, viewModel);
|
||||
});
|
||||
await this.updatePropertiesPane(this._propertiesPane.context ?? 'root');
|
||||
this.updatePropertiesPane(this._propertiesPane.context ?? 'root');
|
||||
}
|
||||
|
||||
private async handleEdit(edit: DesignerEdit): Promise<void> {
|
||||
private handleEdit(edit: DesignerEdit): void {
|
||||
if (this._supressEditProcessing) {
|
||||
return;
|
||||
}
|
||||
await this.applyEdit(edit);
|
||||
const handle = this.startLoading(localize('designer.processingChanges', "Processing changes..."));
|
||||
const result = await this._input.processEdit(edit);
|
||||
this.stopLoading(handle, localize('designer.processingChangesCompleted', "Processing changes completed"));
|
||||
if (result.isValid) {
|
||||
this._supressEditProcessing = true;
|
||||
await this.updateComponentValues();
|
||||
if (edit.type === DesignerEditType.Add) {
|
||||
// Move focus to the first cell of the newly added row.
|
||||
const data = await this._input.getViewModel();
|
||||
const propertyName = edit.property as string;
|
||||
const tableData = data[propertyName] as DesignerTableProperties;
|
||||
const table = this._componentMap.get(propertyName).component as Table<Slick.SlickData>;
|
||||
table.setActiveCell(tableData.data.length - 1, 0);
|
||||
}
|
||||
this._supressEditProcessing = false;
|
||||
} else {
|
||||
//TODO: add error notification
|
||||
}
|
||||
this.applyEdit(edit);
|
||||
this._input.processEdit(edit);
|
||||
}
|
||||
|
||||
private async applyEdit(edit: DesignerEdit): Promise<void> {
|
||||
const viewModel = await this._input.getViewModel();
|
||||
private applyEdit(edit: DesignerEdit): void {
|
||||
const viewModel = this._input.viewModel;
|
||||
switch (edit.type) {
|
||||
case DesignerEditType.Update:
|
||||
if (typeof edit.property === 'string') {
|
||||
@@ -423,9 +498,9 @@ export class Designer extends Disposable implements IThemable {
|
||||
ariaLabel: inputProperties.title,
|
||||
type: inputProperties.inputType,
|
||||
});
|
||||
input.onLoseFocus(async (args) => {
|
||||
input.onLoseFocus((args) => {
|
||||
if (args.hasChanged) {
|
||||
await this.handleEdit({ type: DesignerEditType.Update, property: editIdentifier, value: args.value });
|
||||
this.handleEdit({ type: DesignerEditType.Update, property: editIdentifier, value: args.value });
|
||||
}
|
||||
});
|
||||
if (setWidth && inputProperties.width !== undefined) {
|
||||
@@ -440,8 +515,8 @@ export class Designer extends Disposable implements IThemable {
|
||||
const dropdown = new SelectBox(dropdownProperties.values as string[], undefined, this._contextViewProvider, undefined);
|
||||
dropdown.render(dropdownContainer);
|
||||
dropdown.selectElem.style.height = '25px';
|
||||
dropdown.onDidSelect(async (e) => {
|
||||
await this.handleEdit({ type: DesignerEditType.Update, property: editIdentifier, value: e.selected });
|
||||
dropdown.onDidSelect((e) => {
|
||||
this.handleEdit({ type: DesignerEditType.Update, property: editIdentifier, value: e.selected });
|
||||
});
|
||||
component = dropdown;
|
||||
break;
|
||||
@@ -452,8 +527,8 @@ export class Designer extends Disposable implements IThemable {
|
||||
const checkbox = new Checkbox(checkboxContainer, {
|
||||
label: checkboxProperties.title
|
||||
});
|
||||
checkbox.onChange(async (newValue) => {
|
||||
await this.handleEdit({ type: DesignerEditType.Update, property: editIdentifier, value: newValue });
|
||||
checkbox.onChange((newValue) => {
|
||||
this.handleEdit({ type: DesignerEditType.Update, property: editIdentifier, value: newValue });
|
||||
});
|
||||
component = checkbox;
|
||||
break;
|
||||
@@ -465,8 +540,8 @@ export class Designer extends Disposable implements IThemable {
|
||||
title: addNewText,
|
||||
secondary: true
|
||||
});
|
||||
addRowButton.onDidClick(async () => {
|
||||
await this.handleEdit({
|
||||
addRowButton.onDidClick(() => {
|
||||
this.handleEdit({
|
||||
type: DesignerEditType.Add,
|
||||
property: componentDefinition.propertyName,
|
||||
});
|
||||
@@ -502,8 +577,8 @@ export class Designer extends Disposable implements IThemable {
|
||||
width: propertyDefinition.componentProperties.width as number
|
||||
});
|
||||
table.registerPlugin(checkboxColumn);
|
||||
checkboxColumn.onChange(async (e) => {
|
||||
await this.handleEdit({
|
||||
checkboxColumn.onChange((e) => {
|
||||
this.handleEdit({
|
||||
type: DesignerEditType.Update,
|
||||
property: {
|
||||
parentProperty: componentDefinition.propertyName,
|
||||
@@ -540,10 +615,9 @@ export class Designer extends Disposable implements IThemable {
|
||||
resizable: false,
|
||||
isFontIcon: true
|
||||
});
|
||||
deleteRowColumn.onClick(async (e) => {
|
||||
const viewModel = await this._input.getViewModel();
|
||||
(viewModel[componentDefinition.propertyName] as DesignerTableProperties).data.splice(e.row, 1);
|
||||
await this.handleEdit({
|
||||
deleteRowColumn.onClick((e) => {
|
||||
(this._input.viewModel[componentDefinition.propertyName] as DesignerTableProperties).data.splice(e.row, 1);
|
||||
this.handleEdit({
|
||||
type: DesignerEditType.Remove,
|
||||
property: componentDefinition.propertyName,
|
||||
value: e.item
|
||||
@@ -555,9 +629,9 @@ export class Designer extends Disposable implements IThemable {
|
||||
table.grid.onBeforeEditCell.subscribe((e, data): boolean => {
|
||||
return data.item[data.column.field].enabled !== false;
|
||||
});
|
||||
table.grid.onActiveCellChanged.subscribe(async (e, data) => {
|
||||
table.grid.onActiveCellChanged.subscribe((e, data) => {
|
||||
if (data.row !== undefined) {
|
||||
await this.updatePropertiesPane({
|
||||
this.updatePropertiesPane({
|
||||
parentProperty: componentDefinition.propertyName,
|
||||
index: data.row
|
||||
});
|
||||
@@ -578,21 +652,37 @@ export class Designer extends Disposable implements IThemable {
|
||||
return component;
|
||||
}
|
||||
|
||||
private startLoading(message: string, timeout: number = 500): any {
|
||||
// To make the experience smoother, only show the loading indicator if the request is not returning in 500ms(default value).
|
||||
return setTimeout(() => {
|
||||
private startLoading(message: string, timeout: number): void {
|
||||
this._loadingTimeoutHandle = setTimeout(() => {
|
||||
this._loadingSpinner.loadingMessage = message;
|
||||
this._loadingSpinner.loading = true;
|
||||
this._container.removeChild(this._verticalSplitViewContainer);
|
||||
if (this._container.contains(this._verticalSplitViewContainer)) {
|
||||
this._container.removeChild(this._verticalSplitViewContainer);
|
||||
}
|
||||
}, timeout);
|
||||
}
|
||||
|
||||
private stopLoading(handle: any, message: string): void {
|
||||
clearTimeout(handle);
|
||||
private stopLoading(message: string = ''): void {
|
||||
clearTimeout(this._loadingTimeoutHandle);
|
||||
this._loadingTimeoutHandle = undefined;
|
||||
if (this._loadingSpinner.loading) {
|
||||
this._loadingSpinner.loadingCompletedMessage = message;
|
||||
this._loadingSpinner.loading = false;
|
||||
this._container.appendChild(this._verticalSplitViewContainer);
|
||||
if (!this._container.contains(this._verticalSplitViewContainer)) {
|
||||
this._container.appendChild(this._verticalSplitViewContainer);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private getUIState(): DesignerUIState {
|
||||
return {
|
||||
activeTabId: this._tabbedPanel.activeTabId
|
||||
};
|
||||
}
|
||||
|
||||
private restoreUIState(): void {
|
||||
if (this._input.designerUIState) {
|
||||
this._tabbedPanel.showTab(this._input.designerUIState.activeTabId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,34 +3,50 @@
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { PanelTabIdentifier } from 'sql/base/browser/ui/panel/panel';
|
||||
import { Event } from 'vs/base/common/event';
|
||||
|
||||
export interface DesignerComponentInput {
|
||||
/**
|
||||
* The event that is triggerd when the designer state changes.
|
||||
*/
|
||||
readonly onStateChange: Event<DesignerState>;
|
||||
readonly onStateChange: Event<DesignerStateChangedEventArgs>;
|
||||
|
||||
/**
|
||||
* The event that is triggerd when the designer information is loaded.
|
||||
*/
|
||||
readonly onInitialized: Event<void>;
|
||||
|
||||
/**
|
||||
* The event that is triggerd when an edit is processed.
|
||||
*/
|
||||
readonly onEditProcessed: Event<DesignerEditProcessedEventArgs>;
|
||||
|
||||
/**
|
||||
* Gets the object type display name.
|
||||
*/
|
||||
|
||||
readonly objectTypeDisplayName: string;
|
||||
|
||||
/**
|
||||
* Gets the designer view specification.
|
||||
*/
|
||||
getView(): Promise<DesignerView>;
|
||||
readonly view: DesignerView;
|
||||
|
||||
/**
|
||||
* Gets the view model.
|
||||
*/
|
||||
getViewModel(): Promise<DesignerViewModel>;
|
||||
readonly viewModel: DesignerViewModel;
|
||||
|
||||
/**
|
||||
* Process the edit made in the designer.
|
||||
* Start initilizing the designer input object.
|
||||
*/
|
||||
initialize(): void;
|
||||
|
||||
/**
|
||||
* Start processing the edit made in the designer, the OnEditProcessed event will be fired when the processing is done.
|
||||
* @param edit the information about the edit.
|
||||
*/
|
||||
processEdit(edit: DesignerEdit): Promise<DesignerEditResult>;
|
||||
processEdit(edit: DesignerEdit): void;
|
||||
|
||||
/**
|
||||
* A boolean value indicating whether the current state is valid.
|
||||
@@ -43,16 +59,35 @@ export interface DesignerComponentInput {
|
||||
readonly dirty: boolean;
|
||||
|
||||
/**
|
||||
* A boolean value indicating whether the changes are being saved.
|
||||
* Current in progress action.
|
||||
*/
|
||||
readonly saving: boolean;
|
||||
readonly pendingAction?: DesignerAction;
|
||||
|
||||
/**
|
||||
* The UI state of the designer, used to restore the state.
|
||||
*/
|
||||
designerUIState?: DesignerUIState;
|
||||
}
|
||||
|
||||
export interface DesignerUIState {
|
||||
activeTabId: PanelTabIdentifier;
|
||||
}
|
||||
|
||||
export type DesignerAction = 'save' | 'initialize' | 'processEdit';
|
||||
|
||||
export interface DesignerEditProcessedEventArgs {
|
||||
result: DesignerEditResult;
|
||||
edit: DesignerEdit
|
||||
}
|
||||
|
||||
export interface DesignerStateChangedEventArgs {
|
||||
currentState: DesignerState,
|
||||
previousState: DesignerState
|
||||
}
|
||||
export interface DesignerState {
|
||||
valid: boolean;
|
||||
dirty: boolean;
|
||||
saving: boolean;
|
||||
processing: boolean;
|
||||
pendingAction?: DesignerAction
|
||||
}
|
||||
|
||||
export const NameProperty = 'name';
|
||||
|
||||
@@ -108,6 +108,10 @@ export class TabbedPanel extends Disposable {
|
||||
return this.parent;
|
||||
}
|
||||
|
||||
public get activeTabId(): string | undefined {
|
||||
return this._shownTabId;
|
||||
}
|
||||
|
||||
public override dispose() {
|
||||
this.header.remove();
|
||||
this.tabList.remove();
|
||||
|
||||
@@ -7,7 +7,7 @@ import * as DOM from 'vs/base/browser/dom';
|
||||
|
||||
export interface ITableCellEditorOptions {
|
||||
valueGetter?: (item: Slick.SlickData, column: Slick.Column<Slick.SlickData>) => string,
|
||||
valueSetter?: (context: any, row: number, item: Slick.SlickData, column: Slick.Column<Slick.SlickData>, value: string) => Promise<void>,
|
||||
valueSetter?: (context: any, row: number, item: Slick.SlickData, column: Slick.Column<Slick.SlickData>, value: string) => void,
|
||||
optionsGetter?: (item: Slick.SlickData, column: Slick.Column<Slick.SlickData>) => string[],
|
||||
editorStyler: (component: InputBox | SelectBox) => void
|
||||
}
|
||||
@@ -72,9 +72,9 @@ export class TableCellEditorFactory {
|
||||
this._input.value = this._originalValue;
|
||||
}
|
||||
|
||||
public async applyValue(item: Slick.SlickData, state: string): Promise<void> {
|
||||
public applyValue(item: Slick.SlickData, state: string): void {
|
||||
const activeCell = this._args.grid.getActiveCell();
|
||||
await self._options.valueSetter(context, activeCell.row, item, this._args.column, state);
|
||||
self._options.valueSetter(context, activeCell.row, item, this._args.column, state);
|
||||
}
|
||||
|
||||
public isValueChanged(): boolean {
|
||||
|
||||
Reference in New Issue
Block a user