make the designer event based (#17472)

* make the designer event based

* pr comments
This commit is contained in:
Alan Ren
2021-10-22 17:25:12 -07:00
committed by GitHub
parent 70f6eebc5a
commit 4ba192a5c3
7 changed files with 284 additions and 128 deletions

View File

@@ -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);
}
}
}

View File

@@ -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';

View File

@@ -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();

View File

@@ -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 {